首页
/ DevDocs 抓取器体系参考:UrlScraper 与 FileScraper 的配置、过滤管线与响应缓存

DevDocs 抓取器体系参考:UrlScraper 与 FileScraper 的配置、过滤管线与响应缓存

2026-09-05 09:37:21作者:凌朦慧Richard

本文以 docs/scraper-reference.md 为主线,系统讲解 DevDocs(一个 API 文档浏览器)文档抓取器的完整工作模型:如何从根 URL 递归发现并抓取页面、如何为每份文档配置属性与过滤栈、如何在过滤器之前预处理响应,以及响应缓存与版本追踪机制。读完本文,你可以独立编写、调试一个抓取器,并利用 typescript.rb 等现成示例作为参照。

1. 抓取器概述:从根 URL 到本地文件系统

抓取器的工作流程可以概括为一句话:从一个根 URL 出发,递归地跟随符合规则的一组链接,让每个合法响应依次经过一条过滤器链,最终把文件写到本地文件系统;同时构建页面元数据索引(由 EntriesFilter 确定),并在结束时将其转储为 JSON 文件。

抓取器依赖三个核心库(见 docs/scraper-reference.md):

  • Typhoeus:发起 HTTP 请求;
  • HTML::Pipeline:将各过滤器串成管线执行;
  • Nokogiri:解析 HTML。

1.1 两类抓取器

从源码结构看,仓库中存在两种抓取器,分别位于 lib/docs/core/scrapers/url_scraper.rblib/docs/core/scrapers/file_scraper.rb,二者均继承自 lib/docs/core/scraper.rb 中的抽象基类 Docs::Scraper

  • UrlScraper:通过 HTTP 下载文件。
  • FileScraper:从本地文件系统读取。它在读取前把 base URL 替换成本地路径,默认使用占位 base URL localhost,并在过滤栈末尾加入 CleanLocalUrls 过滤器,用于移除所有指向 localhost 的 URL。

