This commit is contained in:
Daniel
2026-03-03 20:17:38 +08:00
parent 034c088bac
commit 09ec2e3a69
20 changed files with 395 additions and 19 deletions

View File

@@ -102,6 +102,18 @@ print('抓取:', n_fetched, '去重新增:', n_news, '面板写入:', n_panel)
有网络且有关键词命中时,应看到非零数字;再查 `curl -s http://localhost:3001/api/situation` 或前端事件脉络是否出现新数据。
**按时间范围测试(例如 2 月 28 日 0 时至今)**RSS 流水线支持只保留指定起始时间之后的条目,便于测试「从某日 0 点到现在」的数据。
```bash
# 默认从 2026-02-28 0:00 到现在
npm run crawler:once:range
# 或指定起始时间
./scripts/run-crawler-range.sh 2026-02-28T00:00:00
```
需设置环境变量 `CRAWL_START_DATE`ISO 时间,如 `2026-02-28T00:00:00`。GDELT 时间范围在启动 gdelt 服务时设置,例如:`GDELT_TIMESPAN=3d npm run gdelt`(最近 3 天)。
### 4. 仅测提取逻辑(不写库)
```bash
@@ -180,6 +192,69 @@ RSS → 抓取 → 清洗 → 去重 → 写 news_content / situation_update /
---
## 主要新闻资讯来源RSS
配置在 `crawler/config.py``RSS_FEEDS`,当前包含:
| 来源 | URL / 说明 |
|------|------------|
| **美国** | Reuters Top News、NYT World |
| **英国** | BBC World、BBC Middle East、The Guardian World |
| **法国** | France 24 |
| **德国** | DW World |
| **俄罗斯** | TASS、RT |
| **中国** | Xinhua World、CGTN World |
| **凤凰** | 凤凰军事、凤凰国际feedx.net 镜像) |
| **伊朗** | Press TV |
| **卡塔尔/中东** | Al Jazeera All、Al Jazeera Middle East |
单源超时由 `FEED_TIMEOUT`(默认 12 秒)控制;某源失败不影响其他源。
**过滤**:每条条目的标题+摘要必须命中 `config.KEYWORDS` 中至少一个关键词才会进入流水线(伊朗/美国/中东/军事/基地/霍尔木兹等,见 `config.KEYWORDS`)。
### 境内可访问情况(仅供参考,以实际网络为准)
| 通常境内可直接访问 | 说明 |
|-------------------|------|
| **新华网** `english.news.cn/rss/world.xml` | 中国官方外文社 |
| **CGTN** `cgtn.com/rss/world` | 中国国际台 |
| **凤凰** `feedx.net/rss/ifengmil.xml``ifengworld.xml` | 第三方 RSS 镜像,中文军事/国际 |
| **人民网** `people.com.cn/rss/military.xml``world.xml` | 军事、国际 |
| **新浪** `rss.sina.com.cn` 军事/新闻 | 新浪军事、新浪新闻滚动 |
| **中国日报** `chinadaily.com.cn/rss/world_rss.xml` | 国际新闻 |
| **中国军网** `english.chinamil.com.cn/rss.xml` | 解放军报英文 |
| **俄通社 TASS** `tass.com/rss/v2.xml` | 俄罗斯官媒 |
| **RT** `rt.com/rss/` | 俄罗斯今日俄罗斯 |
| **DW** `rss.dw.com/xml/rss-en-world` | 德国之声,部分地区/时段可访问 |
**境内常需代理**Reuters、NYT、BBC、Guardian、France 24、Al Jazeera、Press TV 等境外主站 RSS直连易超时或被墙。境内部署建议`CRAWLER_USE_PROXY=1` 并配置代理,或仅保留上表源(可在 `config.py` 中注释掉不可达的 URL减少超时等待
**国内其他媒体(今日头条、网易、腾讯、新浪微博等)**:今日头条、腾讯新闻、新浪微博等多为 App/信息流产品,**无官方公开 RSS**。如需接入可考虑:第三方 RSS 聚合(如 FeedX、RSSHub 等若有对应频道)、或平台开放 API若有且合规使用。当前爬虫已加入新浪rss.sina.com.cn、人民网、中国日报、中国军网等有明确 RSS 的境内源;网易新闻曾有 RSS 中心页,具体栏目 XML 需在其订阅页查找后加入 `config.py`
---
## 为什么爬虫一直抓不到有效信息0 条)
常见原因与应对如下。
| 原因 | 说明 | 建议 |
|------|------|------|
| **RSS 源在国内不可达** | 多数源为境外站Reuters、BBC、NYT、Guardian、France24、DW、TASS、RT、Al Jazeera、Press TV 等),国内直连易超时或被墙。 | 使用代理:设 `CRAWLER_USE_PROXY=1` 并配置系统/环境 HTTP(S) 代理,或部署到海外服务器再跑爬虫。 |
| **关键词无一命中** | 只有标题或摘要里包含 `KEYWORDS` 中至少一个词才会保留(如 iran、usa、middle east、strike、基地 等)。若当前头条都不涉及美伊/中东,整轮会 0 条。 | 先跑 `npm run crawler:test` 看是否 0 条;若长期为 0 且网络正常,可在 `config.py` 中适当放宽或增加 `KEYWORDS`(如增加通用词做测试)。 |
| **单源超时导致整轮无结果** | 若所有源都在 `FEED_TIMEOUT` 内未返回,则每源返回空列表,汇总仍为 0 条。 | 增大 `FEED_TIMEOUT`(如 20或先单独用浏览器/curl 测某条 RSS URL 是否可访问;国内建议代理后再试。 |
| **分类/清洗依赖 AI 且失败** | 每条命中关键词的条目会调 `classify_and_severity`Ollama 或 DashScope。若本机未起 Ollama、未设 DashScope且规则兜底异常可能影响该条。 | 设 `PARSER_AI_DISABLED=1` 使用纯规则分类,避免依赖 Ollama/DashScope或配置好 `DASHSCOPE_API_KEY` / 本地 Ollama 再跑。 |
| **去重后无新增** | 抓到的条数 >0但经 `news_content` 的 content_hash 去重后「新增」为 0则不会写 `situation_update`,事件脉络不增加。 | 属正常:同一批新闻再次抓取不会重复写入。等有新头条命中关键词后才会出现新条目。 |
**快速自检**
```bash
npm run crawler:test
```
输出「RSS 抓取: N 条」。若始终为 0优先检查网络/代理与 `KEYWORDS`;若 N>0 但面板无新事件,多为去重后无新增或未调 `POST /api/crawler/notify`
---
## 优化后验证效果示例
以下为「正文抓取 + AI 精确提取 + 增量与地点更新」优化后,单条新闻从输入到前端展示的完整示例,便于对照验证。

Binary file not shown.

View File

@@ -42,6 +42,13 @@ RSS_FEEDS = [
# 凤凰网(军事 + 国际,中文视角)
{"name": "凤凰军事", "url": "https://feedx.net/rss/ifengmil.xml"},
{"name": "凤凰国际", "url": "https://feedx.net/rss/ifengworld.xml"},
# 境内媒体(境内直连友好,可补中文视角)
{"name": "人民网军事", "url": "http://www.people.com.cn/rss/military.xml"},
{"name": "人民网国际", "url": "http://www.people.com.cn/rss/world.xml"},
{"name": "新浪军事", "url": "http://rss.sina.com.cn/rss/jczs/index.shtml"},
{"name": "新浪新闻", "url": "http://rss.sina.com.cn/rss/roll/news.xml"},
{"name": "中国日报国际", "url": "http://www.chinadaily.com.cn/rss/world_rss.xml"},
{"name": "中国军网", "url": "https://english.chinamil.com.cn/rss.xml"},
# 伊朗
"https://www.presstv.ir/rss",
# 卡塔尔(中东)

View File

@@ -109,6 +109,33 @@ def run_full_pipeline(
if not items:
return 0, 0, 0
# 可选:仅保留指定起始时间之后的条目(如 CRAWL_START_DATE=2026-02-28T00:00:00
start_date_env = os.environ.get("CRAWL_START_DATE", "").strip()
if start_date_env:
try:
raw = start_date_env.replace("Z", "+00:00").strip()
start_dt = datetime.fromisoformat(raw)
if start_dt.tzinfo is None:
start_dt = start_dt.replace(tzinfo=timezone.utc)
else:
start_dt = start_dt.astimezone(timezone.utc)
before = len(items)
items = [it for it in items if (it.get("published") or datetime.min.replace(tzinfo=timezone.utc)) >= start_dt]
if before > len(items):
print(f" [pipeline] 按 CRAWL_START_DATE={start_date_env} 过滤后保留 {len(items)} 条(原 {before} 条)")
except Exception as e:
print(f" [warn] CRAWL_START_DATE 解析失败,忽略: {e}")
if not items:
return 0, 0, 0
n_total = len(items)
print(f" [pipeline] 抓取 {n_total}")
for i, it in enumerate(items[:5]):
title = (it.get("title") or it.get("summary") or "").strip()[:60]
print(f" [{i + 1}] {title}" + ("" if len((it.get("title") or it.get("summary") or "")[:60]) >= 60 else ""))
if n_total > 5:
print(f" ... 共 {n_total}")
# 2. 清洗(标题/摘要/分类,符合面板 schema
if translate:
from translate_utils import translate_to_chinese
@@ -128,6 +155,11 @@ def run_full_pipeline(
# 3. 去重:落库 news_content仅新项返回
new_items, n_news = save_and_dedup(items, db_path=path)
if new_items:
print(f" [pipeline] 去重后新增 {n_news} 条,写入事件脉络 {len(new_items)}")
for i, it in enumerate(new_items[:3]):
title = (it.get("title") or it.get("summary") or "").strip()[:55]
print(f" 新增 [{i + 1}] {title}" + ("" if len((it.get("title") or it.get("summary") or "").strip()) > 55 else ""))
# 3.5 数据增强:为参与 AI 提取的条目抓取正文,便于从全文提取精确数据(伤亡、基地等)
if new_items:

View File

@@ -313,8 +313,10 @@ def fetch_news() -> None:
if GDELT_DISABLED:
_rss_to_gdelt_fallback()
_notify_node()
if n_fetched > 0:
print(f"[{datetime.now().strftime('%H:%M:%S')}] RSS 抓取 {n_fetched} 条,去重后新增 {n_news} 条资讯,面板 {n_panel}")
ts = datetime.now().strftime("%H:%M:%S")
print(f"[{ts}] RSS 抓取 {n_fetched} 条,去重后新增 {n_news} 条资讯,写入事件脉络 {n_panel}")
if n_fetched == 0:
print(f"[{ts}] 0 条检查网络、RSS 源或 KEYWORDS 过滤)")
except Exception as e:
LAST_FETCH["error"] = str(e)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 新闻抓取失败: {e}")
@@ -433,10 +435,8 @@ def _get_conflict_stats() -> dict:
@app.on_event("startup")
async def startup():
"""仅启动后台定时任务,不阻塞首次抓取,避免启动超时(验证脚本 /crawler/status 可尽快就绪)"""
global _bg_task
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, fetch_news)
await loop.run_in_executor(None, fetch_gdelt_events)
_bg_task = asyncio.create_task(_periodic_fetch())

51
crawler/run_once.py Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
单独运行爬虫一轮:抓取 → 清洗 → 去重 → 写库 → 通知 Node可选
终端直接输出抓取条数及内容摘要,便于排查。
用法(项目根或 crawler 目录):
python run_once.py
python -c "import run_once; run_once.main()"
或: npm run crawler:once
"""
import os
import sys
from datetime import datetime
# 保证可导入同目录模块
if __name__ == "__main__":
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def main():
from config import DB_PATH, API_BASE
from pipeline import run_full_pipeline
crawl_start = os.environ.get("CRAWL_START_DATE", "").strip()
print("========================================")
print("爬虫单次运行RSS → 清洗 → 去重 → 写库)")
print("DB:", DB_PATH)
print("API_BASE:", API_BASE)
if crawl_start:
print("时间范围: 仅保留 CRAWL_START_DATE 之后:", crawl_start)
print("========================================\n")
n_fetched, n_news, n_panel = run_full_pipeline(
db_path=DB_PATH,
api_base=API_BASE,
translate=True,
notify=True,
)
print("")
print("----------------------------------------")
print("本轮结果:")
print(f" 抓取: {n_fetched}")
print(f" 去重后新增资讯: {n_news}")
print(f" 写入事件脉络: {n_panel}")
if n_fetched == 0:
print(" 0 条检查网络、RSS 源或 config.KEYWORDS 过滤)")
print("----------------------------------------")
return 0
if __name__ == "__main__":
sys.exit(main())