# 爬虫逻辑梳理与数据校验 ## 一、两条入口,数据流不同 ### 1. 入口 A:`npm run crawler`(main.py) - **流程**:RSS 抓取 → 关键词过滤 → 分类/严重度 → **直接写 situation_update** → 通知 API - **不经过**:翻译、news_content、AI 提取(战损/基地等) - **写入表**:`situation_update`、`situation.updated_at` - **用途**:轻量、只给「事件脉络」喂新条目,不更新战损/基地/报复指数 ``` RSS_FEEDS → fetch_all() → KEYWORDS 过滤 → parser_ai.classify_and_severity → write_updates(items) → situation_update INSERT + situation 表 touch → notify_api() ``` ### 2. 入口 B:`npm run gdelt`(realtime_conflict_service.py) - **流程**:RSS 抓取 → 翻译 → 清洗 → **news_content 去重** → situation_update → **AI 提取 → db_merge** → GDELT 事件(可选)→ 通知 API - **写入表**:`news_content`、`situation_update`、`situation`;提取后还有 `combat_losses`、`key_location`、`retaliation_*`、`wall_street_trend` 等 - **用途**:完整管线,前端「战损 / 军事基地 / 报复 / 美股」等数据都依赖这条 ``` RSS → fetch_all() → translate_to_chinese → cleaner_ai → save_and_dedup → news_content → write_updates(new_items) → situation_update → _extract_and_merge_panel_data(new_items) → extract_from_news() → db_merge.merge() → (可选) fetch_gdelt_events() → gdelt_events, conflict_stats → _notify_node() ``` **结论**:要检查「抓回的数据是否有效」且包含战损/基地等,应跑 **入口 B**(gdelt 服务);若只关心事件脉络条数,可看入口 A。 --- ## 二、入口 B 逐步拆解(用于逐段校验) ### 2.1 RSS 抓取与过滤 | 步骤 | 位置 | 说明 | |------|------|------| | 源列表 | `config.RSS_FEEDS` | 多国媒体 RSS,见 config.py | | 抓取 | `scrapers.rss_scraper.fetch_all()` | feedparser,单源超时 10s | | 过滤 | `_matches_keywords(text)` | 标题+摘要 至少命中 `config.KEYWORDS` 中一个才保留 | | 去重 | `(title[:80], link)` | 同一条不重复加入当次列表 | | 分类 | `parser_ai.classify_and_severity(text)` | 得到 category、severity(Ollama 或规则) | **校验**:`npm run crawler:test` 看本次抓到的条数;若为 0,查网络或放宽/检查 KEYWORDS。 ### 2.2 翻译与清洗(仅入口 B) | 步骤 | 位置 | 说明 | |------|------|------| | 翻译 | `translate_utils.translate_to_chinese()` | 标题/摘要译成中文(依赖配置) | | 清洗 | `cleaner_ai.clean_news_for_panel()` | 截断、清理;`ensure_category` / `ensure_severity` 合法化 | ### 2.3 落库:news_content(去重)与 situation_update | 步骤 | 位置 | 说明 | |------|------|------| | 去重 | `news_storage.save_and_dedup(items)` | 按 `content_hash(title, summary, url)` 判重,只插入新记录 | | 表 | `news_content` | id, content_hash, title, summary, url, source, published_at, category, severity | | 表 | `situation_update` | 仅对 **去重后的 new_items** 调用 `write_updates()`,供前端「事件脉络」 | **校验**: - `news_content`:`SELECT COUNT(*), MAX(published_at) FROM news_content` - `situation_update`:`SELECT COUNT(*), MAX(timestamp) FROM situation_update` - 服务状态:`GET http://localhost:8000/crawler/status` 看 `last_fetch_items` / `last_fetch_inserted` / `last_fetch_error` ### 2.4 AI 提取与 db_merge(战损 / 基地 / 报复等) | 步骤 | 位置 | 说明 | |------|------|------| | 输入 | `_extract_and_merge_panel_data(new_items)` | 仅处理本次 **新增** 的 new_items,前 limit 条(DashScope 10 条,规则 25 条,Ollama 10 条) | | 文本 | 每条 `title + " " + summary`,长度 < 20 跳过 | | 提取器选择 | 环境变量 | `DASHSCOPE_API_KEY` → extractor_dashscope;`CLEANER_AI_DISABLED=1` → extractor_rules;否则 extractor_ai(Ollama) | | 输出结构 | 见 panel_schema / 各 extractor | `situation_update?`, `combat_losses_delta?`, `retaliation?`, `wall_street?`, `key_location_updates?` | | 合并 | `db_merge.merge(extracted)` | 见下表 | **merge 映射概要**: | 提取字段 | 写入表/逻辑 | |----------|-------------| | situation_update | situation_update 表 INSERT(id 为 hash) | | combat_losses_delta | combat_losses 表,按 side 增量叠加 | | retaliation | retaliation_current 替换 + retaliation_history 追加 | | wall_street | wall_street_trend 表 INSERT | | key_location_updates | key_location 表 UPDATE status/damage_level(name LIKE 关键词) | **校验**: - 战损:`SELECT * FROM combat_losses` - 基地:`SELECT id, name, side, status, damage_level FROM key_location WHERE status != 'operational' OR damage_level > 0` - 报复:`SELECT * FROM retaliation_current` 与 `retaliation_history` 最近几条 - 事件脉络:`SELECT id, timestamp, category, summary, severity FROM situation_update ORDER BY timestamp DESC LIMIT 20` ### 2.5 GDELT(可选) - `GDELT_DISABLED=1` 时跳过 GDELT,仅用 RSS;可用 `_rss_to_gdelt_fallback()` 用 RSS 标题生成 gdelt_events。 - 未禁用时:`fetch_gdelt_events()` 拉 GDELT → 写 `gdelt_events`、`conflict_stats`。 **校验**:`SELECT COUNT(*), MAX(event_time) FROM gdelt_events`;`SELECT * FROM conflict_stats WHERE id=1`。 --- ## 三、如何检查「抓回的数据是否有效」 1. **确认跑的入口** - 只跑 `npm run crawler`:只有 situation_update 会有新数据,战损/基地不会变。 - 跑 `npm run gdelt` 且服务常驻:才会既有 situation_update,又有 combat_losses、key_location 等。 2. **看 DB 与 API** - 同上:查 `news_content`、`situation_update`、`combat_losses`、`key_location`、`retaliation_*`、`gdelt_events`、`conflict_stats`。 - 前端数据来源:`GET /api/situation`(见 server/situationData.js),对照上述表即可。 3. **看提取是否触发** - 若 `combat_losses` / `key_location` 一直不更新:确认是入口 B、有 new_items、提取器未报错;可对单条新闻跑 `extract_from_news(text)` 看是否产出 combat_losses_delta / key_location_updates。 4. **重跑历史提取(补数据)** - `POST http://localhost:8000/crawler/backfill`:用当前 situation_update 最近 50 条重新做一次提取并 merge,可用来修历史未提取的数据。 --- ## 四、配置与环境变量(与数据有效性相关) | 变量 | 作用 | |------|------| | DB_PATH | 与 server 共用的 SQLite 路径,必须一致 | | API_BASE | 通知 Node 的地址,merge 后通知前端 | | DASHSCOPE_API_KEY | 有则用 DashScope 提取;无则用 Ollama 或规则 | | CLEANER_AI_DISABLED=1 | 用规则提取(extractor_rules),不用 Ollama | | GDELT_DISABLED=1 | 不用 GDELT,仅 RSS;RSS 可转 gdelt_events 占位 | | CRAWL_INTERVAL | main.py 抓取间隔(秒) | | RSS_INTERVAL_SEC / FETCH_INTERVAL_SEC | realtime 服务里 RSS / GDELT 间隔 | 按上述顺序对照「入口 → RSS → 去重 → situation_update → 提取 → merge → 表」即可逐段检查爬虫抓回的数据是否有效。