343 lines
17 KiB
Markdown
343 lines
17 KiB
Markdown
# GDELT 实时冲突服务 + 新闻爬虫
|
||
|
||
## 数据来源梳理
|
||
|
||
### 1. GDELT Project (gdelt_events)
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| API | `https://api.gdeltproject.org/api/v2/doc/doc` |
|
||
| 查询 | `query=United States Iran military`(可配 `GDELT_QUERY`) |
|
||
| 模式 | `mode=ArtList`,`format=json`,`maxrecords=30` |
|
||
| 时间范围 | **未指定时默认最近 3 个月**,按相关性排序,易返回较旧文章 |
|
||
| 更新频率 | GDELT 约 15 分钟级,爬虫 60 秒拉一次 |
|
||
|
||
**数据偏老原因**:未传 `timespan` 和 `sort=datedesc`,API 返回 3 个月内“最相关”文章,不保证最新。
|
||
|
||
### 2. RSS 新闻 (situation_update) — 主事件脉络来源
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 源 | 多国主流媒体:美(Reuters/NYT)、英(BBC)、法(France 24)、俄(TASS/RT)、中(Xinhua/CGTN)、伊(Press TV)、卡塔尔(Al Jazeera) |
|
||
| 过滤 | 标题/摘要需含 `KEYWORDS` 之一(iran、usa、strike、military 等) |
|
||
| 更新 | 爬虫 45 秒拉一次(`RSS_INTERVAL_SEC`),优先保证事件脉络 |
|
||
| 优先级 | 启动时先拉 RSS,再拉 GDELT |
|
||
|
||
**GDELT 无法访问时**:设置 `GDELT_DISABLED=1`,仅用 RSS 新闻即可维持事件脉络。部分境外源可能受网络限制。
|
||
|
||
### 3. AI 新闻清洗与分类(可选)
|
||
|
||
- **清洗**:`cleaner_ai.py` 用 Ollama 提炼新闻为简洁摘要,供面板展示
|
||
- **分类**:`parser_ai.py` 用 Ollama 替代规则做 category/severity 判定
|
||
- 需先安装并运行 Ollama:`ollama run llama3.1`
|
||
- 环境变量:`OLLAMA_MODEL=llama3.1`、`PARSER_AI_DISABLED=1`、`CLEANER_AI_DISABLED=1`(禁用对应 AI)
|
||
|
||
---
|
||
|
||
**事件脉络可实时更新**:爬虫抓取后 → 写入 SQLite → 调用 Node 通知 → WebSocket 广播 → 前端自动刷新。
|
||
|
||
## 依赖
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
新增 `deep-translator`:GDELT 与 RSS 新闻入库前自动翻译为中文。
|
||
|
||
## 运行(需同时启动 3 个服务)
|
||
|
||
| 终端 | 命令 | 说明 |
|
||
|------|------|------|
|
||
| 1 | `npm run api` | Node API + WebSocket(必须) |
|
||
| 2 | `npm run gdelt` | GDELT + RSS 爬虫(**事件脉络数据来源**) |
|
||
| 3 | `npm run dev` | 前端开发 |
|
||
|
||
**事件脉络不更新时**:多半是未启动 `npm run gdelt`。只跑 `npm run api` 时,事件脉络会显示空或仅有缓存。
|
||
|
||
## 如何检查爬虫是否工作正常
|
||
|
||
按下面顺序做即可确认整条链路(爬虫 → 数据库 → Node 重载 → API/WebSocket)正常。
|
||
|
||
### 1. 一键验证(推荐)
|
||
|
||
先启动 API,再执行验证脚本(可选是否顺带启动爬虫):
|
||
|
||
```bash
|
||
# 终端 1:必须
|
||
npm run api
|
||
|
||
# 终端 2:执行验证(不启动爬虫,只检查当前状态)
|
||
./scripts/verify-pipeline.sh
|
||
|
||
# 或:顺带启动爬虫并等首次抓取后再验证
|
||
./scripts/verify-pipeline.sh --start-crawler
|
||
```
|
||
|
||
脚本会检查:API 健康、态势数据含 `lastUpdated`、爬虫服务是否可达、`news_content`/situation_update、战损字段、`POST /api/crawler/notify` 是否可用。
|
||
|
||
### 2. 手动快速检查
|
||
|
||
| 步骤 | 命令 / 操作 | 正常表现 |
|
||
|-----|-------------|----------|
|
||
| API 是否在跑 | `curl -s http://localhost:3001/api/health` | 返回 `{"ok":true}` |
|
||
| 态势是否可读 | `curl -s http://localhost:3001/api/situation \| head -c 300` | 含 `lastUpdated`、`usForces`、`recentUpdates` |
|
||
| RSS 能否抓到 | `npm run crawler:test` | 输出「RSS 抓取: N 条」,N>0 表示有命中 |
|
||
| 爬虫服务(gdelt) | `curl -s http://localhost:8000/crawler/status` | 返回 JSON,含 `db_path`/`db_exists` 等 |
|
||
| 库里有无爬虫数据 | `sqlite3 server/data.db "SELECT COUNT(*) FROM situation_update; SELECT COUNT(*) FROM news_content;"` 或访问 `http://localhost:3001/api/db/dashboard` | situation_update、news_content 条数 > 0(跑过流水线后) |
|
||
| 通知后是否重载 | 爬虫写库后会 POST `/api/crawler/notify`,Node 会 `reloadFromFile` 再广播 | 前端/`/api/situation` 的 `lastUpdated` 和内容会更新 |
|
||
|
||
### 3. 跑一轮流水线(不常驻爬虫时)
|
||
|
||
不启动 gdelt 时,可单次跑完整流水线(抓取 → 去重 → 写表 → notify):
|
||
|
||
```bash
|
||
npm run api # 保持运行
|
||
cd crawler && python3 -c "
|
||
from pipeline import run_full_pipeline
|
||
from config import DB_PATH, API_BASE
|
||
n_fetched, n_news, n_panel = run_full_pipeline(db_path=DB_PATH, api_base=API_BASE, notify=True)
|
||
print('抓取:', n_fetched, '去重新增:', n_news, '面板写入:', n_panel)
|
||
"
|
||
```
|
||
|
||
有网络且有关键词命中时,应看到非零数字;再查 `curl -s http://localhost:3001/api/situation` 或前端事件脉络是否出现新数据。
|
||
|
||
### 4. 仅测提取逻辑(不写库)
|
||
|
||
```bash
|
||
npm run crawler:test:extraction # 规则/db_merge 测试
|
||
# 或按 README「快速自测命令」用示例文本调 extract_from_news 看 combat_losses_delta / key_location_updates
|
||
```
|
||
|
||
**常见现象**:抓取 0 条 → 网络/RSS 被墙或关键词未命中;situation_update 为空 → 未跑流水线或去重后无新增;前端不刷新 → 未开 `npm run api` 或未开爬虫(gdelt)。
|
||
|
||
### 5. 爬虫与面板是否联通
|
||
|
||
专门检查「爬虫写库」与「面板展示」是否一致:
|
||
|
||
```bash
|
||
./scripts/check-crawler-panel-connectivity.sh
|
||
```
|
||
|
||
会对比:爬虫侧的 `situation_update` 条数 vs 面板 API 返回的 `recentUpdates` 条数,并说明为何战损/基地等不一定随每条新闻变化。
|
||
|
||
## 爬虫与面板数据联动说明
|
||
|
||
| 面板展示 | 数据来源(表/接口) | 是否由爬虫更新 | 说明 |
|
||
|----------|---------------------|----------------|------|
|
||
| **事件脉络** (recentUpdates) | situation_update → getSituation() | ✅ 是 | 每条去重后的新闻会写入 situation_update,Node 收到 notify 后重载 DB 再广播 |
|
||
| **地图冲突点** (conflictEvents) | gdelt_events 或 RSS→gdelt 回填 | ✅ 是 | GDELT 或 GDELT 禁用时由 situation_update 同步到 gdelt_events |
|
||
| **战损/装备毁伤** (combatLosses) | combat_losses | ⚠️ 有条件 | 仅当 AI/规则从新闻中提取到数字(如「2 名美军死亡」)时,merge 才写入增量 |
|
||
| **基地/地点状态** (keyLocations) | key_location | ⚠️ 有条件 | 仅当提取到 key_location_updates(如某基地遭袭)时更新 |
|
||
| **力量摘要/指数/资产** (summary, powerIndex, assets) | force_summary, power_index, force_asset | ❌ 否 | 仅 seed 初始化,爬虫不写 |
|
||
| **华尔街/报复情绪** (wallStreet, retaliation) | wall_street_trend, retaliation_* | ⚠️ 有条件 | 仅当提取器输出对应字段时更新 |
|
||
|
||
因此:**新闻很多、但战损/基地数字不动**是正常现象——多数标题不含可解析的伤亡/基地数字,只有事件脉络(recentUpdates)和地图冲突点会随每条新闻增加。若**事件脉络也不更新**,请确认 Node 终端在爬虫每轮抓取后是否出现 `[crawler/notify] DB 已重载`;若无,检查爬虫的 `API_BASE` 是否指向当前 API(默认 `http://localhost:3001`)。
|
||
|
||
## 写库流水线(与 server/README 第五节一致)
|
||
|
||
RSS 与主入口均走统一流水线 `pipeline.run_full_pipeline`:
|
||
|
||
1. **抓取** → 2. **AI 清洗**(标题/摘要/分类)→ 3. **去重**(news_content.content_hash)→ 4. **映射到前端库字段**(situation_update、combat_losses、key_location 等)→ 5. **更新表** → 6. **有新增时 POST /api/crawler/notify**
|
||
|
||
- `npm run crawler`(main.py)与 `npm run gdelt`(realtime_conflict_service)的 RSS 分支都调用该流水线。
|
||
- 实现见 `crawler/pipeline.py`。
|
||
|
||
## 数据流
|
||
|
||
```
|
||
GDELT API → 抓取(60s) → SQLite (gdelt_events, conflict_stats) → POST /api/crawler/notify
|
||
RSS → 抓取 → 清洗 → 去重 → 写 news_content / situation_update / 战损等 → POST /api/crawler/notify
|
||
↓
|
||
Node 更新 situation.updated_at + WebSocket 广播
|
||
↓
|
||
前端实时展示
|
||
```
|
||
|
||
## 配置
|
||
|
||
环境变量:
|
||
|
||
- `DB_PATH`: SQLite 路径,默认 `../server/data.db`
|
||
- `API_BASE`: Node API 地址,默认 `http://localhost:3001`
|
||
- **`DASHSCOPE_API_KEY`**:阿里云通义(DashScope)API Key。**设置后全程使用商业模型,无需本机安装 Ollama**(适合 Mac 版本较低无法跑 Ollama 的情况)。获取: [阿里云百炼 / DashScope](https://dashscope.console.aliyun.com/) → 创建 API-KEY,复制到环境变量或项目根目录 `.env` 中 `DASHSCOPE_API_KEY=sk-xxx`。摘要、分类、战损/基地提取均走通义。
|
||
- `GDELT_QUERY`: 搜索关键词,默认 `United States Iran military`
|
||
- `GDELT_MAX_RECORDS`: 最大条数,默认 30
|
||
- `GDELT_TIMESPAN`: 时间范围,`1h` / `1d` / `1week`,默认 `1d`(近日资讯)
|
||
- `GDELT_DISABLED`: 设为 `1` 则跳过 GDELT,仅用 RSS 新闻(GDELT 无法访问时用)
|
||
- `FETCH_INTERVAL_SEC`: GDELT 抓取间隔(秒),默认 60
|
||
- `RSS_INTERVAL_SEC`: RSS 抓取间隔(秒),默认 45(优先保证事件脉络)
|
||
- `OLLAMA_MODEL`: AI 分类模型,默认 `llama3.1`
|
||
- `PARSER_AI_DISABLED`: 设为 `1` 则禁用 AI 分类,仅用规则
|
||
- `CLEANER_AI_DISABLED`: 设为 `1` 则禁用 AI 清洗,仅用规则截断
|
||
- `FETCH_FULL_ARTICLE`: 设为 `0` 则不再抓取正文,仅用标题+摘要做 AI 提取(默认 `1` 抓取正文)
|
||
- `ARTICLE_FETCH_LIMIT`: 每轮为多少条新资讯抓取正文,默认 10
|
||
- `ARTICLE_FETCH_TIMEOUT`: 单篇正文请求超时(秒),默认 12
|
||
- `ARTICLE_MAX_BODY_CHARS`: 正文最大字符数,默认 6000
|
||
- `EXTRACT_TEXT_MAX_LEN`: 送入 AI 提取的原文最大长度,默认 4000
|
||
|
||
**增量与地点**:战损一律按**增量**处理——AI 只填本则报道的「本次/此次」新增数,不填累计总数;合并时与库内当前值叠加。双方攻击地点通过 `key_location_updates` 更新(美军基地被打击 side=us,伊朗设施被打击 side=iran),会写入 `key_location` 的 status/damage_level。
|
||
|
||
---
|
||
|
||
## 优化后验证效果示例
|
||
|
||
以下为「正文抓取 + AI 精确提取 + 增量与地点更新」优化后,单条新闻从输入到前端展示的完整示例,便于对照验证。
|
||
|
||
### 1. 示例输入(新闻摘要/全文片段)
|
||
|
||
```
|
||
伊朗向伊拉克阿萨德空军基地发射 12 枚弹道导弹,造成此次袭击中 2 名美军人员死亡、14 人受伤,
|
||
另有 1 架战机在跑道受损。乌代德基地未遭直接命中。同日以色列对伊朗伊斯法罕一处设施发动打击。
|
||
```
|
||
|
||
### 2. AI 提取输出(增量 + 攻击地点)
|
||
|
||
```json
|
||
{
|
||
"summary": "伊朗导弹袭击伊拉克阿萨德基地致美军 2 死 14 伤,1 架战机受损;以军打击伊斯法罕。",
|
||
"category": "alert",
|
||
"severity": "high",
|
||
"us_personnel_killed": 2,
|
||
"us_personnel_wounded": 14,
|
||
"us_aircraft": 1,
|
||
"us_bases_damaged": 1,
|
||
"key_location_updates": [
|
||
{ "name_keywords": "阿萨德|asad|al-asad", "side": "us", "status": "attacked", "damage_level": 2 },
|
||
{ "name_keywords": "伊斯法罕|isfahan", "side": "iran", "status": "attacked", "damage_level": 1 }
|
||
]
|
||
}
|
||
```
|
||
|
||
说明:战损为**本则报道的新增数**(此次 2 死、14 伤、1 架战机),不是累计总数;地点为双方遭袭设施(美军基地 side=us,伊朗设施 side=iran)。
|
||
|
||
### 3. 合并后数据库变化
|
||
|
||
| 表/字段 | 合并前 | 本则增量 | 合并后 |
|
||
|--------|--------|----------|--------|
|
||
| combat_losses.us.personnel_killed | 127 | +2 | 129 |
|
||
| combat_losses.us.personnel_wounded | 384 | +14 | 398 |
|
||
| combat_losses.us.aircraft | 2 | +1 | 3 |
|
||
| combat_losses.us.bases_damaged | 27 | +1 | 28 |
|
||
| key_location(name 含「阿萨德」) | status=operational | — | status=attacked, damage_level=2 |
|
||
| key_location(name 含「伊斯法罕」) | status=operational | — | status=attacked, damage_level=1 |
|
||
|
||
若 AI 误提「累计 2847 人丧生」并填成 personnel_killed=2847,单次合并会被上限截断(如最多 +500),避免一次写入导致数据剧增。
|
||
|
||
### 4. 前端验证效果
|
||
|
||
- **事件脉络**:出现一条新条目,summary 为上述 1–2 句概括,category=alert、severity=high。
|
||
- **装备毁伤面板**:美军「阵亡」+2、「受伤」+14、「战机」+1;基地毁/损数字随 bases_damaged +1 更新。
|
||
- **地图**:阿萨德基地、伊斯法罕对应点位显示为「遭袭」状态(脉冲/标色随现有地图逻辑)。
|
||
- **API**:`GET /api/situation` 中 `usForces.combatLosses`、`usForces.keyLocations`(含 status/damage_level)为更新后值;`lastUpdated` 为合并后时间。
|
||
|
||
### 5. 快速自测命令
|
||
|
||
```bash
|
||
# 仅测提取逻辑(不写库):用示例文本调 AI 提取,看是否得到增量 + key_location_updates
|
||
cd crawler && python3 -c "
|
||
from extractor_ai import extract_from_news
|
||
text = '''伊朗向伊拉克阿萨德空军基地发射导弹,此次袭击造成 2 名美军死亡、14 人受伤,1 架战机受损。'''
|
||
out = extract_from_news(text)
|
||
print('combat_losses_delta:', out.get('combat_losses_delta'))
|
||
print('key_location_updates:', out.get('key_location_updates'))
|
||
"
|
||
```
|
||
|
||
期望:`combat_losses_delta.us` 含 personnel_killed=2、personnel_wounded=14、aircraft=1 等增量;`key_location_updates` 含阿萨德 side=us 等条目。
|
||
|
||
---
|
||
|
||
## 冲突强度 (impact_score)
|
||
|
||
| 分数 | 地图效果 |
|
||
|------|------------|
|
||
| 1–3 | 绿色点 |
|
||
| 4–6 | 橙色闪烁 |
|
||
| 7–10 | 红色脉冲扩散 |
|
||
|
||
## API
|
||
|
||
- `GET http://localhost:8000/events`:返回事件列表与冲突统计(Python 服务直连)
|
||
- `GET http://localhost:3001/api/events`:从 Node 读取(推荐,含 WebSocket 同步)
|
||
|
||
## 本地验证链路
|
||
|
||
按下面任选一种方式,确认「抓取 → 清洗 → 去重 → 映射 → 写表 → 通知」整条链路正常。
|
||
|
||
### 方式一:最小验证(不启动前端)
|
||
|
||
1. **启动 API(必须)**
|
||
```bash
|
||
npm run api
|
||
```
|
||
保持运行,默认 `http://localhost:3001`。
|
||
|
||
2. **安装爬虫依赖并跑一轮流水线**
|
||
```bash
|
||
cd crawler && pip install -r requirements.txt
|
||
python -c "
|
||
from pipeline import run_full_pipeline
|
||
from config import DB_PATH, API_BASE
|
||
n_fetched, n_news, n_panel = run_full_pipeline(db_path=DB_PATH, api_base=API_BASE, translate=True, notify=True)
|
||
print('抓取:', n_fetched, '去重新增:', n_news, '面板写入:', n_panel)
|
||
"
|
||
```
|
||
- 有网络且有关键词命中时,应看到非零数字;无网络或全被过滤则为 `0 0 0`。
|
||
- 若报错 `module 'socket' has no attribute 'settimeout'`,已修复为 `setdefaulttimeout`,请拉取最新代码。
|
||
|
||
3. **查库确认**
|
||
```bash
|
||
sqlite3 server/data.db "SELECT COUNT(*) FROM situation_update; SELECT COUNT(*) FROM news_content;"
|
||
```
|
||
或浏览器打开 `http://localhost:3001/api/db/dashboard`,看 `situation_update`、`news_content` 是否有数据。
|
||
|
||
4. **确认态势接口**
|
||
```bash
|
||
curl -s http://localhost:3001/api/situation | head -c 500
|
||
```
|
||
应包含 `lastUpdated`、`recentUpdates` 等。
|
||
|
||
### 方式二:用现有验证脚本(推荐)
|
||
|
||
1. 终端 1:`npm run api`
|
||
2. 终端 2(可选):`npm run gdelt`(会定时跑 RSS + GDELT)
|
||
3. 执行验证脚本:
|
||
```bash
|
||
./scripts/verify-pipeline.sh
|
||
```
|
||
若爬虫未启动想一并测爬虫,可:
|
||
```bash
|
||
./scripts/verify-pipeline.sh --start-crawler
|
||
```
|
||
脚本会检查:API 健康、态势数据、爬虫状态、资讯表、战损字段、通知接口。
|
||
|
||
### 方式三:只测 RSS 抓取(不写库)
|
||
|
||
```bash
|
||
npm run crawler:test
|
||
```
|
||
输出为「RSS 抓取: N 条」。0 条时检查网络或 `config.py` 里 `RSS_FEEDS` / `KEYWORDS`。
|
||
|
||
### 常见问题
|
||
|
||
| 现象 | 可能原因 |
|
||
|------|----------|
|
||
| 抓取 0 条 | 网络不通、RSS 被墙、关键词无一命中 |
|
||
| `situation_update` 为空 | 去重后无新增,或未跑流水线(只跑了 `fetch_all` 未跑 `run_full_pipeline`) |
|
||
| 前端事件脉络不刷新 | 未启动 `npm run api` 或 WebSocket 未连上(需通过 Vite 代理访问前端) |
|
||
| 翻译/AI 清洗很慢或报错 | 设 `TRANSLATE_DISABLED=1` 或 `CLEANER_AI_DISABLED=1` 可跳过,用规则兜底 |
|
||
|
||
---
|
||
|
||
## 故障排查
|
||
|
||
| 现象 | 可能原因 | 排查 |
|
||
|------|----------|------|
|
||
| 事件脉络始终为空 | 未启动 GDELT 爬虫 | 另开终端运行 `npm run gdelt`,观察是否有 `GDELT 更新 X 条事件` 输出 |
|
||
| 事件脉络不刷新 | WebSocket 未连上 | 确认 `npm run api` 已启动,前端需通过 `npm run dev` 访问(Vite 会代理 /ws) |
|
||
| GDELT 抓取失败 | 系统代理超时 / ProxyError | 爬虫默认直连,不走代理;若需代理请设 `CRAWLER_USE_PROXY=1` |
|
||
| GDELT 抓取失败 | 网络 / GDELT API 限流 | 检查 Python 终端报错;GDELT 在国外,国内网络可能较慢或超时 |
|
||
| 新闻条数为 0 | RSS 源被墙或关键词不匹配 | 检查 crawler/config.py 中 RSS_FEEDS、KEYWORDS;国内需代理 |
|
||
| **返回数据偏老** | GDELT 默认 3 个月内按相关性 | 设置 `GDELT_TIMESPAN=1d` 限制为近日;加 `sort=datedesc` 最新优先 |
|