Files
news/README.md
T
simon 366e60e8a9 feat: 日报结构化入库(M10 前后端分离数据层)
- 新增 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)
2026-08-03 21:32:07 +08:00

15 KiB

A 股 Deep Research 私有投研平台

面向 A 股投资研究的私有化 Deep Research 平台。

自动抓取财经新闻 → 中文正文提取 → LLM 投资事件抽取 → 向量化 → Qdrant 知识库 → MCP/Agent 深度研究。

项目定位:A 股研究辅助与知识管理平台,自动交易、预测、投资顾问。


技术栈

模块 选型
抓取 Crawl4AI(异步 + JS 渲染)
正文提取 GNE(中文新闻正文识别)
LLM DeepSeek / Qwen(百炼)
Embedding BGE-M3(本地) / Qwen Embedding(远程)
向量库 Qdrant
调度 APScheduler
服务化 MCP
运行时 Python 3.11 + uv
部署 Docker Compose

当前状态

已完成 M0~M8、M9 投研 Agent Prompt、M10 日报结构化入库
进行中 等待 M10 验收
下一步 项目整合与优化

详见 continuation.md。开发遵循 project_plan.md 9 个 Milestone,分阶段交付。


Qdrant 部署模式说明

本项目 Qdrant 支持两种运行模式,默认本地文件模式(零依赖)

模式一:本地文件模式(默认,推荐)

类比:SQLite——无独立进程,Python 进程内嵌加载存储引擎
数据:data/qdrant_storage/ (纯文件,跟随项目目录)
进程:无守护进程
端口:不监听任何端口
并发:单进程串行读写(批处理场景足够)

优点:无需 Docker、无需手动安装、无需 root、树莓派 ARM64 兼容。

启动方式:代码中调用即自动加载,无需额外步骤。

from vectorstore import make_qdrant_client, VectorStore
c = make_qdrant_client()            # 默认 path="data/qdrant_storage"
s = VectorStore(c)
# ... 读写操作 ...
s.close()

模式二:Docker Server 模式(可选,x86 推荐)

类比:PostgreSQL——独立守护进程,通过网络端口访问
数据:Docker volume qdrant_storage/
进程:容器 a_share_qdrant
端口:6333 (REST) / 6334 (gRPC)
并发:多客户端网络访问

适用场景:Cherry Studio 远程连接、多机共享知识库、生产环境。

# 启动 Qdrant 服务
docker compose --profile m6 up -d

# 代码中指定 host
c = make_qdrant_client(host="192.168.1.160", port=6333)

注意:树莓派 5 ARM64 内核使用 16KB 内存页,Qdrant 官方 Docker 镜像内置的 jemalloc 仅支持 4KB 页,在树莓派上会启动即崩溃(exit 134)。树莓派请使用本地文件模式。


快速开始

统一 CLI

所有操作通过 a-share 命令完成,替代之前的 8 个脚本入口:

# 模块操作
uv run a-share crawl                     # M1 抓取
uv run a-share crawl --source cls        # 单源
uv run a-share extract --date 20260616   # M2 提取
uv run a-share dedup --date 20260616     # M3 去重
uv run a-share events --date 20260616    # M4 LLM 抽取
uv run a-share events --provider qwen    # 切换 LLM
uv run a-share embed --date 20260616     # M5 向量化
uv run a-share ingest --date 20260616    # M6 入库

# 全链路
uv run a-share pipeline --once           # 立即执行全链路

# 检索(直接查 Qdrant,无需写代码或配 Cherry Studio)
uv run a-share search "宁德时代固态电池"
uv run a-share search "政策" --source cls --sentiment positive
uv run a-share search "风险" --stock 001212 --min-importance 3

# 日报(结构化入库, 不再生成 HTML; API/前端另行实现, 读取 MySQL news_ 表)
uv run a-share report                     # 生成今日日报并入 myquant 库
uv run a-share report --date 20260616     # 指定日期
uv run a-share report-import              # 历史日报 HTML 解析入库(幂等)
uv run a-share pipeline --once --report   # 全链路末尾自动生成日报

# 状态总览
uv run a-share status

环境准备

# 1. 安装 uv (macOS)
brew install uv

