|
@@ -0,0 +1,210 @@
|
|
|
|
|
+# 非遗新闻采集(newCollection)
|
|
|
|
|
+
|
|
|
|
|
+针对 [广州日报新花城](https://www.gz-cmc.com/) 的非遗相关新闻采集、清洗、存储与产出工具集。
|
|
|
|
|
+
|
|
|
|
|
+全流程分三阶段:
|
|
|
|
|
+
|
|
|
|
|
+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`):
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+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` 并填入真实口令:
|
|
|
|
|
+
|
|
|
|
|
+```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. 编译验证
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+mvn -q compile
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4. 运行两阶段采集
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 阶段一:列表采集(频道 × 关键词全量,去重入库)
|
|
|
|
|
+mvn test -Dtest=ListCollectTest
|
|
|
|
|
+
|
|
|
|
|
+# 阶段二:详情采集 + 清洗 + 记者编辑解析(content IS NULL 队列)
|
|
|
|
|
+mvn test -Dtest=DetailCollectTest
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+两阶段均可重复运行:阶段一只插缺失 id,阶段二只处理 `content IS NULL`。
|
|
|
|
|
+
|
|
|
|
|
+### 5. 数据核对
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+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):
|
|
|
|
|
+ - 移除 `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,不报错。
|
|
|
|
|
+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` 重跑会清空 `news`、`channel` 表。
|
|
|
|
|
+
|
|
|
|
|
+## 参考文档
|
|
|
|
|
+
|
|
|
|
|
+- `todo.md`:实现规划(权威规范)
|
|
|
|
|
+- `todoDetail.md`:逐步实施明细、解析变体清单、实测结论
|