skander101 commited on
Commit
48aff15
·
1 Parent(s): e697eec
Files changed (4) hide show
  1. main.py +18 -6
  2. src/analyzer.py +23 -0
  3. src/html_scraper.py +87 -0
  4. webapp.py +4 -2
main.py CHANGED
@@ -14,7 +14,14 @@ from src.presenter import NewsPresenter
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
- NAWAAT_DOMAINS = {"nawaat.org", "www.nawaat.org"}
 
 
 
 
 
 
 
18
 
19
  _translator = None
20
 
@@ -65,13 +72,14 @@ def run_pipeline(items: list[NewsItem], cfg: Config, skip_extraction: bool = Fal
65
  else:
66
  logger.info("═══ Extraction skipped (articles already loaded) ═══")
67
 
68
- logger.info("═══ Translating Nawaat articles ═══")
69
  for item in items:
70
  art = item.article
71
  post = item.post
72
  if not art or not post or not post.source_domain:
73
  continue
74
- if post.source_domain not in NAWAAT_DOMAINS:
 
75
  continue
76
  t_title = _translate_text(art.title)
77
  if t_title and t_title != art.title:
@@ -148,11 +156,15 @@ def main():
148
 
149
  else:
150
  from src.rss_feed_scraper import RSSFeedScraper
 
151
  print("═══ Fetching RSS news feeds ═══")
152
- scraper = RSSFeedScraper(cfg)
153
- posts = scraper.fetch_posts()
 
 
 
 
154
  n_posts = len(posts)
155
- print(f" → {n_posts} posts collected\n")
156
  if not posts:
157
  sys.exit(1)
158
  items = [NewsItem(post=p) for p in posts]
 
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
+ TRANSLATE_DOMAINS = {
18
+ "nawaat.org", "www.nawaat.org",
19
+ "tunisienumerique.com", "www.tunisienumerique.com",
20
+ "lapresse.tn", "www.lapresse.tn",
21
+ "webmanagercenter.com", "www.webmanagercenter.com",
22
+ "directinfo.webmanagercenter.com",
23
+ "tuniscope.com", "www.tuniscope.com",
24
+ }
25
 
26
  _translator = None
27
 
 
72
  else:
73
  logger.info("═══ Extraction skipped (articles already loaded) ═══")
74
 
75
+ logger.info("═══ Translating Arab/Tunisian articles ═══")
76
  for item in items:
77
  art = item.article
78
  post = item.post
79
  if not art or not post or not post.source_domain:
80
  continue
81
+ domain = post.source_domain.lower()
82
+ if domain not in TRANSLATE_DOMAINS:
83
  continue
84
  t_title = _translate_text(art.title)
85
  if t_title and t_title != art.title:
 
156
 
157
  else:
158
  from src.rss_feed_scraper import RSSFeedScraper
159
+ from src.html_scraper import HTMLSiteScraper
160
  print("═══ Fetching RSS news feeds ═══")
161
+ rss = RSSFeedScraper(cfg).fetch_posts()
162
+ print(f" → {len(rss)} RSS posts collected")
163
+ print("═══ Scraping HTML sites ═══")
164
+ html_posts = HTMLSiteScraper(cfg).fetch_posts()
165
+ print(f" → {len(html_posts)} HTML posts collected")
166
+ posts = rss + html_posts
167
  n_posts = len(posts)
 
168
  if not posts:
169
  sys.exit(1)
170
  items = [NewsItem(post=p) for p in posts]
src/analyzer.py CHANGED
@@ -16,6 +16,8 @@ RELIABLE_DOMAINS = {
16
  "sciencenews.org": 0.14, "medscape.com": 0.12, "nih.gov": 0.16,
17
  "news.harvard.edu": 0.14, "nawaat.org": 0.12, "tunisienumerique.com": 0.10,
18
  "lapresse.tn": 0.12, "allafrica.com": 0.10,
 
 
19
  }
20
 
21
  UNRELIABLE_DOMAINS = {
@@ -376,6 +378,24 @@ SPONSOR_INFO: dict[str, dict] = {
376
  "wikipedia": "https://en.wikipedia.org/wiki/AllAfrica.com",
377
  "owner_wikis": {},
378
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  }
380
 
381
  CLICKBAIT_PATTERNS = [
@@ -776,6 +796,9 @@ class NewsAnalyzer:
776
  "medscape.com": "health",
777
  "nih.gov": "health",
778
  "news.harvard.edu": "health",
 
 
 
779
  }
780
 
781
  def _keyword_topic_match(self, article: Article, summary: str) -> list[str]:
 
16
  "sciencenews.org": 0.14, "medscape.com": 0.12, "nih.gov": 0.16,
17
  "news.harvard.edu": 0.14, "nawaat.org": 0.12, "tunisienumerique.com": 0.10,
18
  "lapresse.tn": 0.12, "allafrica.com": 0.10,
19
+ "webmanagercenter.com": 0.08, "directinfo.webmanagercenter.com": 0.08,
20
+ "tuniscope.com": 0.08,
21
  }
22
 
23
  UNRELIABLE_DOMAINS = {
 
378
  "wikipedia": "https://en.wikipedia.org/wiki/AllAfrica.com",
379
  "owner_wikis": {},
380
  },
381
+ "webmanagercenter.com": {
382
+ "display": "Webmanagercenter", "parent": "Webmanagercenter", "category": "independent", "bias": "center", "factuality": "mixed",
383
+ "owners": ["Independent (Tunisian editorial team)"],
384
+ "wikipedia": "",
385
+ "owner_wikis": {},
386
+ },
387
+ "directinfo.webmanagercenter.com": {
388
+ "display": "Directinfo", "parent": "Webmanagercenter", "category": "independent", "bias": "center", "factuality": "mixed",
389
+ "owners": ["Independent (Tunisian editorial team)"],
390
+ "wikipedia": "",
391
+ "owner_wikis": {},
392
+ },
393
+ "tuniscope.com": {
394
+ "display": "Tuniscope", "parent": "Tuniscope", "category": "independent", "bias": "center", "factuality": "mixed",
395
+ "owners": ["Independent (Tunisian editorial team)"],
396
+ "wikipedia": "",
397
+ "owner_wikis": {},
398
+ },
399
  }
