跳到正文
Joeplover
后端开发·2026-03-12·约 5 分钟阅读

data-clean:AI 招标数据清洗管线 — ES 检索 + AI 提取 + 可视化报告

从 ES 检索招标公告到 AI 提取公司信息、金额、联系人,再到 MariaDB 入库和 ECharts 报告生成,一条完整的招标数据智能清洗管线搭建记录。

数据分析仪表盘 — AI 数据清洗管线

项目背景

data-clean 是 information-collection-s 的配套 AI 数据清洗管线,目标是解决招标公告数据中「非结构化内容 → 结构化公司信息」的自动提取问题。

上游的 information-collection-s 从各个招标网站采集了数万条公告数据,但这些公告的正文是 HTML 格式的非结构化文本,包含大量无关标签、样式代码和冗余信息。需要一条 AI 管线来自动清洗 HTML、提取中标公司、金额、排名、联系人等关键字段,最终写入 MariaDB 供业务系统查询。

技术栈

组件版本用途
FastAPI0.104.1API 服务框架
Elasticsearch7.10.1公告数据检索
Ollamaqwen2.5:1.5b本地 AI 提取模型(降级方案)
GLM (GPUStack)qwen3.5-4b主 AI 提取模型
MariaDB—结构化数据存储
BeautifulSoup4.12.2HTML 内容清洗
ECharts 5—可视化报告生成

管线架构

整个数据清洗管线分 5 步:

ES 检索 → HTML 清洗 → AI 提取 → 数据库入库 → 报告生成

第一步:ES 语义检索

通过 es_service.py 调用 ES 索引 information-collection-dev-s-file,支持:

  • 多字段匹配:title(权重3) + content(权重2) + realNoticeTypeName(权重2)
  • 模糊搜索:fuzziness: AUTO 自动处理错别字
  • 短语匹配:title 字段额外 boost=5 的 match_phrase 精准搜索
  • 响应精简:只返回 title、content、budget、releaseTime、realNoticeTypeName、budgetAmount 等关键字段

第二步:HTML 内容清洗

招标公告的 content 字段存储的是完整 HTML 页面,包含大量无用标签。通过 BeautifulSoup + lxml 解析器做清洗:

# 核心逻辑:去除 script、style、nav、footer 等标签
# 提取纯文本内容 + 保留关键表格结构

清洗后的纯文本长度通常只有原始 HTML 的 10%-20%,大幅降低 AI 模型的 token 消耗。

第三步:AI 智能提取(核心)

通过 ollama_service.py 调用 AI 模型从非结构化文本中提取结构化公司信息。支持双模型:

模式模型部署方式适用场景
主方案qwen3.5-4b (GLM)GPUStack 远程调用精度优先
降级方案qwen2.5:1.5b本地 Ollama稳定性优先

提取的 JSON 格式:

{
  "companies": [
    {
      "company": "龙昊通用航空集团股份有限公司",
      "rank": 1,
      "amount": 49944800.00,
      "is_winner": true,
      "contact_person": "张三",
      "contact_phone": "13800138000"
    }
  ],
  "total_amount": 49944800.00,
  "company_count": 1
}

提示词工程要点:

  • 针对 1.5b 小模型,提示词采用「示例驱动」策略——提供 5 个完整示例覆盖常见场景
  • 金额单位自动换算:"4994.48万元" → 49944800.00元
  • 联系人提取规则:优先提取「联系人」「负责人」「经办人」后的姓名
  • 电话号码提取规则:优先提取 11 位手机号,其次取座机号
  • 自动降级:GLM 调用失败后自动切换到本地 Ollama

第四步:数据库入库

提取结果写入 MariaDB 的 tender_data 表:

CREATE TABLE IF NOT EXISTS tender_data (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    record_id VARCHAR(255),
    title TEXT NOT NULL,
    company_name VARCHAR(500),
    amount DECIMAL(20, 2) DEFAULT 0,
    is_winner TINYINT(1) DEFAULT 0,
    rank INT,
    contact_person VARCHAR(255),
    contact_phone VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_record_company (record_id, company_name)
);

关键设计:

  • 唯一约束:(record_id, company_name) 防止同一公告同公司重复入库
  • 完整索引:对 record_id、title、company_name、amount、contact_person 等字段都建立了索引

第五步:可视化报告

每次提取完成后自动生成 HTML 报告,包含:

  • ECharts 饼图:中标金额分布
  • 公司列表:按中标金额排序,支持点击交互
  • 中标详情:展示每个公司的中标项目明细
  • 统计摘要:ES 检索总数、AI 处理数、成功率、涉及总金额

踩坑记录

1. 金额单位不一致

AI 模型有时输出万元(如 4994.48 万元对应 4994.48),有时输出元(如 49944800.00 元对应 49944800.00)。需要在后处理中做单位修正:

# 如果金额的小数点前有 5 位及以上,说明是元单位但 AI 误写成万元
if len(str(int(amount))) >= 5:
    amount = amount / 10000

2. AI 返回字符串型 null

模型有时输出 "contact_person": "null" 而不是 JSON 的 null。需要在三个地方(Ollama 调用、GLM 调用、Ollama 重试)做转换:

if contact_person in ['null', 'None', '']:
    company['contact_person'] = None

3. 数据库唯一约束过严

初始版本唯一约束包含了 rank 字段,但 rank 可能为 NULL(部分公告不排名),导致唯一约束失效。改为只使用 (record_id, company_name) 后解决。

4. 连接池未管理

初始版本的 MariaDB 连接没有上下文管理,异常情况下连接不会正确关闭。修复为 @contextmanager 模式,确保 commit/rollback 前后检查连接状态。

效果数据

以「飞行」关键词为例的一次提取:

指标数据
ES 检索总数约 100 条
AI 处理数约 80 条
AI 提取成功率~85%
提取中标公司数20+ 家
涉及总金额数亿元
单条处理耗时~3-5 秒(Ollama)

总结

这个项目的核心价值在于打通了「非结构化公告 → 结构化公司信息」的全流程自动化。

对上游的 information-collection-s 来说,采集的公告数据从「可检索的文档」变成了「可分析的数据库」,新增的联系人和电话字段可以直接用于业务系统的线索管理。对整个招标数据平台来说,data-clean 就是 AI 能力落地的那一层——用相对轻量的小模型 + 精心设计的提示词,替代了人工逐条录入的繁琐工作。

后续优化方向:将手动触发的提取流程升级为 ES 增量监听自动触发,以及用更大的模型(7B+)替换 1.5b 以提高提取准确率。