这一默认值可以在源码中直接确认(file_scraper.rb#L14-L16):

self.base_url = 'http://localhost/'

html_filters.push 'clean_local_urls'

FileScraper 本质上和 UrlScraper 使用同一套 URL 操作逻辑,唯一的区别是把 base_url 替换为 dir 来读文件(见 file_scraper.rb#L88-L90url_to_path 实现)。

1.2 响应被处理的准入条件

一个响应要进入过滤管线,必须同时满足:

  1. 状态码为 200;
  2. 内容类型为 HTML;
  3. 重定向之后的有效 URL(effective URL)位于 base_url 之内

其中“之内”大致等同于“以 base_url 开头”,但有边界规则:/docs 不算 /doc 的内部(/doc/ 则算)。UrlScraper 的这段判定逻辑实现在 url_scraper.rb#L51-L67

def process_response?(response)
  if response.error?
    raise <<~ERROR ...
  elsif response.blank?
    raise "Empty response body: #{response.url}"
  end

  response.success? && response.html? && process_url?(response.effective_url)
end

def process_url?(url)
  base_url.contains?(url)
end

FileScraper 的判定简单得多——只检查文件存在且非空(file_scraper.rb#L84-L86):

def process_response?(response)
  response.body.present?
end

另外,每个 URL 只会被请求一次(大小写不敏感)。基类 build_pages 用一个 Set 维护已访问历史,仅当 history.add?(url.downcase) 返回 true 时才把该 URL 加入下一轮队列(scraper.rb#L72-L84),这既防止了环路,也天然去除了大小写重复。

2. 配置体系:类属性

抓取器全部通过类属性配置,分为三大类:属性(Attributes)过滤栈(Filter stacks)过滤器选项(Filter options)

命名约束:抓取器位于 lib/docs/scrapers 目录,类名必须是文件名的 CamelCase 等价形式(例如 kotlin.rb 对应 Docs::Kotlin)。

2.1 属性一览

属性 类型 说明
name String 必须唯一。默认为类名。
slug String 必须唯一、小写、不能含连字符(下划线可以)。默认是 name 的小写形式。
type String 必填,可继承。定义添加到每个页面的 CSS 类名(_[type])和自定义 JS 类(app.views.[Type]Page)。结构相似的文档(同一工具生成或源自同一网站)应复用同一 type,避免重复维护 CSS/JS。只能含小写字母。
release String 必填。抓取器最后一次运行时软件所处的版本,仅作信息记录,不影响抓取行为。
base_url String UrlScraper必填。文档所在地址,只会抓取“内部”URL。FileScraper 中默认为 localhost(指向它的内容会被 CleanLocalUrls 过滤器移除;若文档在线可访问,应覆盖该值)。未设置 root_path 时,根/初始 URL 即等于 base_url
base_urls Array include MultipleBaseUrls 模块。用于文档分散在多个 URL、或多个 URL 组合才完整的情况。
root_path String 可继承。根 URL 相对 base_url 的路径。
initial_paths Array 可继承。加入初始队列的路径列表(相对 base_url),适合抓取相互孤立的文档。默认 [](运行时会把 root_path 追加进数组)。
dir String FileScraper 专属必填。文件在本地文件系统的绝对路径。
params Hash 可继承,UrlScraper 专属。追加到每个 URL 上的查询参数,如 { format: 'raw' }?format=raw。默认 {}
abstract Boolean 将抓取器标记为抽象/不可运行,用于与其他类共享行为(例如所有 MDN 抓取器都继承自抽象基类 Mdn)。默认 false

这些属性的默认值与继承行为可以在 lib/docs/core/doc.rb 中看到:name 未显式设置时回退为类名去掉模块前缀,slugname.downcase 推导(doc.rb#L47-L53);abstract 类在实例化时直接抛出异常(doc.rb#L162-L164)。

继承机制的关键在于基类的 inherited 钩子(scraper.rb#L8-L23):子类生成时会 deep_dup 一份 optionsinheritable_copy 一份两个过滤栈,因此子类的修改不会污染父类——这也是多版本文档(version 块)能够各自定制过滤栈的基础。

base_urls 的实现在 url_scraper.rb#L82-L126MultipleBaseUrls 模块中:设置 base_urls 时首个 URL 同时充当 base_urlinitial_urls 会额外并入其余 base URL 作为初始抓取点,而 process_url? 改为“任一 base_url 包含该 URL 即通过”。

2.2 完整配置示例

lib/docs/scrapers/typescript.rb 是一个典型的抓取器配置,集中展示了 nametyperoot_path、过滤栈追加、过滤器选项与多版本声明:

module Docs
  class Typescript < UrlScraper
    self.name = 'TypeScript'
    self.type = 'typescript'

    self.root_path = 'docs/'

    self.links = {
      home: 'https://www.typescriptlang.org',
      code: 'https://github.com/Microsoft/TypeScript'
    }

    html_filters.push 'typescript/entries', 'typescript/clean_html', 'title'

    options[:only_patterns] = [
      /\Adocs\Z/,
      /\Adocs\/handbook/,
      /\Atsconfig/,
    ]
    options[:skip_patterns] = [
      /\Abranding/,
      /\Acommunity/,
      /\Adocs\Z/,
      /\Atools/,
      /react.*webpack/,
      /release-notes/,
      /dt\/search/,
      /play/
    ]

    options[:attribution] = <<-HTML
      &copy; 2012-2026 Microsoft<br>
      Licensed under the Apache License, Version 2.0.
    HTML

    version do
      self.release = '6.0.3'
      self.base_url = 'https://www.typescriptlang.org/'
    end

    version '5.1' do
      self.release = '5.1.3'
    end

    def get_latest_version(opts)
      get_latest_github_release('Microsoft', 'TypeScript', opts)
    end
  end
end

其中 version DSL 会为每个版本生成一个匿名子类并复制父类的 name/slug/release 等属性(doc.rb#L16-L29),版本化的 slug 形如 typescript~5_1+p#s 等规则见 doc.rb#L56-L63)。

3. 过滤栈(Filter Stacks)

每个抓取器持有两个过滤栈(lib/docs/core/scraper.rb#L41-L42):

  • html_filters:先执行,操作的是解析后的文档(Nokogiri 节点对象);
  • text_filters:后执行,把文档当作字符串操作。

二者最终合并为一条 HTML::Pipeline 管线(scraper.rb#L110-L114),每个过滤器的输出成为下一个过滤器的输入。HTML/文本分离的意义在于避免对文档重复解析

3.1 过滤栈的修改方法

过滤栈的行为类似有序集合,可用 FilterStack 提供的以下方法修改:

push(*names)                 # 在末尾追加一个或多个过滤器
insert_before(index, *names) # 在某过滤器之前插入(index 可以是过滤器名)
insert_after(index, *names)  # 在某过滤器之后插入(index 可以是过滤器名)
replace(index, name)         # 用另一个过滤器替换(index 可以是过滤器名)

“names” 是相对 Docsrequire 路径。名字到常量的解析逻辑见 filter_stack.rb#L44-L50

def filter_const(name)
  ...
  Docs.const_get "#{name}_filter".camelize
end

'jquery/clean_html' 会解析为 Docs::Jquery::CleanHtmlFilter 这样的类。此外,Scraper.inherited 里还有 autoload_all "docs/filters/..." 的自动装载(scraper.rb#L11-L14),子类的专属过滤器目录(如 lib/docs/filters/typescript)会在需要时自动加载。

3.2 默认过滤器

文档列出的默认 html_filters(按顺序):

  • ContainerFilter — 更换文档根节点(移除容器之外的一切);
  • CleanHtmlFilter — 移除 HTML 注释、<script><style> 等;
  • NormalizeUrlsFilter — 把所有 URL 替换为完整限定形式;
  • InternalUrlsFilter — 识别内部 URL(待抓取的链接)并替换为未限定的相对形式;
  • NormalizePathsFilter — 让内部路径保持一致(例如始终以 .html 结尾);
  • CleanLocalUrlsFilter — 移除指向 localhost 的链接、iframe 与图片(仅 FileScraper)。

从源码结构看,基类实际 push 的默认栈比文档列表更完整(scraper.rb#L44-L46):

html_filters.push 'apply_base_url', 'container', 'clean_html', 'normalize_urls', 'internal_urls', 'normalize_paths', 'parse_cf_email'
text_filters.push 'images' # ensure the images filter runs after all html filters
text_filters.push 'inner_html', 'clean_text', 'attribution'

其中 apply_base_urlparse_cf_email 在官方文档中未单独列出,文本栈首部的 images 被特意保证在所有 HTML 过滤器之后运行。默认的 text_filters 即文档所述的:

  • InnerHtmlFilter — 把文档转成字符串;
  • CleanTextFilter — 移除空节点;
  • AttributionFilter — 追加版权与原文链接。

此外还有两个特殊过滤器:

  • TitleFilter:核心 HTML 过滤器,默认禁用,为文档头部添加标题(<h1>);
  • EntriesFilter:抽象 HTML 过滤器,每个具体抓取器必须实现,负责提取页面元数据(即最终写入索引的条目)。

4. 过滤器选项(Filter Options)

过滤器选项统一存放在 options Hash 中,该 Hash 可继承(递归拷贝)、默认空。每个抓取器实例化管线时会拿到 options.deep_dup 的副本,并自动合并 base_urlroot_urlroot_pathinitial_pathsversionrelease 等运行时上下文(scraper.rb#L116-L133)。以下是各过滤器常用选项的完整说明。

4.1 ContainerFilter — :container

  • :container [String 或 Proc]:容器元素的 CSS 选择器。容器之外的一切会被移除,其他过滤器也无法再访问。多个元素匹配时取 DOM 中最靠前的一个;若无匹配则抛出错误。值是 Proc 时,对每页以过滤器实例为参数调用,应返回选择器或 nil。默认容器是 <body>

注意:容器之外的链接不会被抓取器跟随。要移除那些“其实需要跟随”的链接,应在栈的后半段使用一个 CleanHtml 过滤器。

4.2 NormalizeUrlsFilter — URL 改写规则

该过滤器用于移除重复页(同一页面可从多个 URL 访问)以及修复“重定向过多”的网站(本应被抓取的 URL 藏在 base_url 之外的重定向后面,MDN 抓取器即为此类示例)。

  • :replace_urls [Hash]:把某个 URL 的所有出现替换为另一个。格式 { 'original_url' => 'new_url' }
  • :replace_paths [Hash]:把某个子路径(相对 base_url)的所有出现替换为另一个。格式 { 'original_path' => 'new_path' }
  • :fix_urls [Proc]:对每个 URL 调用;返回 nil 表示不修改,否则返回值用作替换。

这些规则应用之前,所有 URL 会先被转换为完整限定形式(http://...)。

4.3 InternalUrlsFilter — 抓取范围控制

内部 URL 即位于 base_url 之内的 URL(/docs 不属于 /doc 内部)。它们默认会被抓取,除非被以下规则排除;所有内部 URL 在页面内都会转换为相对 URL:

  • :skip_links [Boolean 或 Proc]:为 false 时不转换、也不跟随任何内部 URL(形成单页文档);为 Proc 时对每页以过滤器实例为参数调用;
  • :follow_links [Proc]:对每页调用;返回 false 时不把该页的内部 URL 加入队列;
  • :trailing_slash [Boolean]true 给所有内部 URL 追加末尾斜杠,false 则移除。这是去除重复页的又一手段;
  • :skip [Array]:忽略子路径在数组中的内部 URL(大小写不敏感);
  • :skip_patterns [Array]:忽略子路径匹配任一正则的内部 URL;
  • :only [Array]:忽略子路径不在数组中(大小写不敏感)且不匹配 :only_patterns 中任一正则的内部 URL;
  • :only_patterns [Array]:忽略子路径不匹配任一正则且不在 :only 中的内部 URL。

两条自动规则(文档规则,且与源码一致,见 scraper.rb#L122-L128):

  • 若抓取器设置了 root_path,空路径与 / 会被自动加入跳过;
  • 若设置了 :only:only_patterns,根路径会被自动追加进 :only

提示:也可以借助 Entries 过滤器按内容把页面排除出索引。此时这些 URL 仍会在其他页面中被转成相对链接,点击会得到 404。虽非理想,但通常比维护一长串 :skip 列表更好。

4.4 AttributionFilter 与 TitleFilter

  • AttributionFilter:attribution [String](必填):包含版权与许可信息的 HTML 字符串,可参考其他抓取器中的写法(如上节 typescript.rb 示例)。
  • TitleFilter(默认禁用):
    • :title [String 或 Boolean 或 Proc]:值不为 false 时为每页添加标题。值为 nil 时取 Entries 过滤器确定的页面名;否则取该 String 或 Proc 的返回值(Proc 每页调用一次,返回 nil/false 时不添加标题);
    • :root_title [String 或 Boolean]:仅对根页覆盖 :title

5. 过滤器之前的响应预处理

过滤栈执行前有两个可直接处理响应的扩展点(文档原文如此,基类中均为 raise NotImplementedError,见 scraper.rb#L145-L147):

5.1 process_response?(response)

决定是否处理某个响应:返回 false 时该响应被丢弃。适合按内容过滤空页、无效页或重定向页。参考示例:lib/docs/scrapers/kotlin.rbUrlScraper 的默认实现(url_scraper.rb#L51-L63)即上文 1.2 节的三重检查,遇到非 2xx 会直接抛错并把请求头打印出来便于排障。

5.2 parse(response)

解析 HTTP/文件响应,默认转换为 Nokogiri 文档。若希望在 Nokogiri 解析前修改 HTML 源码,可覆盖此方法——典型用途是保留非 <pre> 代码块中代码片段的空白(Nokogiri 可能会删除它们)。参考示例:lib/docs/scrapers/go.rb。默认的解析实现在 scraper.rb#L187-L190,经由 Parser 返回 [html, title]

6. 响应缓存

UrlScraper 会把获取到的每个响应存到 tmp/cache/[slug] 目录,后续运行直接从中读取。因此调整过滤器后再次执行 thor docs:generate 很快,也不会给源站点造成任何负担(FileScraper 不需要缓存,它本来就读本地文件系统)。

关键行为(结合 lib/docs/core/response_cache.rb 源码):

  • 缓存永不过期。运行 thor docs:clean 清空,这是在源站更新后拿到新内容的必要步骤。ResponseCache.clean 通过每个缓存目录里的 .scraper_cache 标记文件来定位各抓取器的缓存(response_cache.rb#L29-L33);
  • 只缓存成功响应,超时、404 与服务器错误会在下次运行时重新请求;
  • 每个响应单独存为一个文件,文件名取自请求的哈希——因此修改抓取器的 paramsheaders 会使缓存失效;
  • 文件为 JSON,条目结构遵循 HAR(HTTP Archive)规范,方便直接查看抓取器拿回了什么。示例(无关字段已省略):
{
  "startedDateTime": "2026-08-15T11:05:10.430Z",
  "time": 412,
  "request": {
    "method": "GET",
    "url": "https://vite.dev/guide/",
    "headers": [{ "name": "User-Agent", "value": "DevDocs" }]
  },
  "response": {
    "status": 200,
    "headers": [{ "name": "Content-Type", "value": "text/html; charset=utf-8" }],
    "content": { "size": 57302, "mimeType": "text/html; charset=utf-8", "text": "<!doctype html>…" }
  },
  "_effectiveUrl": "https://vite.dev/guide/"
}

重定向被透明跟随,所以一条目只保存重定向链上最后一个响应,_effectiveUrl 记录最终到达的 URL。缓存与抓取器的接线见 url_scraper.rb#L47-L49

def response_cache
  @response_cache ||= ResponseCache.new(File.join(Docs.cache_path, self.class.slug))
end

7. 保持抓取器与文档同步

要让抓取器持续跟进上游文档版本,应覆盖 get_latest_version(opts) 方法,其契约(doc.rb#L186-L192):

  • 若定义了 self.release:返回文档的最新版本;
  • 若未定义 release:返回文档最后修改的 Epoch 时间;
  • 若文档永不变化:直接返回 1.0.0

该方法的结果会被定期汇总到一个 “Documentation versions report” issue 中,帮助维护者追踪过时的文档。基类还提供了 outdated_statedoc.rb#L205-L220),按 semver 风格比较前两位版本号,区分 “Outdated major version”“Outdated minor version” 与 “Up-to-date”(补丁版本变化不视为过时);不符合 semver 习惯的文档应自行覆盖。

为简化 get_latest_version 的编写,Doc 提供了一批工具方法(doc.rb#L228-L307):

通用 HTTP 方法

方法 说明 示例
fetch(url, opts) GET 请求,返回响应体 lib/docs/scrapers/bash.rb
fetch_doc(url, opts) GET 请求,返回转为 Nokogiri 文档的 HTML lib/docs/scrapers/git.rb
fetch_json(url, opts) GET 请求,返回转为字典的 JSON lib/docs/scrapers/mdn/mdn.rb

包仓库方法

  • get_npm_version(package, opts):返回指定 npm 包的最新版(内部请求 https://registry.npmjs.com/<package> 并读取 dist-tags)。示例:lib/docs/scrapers/bower.rb

GitHub 方法

  • get_latest_github_release(owner, repo, opts):返回最新 release 的 tag 名,前缀 v 会被去掉。示例:lib/docs/scrapers/jsdoc.rb
  • get_github_tags(owner, repo, opts):返回仓库的 tag 列表。示例:lib/docs/scrapers/liquid.rb
  • get_github_file_contents(owner, repo, path, opts):返回默认分支上指定文件的内容(对返回的 base64 内容解码)。示例:lib/docs/scrapers/minitest.rb
  • get_latest_github_commit_date(owner, repo, opts):返回默认分支最近一次提交的日期(Epoch 秒)。示例:lib/docs/scrapers/reactivex.rb

GitLab 方法

  • get_gitlab_tags(hostname, group, project, opts):返回指定 GitLab 项目的 tag 列表。示例:lib/docs/scrapers/gtk.rb

注意 fetch 系列会自动携带 GitHub Token:优先 opts[:github_token],回退到环境变量 GITHUB_TOKENdoc.rb#L228-L235),这对避免 API 限流很有用。前文 typescript.rb 末尾的 get_latest_version 就是最典型的用法——直接调用 get_latest_github_release('Microsoft', 'TypeScript', opts)

8. 小结:一次抓取的完整调用链

把前述机制串起来,UrlScraper 处理一页文档的完整链路为:

  1. build_pagesinitial_urls(根 URL + initial_paths)出发并发请求(带缓存命中检查);
  2. process_response? 做 200/HTML/base_url 三重准入;
  3. parse 产出 Nokogiri 文档(可覆盖以预处理源码);
  4. HTML::Pipeline 依次执行 html_filters + text_filters,选项来自 options
  5. InternalUrlsFilter 产出下一批待抓 URL(经 :skip/:only/大小写历史去重);
  6. 各页的 entriesoutput 汇总后写入存储:页面文件、index.json(索引)、db.json(页面数据库)与 meta.json(含 mtime、db_size,见 doc.rb#L154-L159)。

若某份文档需要先“构建”出 HTML 再抓取(如 R、Rails、Scala 3 等),则改用 FileScraper 并按 docs/file-scrapers.md 中对应小节的说明准备 docs/<slug> 目录;更多过滤器细节可查阅 docs/filter-reference.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384