# 2. 创建虚拟环境并同步依赖
uv sync

# 3. 复制环境变量模板
cp .env.example .env
# 编辑 .env 填写 API Key 等敏感配置

# 4. 验证 Python 版本(应为 3.11.x)
uv run python --version

# 5. 首次安装浏览器(Crawl4AI 依赖 Playwright Chromium)
uv run python -m playwright install chromium

运行抓取(M1)

# 抓取全部启用源
uv run python -m scripts.run_crawler

# 只抓单个源
uv run python -m scripts.run_crawler --source cls

# 只试跑不写文件
uv run python -m scripts.run_crawler --no-save

# 调试日志
uv run python -m scripts.run_crawler --log-level DEBUG

抓取产物路径:data/raw/{source_id}/{YYYYMMDD}/

  • {url_hash}.html 原始 HTML
  • {url_hash}.md Crawl4AI 输出的 Markdown
  • index.jsonl 元数据(每行一条 CrawlResult,不含正文大字段)

日志:logs/crawler.log(自动轮转 10MB,保留 5 份)

运行正文提取(M2)

M2 以 M1 的产物为输入,从 HTML 抽出标准化 Article(标题/正文/时间/作者/图片)。

# 处理今日所有源
uv run python -m scripts.run_extractor

# 指定日期
uv run python -m scripts.run_extractor --date 20260616

# 只处理单个源
uv run python -m scripts.run_extractor --source sina --date 20260616

提取产物路径:data/processed/{source_id}/{YYYYMMDD}/

  • {url_hash}.json 完整 Article(含正文)
  • index.jsonl 扁平元数据(每行一条,不含正文,便于检索)

日志:logs/extractor.log

运行去重(M3)

M3 在 M2 输出之上做三层判重(URL Hash → 内容 Hash → SimHash 模糊),指纹持久化到 SQLite。

# 处理今日全部源
uv run python -m scripts.run_dedup

# 指定日期
uv run python -m scripts.run_dedup --date 20260616

# 只处理单源
uv run python -m scripts.run_dedup --source sina --date 20260616

# 调阈值/窗口
uv run python -m scripts.run_dedup --simhash-threshold 3 --window-days 30

# 重建指纹库(全量重跑时使用)
uv run python -m scripts.run_dedup --reset

去重产物路径:

  • data/dedup/fingerprints.sqlite3 指纹库(跨日累积)
  • data/deduped/{YYYYMMDD}/uniques/{url_hash}.json 唯一文章(可送 M4+ 处理)
  • data/deduped/{YYYYMMDD}/duplicates.jsonl 重复记录(含命中层 / 命中目标)

日志:logs/dedup.log

运行 LLM 事件抽取(M4)

M4 调用 DeepSeek 或 Qwen,从 M3 唯一文章中抽取结构化投资事件(stock_codes/sentiment/importance/event_type 等)。

# 默认 DeepSeek(读 .env 的 DEEPSEEK_API_KEY)
uv run python -m scripts.run_event_extraction --date 20260616

# 切换 Qwen(百炼)
uv run python -m scripts.run_event_extraction --provider qwen --date 20260616

# 指定模型
uv run python -m scripts.run_event_extraction --provider qwen --model qwen-plus

# 联调小批量
uv run python -m scripts.run_event_extraction --limit 5

# 提速(并发,默认 3)
uv run python -m scripts.run_event_extraction --concurrency 5

# 跳过 M3,直接读 M2 处理产物(单源)
uv run python -m scripts.run_event_extraction --no-deduped --source cls

抽取产物路径:data/events/{YYYYMMDD}/

  • {url_hash}.json 完整 ExtractedEvent
  • index.jsonl 扁平摘要(每行一条)
  • failed.jsonl 失败列表(若有)

环境变量(.env):

  • LLM_PROVIDER 默认 deepseek,可选 qwen
  • LLM_MODEL 覆盖默认模型
  • LLM_TEMPERATURE 默认 0.1
  • LLM_TIMEOUT_SEC 默认 60
  • DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL
  • DASHSCOPE_API_KEY / QWEN_BASE_URL

日志:logs/llm.log

运行 Embedding 向量化(M5)

