您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # Anya
  2. A news headline aggregator that clusters near-duplicate headlines into **stories**
  3. and reports only the stories covered by a minimum number of distinct outlets.
  4. Instead of printing every pairwise "duplicate found" alert, Anya groups matching
  5. headlines transitively (union-find over cosine similarity) and answers the useful
  6. question: *which stories are multiple independent sources reporting right now?*
  7. ## How it works
  8. 1. **Load** source URLs and stopwords from `resources/`.
  9. 2. **Fetch** each source. RSS/Atom feeds are parsed as structured XML; HTML pages
  10. are first checked for embedded JSON (JSON-LD / Next.js SSR state) before
  11. falling back to scraping `<a>`/`<span>` text, associating each headline with
  12. the nearest `<time>` publication timestamp.
  13. 3. **Normalize** each headline (lowercase, strip punctuation, apply aliases so
  14. "Federal Reserve" becomes "fed", then remove stopwords).
  15. 4. **Cluster** headlines into stories using pairwise cosine similarity, linking
  16. matches transitively so a chain of near-duplicates collapses into one story.
  17. 5. **Filter & report** stories with at least `--min-sources` distinct outlets,
  18. optionally restricted to a date window.
  19. ## Requirements
  20. - Python 3.11+ (developed and tested on 3.14)
  21. - `requests`
  22. ```bash
  23. pip install requests
  24. ```
  25. ## Setup
  26. ```bash
  27. git clone https://ikibani.com/jbell730/anya.git
  28. cd anya
  29. pip install requests
  30. ```
  31. Run from the project root — the resource paths are relative (`./resources/...`).
  32. ## Configuration
  33. - `resources/sources.txt` — one source URL per line (HTML pages **or** RSS/Atom
  34. feeds; `#` comments and blank lines are ignored).
  35. - `resources/stopwords.txt` — one stopword per line.
  36. - `resources/excluded_phrases.txt` — boilerplate link text to ignore (see below).
  37. - `resources/aliases.toml` — full-name → short-form aliases (see below).
  38. - Defaults live at the top of `main.py` (`SIMILARITY_THRESHOLD = 0.64`,
  39. `MIN_SOURCES = 3`) and can be overridden on the command line.
  40. ## Usage
  41. ```bash
  42. python main.py [options]
  43. ```
  44. | Option | Description | Default |
  45. | --- | --- | --- |
  46. | `--min-sources N` | Only output stories reported by at least N distinct sources | `3` |
  47. | `--threshold T` | Cosine similarity used to consider two headlines the same story | `0.64` |
  48. | `--since YYYY-MM-DD` | Only consider headlines published on or after this date | *(none)* |
  49. | `--until YYYY-MM-DD` | Only consider headlines published on or before this date | *(none)* |
  50. | `--verbose` | Enable debug logging | off |
  51. ### Examples
  52. ```bash
  53. # Stories reported by 3+ distinct outlets
  54. python main.py --min-sources 3
  55. # Stories from the last week, need 2+ outlets
  56. python main.py --since 2026-09-07
  57. # A specific range with a stricter similarity threshold
  58. python main.py --since 2026-09-01 --until 2026-09-14 --threshold 0.8
  59. ```
  60. Sample output:
  61. ```
  62. News stories covered by at least 3 distinct sources (published on or after 2026-09-13)
  63. ============================================================
  64. [3 source(s)] Federal Reserve holds interest rates steady (2026-09-14)
  65. cnn.com, foxnews.com, reuters.com
  66. [2 source(s)] Senate passes major infrastructure bill (2026-09-14)
  67. npr.org, nbcnews.com
  68. ```
  69. ## Design notes
  70. - **A "source" is a distinct domain**, not a distinct URL. `www.cnn.com/us` and
  71. `cnn.com/politics` both normalize to `cnn.com`, so one outlet counts once even
  72. when listed under multiple sections. Feed/redirect subdomains (`feeds.`, `rss.`,
  73. `moxie.`) are also stripped, so a feed and an HTML page from the same outlet
  74. still collapse to one source.
  75. - **RSS/Atom sources are preferred when available.** They're structured and far
  76. more reliable than scraping JavaScript-heavy or paywalled pages, and they carry
  77. publication timestamps directly. Feed type is auto-detected from the content, so
  78. HTML and feed URLs can live side by side in `sources.txt`.
  79. - **JS-rendered pages are recovered without a browser.** For HTML that embeds its
  80. data as JSON — JSON-LD structured data or Next.js `__NEXT_DATA__` SSR state —
  81. Anya parses that directly and never executes JavaScript. This is far lighter than
  82. a headless browser, at the cost of some per-site variation in the JSON shape.
  83. - **Date windowing drops undated headlines.** When `--since` or `--until` is set,
  84. any headline whose page carries no parseable timestamp is excluded because its
  85. recency can't be established (the count is logged). Without a date flag,
  86. everything is included.
  87. - **Publication dates** are drawn from `<time>` elements (their `datetime`/`title`
  88. attributes or inner text) and associated with headlines in document order; the
  89. association resets at each `<article>`/`<li>` boundary so undated headlines don't
  90. inherit a neighboring story's date.
  91. - **Boilerplate link text is filtered.** Navigation/footer/legal links like
  92. "skip to content" or "your privacy choices" are dropped by matching against
  93. `excluded_phrases.txt` (case- and punctuation-insensitive; an entry matches when
  94. it appears anywhere in the link text as a run of words, so `privacy choices`
  95. also catches "Your Privacy Choices"). Extend the file rather than editing code.
  96. - **Aliases unify equivalent names.** `aliases.toml` maps full names to a shared
  97. short form so "Federal Reserve" and "the Fed" normalize to the same token and
  98. cluster. Matching is whole-word only ("united states" won't match inside
  99. "united statesman") and longest-phrase-first ("president of the united states"
  100. becomes "potus" before "united states" can fire). The canonical form must not be
  101. a stopword — e.g. "united nations" uses `unitednations`, not `un`, because `un`
  102. is stripped during normalization. Extend the file rather than editing code.
  103. ## Tests
  104. ```bash
  105. python -m unittest discover -s tests -p 'test_*.py'
  106. ```
  107. ## Project structure
  108. ```
  109. anya/
  110. ├── main.py # CLI entry point and orchestration
  111. ├── services/
  112. │ ├── headlines.py # fetch + parse headlines (and timestamps)
  113. │ ├── feeds.py # RSS/Atom feed detection and parsing
  114. │ ├── ssr.py # JSON-LD / Next.js embedded-JSON extraction
  115. │ ├── normalization.py # stopword/alias/phrase loading and headline normalization
  116. │ ├── similarity.py # cosine similarity over token lists
  117. │ ├── sources.py # load source URLs
  118. │ ├── stories.py # cluster headlines into stories
  119. │ └── dates.py # date parsing and window filtering
  120. ├── structs/
  121. │ ├── headline.py # Headline model (text, domain, published_at)
  122. │ └── story.py # Story model (sources, representative, latest_date)
  123. ├── resources/
  124. │ ├── sources.txt # one source URL per line
  125. │ ├── stopwords.txt # one stopword per line
  126. │ ├── aliases.toml # full-name → short-form aliases
  127. │ └── excluded_phrases.txt # boilerplate link text to ignore
  128. └── tests/ # unit tests
  129. ```