- 新增 report_db 包: MySQL 连接/建表/幂等写入 (news_report/news_event, myquant 库) - 新增 report_import 包: 历史 178 份日报 HTML 解析入库, 表头驱动列映射 - reporter.py 完全切换: generate_report 结构化入库, 不再生成/上传 HTML - CLI: 新增 report-import 子命令 - 依赖: uv add pymysql; 配置: NEWS_DB_* / REPORT_HISTORY_DIR - 文档: docs/report_db_design.md(实现逻辑), docs/db_schema.md(表结构供 API/前端) - 测试: 24 个单测通过 (parser/builder/models/importer)
261 lines
9.1 KiB
Markdown
261 lines
9.1 KiB
Markdown
# continuation.md
|
||
|
||
> `checkpoint` @ 2026-08-03 21:30
|
||
|
||
---
|
||
|
||
## 当前状态
|
||
|
||
| 项目 | 值 |
|
||
| --- | --- |
|
||
| 新闻源 | 14 个(13 Web + 1 API: xwlb 新闻联播) |
|
||
| Qdrant | 本地文件模式 `data/qdrant_storage/` |
|
||
| 日报 | **M10 完成: 结构化入库 MySQL(myquant 库 news_report/news_event 表),不再生成 HTML** |
|
||
| 调度器 | APScheduler,systemd `a-share-research.service` |
|
||
| LLM | `deepseek-v4-flash`(绝不允许擅自修改) |
|
||
| 服务器 | `pi@192.168.1.160`,项目 `/home/pi/news/` |
|
||
| 抓取方式 | js_render=false → httpx 直连;js_render=true → Playwright |
|
||
|
||
---
|
||
|
||
## 本次完成 (2026-08-03) — M10 日报结构化入库
|
||
|
||
**目标:** 日报前后端分离的数据层——日报内容结构化存入 MySQL(`news_` 前缀表),历史 178 份日报 HTML 解析入库;本项目不做 API/前端(用户另行实现)。
|
||
|
||
**表结构(myquant 库,见 docs/db_schema.md):**
|
||
- `news_report`:主表,唯一键 (report_date, report_type, file_name);`file_name=''` 表示新生成日报(每天每类型一行,重复生成覆盖)
|
||
- `news_event`:事件明细,section ∈ xwlb/news/cninfo/intl
|
||
|
||
**新增/修改文件:**
|
||
- `report_db/`(models/schema/db):连接、建表、幂等 save_report
|
||
- `report_import/`(parser/importer):历史 HTML 解析(表头驱动列映射)+ 批量导入
|
||
- `scheduler/reporter.py`:`generate_report()` 完全切换为结构化入库;`_render_html/_upload` 标记废弃保留
|
||
- `a_share_cli/main.py`:新增 `report-import` 子命令;`report` 命令改为入库提示
|
||
- `pyproject.toml`:`uv add pymysql`(唯一新增依赖)
|
||
- `.env`:新增 `NEWS_DB_*`(连接 127.0.0.1:13306,需 ssh 隧道)、`REPORT_HISTORY_DIR`
|
||
- `docs/report_db_design.md`(实现逻辑)、`docs/db_schema.md`(表结构,供 API/前端)
|
||
|
||
**验证结果:**
|
||
- 历史 178 份全部入库:scanned=178 imported=7(首轮)+170 skipped=171 failed=0;DB 177 行(finance 49 + intl 128,1 个跨目录同名文件被幂等合并)+ 事件 4222 条
|
||
- 端到端:`a-share report --date 20260616` → report_id=180 入库成功,无 HTML 产出
|
||
- 单测 24 个通过(parser 19 + builder 4 + models/import 补充)
|
||
|
||
**命令:**
|
||
```bash
|
||
ssh -L 13306:127.0.0.1:13306 pi # Mac 开发连库隧道
|
||
uv run a-share report --date YYYYMMDD # 生成日报入库
|
||
uv run a-share report-import # 历史导入(幂等)
|
||
```
|
||
|
||
**注意:** 本机 .env 无 LLM API key,AI 摘要会降级 WARNING(不影响入库);生产 pi5 需配置 NEWS_DB_PASSWORD 且解决 13306 隧道可达性(见待确认项)。
|
||
|
||
---
|
||
|
||
## 历史 (2026-07-17) — 禁用个股日报
|
||
|
||
**操作:** `STOCK_REPORT_TIME=` 设为空,`run_scheduler.py` 加空值守卫。
|
||
|
||
**文件:**
|
||
- `scripts/run_scheduler.py` L127-141:`STOCK_REPORT_TIME` 为空时跳过注册并输出 `已禁用个股日报`
|
||
- Pi `.env`:`STOCK_REPORT_TIME=08:00` → `STOCK_REPORT_TIME=`
|
||
|
||
**恢复方法:** 改回 `STOCK_REPORT_TIME=08:00`,重启 `a-share-research`。
|
||
|
||
---
|
||
|
||
## 本次完成 (2026-07-13) — eeo 反爬修复 + 静态直连 + 超时保护
|
||
|
||
### 问题
|
||
|
||
eeo(经济观察网)在 Pi 上用 Playwright 连续抓取后触发反爬,服务器故意不响应导致 `page.goto()` 超时(30s → 60s 均超时)。而在本机 Mac 上一切正常——反爬是 **IP 级别**的,Pi 的 IP 已被限流。
|
||
|
||
### 根因
|
||
|
||
eeo 是纯静态页面(`js_render: false`),但 Crawl4AI 始终走 Playwright。eeo 检测到 headless Chrome 的连续请求模式后,对后续请求挂起 TCP 连接直至超时。
|
||
|
||
### 修复
|
||
|
||
**`crawler/engine.py` — 三个改动:**
|
||
|
||
1. **新增 `_fetch_static()`**:`js_render=False` 且无 `wait_for` 的源用 `httpx` 直连,绕过 Playwright 反爬。HTML → Markdown 用 `markdownify` + BeautifulSoup 清洗。
|
||
|
||
2. **新增 `_TimeoutGuard`**:同源连续超时 2 次后自动跳过剩余请求,防止整个 pipeline 被单一故障源拖死。
|
||
|
||
3. **`_crawl_once` 分流逻辑**:
|
||
```
|
||
js_render=False 且无 wait_for → _fetch_static() (httpx)
|
||
其他 → Playwright
|
||
```
|
||
|
||
**`configs/sources.yaml` — eeo 源两处调整:**
|
||
- 删除 `wait_for: "css:body"`(静态页面无需等待 CSS 选择器)
|
||
- `page_timeout_ms: 30000 → 60000`(恢复 Crawl4AI 默认值)
|
||
|
||
### 测试结果
|
||
|
||
| 环境 | Playwright (修复前) | httpx (修复后) |
|
||
|------|---------------------|-----------------|
|
||
| Mac 本地 | 全部通过 | 36 篇 / 7 秒 |
|
||
| Pi 服务器 | **前 3 个请求全部 15s 超时** | 36 篇 / 9 秒,0 失败 |
|
||
|
||
### 验证方法
|
||
|
||
Pi 上本地复现了反爬(Playwright 前 3 个 URL 全部超时),同批 URL 用 httpx 全部瞬间返回——证明方案有效。
|
||
|
||
---
|
||
|
||
## 历史 (2026-06-24)
|
||
|
||
### 1. 环境变量全面审计与修复
|
||
|
||
**触发:** 服务器 `dedup` 步骤超时 120s。
|
||
|
||
**修复 6 个文件:**
|
||
|
||
| # | 文件 | 修改 |
|
||
|---|------|------|
|
||
| 1 | `scheduler/pipeline.py` | 超时解析重写:`TIMEOUT_{NAME}` > `PIPELINE_STEP_TIMEOUT` > 硬编码 > 1800s |
|
||
| 2 | `a_share_cli/main.py` | 新增 `_resolve_timeout()`,CLI 命令超时改为读环境变量 |
|
||
| 3 | `crawler/cninfo.py` | `CNINFO_PDF_BASE` 从硬编码改为 `os.environ.get()` |
|
||
| 4 | `scheduler/reporter.py` | `STOCK_REPORT_DAYS` 默认值 7→15;`max_tokens` 600→1500 |
|
||
| 5 | `.env` | 清除 10 个死配置;`EMBEDDING_MODEL`→`LOCAL_EMBEDDING_MODEL`;补全遗漏参数 |
|
||
| 6 | `.env.example` | 重写为仅包含实际生效配置 |
|
||
|
||
**审计结果:**
|
||
|
||
| 类别 | 数量 | 处理 |
|
||
|------|------|------|
|
||
| ✅ 正常工作 | 23 | — |
|
||
| ❌ 死配置 | 10 | 已清除 |
|
||
| ⚠️ 默认值不一致 | 1 | 已统一 |
|
||
| 🔧 硬编码 | 1 | 已修复 |
|
||
|
||
### 2. AI 摘要截断修复
|
||
|
||
**触发:** 日报 AI 摘要内容极少(137 字),末尾被截断。
|
||
|
||
**根因:** `_llm_call()` `max_tokens=600` 太低,deepseek-v4-flash 输出被硬截断。
|
||
|
||
**修复:** `max_tokens` 三处提升 + 截断检测日志。
|
||
|
||
| 位置 | 修复前 | 修复后 |
|
||
|------|--------|--------|
|
||
| `_llm_call()` 默认 | 600 | 1500 |
|
||
| 分块摘要 | 400 | 800 |
|
||
| 合并摘要 | 600 | 1500 |
|
||
|
||
**验证:** 手动生成日报,摘要从 137 字 → 486 字,结构完整无截断。
|
||
|
||
---
|
||
|
||
## 超时优先级(最终设计)
|
||
|
||
```
|
||
TIMEOUT_{NAME} 环境变量 ← 单步精确控制
|
||
↓ 未设
|
||
PIPELINE_STEP_TIMEOUT 环境变量 ← 全局兜底 (覆盖所有硬编码)
|
||
↓ 未设
|
||
STEP_TIMEOUTS 硬编码字典 ← 代码内默认值
|
||
↓ 步骤不在字典中
|
||
1800s ← 最终兜底
|
||
```
|
||
|
||
---
|
||
|
||
## 服务器 .env 当前配置
|
||
|
||
```env
|
||
# ---- 调度 ----
|
||
SCHEDULE_TIMES=07:00,12:00,18:00,22:00
|
||
CNINFO_SCHEDULE_TIME=06:00
|
||
STOCK_REPORT_TIME=08:00
|
||
STOCK_REPORT_DAYS=15
|
||
|
||
# ---- Pipeline 超时 ----
|
||
PIPELINE_STEP_TIMEOUT=3600
|
||
TIMEOUT_CRAWLER=900
|
||
TIMEOUT_XWLB=60
|
||
TIMEOUT_EXTRACTOR=300
|
||
TIMEOUT_DEDUP=900
|
||
TIMEOUT_LLM=900
|
||
TIMEOUT_EMBEDDING=900
|
||
TIMEOUT_QDRANT=1800
|
||
TIMEOUT_CNINFO_CRAWL=900
|
||
TIMEOUT_CNINFO_EXTRACT=900
|
||
TIMEOUT_CNINFO_PDF=900
|
||
```
|
||
|
||
---
|
||
|
||
## 调度器
|
||
|
||
| 时间 | 步骤 |
|
||
|------|------|
|
||
| 06:00 | cninfo 公告管道 |
|
||
| 07:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant→**日报** |
|
||
| 08:00 | 个股日报 |
|
||
| 12:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
|
||
| 18:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
|
||
| 22:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
|
||
|
||
---
|
||
|
||
## 常用命令
|
||
|
||
```bash
|
||
# 全链路 + 日报
|
||
uv run a-share pipeline --once --report
|
||
|
||
# 仅日报 (指定日期)
|
||
/home/pi/.local/bin/uv run python -m scripts.run_scheduler --once --date 20260623 --steps report
|
||
|
||
# 检索
|
||
uv run a-share search "关键词" --top 5
|
||
|
||
# 同步到服务器
|
||
scp <file> pi@192.168.1.160:/home/pi/news/<path>/
|
||
|
||
# 重启服务
|
||
ssh pi@192.168.1.160 "sudo systemctl restart a-share-research"
|
||
```
|
||
|
||
---
|
||
|
||
## 已知技术债务
|
||
|
||
| 问题 | 文件 | 状态 |
|
||
|------|------|------|
|
||
| QdrantClient HTTP 超时硬编码 10s | `vectorstore/client.py:93` | 无可配环境变量 |
|
||
| DashScope HTTP 超时硬编码 60s | `embedding/remote.py:81` | 无可配环境变量 |
|
||
| 超时解析逻辑在 pipeline.py 和 main.py 各一份 | 两处 | 可抽取公共函数 |
|
||
| `deepseek-v4-flash` 概率性返回空 | — | 分块处理 + 失败跳过 |
|
||
| wallstreetcn JS 渲染拦截 | — | 正文提取率 0% |
|
||
| eeo 列表页仍走 Playwright(仅文章页走 httpx) | `crawler/engine.py` | 列表页目前正常,暂不改动 |
|
||
|
||
---
|
||
|
||
## 架构备忘:静态/动态抓取分流
|
||
|
||
```
|
||
SourceConfig.js_render == False 且 wait_for == None
|
||
→ _fetch_static() → httpx 直连 (无浏览器, 快, 不被反爬)
|
||
→ markdownify + BeautifulSoup (清洗 script/style 后转 Markdown)
|
||
|
||
SourceConfig.js_render == True 或 wait_for 有值
|
||
→ _crawl_once() → Playwright (现有逻辑)
|
||
```
|
||
|
||
`_TimeoutGuard`:
|
||
- 同源连续超时 2 次 → `skip=True`
|
||
- 后续请求在 `semaphore` 内检查 `guard.skip`,立即返回 `Skipped`
|
||
- 一次成功即重置计数器
|
||
|
||
---
|
||
|
||
## 记忆
|
||
|
||
- `never-change-llm-model.md` — 绝不允许修改大模型配置
|
||
- `deploy-html.md` — user_guide.html 同步到 Pi 和 Web 服务器
|
||
- `sync-sources-yaml.md` — 修改 sources.yaml 前从 Pi 拉取,改完推回
|
||
- `update-user-guide.md` — 更新 md+html+部署
|