M5 把 M4 输出的事件用 DashScope text-embedding-v3(默认,1024 维)或本地 BGE-M3 向量化。

# 默认 DashScope(读 .env 的 DASHSCOPE_API_KEY)
uv run python -m scripts.run_embedding --date 20260616

# 强制覆盖 provider/model(避免 .env 旧值干扰)
uv run python -m scripts.run_embedding --provider dashscope --model text-embedding-v3

# 用 M3 唯一文章为输入(跳过 M4 事件)
uv run python -m scripts.run_embedding --input deduped

# 用 M2 处理产物为输入(跳过 M3/M4)
uv run python -m scripts.run_embedding --input articles --source cls

# 启用本地 BGE-M3(需先 uv sync --extra local-embedding)
uv run python -m scripts.run_embedding --provider local-bge

输出路径:data/embeddings/{YYYYMMDD}/

  • {url_hash}.json 完整 EmbeddingResult(含 1024 维向量)
  • index.jsonl 扁平摘要(不含向量)
  • failed.jsonl 失败列表

环境变量(.env):

  • EMBEDDING_PROVIDER 默认 dashscope,可选 local-bge
  • DASHSCOPE_EMBEDDING_MODEL DashScope 模型(默认 text-embedding-v3)
  • LOCAL_EMBEDDING_MODEL 本地模型(默认 BAAI/bge-m3)
  • DASHSCOPE_API_KEY / QWEN_BASE_URL(M4 已配)

日志:logs/embedding.log

运行 Qdrant 入库与检索(M6)

M6 把 M5 的嵌入向量 + M4 的事件标签写入 Qdrant 本地知识库(文件模式,无需 Docker)。

# 入库(默认本地文件模式 data/qdrant_storage/)
uv run python -m scripts.run_qdrant_ingest --date 20260616

# 重建 collection + 全量入库
uv run python -m scripts.run_qdrant_ingest --recreate

# 试跑 N 条
uv run python -m scripts.run_qdrant_ingest --limit 10

# 指定存储路径
uv run python -m scripts.run_qdrant_ingest --path /mnt/data/qdrant

# 内存模式(测试)
uv run python -m scripts.run_qdrant_ingest --memory

检索示例:

from vectorstore import VectorStore, SearchFilter, make_qdrant_client
import json

c = make_qdrant_client()
store = VectorStore(c)

# 基础语义检索
probe = json.load(open("data/embeddings/20260616/abc.json"))
hits = store.query(query_vector=probe["vector"], top_k=10)

# 结构化过滤
hits = store.query(
    query_vector=probe["vector"], top_k=10,
    filter=SearchFilter(source_id="cls", importance_min=3, sentiment="positive"),
)
store.close()

环境变量(.env):

  • QDRANT_COLLECTION Collection 名(默认 a_share_news)
  • QDRANT_HOST/QDRANT_PORT 远程 HTTP 模式(不常用)

日志:logs/qdrant.log

运行全链路定时任务(M7)

M7 把 M1→M6 串成一个 Pipeline,支持单次执行和定时守护。

# 立即执行一次全链路
uv run python -m scripts.run_scheduler --once

# 指定日期
uv run python -m scripts.run_scheduler --once --date 20260616

# 只执行部分步骤(逗号分隔)
uv run python -m scripts.run_scheduler --once --steps crawler,extractor,llm

# 启动定时守护进程(按 .env 中 SCHEDULE_TIMES 自动触发)
uv run python -m scripts.run_scheduler

定时时间由 .envSCHEDULE_TIMES 控制(默认 07:00,12:00,18:00,22:00)。

Pipeline 总耗时约 4-5 分钟(100 篇文章),其中 M1 抓取(含浏览器渲染)最耗时(~3 分钟)。

日志:logs/scheduler.log

MCP 服务(M8)

M8 暴露 5 个 MCP 工具给 Cherry Studio / Claude Code,对接 Qdrant 知识库。

5 个工具:

工具 功能 过滤字段
search_news 通用语义检索 -
search_company_news 按公司检索 company_names
search_industry_news 按行业检索 industries
search_stock_events 按股票代码检索 stock_codes
search_sentiment_trend 按情绪检索+统计 sentiment

