Browse Source

docs:readme文件

yangyi 5 days ago
parent
commit
c61f674a3c
1 changed files with 210 additions and 0 deletions
  1. 210 0
      README.md

+ 210 - 0
README.md

@@ -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`:逐步实施明细、解析变体清单、实测结论