数据源:广州日报新花城(需求详见仓库根目录 非遗新闻采集.md)
实现到:数据采集 -> 数据清洗 -> 存储到数据库。PDF 导出与数据表格交付留待下阶段。
HttpClientjsoup 解析/清洗 HTMLjackson 解析 JSONmybatis-plus(standalone 模式,不使用 Spring,MybatisPlusSqlSessionFactoryBuilder)HikariCP 连接池yangyi,模式:new_collectionjdbc:postgresql://localhost:5432/yangyi?currentSchema=new_collection,用户 yangyi / 密码 MIMA2004CREATE SCHEMA IF NOT EXISTS new_collection;CREATE TABLE IF NOT EXISTS new_collection.news (...),无需迁移工具sql/init.sql(重跑会清空 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.9、HikariCP:6.3.3、postgresql:42.7.4、jsoup:1.16.1、jackson-databind:2.18.3、junit-jupiter:5.14.0、maven-surefire-plugin:3.5.6
对配置中每个频道(channel.ids 逗号分隔)× 每个关键词(keywords 逗号分隔)做双层循环(组合数 O(频道数×关键词数) = O(n²)),每个组合独立全量采集;后期加词/加频道只改配置:
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 为空referer: https://www.gz-cmc.com/data.publishTime(格式 yyyy-MM-dd HH:mm:ss)过滤在 [publish.start, publish.end] 内selectBatchIds 查出本页已存在 id,仅插入缺失记录(幂等,可重复运行)content 为 NULL,作为阶段二待补队列content IS NULL 的记录,按时间排序逐条处理fetchDetail(url) 获取完整 html(url 形如 https://huacheng.gz-cmc.com/pages/yyyy/MM/dd/{id}.html)meta、script 标签id="hidden-box" 元素div.container 的直接子 div 仅保留 .article-title、.not-exist-media-leader、.article-content,其余移除contentcontent/editor/各记者列/content_fetched_at对清洗后 .article-content 纯文本按子句匹配,顺序:
文、图/…记者:X -> X 同时进文字记者和图片记者文/…记者:X -> 文字记者图/…记者:X -> 图片记者通讯员:X -> 通讯员新花城编辑:X / 编辑:X -> editor要点:
图/广州日报新花城记者:曾焕阳 通讯员:潘永光、陈诺 -> 图片[曾焕阳],通讯员[潘永光, 陈诺]、 / , / , / 空格分隔实习生 等非 spec 字段忽略editor 单一字段:阶段一由列表接口 userName 填充,阶段二详情页解析到编辑时覆盖(已合并原 userName 列)collection.propertiesdb.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/
mvn -q compilemvn 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"mvn test -Dtest=DetailCollectTest,抽查 content 清洗结果、图文记者/通讯员/编辑字段