todo.md 6.4 KB

非遗新闻采集 - 实现规划

数据源:广州日报新花城(需求详见仓库根目录 非遗新闻采集.md

目标(本次范围)

实现到:数据采集 -> 数据清洗 -> 存储到数据库。PDF 导出与数据表格交付留待下阶段。

技术栈

  • JDK 25、Maven
  • JDK 内置 HttpClient
  • jsoup 解析/清洗 HTML
  • jackson 解析 JSON
  • mybatis-plus(standalone 模式,不使用 Spring,MybatisPlusSqlSessionFactoryBuilder
  • HikariCP 连接池
  • PostgreSQL 存储
  • JUnit 5 作为两个阶段的启动入口

数据库

  • 库:yangyi,模式:new_collection
  • 连接:jdbc:postgresql://localhost:5432/yangyi?currentSchema=new_collection,用户 yangyi / 密码 MIMA2004
  • 实现时执行 CREATE SCHEMA IF NOT EXISTS new_collection;
  • 程序启动时 CREATE TABLE IF NOT EXISTS new_collection.news (...),无需迁移工具
  • 初始化脚本见 sql/init.sql(重跑会清空 news 表)

表结构 news

类型 说明
id varchar(64) PK 接口 data.id,唯一标识
title text 标题(列表接口)
url text 新闻地址(列表接口)
publish_time timestamp 发布时间(列表接口)
channel_id varchar(64) 频道 id
channel_name varchar(128) 频道名称
editor varchar(255) 编辑(阶段一取列表接口 data.userName;阶段二详情页解析到则覆盖)
text_reporters text 文字记者,逗号分隔
image_reporters text 图片记者,逗号分隔
correspondents text 通讯员,逗号分隔
content text 清洗后的详情 html;NULL 表示待补详情
content_fetched_at timestamp 详情采集时间
created_at timestamp default now() 入库时间

目录结构

生产代码 src/main/java/space/anyi/

职责
entity/News @TableName("news") 实体
mapper/NewsMapper BaseMapper<News>
dto/ChannelAllContentsResponse 列表接口响应(status/msg/total/pages/list)
dto/NewsItem list 元素(id + data)
dto/NewsItemData data 字段映射(id/title/url/publishTime/channelId/channelName/userName)
config/Config 加载 collection.properties,类型化 getter
db/Db HikariCP DataSource + 建 schema/表
db/Mybatis 构建 SqlSessionFactory,统一会话获取/提交/回滚
client/GzCmcClient JDK HttpClient 封装:search / fetchDetail
cleaner/DetailCleaner jsoup 清洗 + 记者/编辑解析
service/ListCollectService 阶段一:列表采集 + 去重 + 入库
service/DetailCollectService 阶段二:补详情 + 清洗 + 解析 + 更新

测试代码 src/test/java/space/anyi/(启动入口,两阶段分离):

方法 运行
ListCollectTest @Test void collectList() mvn test -Dtest=ListCollectTest
DetailCollectTest @Test void collectDetail() mvn test -Dtest=DetailCollectTest

依赖:mybatis-plus:3.5.9HikariCP:6.3.3postgresql:42.7.4jsoup:1.16.1jackson-databind:2.18.3junit-jupiter:5.14.0maven-surefire-plugin:3.5.6

阶段一:列表采集(ListCollectTest)

对配置中每个频道(channel.ids 逗号分隔)× 每个关键词(keywords 逗号分隔)做双层循环(组合数 O(频道数×关键词数) = O(n²)),每个组合独立全量采集;后期加词/加频道只改配置:

  1. pageSize=2000 分页请求 https://www.gz-cmc.com/contentapi/api/content/getChannelAllContents?siteId=5e88c884e2ed4e7a9a8d5225c299f707&keyword={kw}&channelId={cid}&pageNum={n}&pageSize=2000(channelId 取自配置,可能多个),直到 pageNum > pages 或 list 为空
  2. 请求头必带 referer: https://www.gz-cmc.com/
  3. 每条记录映射为实体,按 data.publishTime(格式 yyyy-MM-dd HH:mm:ss)过滤在 [publish.start, publish.end]
  4. 去重:selectBatchIds 查出本页已存在 id,仅插入缺失记录(幂等,可重复运行)
  5. 此时 content 为 NULL,作为阶段二待补队列

阶段二:详情采集 + 清洗(DetailCollectTest)

  1. content IS NULL 的记录,按时间排序逐条处理
  2. fetchDetail(url) 获取完整 html(url 形如 https://huacheng.gz-cmc.com/pages/yyyy/MM/dd/{id}.html
  3. jsoup 清洗(严格按 spec):
    • 移除 metascript 标签
    • 移除 id="hidden-box" 元素
    • div.container 的直接子 div 仅保留 .article-title.not-exist-media-leader.article-content,其余移除
    • 清洗结果存入 content
  4. 从保留文本解析记者/编辑(见下)
  5. 更新 content/editor/各记者列/content_fetched_at
  6. 请求间 sleep(默认 300ms),失败重试 2 次

记者/编辑解析规则(DetailCleaner)

对清洗后 .article-content 纯文本按子句匹配,顺序:

  • 文、图/…记者:X -> X 同时进文字记者和图片记者
  • 文/…记者:X -> 文字记者
  • 图/…记者:X -> 图片记者
  • 通讯员:X -> 通讯员
  • 新花城编辑:X / 编辑:X -> editor

要点:

  • 记者、通讯员可能同一行:图/广州日报新花城记者:曾焕阳 通讯员:潘永光、陈诺 -> 图片[曾焕阳],通讯员[潘永光, 陈诺]
  • 多人名以 / , / / 空格分隔
  • 实习生 等非 spec 字段忽略
  • editor 单一字段:阶段一由列表接口 userName 填充,阶段二详情页解析到编辑时覆盖(已合并原 userName 列)

配置 collection.properties

db.url=jdbc:postgresql://localhost:5432/yangyi?currentSchema=new_collection
db.user=yangyi
db.password=MIMA2004
db.schema=new_collection
channel.ids=6d99c57fe7dd46aa8d6ecf697da08268
keywords=非遗
publish.start=2024-01-01 00:00:00
publish.end=2025-12-31 23:59:59
list.page.size=2000
detail.sleep.ms=300
client.timeout.ms=20000
referer=https://www.gz-cmc.com/

验证

  1. mvn -q compile
  2. mvn test -Dtest=ListCollectTest,psql 检查: PGPASSWORD=MIMA2004 psql -h 127.0.0.1 -U yangyi -d yangyi -c "select count(*), min(publish_time), max(publish_time) from new_collection.news"
  3. mvn test -Dtest=DetailCollectTest,抽查 content 清洗结果、图文记者/通讯员/编辑字段
  4. 重跑验证幂等(不重复插入)

不在本次范围

  • PDF 导出(命名:发布时间-新闻标题-记者.pdf)
  • 数据表格交付(标题/记者/发布时间/新闻地址)