Cherry Studio 配置(设置 → MCP 服务器 → 添加):

{
  "mcpServers": {
    "a-share-research": {
      "command": "uv",
      "args": ["run", "python", "-m", "scripts.run_mcp_server"],
      "cwd": "/home/pi/news"
    }
  }
}

Claude Code 配置(.mcp.json):

{
  "mcpServers": {
    "a-share-research": {
      "command": "uv",
      "args": ["run", "python", "-m", "scripts.run_mcp_server"],
      "cwd": "/home/pi/news"
    }
  }
}

调试(HTTP SSE 模式,浏览器访问 http://<host>:8765/sse):

uv run python -m scripts.run_mcp_server --sse 8765

日志:logs/mcp_server.log

使用示例(在 Cherry Studio / Claude Code 中输入):

用 search_news 搜索"宁德时代固态电池最新进展"

用 search_stock_events 查 300750 最近的重大事件

用 search_sentiment_trend 分析"AI 算力"相关新闻的情绪变化

用 search_company_news 搜"贵州茅台"关于价格调整的新闻

用 search_industry_news 搜"半导体"行业的最新政策

工作原理:每个查询 → DashScope 嵌入 → Qdrant 语义检索(2.3ms)→ 格式化 Markdown 返回。

systemd 部署(推荐)

将调度器安装为系统服务,开机自启、崩溃自动重启。

# 安装
sudo cp scripts/a-share-research.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable a-share-research

# 日常操作
sudo systemctl start a-share-research     # 启动
sudo systemctl stop a-share-research      # 停止
sudo systemctl restart a-share-research   # 重启
sudo systemctl status a-share-research    # 查看状态

# 查看日志
journalctl -u a-share-research -f         # 实时
tail -f logs/scheduler.log               # 文件

服务进程:uv run python -m scripts.run_scheduler(PID 见 systemctl status)

修改新闻源

编辑 configs/sources.yaml,字段说明见文件顶部注释。新增源无需改代码(配置驱动原则)。

运行测试

uv run pytest                                  # 单元测试(默认排除集成)
uv run pytest -m integration                   # 真实抓取烟测(需联网+浏览器)
uv run pytest tests/test_crawler.py -v
uv run pytest tests/test_crawler.py::test_extract_article_links_basic
uv run pytest --cov=crawler                    # 覆盖率

代码质量

uv run ruff check .       # Lint
uv run ruff format .      # 格式化
uv run mypy crawler/      # 类型检查

Docker(M6 起启用)

docker compose --profile m6 up -d   # 启动 Qdrant
docker compose logs -f
docker compose down

目录结构

news/
├─ crawler/        # M1 新闻抓取(Crawl4AI)
├─ extractor/      # M2 中文正文提取(GNE)
├─ dedup/          # M3 三层去重
├─ llm/            # M4 投资事件抽取
├─ embedding/      # M5 向量生成
├─ vectorstore/    # M6 Qdrant 客户端封装
├─ scheduler/      # M7 定时任务
├─ mcp_server/     # M8 MCP 服务
├─ api/            # 对外接口
├─ app/            # 应用入口
├─ configs/        # 网站源 / 系统配置(YAML)
├─ prompts/        # LLM Prompt 模板
├─ tests/          # pytest 测试
├─ scripts/        # 运维脚本(含 run_crawler)
├─ docs/           # 设计文档与决策记录
├─ logs/           # 运行日志(git ignored)
├─ data/           # 抓取与缓存(git ignored)
├─ pyproject.toml
├─ docker-compose.yml
├─ .env.example
├─ CLAUDE.md       # Claude Code 开发约束
├─ project_plan.md # 项目计划
├─ continuation.md # 上下文恢复
└─ README.md

开发约束

详见 CLAUDE.md。关键纪律:

  • 分阶段开发,每个 Milestone 验收通过后再进入下一个;
  • 配置驱动,不硬编码;
  • 异常必须记录日志并显式抛出;
  • 所有新增代码须含类型注解;
  • Prompt 单独文件维护,不写死在代码里;
  • 代码用英文命名,注释/文档用中文。

许可

私有项目,未授权禁止使用与传播。