service.xpcool.com/internal/service/recruitment/keywords.go
夏犀麟 ad4567fc01 feat: 招聘爬虫健壮性 + 通知记录增强(归档工作区留存改动)
这两批改动此前一直压在工作区未提交,本次一并归档。go build ./... 通过。

一、招聘爬虫健壮性
- 新增 internal/service/recruitment/source_config.go:
  crawl_source.config(VARCHAR(1000) JSON)的源级抓取参数解析 —— 增量窗口天数、
  单次详情页上限、最大翻页数、详情间隔限速、自定义标准词/排除词。
  全部字段可选,JSON 解析失败即回退默认值,保证历史数据零迁移可用,
  且一个源的脏配置不会拖垮整个调度。
- 新增 internal/service/recruitment/keywords.go(含 keywords_test.go 与 testdata/):
  链接筛选由「URL 形态命中 AND 文本语义命中」改为评分制 —— URL 形态加分、
  标准词加分、排除词大幅减分,达阈值即入选。原与逻辑会系统性漏抓
  「补充工作人员的通知」「公开选调公务员简章」等标题变体,且静默无报错。
- 新增 manifest/sql/recruitment/004_crawl_source_status.sql:
  crawl_source 增加「最近一次运行状态」冗余列,抓取结束时写入,
  使 Sources() 查询零 JOIN 零扫描 —— 规避原实现全表扫描只增不减的 crawl_log、
  随运行时间线性劣化的问题。幂等,可重复执行。
- internal/service/recruitment/crawler.go:在既有「静态两级 / SPA / curl 回退」
  流程上做锚点抽取与过滤、编码回退的健壮性增强。
- internal/service/recruitment/bark.go、recruitment.go:配合上述调整。

二、通知记录增强
- api/notice、internal/controller/notice、internal/service/notice、
  internal/model/{dto,entity,do}/notice.go:通知日志查询与操作扩展 ——
  批次号、状态、重试次数、来源、耗时、详情、删除、清空。
- 新增 internal/model/dto/notice_meta.go(字典选项元数据,供前端筛选器取选项)。
- 新增 manifest/sql/017_notice_log_enhance.sql:notice_log 扩展列 +
  删除/清空/详情/字典选项接口权限,幂等(ALTER 走 information_schema 判断,
  菜单 ON DUPLICATE KEY UPDATE,角色绑定 INSERT IGNORE)。

三、仓库卫生
- .gitignore 补 rc_*.log 与 rc_verify*,挡掉本地离线验证产物
  (含 30MB 的 rc_verify.exe),避免误入库。

设计依据见 docs/recruitment-crawler-design.md。

注:017 与 recruitment/004 两个 SQL 为增量迁移,DB 结构变更不自动 DDL,
生产库需手动导入。
2026-09-14 01:22:39 +08:00

