java爬虫;数据采集和数据清洗;JDK HTTP Client、Jsoup、Postgresql、Mybatis。
|
|
5 روز پیش | |
|---|---|---|
| sql | 5 روز پیش | |
| src | 5 روز پیش | |
| .gitignore | 5 روز پیش | |
| README.md | 5 روز پیش | |
| pom.xml | 5 روز پیش | |
| temp.html | 5 روز پیش | |
| todo.md | 5 روز پیش | |
| todoDetail.md | 5 روز پیش |
针对 广州日报新花城 的非遗相关新闻采集、清洗、存储与产出工具集。
全流程分三阶段:
前两个阶段以 JUnit 测试作为启动入口,可重复运行且幂等。
[publish.start, publish.end] 窗口内的记录。data.id 为主键,插入前 selectBatchIds 校验,重复运行不产生重复行。content IS NULL 记录组成待补队列,逐条抓详情页并按 spec 清洗;单条失败不中断,留待下轮重补。、/,/,/空格 分隔、(…)括注剥离、实习生 等杂项过滤。 实体,规避严格 XML 解析器报错。| 组件 | 版本 | 说明 |
|---|---|---|
| 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 # 阶段二:补详情 + 清洗 + 解析 + 更新
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 重建)。
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/ |
mvn -q compile
# 阶段一:列表采集(频道 × 关键词全量,去重入库)
mvn test -Dtest=ListCollectTest
# 阶段二:详情采集 + 清洗 + 记者编辑解析(content IS NULL 队列)
mvn test -Dtest=DetailCollectTest
两阶段均可重复运行:阶段一只插缺失 id,阶段二只处理 content IS NULL。
PGPASSWORD=<db_password> psql -h localhost -U <db_user> -d yangyi \
-c "select count(*), min(publish_time), max(publish_time) from new_collection.news"
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)channel.ids × keywords 每个组合独立分页调用 getChannelAllContents(带 referer),直至 pageNum > pages。News;时间格式 yyyy-MM-dd HH:mm:ss,解析失败或不在窗口内则跳过。content/记者字段均为 NULL,留给阶段二。DetailCollectService)content IS NULL 记录,按 publish_time 升序逐条处理。fetchDetail(url) 抓详情页 → DetailCleaner.clean() 清洗 → parsePersons() 解析 → updateById 回写 content/editor/记者各列/content_fetched_at。meta、script 与 #hidden-box;div.container 直接子 div 仅保留 .article-title、.not-exist-media-leader、.article-content(及 .article-description),并移除保留区内 video;div.container(如分享页)时回退保留 div.article-detail / div#articleContent,解析结果可能全 NULL,不报错。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,务必不要提交。仓库内保留的是占位符模板。detail.sleep.ms)。url(若拼接 0000/00/00/ 会 302)。/usr/share/fonts/yangyi/SourceHanSerifCN-Regular.ttf 与 -Bold.ttf,缺失会导致中文不显示/渲染异常。html2pdf 会序列化为 XML 语法并需去重重复属性;页面 CSS 若含 SVG/MathML 需在 pom.xml 取消对应可选依赖注释。sql/init.sql 重跑会清空 news、channel 表。todo.md:实现规划(权威规范)todoDetail.md:逐步实施明细、解析变体清单、实测结论