java爬虫;数据采集和数据清洗;JDK HTTP Client、Jsoup、Postgresql、Mybatis。

yangyi c61f674a3c docs:readme文件 5 日 前
sql 3744a70919 feat:频道化采集、PDF/视频产出工具链与清洗加固 5 日 前
src 3744a70919 feat:频道化采集、PDF/视频产出工具链与清洗加固 5 日 前
.gitignore 3744a70919 feat:频道化采集、PDF/视频产出工具链与清洗加固 5 日 前
README.md c61f674a3c docs:readme文件 5 日 前
pom.xml 3744a70919 feat:频道化采集、PDF/视频产出工具链与清洗加固 5 日 前
temp.html 3744a70919 feat:频道化采集、PDF/视频产出工具链与清洗加固 5 日 前
todo.md 3744a70919 feat:频道化采集、PDF/视频产出工具链与清洗加固 5 日 前
todoDetail.md 3744a70919 feat:频道化采集、PDF/视频产出工具链与清洗加固 5 日 前

README.md

非遗新闻采集(newCollection)

针对 广州日报新花城 的非遗相关新闻采集、清洗、存储与产出工具集。

全流程分三阶段:

  1. 列表采集:按频道 × 关键词全量分页拉取列表,过滤发布窗口后去重入库。
  2. 详情采集与清洗:补抓详情页,jsoup 清洗正文,解析文字/图片记者、通讯员、编辑并回写。
  3. 分析产出:视频统计、二次筛选、目标表格(CSV)+ PDF + 正文视频打包交付。

前两个阶段以 JUnit 测试作为启动入口,可重复运行且幂等

功能特性

  • 频道化列表采集:配置的频道 × 关键词做双层循环(组合数 O(频道数×关键词数)),每个组合独立全量分页采集;到达服务端检索上限(offset≥10000)时优雅停止该组合,不中断整体。
  • 发布窗口过滤:仅保留 [publish.start, publish.end] 窗口内的记录。
  • 去重入库:以列表接口 data.id 为主键,插入前 selectBatchIds 校验,重复运行不产生重复行。
  • 详情补全 + HTML 清洗content IS NULL 记录组成待补队列,逐条抓详情页并按 spec 清洗;单条失败不中断,留待下轮重补。
  • 记者/编辑解析:从正文纯文本按子句正则提取文字记者、图片记者、通讯员与编辑,支持同句多人、/,//空格 分隔、(…)括注剥离、实习生 等杂项过滤。
  • PDF 生成:openhtmltopdf 渲染清洗后 HTML,思源宋体注册到页面字体族名下保证中文渲染;自动修复来源页的重复属性与   实体,规避严格 XML 解析器报错。
  • 产出工具:视频包含统计、二次筛选命中(标题 1 次 / 正文 2 次关键词)、目标表格 + PDF + 正文视频逐条下载打包。

技术栈

组件 版本 说明
JDK 25 Maven 编译/运行目标
Maven surefire 3.5.6
JDK HttpClient 内置 GET + referer,失败重试
jsoup 1.16.1 HTML 解析与清洗
jackson-databind 2.18.3 列表接口 JSON 反序列化
mybatis-plus 3.5.9 standalone 模式(无 Spring)
HikariCP 6.3.3 数据库连接池
PostgreSQL JDBC 42.7.4 驱动
openhtmltopdf 1.0.10 HTML → PDF 渲染
logback-classic 1.5.18 日志输出
JUnit Jupiter 5.14.0 阶段入口与工具测试

目录结构

├── pom.xml
├── sql/init.sql                # 数据库初始化(重跑会清空 news 表)
├── todo.md / todoDetail.md     # 实现规划与详细计划(权威规范)
├── src/main/resources/
│   ├── collection.properties   # 运行配置(含 DB 口令,已被 .gitignore 忽略)
│   └── logback.xml             # 日志:控制台 + 滚动文件 logs/app.log
└── src/main/java/space/anyi/
    ├── config/Config.java      # 加载配置,类型化 getter
    ├── dto/                    # 列表接口响应模型
    ├── entity/News.java        # news 表实体
    ├── mapper/NewsMapper.java  # MyBatis-Plus Mapper
    ├── db/Db.java              # 建 schema/表 + HikariCP 连接池(单例)
    ├── db/Mybatis.java         # standalone SqlSessionFactory + 会话封装
    ├── client/GzCmcClient.java # HTTP 客户端:search / fetchDetail
    ├── cleaner/DetailCleaner.java # 清洗 / PDF / 记者编辑解析(静态方法)
    └── service/
        ├── ListCollectService.java   # 阶段一:列表采集 + 去重 + 入库
        └── DetailCollectService.java # 阶段二:补详情 + 清洗 + 解析 + 更新

快速开始

1. 环境准备

  • JDK 25、Maven、本地 PostgreSQL(默认 localhost:5432)。
  • 建库建 schema(应用启动也会自动执行 CREATE SCHEMA/TABLE IF NOT EXISTS):

    PGPASSWORD=<db_password> psql -h localhost -U <db_user> -d yangyi -c "CREATE SCHEMA IF NOT EXISTS new_collection;"
    

或用 sql/init.sql 初始化(注意:会 DROP TABLE 重建)。

2. 配置 collection.properties

复制仓库模板到 src/main/resources/collection.properties 并填入真实口令:

db.url=jdbc:postgresql://localhost:5432/yangyi?currentSchema=new_collection
db.user=<db_user>
db.password=<db_password>
db.schema=new_collection
channel.ids=<频道id1>,<频道id2>
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/

配置项说明:

含义 默认/示例
db.* JDBC 地址 / 用户 / 口令 / schema 见上
channel.ids 采集频道 id,逗号分隔(取自 compare/channel 表) 多个
keywords 采集关键词,逗号分隔 非遗…
publish.start/end 发布窗口(含端点),格式 yyyy-MM-dd HH:mm:ss
list.page.size 列表分页大小 2000
detail.sleep.ms 详情请求间隔(限速),毫秒 300
client.timeout.ms HTTP 超时,毫秒 20000
referer 请求头 referer(接口必带) https://www.gz-cmc.com/

3. 编译验证

mvn -q compile

4. 运行两阶段采集

# 阶段一:列表采集(频道 × 关键词全量,去重入库)
mvn test -Dtest=ListCollectTest

# 阶段二:详情采集 + 清洗 + 记者编辑解析(content IS NULL 队列)
mvn test -Dtest=DetailCollectTest

两阶段均可重复运行:阶段一只插缺失 id,阶段二只处理 content IS NULL

5. 数据核对

PGPASSWORD=<db_password> psql -h localhost -U <db_user> -d yangyi \
  -c "select count(*), min(publish_time), max(publish_time) from new_collection.news"

数据库结构(schema new_collection

news 表:

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

采集流程细节

阶段一:列表采集(ListCollectService

  1. channel.ids × keywords 每个组合独立分页调用 getChannelAllContents(带 referer),直至 pageNum > pages
  2. 每条映射为 News;时间格式 yyyy-MM-dd HH:mm:ss,解析失败或不在窗口内则跳过。
  3. 本页 id 批量查已存在 → 仅插入缺失记录。
  4. 此时 content/记者字段均为 NULL,留给阶段二。

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

  1. content IS NULL 记录,按 publish_time 升序逐条处理。
  2. fetchDetail(url) 抓详情页 → DetailCleaner.clean() 清洗 → parsePersons() 解析 → updateById 回写 content/editor/记者各列/content_fetched_at
  3. 清洗规则(严格按 spec):
    • 移除 metascript#hidden-box
    • div.container 直接子 div 仅保留 .article-title.not-exist-media-leader.article-content(及 .article-description),并移除保留区内 video
    • 页面无 div.container(如分享页)时回退保留 div.article-detail / div#articleContent,解析结果可能全 NULL,不报错。
  4. 单条失败仅计数,失败记录保留空 content 供下轮重补;每 100 条打印进度。

记者/编辑解析规则(DetailCleaner.parsePersons

.article-content 纯文本按子句全局正则匹配:

  • 文、图/…记者:X → X 同时进文字记者 + 图片记者
  • 文/…记者:X → 文字记者;图/…记者:X → 图片记者
  • 通讯员[::]?X → 通讯员
  • (广州日报)?新花城编辑:X / 编辑:X → editor(取第一个合法人名)

名称段在遇到 文、图/文/图/视频/通讯员编辑实习生 或行尾时终止捕获;按 /,//空格 拆分后丢弃含 / 的 token(实习生:X图片由受访者提供 等),剥离人名尾部 (…) 括注。

注意:userName(列表接口)与 editor(详情页解析)是两个独立字段,阶段二解析到编辑时才覆盖,勿混用。

产出工具(MyListTest

测试方法 作用
cleanHtmlTest 单个样例 HTML 的清洗结果输出
html2pdfTest 样例 HTML → output/ 下 PDF
html2plaintextTest 样例 HTML 纯文本输出
test() 取最近 100 条 content 非空新闻批量生成 PDF(output/
titleCleanTest 清洗库中标题残留的 HTML 标签并回写
videoCountTest 按窗口统计新闻总量与包含 <video> 的条数,明细写入 videoCountResult.csv
secondSelectTest 读取 videoCountResult.csv,按「标题 ≥1 次 / 正文 ≥2 次关键词」二次筛选,结果写 result.csv
resultCollectTest result.csv,逐条:URL 抓正文 → 下载正文视频到 docN/<序号>/1.ext、2.ext… → 清洗计数 → 生成 PDF(命名 序号-发布时间-标题-编辑.pdf)→ 汇总表格 CSV

运行单条:mvn test -Dtest=MyListTest#videoCountTest

已知要点 / 注意事项

  • collection.properties 含数据库口令,已 gitignore,务必不要提交。仓库内保留的是占位符模板。
  • 列表接口存在偶发 500,客户端统一重试 5 次(间隔 detail.sleep.ms)。
  • 详情页 URL 直接用列表接口返回的 url(若拼接 0000/00/00/ 会 302)。
  • PDF 渲染依赖本机字体:/usr/share/fonts/yangyi/SourceHanSerifCN-Regular.ttf-Bold.ttf,缺失会导致中文不显示/渲染异常。
  • html2pdf 会序列化为 XML 语法并需去重重复属性;页面 CSS 若含 SVG/MathML 需在 pom.xml 取消对应可选依赖注释。
  • sql/init.sql 重跑会清空 newschannel 表。

参考文档

  • todo.md:实现规划(权威规范)
  • todoDetail.md:逐步实施明细、解析变体清单、实测结论