osint-dashboard/news/scraper/newsScraper/spiders/news_spider.py
Sirius DevOps 91f436390b
Some checks failed
build-and-deploy / build (push) Failing after 4s
Add news pipeline: hourly scraper + Gemini summarizer in compose
Vendor the newsPipeline scraper + summarizer into the repo and wire them into
docker-compose against the EXISTING osint-db (no second Postgres), replacing
the upstream k8s CronJobs with in-compose wall-clock loops (:00 scrape, :05
summarize).

- news/scraper: vendored Scrapy project (257 RSS feeds) + hourly loop
  scheduler (run_news_scraper.py)
- news/summerizer: vendored Gemini map-reduce summarizer, cleaned:
  * fix broken google-genai response handling (_extract_text, defensive)
  * fix malformed INSERT/GRANT query in save_summary_to_db
  * OSINT-neutral default MAP_PROMPT; futures/markets language gated behind
    INCLUDE_FUTURES=0 (yfinance lazy-imported)
  * env-configurable model, batch size, lookback window
  + hourly loop scheduler (run_news_summarizer.py, :05)
- alembic 003_news: idempotent articles + article_summaries tables
- API: GET /api/news and GET /api/news/summaries (+ models, schemas)
- tests/test_api_news.py: 5 DB-backed contract tests (all pass vs real PG)
- docs/news.md + .env.example updates

Both services run under the `ingest` compose profile (matching the
ingester/camera-scraper pattern) and build arm64 on the Pi via the existing
Forgejo CI workflow. telebot left out of scope (reserved env only).
2026-08-24 17:28:46 -04:00

50 lines
1.9 KiB
Python

import scrapy
from scrapy.spiders import XMLFeedSpider
from urllib.parse import urlparse
import datetime
import re
class NewsRSSSpider(XMLFeedSpider):
name = "articles"
iterator = 'xml'
itertag = 'item' # Standard RSS tag for an article
namespaces = [
('dc', 'http://purl.org/dc/elements/1.1/'),
('content', 'http://purl.org/rss/1.0/modules/content/'),
('media', 'http://search.yahoo.com/mrss/')
]
def __init__(self, filename='urls.txt', *args, **kwargs):
super(NewsRSSSpider, self).__init__(*args, **kwargs)
with open(filename, 'r') as f:
# We filter for RSS feeds only here
self.start_urls = [line.strip() for line in f if '/rss' in line or '/feed' in line]
def parse_node(self, response, node):
"""This runs for every <item> found in the RSS XML"""
title = node.xpath('title/text()').get()
link = node.xpath('link/text()').get()
pub_date = node.xpath('pubDate/text()').get()
# We now yield a Request to the actual article to get the full text
# Since these are RSS links, they are usually 'clean' HTML
if link:
yield scrapy.Request(link, callback=self.parse_article, meta={'title': title, 'date': pub_date})
def parse_article(self, response):
title = response.meta.get('title')
# Greedy search for the main text body
article_text = response.xpath('//article//p/text() | //main//p/text() | //div[contains(@class, "body")]//p/text()').getall()
pure_text = " ".join(article_text)
pure_text = re.sub(r'\s+', ' ', pure_text).strip()
if len(pure_text) > 300:
yield {
'title': title,
'url': response.url,
'text': pure_text,
'domain': urlparse(response.url).netloc,
'timestamp': datetime.datetime.now().isoformat()
}