171 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package recruitment 的链接筛选词表与评分规则。
//
// 背景:政府站列表页包含大量导航/栏目/互动链接,需要从中挑出「招聘公告详情页」。
// 早期实现要求「URL 形态命中」与「文本语义命中」同时成立,导致「补充工作人员的通知」
// 「公开选调公务员简章」等标题变体被系统性漏抓(且静默无报错)。
//
// 现改为评分制URL 形态加分、标准词加分、排除词大幅减分,达到阈值即入选。
// 词表与阈值均可通过 crawl_source.config 覆盖(见 source_config.go
package recruitment
import "strings"
// defaultIncludeWords 招聘类公告常见语义词(标准词)。
// 命中即认为「像一条招聘公告」,覆盖招聘/考试/人才引进/选调遴选等各口径。
var defaultIncludeWords = []string{
"招聘", "招考", "招录", "招募", "公招",
"选调", "遴选", "选聘", "聘用", "拟聘",
"引进", "人才", "急需紧缺",
"公告", "简章", "启事", "通知", "公示",
"报名", "笔试", "面试", "体检", "考察", "资格复审", "递补",
"考试", "考录", "录用", "录取", "拟录用",
"事业单位", "公务员", "国企", "央企", "编制",
}
// defaultExcludeWords 明确的非公告页面词汇,命中则大幅减分(优先排除)。
// 这些是政府站常见的固定栏目与工具页,其 URL 形态常与详情页相似,仅靠 URL 无法区分。
var defaultExcludeWords = []string{
"政策解读", "常见问题", "办事指南", "下载中心", "联系我们", "网站地图",
"市长信箱", "在线咨询", "意见建议", "问卷调查", "友情链接", "版权声明",
"无障碍", "长者版", "繁體", "英文版", "手机版", "返回顶部",
"信息公开指南", "依申请公开", "年度报告", "财政预决算", "机构职能",
"领导之窗", "内设机构", "直属单位", "RSS",
// 以下为实测中被误抓的信息公开类导航页(贵阳市人社局站点)。
"政府信息公开制度", "政府信息公开年报", "政府非公开信息目录",
"建议提案公开", "重点领域公开", "信息公开目录", "信息公开年报",
"公开指南", "公开制度", "预决算公开", "三公经费", "权责清单",
// 友情链接 / 外部站点入口政府站页脚常见URL 常以 .html 结尾易误判)。
"友情链接", "贵阳人才网", "信用中国", "人才服务中心",
"官方网站", "政务服务网", "返回", "首页",
// 站内智能客服 / AI 助手入口URL 形如 /ai/{id}/index.html
"人社小智", "智能客服", "在线客服", "AI助手", "智能问答",
}
// defaultExcludeHosts 排除的域名关键词——属于「外部链接」而非本站公告。
// 政府站页脚常挂友链(人才网、信用中国等),其 URL 形态与正文页相似,
// 仅靠路径无法区分,故按域名做二级过滤。
var defaultExcludeHosts = []string{
"gyrc.cn", // 贵阳人才网
"cx.guiyang.gov.cn", // 信用中国(贵州贵阳)
"zwfw.guizhou.gov.cn", // 贵州政务服务网
}
// contentSuffixes 内容页 URL 后缀——真实公告正文的特征。
//
// 核心区分逻辑(经实测校准):
// - 栏目目录页以「/」结尾,如 `/zfxxgk/fdzdgklm/zfxxgkrsxx/rszk/`
// - 公告正文以文件后缀结尾,如 `.../rszk/202609/t20260908_90834079.html`。
//
// 注意:两者共享前半段栏目路径,故**绝不能**按路径段判断,
// 只能按「结尾形态」区分——这是本模块最易踩的坑。
var contentSuffixes = []string{".html", ".shtml", ".htm", ".jsp", ".asp", ".php"}
// contentPathHints 内容页路径关键词(部分 CMS 用无后缀的动态路径,如 /art/123456
var contentPathHints = []string{"/art/", "/info/", "/content", "/detail"}
// columnPathHints 栏目目录页的「结尾路径段」——仅当 URL 以此结尾(或以 / 续接)时判为栏目页。
// 用于压制「人事信息」「重点领域公开」等导航链接,但不误伤嵌在同名栏目下的正文。
var columnPathHints = []string{
"/zfxxgk", "/fdzdgklm", "/zdlygk", "/zffgk", "/xxgkml", "/ysqgk",
"/rszk", "/rsxx", "/zwgk", "/tzgg", "/gsgg", "/ksxx", "/zfcg",
"/zfxxgkzn", "/zfxxgkzd", "/zfxxgknb", "/zffgkxxml", "/jytagk",
"/index", "/list", "/more",
}
// scoreAnchor 对单个锚点按评分制打分,分数 >= minScore 视为公告正文页。
//
// 评分规则(阈值可调,见 source_config.go 的 MinScore
// - URL 以内容页后缀结尾(.html 等) +3 ← 最强正向特征
// - URL 命中动态内容路径(/art/、/info/ 等) +2
// - URL 含年月日期路径(/2026/09/13/ +1
// - 文本命中标准词 +2
// - 文本命中排除词 -6强排除
// - URL 为纯目录(以「/」结尾,无文件后缀) -5 ← 栏目页特征
// - 文本过短(<6 字) -2
//
// 设计取舍相比原「URL 与文本必须同时命中」的硬条件,评分制允许
// 「标题不含标准词但 URL 是正文页」的情况入选(修复漏抓),
// 同时以「结尾形态」压制栏目页噪声(避免误抓导航链接)。
func scoreAnchor(a anchor, incWords, excWords []string, minScore int) bool {
text := strings.TrimSpace(a.Text)
lower := strings.ToLower(a.URL)
score := 0
// 0. 域名硬排除:友链/外部站点入口直接判定为非公告(不问评分)。
for _, h := range defaultExcludeHosts {
if strings.Contains(lower, h) {
return false
}
}
// 1. 最强正向特征:以内容页后缀结尾。
hasContentSuffix := false
for _, suf := range contentSuffixes {
if strings.HasSuffix(lower, suf) {
score += 3
hasContentSuffix = true
break
}
}
// 2. 动态内容路径(无后缀的 CMS 详情页)。
if !hasContentSuffix {
for _, p := range contentPathHints {
if strings.Contains(lower, p) {
score += 2
break
}
}
}
// 3. URL 含年月日期路径(内容页的常见特征)。
if yearPathRe.MatchString(a.URL) || dateRe.MatchString(a.URL) {
score++
}
// 4. 文本标准词。
if containsAny(text, incWords) {
score += 2
}
// 5. 负向:排除词(导航/工具页)。
if containsAny(text, excWords) {
score -= 6
}
// 6. 负向:纯目录页。判定依据是「以 / 结尾」——此形态必为栏目而非正文。
// 同时要求 URL 的末段命中栏目路径词,进一步确保是导航目录。
if strings.HasSuffix(lower, "/") {
score -= 5
} else {
for _, p := range columnPathHints {
if strings.HasSuffix(lower, p) {
score -= 5
break
}
}
}
// 7. 负向:过短文本(「更多/详细/MORE」等分页锚点
if len([]rune(text)) < 6 {
score -= 2
}
return score >= minScore
}
// containsAny 判断文本是否命中任一词(不区分大小写,兼顾英文词如 RSS
func containsAny(text string, words []string) bool {
if text == "" || len(words) == 0 {
return false
}
lower := strings.ToLower(text)
for _, w := range words {
if w == "" {
continue
}
if strings.Contains(lower, strings.ToLower(w)) {
return true
}
}
return false
}