400
 
401
  CLICKBAIT_PATTERNS = [
 
796
  "medscape.com": "health",
797
  "nih.gov": "health",
798
  "news.harvard.edu": "health",
799
+ "webmanagercenter.com": "tunisia",
800
+ "directinfo.webmanagercenter.com": "tunisia",
801
+ "tuniscope.com": "tunisia",
802
  }
803
 
804
  def _keyword_topic_match(self, article: Article, summary: str) -> list[str]:
src/html_scraper.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import time
3
+ from urllib.parse import urljoin, urlparse
4
+
5
+ import requests
6
+ from bs4 import BeautifulSoup
7
+
8
+ from .models import RedditPost
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ HTML_SITES = [
13
+ {
14
+ "name": "Webmanagercenter",
15
+ "url": "https://www.webmanagercenter.com/",
16
+ "selectors": [".entry-title a", "h3 a", "h2 a"],
17
+ "domain": "webmanagercenter.com",
18
+ },
19
+ {
20
+ "name": "Directinfo",
21
+ "url": "https://directinfo.webmanagercenter.com/",
22
+ "selectors": [".entry-title a", "h3 a", "h2 a"],
23
+ "domain": "directinfo.webmanagercenter.com",
24
+ },
25
+ {
26
+ "name": "Tuniscope",
27
+ "url": "https://www.tuniscope.com/",
28
+ "selectors": [".entry-title a", "h3 a", "h2 a", ".article-title a"],
29
+ "domain": "tuniscope.com",
30
+ },
31
+ ]
32
+
33
+
34
+ class HTMLSiteScraper:
35
+ def __init__(self, config):
36
+ self.config = config
37
+ self.session = requests.Session()
38
+ self.session.headers.update({
39
+ "User-Agent": "Mozilla/5.0 (compatible; newsapp/1.0)",
40
+ })
41
+
42
+ def fetch_posts(self) -> list[RedditPost]:
43
+ seen_urls = set()
44
+ posts = []
45
+ for site in HTML_SITES:
46
+ logger.info("Scraping HTML: %s (%s)", site["name"], site["url"])
47
+ try:
48
+ resp = self.session.get(site["url"], timeout=15)
49
+ resp.raise_for_status()
50
+ soup = BeautifulSoup(resp.text, "html.parser")
51
+ links = self._find_article_links(soup, site)
52
+ logger.info(" %s → %d links found", site["name"], len(links))
53
+ for a_tag in links[: self.config.posts_per_subreddit]:
54
+ title = a_tag.get_text(strip=True)
55
+ href = a_tag.get("href", "")
56
+ if not title or not href:
57
+ continue
58
+ full_url = urljoin(site["url"], href)
59
+ if full_url in seen_urls:
60
+ continue
61
+ seen_urls.add(full_url)
62
+ domain = site.get("domain") or urlparse(full_url).netloc
63
+ post = RedditPost(
64
+ id=full_url,
65
+ title=title,
66
+ url=full_url,
67
+ subreddit=domain,
68
+ score=0,
69
+ num_comments=0,
70
+ source_domain=domain,
71
+ image_url="",
72
+ published="",
73
+ published_iso="",
74
+ )
75
+ posts.append(post)
76
+ except Exception as exc:
77
+ logger.warning("Failed HTML scrape %s: %s", site["name"], exc)
78
+ time.sleep(1.0)
79
+ return posts
80
+
81
+ @staticmethod
82
+ def _find_article_links(soup: BeautifulSoup, site: dict) -> list:
83
+ for selector in site["selectors"]:
84
+ found = soup.select(selector)
85
+ if found:
86
+ return found
87
+ return []
webapp.py CHANGED
@@ -13,6 +13,7 @@ from config import Config
13
  from main import run_pipeline
