Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

normalization.py 6.4 KiB

pirms 4 dienas
pirms 4 dienas
pirms 4 dienas
pirms 4 dienas
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import logging
  2. import string
  3. logger = logging.getLogger(__name__)
  4. ALIASES = {
  5. 'federal reserve': 'fed',
  6. 'united states': 'us',
  7. 'united kingdom': 'uk',
  8. 'european union': 'eu',
  9. 'north atlantic treaty organization': 'nato',
  10. 'white house': 'wh',
  11. 'united nations': 'un',
  12. 'international monetary fund': 'imf',
  13. 'world trade organization': 'wto',
  14. 'central intelligence agency': 'cia',
  15. 'federal bureau of investigation': 'fbi',
  16. 'department of justice': 'doj',
  17. 'department of defense': 'dod',
  18. 'environmental protection agency': 'epa',
  19. 'securities and exchange commission': 'sec',
  20. 'internal revenue service': 'irs',
  21. 'social security administration': 'ssa',
  22. 'centers for disease control': 'cdc',
  23. 'national security agency': 'nsa',
  24. 'department of homeland security': 'dhs',
  25. 'supreme court': 'scotus',
  26. 'republican party': 'gop',
  27. 'democratic party': 'democrats',
  28. 'president of the united states': 'potus',
  29. 'vice president': 'vp',
  30. 'prime minister': 'pm',
  31. 'secretary of state': 'secstate',
  32. 'attorney general': 'ag',
  33. 'gross domestic product': 'gdp',
  34. 'consumer price index': 'cpi',
  35. 'unemployment rate': 'unemployment',
  36. 'inflation rate': 'inflation',
  37. 'national debt': 'debt',
  38. 'budget deficit': 'deficit',
  39. 'middle east': 'mideast',
  40. 'asia pacific': 'apac',
  41. 'latin america': 'latam'
  42. }
  43. def get_stopwords(path):
  44. logger.info("Attempting to load stopwords from file: '%s'", path)
  45. try:
  46. with open(path, 'r', encoding='utf-8-sig') as stopwords_file:
  47. logger.debug("Opened stopwords file '%s'", path)
  48. lines = stopwords_file.read().splitlines()
  49. stopwords = set(lines)
  50. logger.info("Successfully loaded %d unique stopwords from '%s' (%d total lines)", len(stopwords), path, len(lines))
  51. return stopwords
  52. except FileNotFoundError:
  53. logger.error("Stopwords file not found at path: '%s'", path, exc_info=True)
  54. raise
  55. except PermissionError:
  56. logger.error("Permission denied accessing stopwords file: '%s'", path, exc_info=True)
  57. raise
  58. except Exception as e:
  59. logger.error("Failed to load stopwords from '%s': %s", path, e, exc_info=True)
  60. raise
  61. def get_excluded_phrases(path):
  62. """Load boilerplate link phrases from a file (one per line, '#' comments).
  63. These are strings that appear in navigation/footer links ("skip to content",
  64. "your privacy choices", "terms of service") and should never be treated as
  65. headlines. Returns a list of stripped, non-empty phrases in file order.
  66. """
  67. logger.info("Attempting to load excluded phrases from file: '%s'", path)
  68. try:
  69. with open(path, 'r', encoding='utf-8-sig') as phrases_file:
  70. lines = phrases_file.read().splitlines()
  71. except FileNotFoundError:
  72. logger.error("Excluded phrases file not found at path: '%s'", path, exc_info=True)
  73. raise
  74. except PermissionError:
  75. logger.error("Permission denied accessing excluded phrases file: '%s'", path, exc_info=True)
  76. raise
  77. except Exception as e:
  78. logger.error("Failed to load excluded phrases from '%s': %s", path, e, exc_info=True)
  79. raise
  80. phrases = []
  81. for line in lines:
  82. stripped = line.strip()
  83. if not stripped or stripped.startswith('#'):
  84. continue
  85. phrases.append(stripped)
  86. logger.info("Successfully loaded %d excluded phrases from '%s'", len(phrases), path)
  87. return phrases
  88. def canonicalize(text):
  89. """Lowercase, turn punctuation into spaces, and collapse whitespace.
  90. Produces a comparable form of free text for phrase matching — e.g.
  91. ``"Terms-of-Service."`` becomes ``"terms of service"``.
  92. """
  93. text = (text or '').lower()
  94. for char in string.punctuation:
  95. text = text.replace(char, ' ')
  96. return ' '.join(text.split())
  97. def is_excluded(text, excluded_phrases):
  98. """Return True if ``text`` contains an excluded phrase as a contiguous
  99. token subsequence (case-insensitive, punctuation-insensitive).
  100. Matching is sub-phrase aware, so the entry ``privacy choices`` also matches
  101. the link text "Your Privacy Choices" without needing a separate exact row.
  102. """
  103. if not excluded_phrases:
  104. return False
  105. tokens = canonicalize(text).split()
  106. if not tokens:
  107. return False
  108. for phrase in excluded_phrases:
  109. phrase_tokens = canonicalize(phrase).split()
  110. if not phrase_tokens:
  111. continue
  112. for start in range(len(tokens) - len(phrase_tokens) + 1):
  113. if tokens[start:start + len(phrase_tokens)] == phrase_tokens:
  114. logger.debug("Text excluded by phrase %r: %r", phrase, text)
  115. return True
  116. return False
  117. def remove_stopwords(text, stopwords):
  118. logger.debug("Removing stopwords from text (%d chars): %r (available stopwords: %d)", len(text), text, len(stopwords))
  119. # split() without arguments handles all whitespace (spaces, tabs, newlines)
  120. words = text.split()
  121. sentence_words = []
  122. for word in words:
  123. # Strip surrounding punctuation and lowercase for comparison
  124. cleaned_word = word.strip(string.punctuation).lower()
  125. if cleaned_word and cleaned_word not in stopwords:
  126. sentence_words.append(word)
  127. elif cleaned_word in stopwords:
  128. logger.debug("Word filtered out as stopword: %r (original: %r)", cleaned_word, word)
  129. else:
  130. logger.debug("Word dropped (empty after stripping punctuation): %r", word)
  131. logger.debug("Filtered words: input=%d words, output=%d words -> %s", len(words), len(sentence_words), sentence_words)
  132. return sentence_words
  133. def normalize_headline(text, stopwords):
  134. logger.debug("Starting normalization of headline: %r", text)
  135. headline = text.strip().lower()
  136. punctuation = string.punctuation
  137. for char in punctuation:
  138. headline = headline.replace(char, '')
  139. logger.debug("Headline after lowercasing and punctuation removal: %r", headline)
  140. headline = replace_common_aliases(headline)
  141. logger.debug("Headline after replacing common aliases: %r", headline)
  142. normalized = remove_stopwords(headline, stopwords)
  143. logger.debug("Completed normalization for %r -> %s", text, normalized)
  144. return normalized
  145. def replace_common_aliases(headline):
  146. for alias in ALIASES:
  147. headline = headline.replace(alias, ALIASES[alias])
  148. return headline