@sanmis/crawler
v1.0.1
Published
Enterprise-grade declarative web crawler engine for policy & information monitoring — multi-source, LLM extraction, pluggable industry presets (tax, education, social-security, government)
Maintainers
Readme
@sanmis/crawler
企业级通用 Web 爬虫引擎 — 声明式 DSL,一行配置即可监控任意行业的政策/信息变更
目录
核心理念
信息差 = 竞争力。本引擎解决的核心问题:
- 自动化监控:无人值守监控任意网站的信息变更
- 结构化提取:将非结构化的 HTML 页面转为结构化数据
- 智能差异:精准识别新增/修改/不变的内容
- 零代码配置:通过声明式 DSL 定义数据源,无需编写爬虫代码
- 行业通用:不绑定特定领域,支持税务、教育、社保、金融等所有行业
行业支持
| 行业 | 预置数据源 | Prompt 模板 | 典型场景 |
|------|-----------|-------------|---------|
| 税务 | 中/美/英/港/新/日/OECD 共 7 个 | tax-policy-extraction | 个税、企税、增值税政策监控 |
| 教育 | 中国研招网、美国教育部 共 2 个 | education-policy-extraction | 考研、专升本、四六级、留学生政策 |
| 社保 | 中国人社部、美国 SSA、日本厚劳省 共 3 个 | social-security-extraction | 养老、医保、失业、工伤政策 |
| 政府 | 可配置任意政府网站 | government-announcement-extraction | 国务院/部委公告、政策解读 |
| 通用 | 任意网站 | general-policy-extraction | 所有行业的政策信息提取 |
架构概览
┌─ Source Config DSL ───────────────────────────────────────┐
│ 声明式配置:URL、提取规则、调度策略、LLM 参数、行业分类 │
└──────────────────────┬────────────────────────────────────┘
▼
┌─ Pipeline 管线 ───────────────────────────────────────────┐
│ │
│ Scrape ──► Detail Crawl ──► Extract ──► Normalize │
│ │ │ │ │
│ │ ┌──────────┴──────┐ │ │
│ │ │ CSS LLM Hybrid │ │ │
│ │ └──────────┬──────┘ │ │
│ │ ▼ │ │
│ │ Validate ──► Dedup ──► Diff │ │
│ │ │ │ │
│ └────────────────────────────────────┘ │ │
│ ▼ │ │
│ ReviewGate ──► Store │ │
│ │
└──────────────────────┬────────────────────────────────────┘
▼
┌─ Storage Adapters ────────────────────────────────────────┐
│ PostgreSQL │ Memory (更多适配器开发中) │
└───────────────────────────────────────────────────────────┘核心特性
- 声明式 DSL — JSON/TypeScript 配置即数据源,零爬虫代码
- 4 种爬取器 — HTTP (cheerio) / Browser (Playwright) / RSS / API
- 3 种提取策略 — CSS 选择器 / LLM 语义 / Hybrid 混合
- 智能差异检测 — 哈希 + Jaccard + TF-IDF 语义相似度
- 9 步质量管线 — Scrape → DetailCrawl → Extract → Normalize → Validate → Dedup → Diff → Review → Store
- 三级审查门控 — Auto Approved / Auto Flagged / Pending Review
- 12 个行业预设 — 税务 7 + 教育 2 + 社保 3,覆盖中/美/英/港/新/日/OECD
- 6 个 LLM Prompt 模板 — 税务/教育/社保/政府/通用/文章提取
- 自动调度 — 内置 Cron 调度器 + Webhook 回调
- Dry Run 模式 — 预览抓取结果但不存储
- Proxy + ETag + Cookie — 全传输层支持(含 Browser 模式)
- 并发详情页爬取 — 列表页 → 详情页自动跟进
- 增量模式 — 跳过已处理的 URL,大幅节省请求
- 结构化错误 — SyncError code/category/message/detail/url/stack
- 双存储适配器 — Memory(测试)/ PostgreSQL(生产)
- CJS + ESM 双输出 — 兼容所有 Node.js / Bundler 环境
快速开始
安装
npm install @sanmis/crawler
# 可选:需要浏览器渲染时安装
npm install playwright
npx playwright install chromium5 分钟示例
import { CrawlerEngine } from "@sanmis/crawler"
// 1. 创建引擎(默认不注册任何预设,按需注册)
const engine = new CrawlerEngine({
maxConcurrency: 2,
logLevel: "info",
})
// 2. 配置 LLM(可选,用于语义增强)
if (process.env.LLM_API_KEY) {
engine.useGenericLLM(
process.env.LLM_API_KEY,
process.env.LLM_BASE_URL || "https://api.deepseek.com/v1",
"deepseek-chat"
)
}
// 3. 注册行业预设数据源
await engine.registerTaxPresets() // 税务
// await engine.registerEducationPresets() // 教育
// await engine.registerSocialSecurityPresets() // 社保
// 4. 或注册自定义数据源
engine.registerSource(myCustomSource)
// 5. 从文件/URL 加载配置
// await engine.registerSourceFromFile("./my-source.json")
// await engine.registerSourceFromUrl("https://example.com/source.json")
// 6. 初始化存储
await engine.init()
// 7. 监听新增
engine.on("item:new", (payload) => {
console.log(`New item: ${payload.item?.title}`)
})
// 8. 执行同步
const report = await engine.syncAll()
console.log(report)
// {
// totalNew: 5,
// totalChanged: 2,
// autoApproved: 4,
// pendingReview: 1,
// errors: 0,
// durationMs: 3200
// }按行业/分类同步
// 按分类同步
const report = await engine.syncByCategory("education")
// 按国家同步
const report = await engine.syncByCountry("中国")
// 同步单个数据源
const result = await engine.syncSource("china-tax-announcements")Source Config DSL
核心概念:一个数据源就是一个完整的配置对象,包含传输、爬取、提取、审查的全套规则。
完整示例:中国教育部
const educationSource: SourceConfig = {
id: "china-education-policy",
name: "研招网 — 考研政策",
country: "中国",
language: ["zh-CN"],
category: "education", // 行业分类
// ── 传输配置 ──
transport: {
baseUrl: "https://yz.chsi.com.cn",
rateLimit: { requestsPerMinute: 20, concurrency: 2 },
retry: { maxRetries: 3, backoff: "exponential", baseDelayMs: 1000, retryOnStatus: [429, 503, 502] },
userAgent: { strategy: "rotate" },
timeout: 15000,
// proxy: "http://127.0.0.1:7890", // 代理配置
// cookies: "session=xxx", // 登录态 Cookie
},
// ── 爬取策略 ──
scraper: {
type: "http", // http | rss | browser | api
entryPoints: [
{ url: "https://yz.chsi.com.cn/kyzx/kydt/" },
],
pagination: {
type: "css",
selector: ".pageBox a.next",
maxPages: 3,
},
// 详情页二级爬取:跟进每个链接获取全文
detailLink: {
selector: "a[href*='content']",
attribute: "href",
type: "url",
transform: ["absoluteUrl"],
},
incremental: true, // 增量模式:跳过已处理的 URL
preprocess: {
removeSelectors: ["script", "style", ".footer"],
},
},
// ── 提取规则 ──
extractor: {
strategy: "hybrid", // css | llm | hybrid
cssRules: {
container: ".news-list li a",
fields: {
title: { selector: "$self", type: "text", required: true, transform: ["trim"] },
url: { selector: "$self", attribute: "href", type: "url", transform: ["absoluteUrl"] },
},
},
llmRules: {
enabled: true,
promptTemplate: "education-policy-extraction", // 教育专用模板
condition: "always",
fields: [
{ name: "educationType", description: "教育类型", type: "enum", enumValues: ["postgraduate", "undergraduate_upgrade", "cet"] },
{ name: "examName", description: "考试名称", type: "string" },
{ name: "importantDates", description: "关键日期", type: "array" },
],
},
},
dedup: {
strategies: [
{ type: "url", priority: 1 },
{ type: "title-fingerprint", priority: 2 },
],
ttl: 365,
},
schedule: {
cron: "0 9,15 * * *",
timezone: "Asia/Shanghai",
enabled: true,
},
}注册方式
// 方式1:代码内声明
engine.registerSource(mySource)
// 方式2:从 JSON 文件加载
await engine.registerSourceFromFile("./my-source.json")
// 方式3:从远程 URL 加载
await engine.registerSourceFromUrl("https://config.example.com/source.json")
// 方式4:使用内置行业预设
await engine.registerTaxPresets()
await engine.registerEducationPresets()
await engine.registerSocialSecurityPresets()
await engine.registerAllPresets() // 全部加载管线生命周期
每次 syncSource() 调用会经过以下阶段:
| 阶段 | 组件 | 说明 | |------|------|------| | 1. Scrape | HttpScraper / RssScraper / BrowserScraper | 爬取列表页,支持分页 | | 2. Detail Crawl | Pipeline (Optional) | 跟进详情页链接获取全文 | | 3. Extract | CssExtractor / HybridExtractor | 提取结构化数据 | | 4. Normalize | Normalizer | 标准化(日期/URL/空白字符) | | 5. Validate | Validator | 数据校验,过滤无效 | | 6. Dedup | Deduplicator | 三级去重(URL/标题/内容) | | 7. Diff | DiffEngine | 与历史快照对比 | | 8. Review | ReviewGate | 置信度评分,自动/人工分流 | | 9. Store | StorageAdapter | 持久化 + 更新快照 | | 10. Notify | EventEmitter | 触发事件通知 |
事件系统
engine.on("item:new", async (payload) => {
await db.insert(policyUpdates).values({ ... })
})
engine.on("item:changed", async (payload) => {
// 记录变更历史
})
engine.on("review:needed", async (payload) => {
await sendNotif(`New item needs review: ${payload.item?.title}`)
})提取策略
CSS 提取器(免费,95%+ 场景)
LLM 提取器(付费,边缘场景)
Hybrid 混合提取器(推荐)
内置预设数据源
税务
| ID | 名称 | 国家 |
|----|------|------|
| china-tax-announcements | 国家税务总局 | 中国 |
| us-irs-news | IRS News | 美国 |
| uk-hmrc-news | HMRC News | 英国 |
| hk-ird-press-releases | 香港税务局 | 香港 |
| sg-iras-news | IRAS | 新加坡 |
| jp-nta-news | 日本国税庁 | 日本 |
| oecd-tax-rss | OECD Tax RSS | 国际 |
教育
| ID | 名称 | 国家 |
|----|------|------|
| china-education-policy | 研招网 | 中国 |
| us-education-policy | US Dept of Education | 美国 |
社保
| ID | 名称 | 国家 |
|----|------|------|
| china-social-security-policy | 人社部/医保局 | 中国 |
| us-social-security-policy | SSA & Medicare | 美国 |
| jp-social-security-policy | 厚生劳动省 | 日本 |
API 参考
CrawlerEngine
| 方法 | 说明 |
|------|------|
| syncAll(opts?) | 同步所有启用数据源(支持 { dryRun: true }) |
| syncSource(id, opts?) | 同步单个数据源(支持 { dryRun: true }) |
| syncByCountry(country, opts?) | 按国家同步 |
| syncByCategory(category, opts?) | 按行业分类同步 |
| registerSource(config) | 注册数据源(含配置合法性校验) |
| registerSources(configs) | 批量注册 |
| unregisterSource(id) | 注销数据源 |
| registerSourceFromFile(path) | 从 JSON 文件加载 |
| registerSourceFromUrl(url) | 从远程 URL 加载 |
| registerTaxPresets() | 注册税务预设 |
| registerEducationPresets() | 注册教育预设 |
| registerSocialSecurityPresets() | 注册社保预设 |
| registerAllPresets() | 一次性加载所有预设 |
| startAutoSchedule(opts?) | 开启自动调度(支持 webhook 回调) |
| stopAutoSchedule() | 关闭自动调度 |
| tick() | 手动触发一轮调度 |
| getStats() | 获取引擎运行统计(分类/国家分布、指标) |
| getSources() | 获取所有注册的数据源 |
| useDeepSeek(key) | 设置 DeepSeek |
| useOpenAI(key) | 设置 OpenAI |
| useGenericLLM(key, url, model?) | 设置兼容 OpenAI 的 LLM |
| setStorage(adapter) | 设置存储适配器 |
| init() | 初始化(建表/连接) |
| shutdown() | 停止引擎(关闭调度、断开存储) |
| on(event, handler) | 事件监听 |
Events
| 事件 | 触发时机 |
|------|---------|
| source:start | 数据源开始同步 |
| source:done | 数据源同步完成 |
| source:error | 数据源同步出错 |
| item:new | 发现新增条目 |
| item:changed | 发现变更条目(内容更新) |
| review:needed | 条目需要人工审核 |
| sync:complete | 所有数据源同步完成 |
