Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

normalization.py 6.3 KiB

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