配套 todo.md(权威规范)。本文档为逐步实施明细,实施中若与 todo.md 冲突以 todo.md 为准,并反向同步。
total=16458, pages=9(pageSize=2000),按 publishTime 降序返回;字段与 todo.md 一致。search 必须重试 2 次。https://huacheng.gz-cmc.com/pages/{yyyy}/{MM}/{dd}/{id}.html,用列表接口返回的 url 直取,不要拼 0000/00/00/(会 302)。div.container 直接子 div(实测顺序):header-guide-box / article-title / not-exist-media-leader / article-video-fixed / article-content / read-like-number / article-copyright / media-list / article-launch-app-box / article-comment / related-content / related-channel / article-operate。只保留 3 个,其余移除。.article-content),解析为全 NULL,不报错。匹配子句(严格按 spec,顺序无关,正则全局匹配):
| 子句 | 前缀 | 目标列 |
|---|---|---|
文、图/…记者:X |
文、图 |
text + image |
文/…记者:X |
文 |
text |
图/…记者:X |
图 |
image |
通讯员:X |
- | correspondents |
新花城编辑:X / 编辑:X |
- | editor |
实现要点:
文、图/、文/、图/、视频/、通讯员、(广州日报)?新花城编辑、编辑:、实习生、行尾。、 , , 空白 拆分;分词后丢弃含 : 或 / 的 token(实测 实习生:邹文婧、图片由受访者提供、视频/…记者:… 均由此清除)。(…) 括注(实测 王理润(除署名外) → 王理润)。浏览量: 等杂项会被 token 过滤吞掉)。视频/…记者、图、视频/…提供 等非 spec 写法一律忽略。实测样例(须全部正确):
文、图/广州日报新花城记者:曾焕阳 通讯员:潘永光、陈诺广州日报新花城编辑:石忠情 → 文字[曾焕阳] 图片[曾焕阳] 通讯[潘永光,陈诺] 编辑[石忠情]文/广州日报新花城记者:孙嘉晖 通讯员:袁智斌 图/广州日报新花城记者:杨泽彬 通讯员:黄晋文 视频/广州日报新花城记者:杨泽彬 广州日报新花城编辑:杜娟 → 文字[孙嘉晖] 图片[杨泽彬] 通讯[袁智斌,黄晋文] 编辑[杜娟]文、图/广州日报新花城记者:轩慧 广州日报新花城编辑:童丹 → 文字[轩慧] 图片[轩慧] 编辑[童丹]文/广州日报新花城记者:陈家源、庄小龙、张忠安、倪明、刘幸、曾繁莹、李波 通讯员:潮宣、王理润(除署名外) 广州日报新花城编辑:赵小满 → 文字7人 通讯[潮宣,王理润] 编辑[赵小满]文/广州日报新花城记者:廖靖文 实习生:邹文婧 图片由受访者提供 广州日报新花城编辑:时秀芙 → 文字[廖靖文] 编辑[时秀芙]文/广州日报新花城记者:黄子宁 通讯员:徐韬 图、视频/北滘宣办提供 广州日报新花城编辑:何波 → 文字[黄子宁] 通讯[徐韬] 编辑[何波]pom.xml 增加(版本号照 todo.md):
com.baomidou:mybatis-plus:3.5.9com.zaxxer:HikariCP:6.3.3org.postgresql:postgresql:42.7.4org.jsoup:jsoup:1.16.1com.fasterxml.jackson.core:jackson-databind:2.18.3org.junit.jupiter:junit-jupiter:5.14.0(test scope)maven-surefire-plugin:3.5.6(含 junit-platform 集成)验证:mvn -q compile
collection.properties(gitignore 已含,不提交),内容照 todo.md,含数据库口令。config/Config:
dbUrl/dbUser/dbPassword/dbSchema、keywords()(逗号拆 List)、publishStart()/publishEnd()(yyyy-MM-dd HH:mm:ss → LocalDateTime)、pageSize()/sleepMs()/timeoutMs()/referer()。
dto/ChannelAllContentsResponse:status(Integer) msg(String) total(Integer) pages(Integer) list(List),jackson 直配 camelCase。
dto/NewsItem:id(String) contentType(Integer) contentId(String) siteId(String) data(NewsItemData)。dto/NewsItemData:id/title/url/channelId/channelName/userName(String)、publishTime(String,手动解析)。entity/News:@TableName("news"),字段与表列一一对应(snake_case 由 mapUnderscoreToCamelCase 映射),@TableId(type=IdType.INPUT)(id 来自接口)。db/Db:
DriverManager 连接(url 不带 currentSchema)执行 CREATE SCHEMA IF NOT EXISTS new_collection 与 CREATE TABLE IF NOT EXISTS new_collection.news (...)(DDL 见 todo.md,id varchar(64) PK)。?currentSchema=new_collection,timeout 从配置取。getDataSource() 与 init(Config) 静态引导。db/Mybatis:
MybatisPlusSqlSessionFactoryBuilder 构建 SqlSessionFactory;Configuration.setMapUnderscoreToCamelCase(true);注册 NewsMapper。getMapper()、commit()、rollback()、close() 便捷方法。client/GzCmcClient(单例,复用 HttpClient):
search(String keyword, int pageNum, int pageSize):URL 编码 keyword,GET + 头 referer;失败/非 200/响应 status!=200 时重试 2 次(间隔 sleep);jackson 反序列化返回 ChannelAllContentsResponse。fetchDetail(String url):GET + referer,返回 body 字符串;重试 2 次。cleaner/DetailCleaner(静态方法):
clean(String html):doc.select("meta, script").remove()doc.select("#hidden-box").remove()div.container 直接子 div 仅保留 .article-title/.not-exist-media-leader/.article-content,其余 remove()doc.html()articleContentText(Document):取 div.article-content 纯文本(无则返回空串)。parsePersons(String text):按上文「解析变体清单」实现,返回 PersonInfo(editor, textReporters, imageReporters, correspondents)(record,List 无则空)。
service/ListCollectService.run():
pageNum=1 循环。search 取本页;list 为空或 pageNum > pages 退出。publishTime 用 yyyy-MM-dd HH:mm:ss 解析;窗口过滤 [publishStart, publishEnd] 含端点;content=null。selectBatchIds 查出已存在 id → 只 insert 缺失行(逐条 insert,简单可靠)。service/DetailCollectService.run():
QueryWrapper 查 content IS NULL 按 publish_time ASC。fetchDetail(url) → clean → articleContentText → parsePersons → updateById(content/editor/textReporters/imageReporters/correspondents/content_fetched_at=now)。detail.sleep.ms(300);重试失败仍 NULL 留待下轮。ListCollectTest.collectList():init Config+Db+Mybatis+GzCmcClient → ListCollectService.run() → 打印插入统计。DetailCollectTest.collectDetail():同上初始化 → DetailCollectService.run()。mvn -q compilemvn test -Dtest=ListCollectTestPGPASSWORD=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(约 50~60 分钟,300ms 间隔)content 清洗结果与 text_reporters/image_reporters/correspondents/editor。