14
  from src.models import NewsItem
15
  from src.rss_feed_scraper import RSSFeedScraper
 
16
 
17
  logging.basicConfig(level=logging.INFO, format="%(levelname).1s %(message)s", stream=sys.stderr)
18
  logger = logging.getLogger(__name__)
@@ -117,8 +118,9 @@ def refresh_data():
117
  with cache_lock:
118
  cached_status = "running"
119
  try:
120
- scraper = RSSFeedScraper(cfg)
121
- posts = scraper.fetch_posts()
 
122
  items = [NewsItem(post=p) for p in posts]
123
  new_clusters = run_pipeline(items, cfg)
124
  html_dicts = [_cluster_to_html_dict(c) for c in new_clusters]
 
13
  from main import run_pipeline
14
  from src.models import NewsItem
15
  from src.rss_feed_scraper import RSSFeedScraper
16
+ from src.html_scraper import HTMLSiteScraper
17
 
18
  logging.basicConfig(level=logging.INFO, format="%(levelname).1s %(message)s", stream=sys.stderr)
19
  logger = logging.getLogger(__name__)
 
118
  with cache_lock:
119
  cached_status = "running"
120
  try:
121
+ rss_posts = RSSFeedScraper(cfg).fetch_posts()
122
+ html_posts = HTMLSiteScraper(cfg).fetch_posts()
123
+ posts = rss_posts + html_posts
124
  items = [NewsItem(post=p) for p in posts]
125
  new_clusters = run_pipeline(items, cfg)
126
  html_dicts = [_cluster_to_html_dict(c) for c in new_clusters]