# 非遗新闻采集 - 实现规划 数据源:[广州日报新花城](https://www.gz-cmc.com/)(需求详见仓库根目录 `非遗新闻采集.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` | | `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` ## 阶段一:列表采集(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): - 移除 `meta`、`script` 标签 - 移除 `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` ```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) - 数据表格交付(标题/记者/发布时间/新闻地址)