文档开发工具后端前端【免费下载链接】devdocsAPI Documentation Browser项目地址https://gitcode.com/GitHub_Trending/de/devdocs点击查看免费下载导读本指南围绕 devdocs 项目的 docs/filter-reference.md 展开系统讲解项目核心的过滤器Filter机制从基于 HTML::Pipeline 的管道模型、HTML 过滤器与文本过滤器的分工到Docs::Filter基类提供的全部实例方法、13 个内置核心过滤器再到自定义CleanHtmlFilter与EntriesFilter的完整编写规范。读完本文你将掌握如何为任意文档站点编写一个可用的 scraper 过滤器并理解页面元数据entries是如何被提取、索引并最终呈现在 devdocs 侧边栏中的。一、Overview过滤器与管道的运行模型在 devdocs 中过滤器是处理文档页面的最小单元。它们基于 HTML::Pipeline 库实现每个过滤器接收一段 HTML 字符串或一个 Nokogiri 节点对象作为输入执行修改和/或信息提取然后输出结果。多个过滤器首尾相接形成一条管道pipeline前一个过滤器的输出恰好是后一个过滤器的输入。每一份文档页面在写入本地文件系统之前都必须完整经过这条管道。从 lib/docs/core/scraper.rb 的源码可以看到管道是如何组装的def pipeline pipeline || ::HTML::Pipeline.new(self.class.filters).tap do |pipeline| pipeline.instrumentation_service Docs end end而self.class.filters正是由两类过滤器堆栈拼接而成html_filters text_filters对应 lib/docs/core/filter_stack.rb 的实现。1.1 两类过滤器HTML 过滤器与文本过滤器过滤器按操作对象分为两类这是 devdocs 管道的核心设计约束HTML 过滤器操作 Nokogiri 节点对象doc及其相关方法。它们必须先于文本过滤器执行。文本过滤器操作文档的字符串表示html。它们绝不能在 Nokogiri 节点对象上做手脚。⚠️硬性规则HTML 过滤器禁止修改 HTML 字符串文本过滤器禁止修改 Nokogiri 节点对象。call方法的返回值必须与过滤器类型一致——HTML 过滤器返回doc文本过滤器返回html。这样分工的唯一原因是避免文档被反复解析既然 HTML 过滤器已经完成了解析并提取了节点对象后续对字符串的清洗就不必再次 Nokogiri 化。这一点在 docs/scraper-reference.md 的 Filter stacks 一节中有同样明确的说明。1.2 默认过滤器堆栈lib/docs/core/scraper.rb 中定义了所有 scraper 共享的默认堆栈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可以看到 HTML 过滤器堆栈依次为apply_base_url→container→clean_html→normalize_urls→internal_urls→normalize_paths→parse_cf_email文本过滤器堆栈为images→inner_html→clean_text→attribution。子类 scraper 通过html_filters.push/text_filters.push追加自己的自定义过滤器如async的async/entries、async/clean_html。FilterStack 的堆栈操作方法见 lib/docs/core/filter_stack.rbpush(*names) # 在栈尾追加一个或多个过滤器 insert_before(index, *names) # 在某个过滤器之前插入index 可以是名称 insert_after(index, *names) # 在某个过滤器之后插入index 可以是名称 replace(index, name) # 用另一个过滤器替换index 可以是名称1.3 最小的过滤器实现所有过滤器都继承自Docs::Filterlib/docs/core/filter.rb并必须实现call方法。一个最基本的 HTML 过滤器长这样module Docs class CustomFilter Filter def call doc end end end1.4 命名与文件位置约定自定义过滤器存放在 lib/docs/filters 目录下类名必须是文件名的 CamelCase 形式。例如文件lib/docs/filters/async/clean_html.rb中的类名是Async::CleanHtmlFilter而 lib/docs/core/filter_stack.rb 中filter_const的常量解析逻辑Docs.const_get #{name}_filter.camelize也印证了这一点——堆栈中写的async/clean_html最终会被转成Async::CleanHtmlFilter类。二、Instance methodsFilter 基类实例方法全解lib/docs/core/filter.rb 是Docs::Filter的完整实现它继承了HTML::Pipeline::Filter。以下是文档列出的全部实例方法及其源码级说明。2.1 核心数据访问方法类型说明docNokogiri::XML::Node容器元素的 Nokogiri 表示可用 Nokogiri 的节点 API 进行操作htmlString容器元素的字符串表示contextHash(frozen)即 scraper 的options外加:base_url、:root_url、:root_page、:url等额外键resultHash存储页面元数据并向 scraper 回传信息的结果容器context的构成可从 lib/docs/core/scraper.rb 的options方法还原它由self.class.options深度拷贝后合并:base_url、:root_url、:root_path、:initial_paths、:version、:release等键最终被freeze冻结。此外pipeline_contextlib/docs/core/scraper.rb还会在每次响应处理时把url: response.url并入其中即context[:url]是当前页面的真实 URL。result的可用键:path—— 页面归一化后的路径不含扩展名根页为index:store_path—— 页面实际存储路径等于:path加上.html后缀:internal_urls—— 页面内发现的、去重后的内部 URL 列表:entries—— 需要加入索引的Entry对象数组result键的写入方是核心过滤器:subpath由InternalUrlsFilter写入lib/docs/filters/core/internal_urls.rb:path与:store_path由NormalizePathsFilter写入lib/docs/filters/core/normalize_paths.rb:entries由EntriesFilter写入lib/docs/filters/core/entries.rb。2.2 查询与 URL 快捷方法css、at_css、xpath、at_xpathdoc.css、doc.xpath等的简写实现于 lib/docs/core/filter.rb底层直接委托给 Nokogiri 的节点 API。base_url、current_url、root_url分别为context[:base_url]、context[:url]、context[:root_url]的简写返回Docs::URL对象。Docs::URL继承自URI::Generic见 lib/docs/core/url.rb额外提供了subpath_to、relative_path_to等路径计算能力。root_pathcontext[:root_path]的简写。version、release、links基类中还提供了这三个方法lib/docs/core/filter.rb分别对应context[:version]、context[:release]、context[:links]文档中未提及但实际可用。2.3 路径计算subpath 与 slugsubpath当前 URL 相对 base URL 的子路径。实现于 lib/docs/core/filter.rb通过base_url.subpath_to(current_url, ignore_case: true)计算。底层算法在 lib/docs/core/url.rb先比较 originscheme host port再取 dest 在 base 之后的部分。示例base_url为example.com/docs、current_url为example.com/docs/file?raw时返回/file。slugsubpath去掉开头的/和末尾的.html扩展名。实现为subpath.sub(/\A\//, ).remove(/\.html\z/)lib/docs/core/filter.rb。示例subpath为/dir/file.html时返回dir/file。2.4 页面身份判断root_page?当前页面是否为根页面。判定条件是subpath.blank? || subpath / || subpath root_pathlib/docs/core/filter.rb。initial_page?当前页面是否为根页面或subpath命中 scraper 的initial_paths之一lib/docs/core/filter.rb。2.5 URL 类型判断与路径清洗辅助方法基类还提供了一组实用的工具方法文档未逐一列举但在核心过滤器中被广泛使用fragment_url_string?(str) # 是否为 #fragment data_url_string?(str) # 是否为 data: URI relative_url_string?(str) # 是否为相对 URL absolute_url_string?(str) # 是否为绝对 URL含 scheme clean_path(path) # 将 !;: 替换为 -将 替换为 _plus_ parse_html(html) # 解析 HTML若非测试环境重复解析会发出警告例如InternalUrlsFilter正是用absolute_url_string?筛选候选内部链接NormalizePathsFilter用relative_url_string?判断需要归一化的 href。此外clean_path在decode_and_clean_paths选项开启时会被调用。三、Core filters13 个内置核心过滤器逐一解析以下每个核心过滤器都位于 lib/docs/filters/core 目录其源码可在仓库中直接查阅。3.1ContainerFiltercontainer.rb职责更换文档的根节点移除容器以外的所有内容。实现要点它读取context[:container]——既可以是一个 CSS 选择器字符串也可以是一个Proc执行后返回选择器。找到容器后doc.at_css(container)作为新的根找不到则抛出ContainerNotFound异常。若未配置容器则退回body对完整 HTML 文档或原doc对片段文档。3.2CleanHtmlFilterclean_html.rb职责移除 HTML 注释、script、style、link等非文档内容并把普通文本中的连续空白折叠为单个空格。注意两点实现细节空白折叠会跳过pre、code以及带prismclass 的div内的文本避免破坏代码与语法高亮content.valid_encoding?检查保证了非法编码文本不会被处理。每个 scraper 还必须自定义一个CleanHtmlFilter见下文第四节用于处理该站点特有的脏标记。3.3NormalizeUrlsFilternormalize_urls.rb职责把a[href]、img[src]、iframe[src]上的所有 URL 替换为完整限定的绝对 URL跳过#fragment和data:URI。源码中的处理链剥离空白 → 空格转%20→ 可选:fix_urls_before_parse回调 → 解析为绝对 URL → 循环应用:redirections路径大小写不敏感重定向、:replace_paths、:replace_urls、:fix_urls等修复规则 → 输出。若 URL 非法URI::InvalidURIError非 iframe 的链接被替换为#。3.4InternalUrlsFilterinternal_urls.rb职责识别内部 URL即后续需要抓取的页面并把它们替换为非限定、相对的链接同时把去重后的内部 URL 列表写入result[:internal_urls]供 scraper 递归爬取。关键机制to_internal_url要求 URL 是绝对 URL、且subpath_to(url)不为空即同源且位于 base 之下再经过normalize_subpath受:trailing_slash选项控制与skip_subpath?过滤。skip_subpath?综合:only/:only_patterns/:skip/:skip_patterns选项决定哪些子路径不视为内部链接。internal_path_to通过effective_url.relative_path_to(url)lib/docs/core/url.rb计算从当前页面到目标页面的相对路径并保留 query 与 fragment指向根 URL 的链接会被归一化为索引页路径。skip_links?与follow_links?分别受:skip_links与:follow_links选项控制两者都支持Proc形式的动态判断。3.5NormalizePathsFilternormalize_paths.rb职责让内部路径保持一致例如统一以.html结尾并写入result[:path]与result[:store_path]。路径归一化规则normalize_pathnormalize_paths.rb整体转小写开启:decode_and_clean_paths时先做 URL 解码再用clean_path清洗.变成index以/结尾的路径追加index.html/.md后缀被去掉。根页面路径固定为index。该过滤器还会把文档中仍为相对路径的href/xlink:href逐一归一化。3.6CleanLocalUrlsFilterclean_local_urls.rb职责仅当base_url.host localhost即FileScraper抓取本地文件时生效——移除指向http://localhost的图片与 iframe并把指向 localhost 的a降级为span去掉 href从而防止离线文档出现指向本机的死链接。3.7InnerHtmlFilterinner_html.rb职责HTML 过滤器阶段与文本过滤器阶段的转换器——把 Nokogiri 节点转回字符串doc.inner_html供后续文本过滤器使用。若字符串非法编码会先转成 UTF-16丢弃非法字节再转回 UTF-8保证下游拿到的是合法字符串。3.8CleanTextFilterclean_text.rb职责删除“空节点”——用正则EMPTY_NODES_RGX反复匹配并删除内容仅为空白的标签对td、th、iframe、mspace及若干 SVG 元素被排除避免误删表格单元格与图形。可通过context[:clean_text] false关闭。该过滤器会调用html.strip!并返回字符串是一个典型的文本过滤器。3.9AttributionFilterattribution.rb职责把版权/许可信息context[:attribution]可以是字符串或Proc以_attribution块的形式追加到文档末尾并附上指向原始页面的链接本地抓取localhost时不生成链接。正是它保证了 devdocs 中的文档都保留原作者署名与出处。3.10ImagesFilterimages.rb职责下载页面图片并内联为 data URI同时做体积优化与格式重编码——PNG/GIF 转为无损 WebPJPEG 转为有损 WebPq80、开启-sharp_yuv保持截图与示意图边缘锐利。关键参数DEFAULT_MAX_SIZE 120_000120 KB可通过:max_image_size覆盖context[:download_images] false可整体关闭context[:optimize_images] false可跳过 image_optim 优化。转换结果比原图大时webp.bytesize data.bytesize会自动放弃转换因此不会出现“越优化越大”的情况。所有请求经Request.run异步完成损坏/非图片/超限资源会以broken.image、invalid.image、too_big.image等事件上报。3.11TitleFiltertitle.rb职责在文档开头插入一个h1标题节点默认关闭。标题来源优先级根页面的:root_title→:title字符串或Proc→ 默认取result[:entries].first.name即首个 entry 的名称。开启后适合给原本无标题的抓取页面补充标题层级。3.12EntriesFilterentries.rb职责抽象过滤器用于提取页面元数据是所有自定义 entries 过滤器的基类。其默认实现只做一件事result[:entries] entries随后把默认 entry 与additional_entries合并为Entry对象列表详细机制见下节。它是一个HTML 过滤器必须加入html_filters堆栈。补充core 目录下还有一个未在文档中列出的 parse_cf_email.rb它作为默认 HTML 堆栈的一环处理 Cloudflare 邮箱混淆链接与上述过滤器一起构成了完整的默认管道。四、Custom filters自定义过滤器的编写规范每个 scraper 可以拥有任意数量的自定义过滤器但至少必须实现下面两个CleanHtmlFilter与EntriesFilter。它们位于 lib/docs/filters 目录下类名必须是文件名的 CamelCase 形式。4.1CleanHtmlFilter清洗页面标记CleanHtmlFilter的任务是在必要处清洗 HTML 标记移除一切多余或非必要的元素最终只保留核心文档内容。Nokogiri 提供了大量 jQuery 风格的查询与修改方法css、at_css、remove、name、content等让这项工作变得简单。文档给出的覆盖最常见场景的完整示例module Docs class MyScraper class CleanHtmlFilter Filter def call css(hr).remove css(#changelog).remove if root_page? # 把空 a 上的 id 转移到 h3 上 css(h3).each do |node| node[id] node.at_css(a)[id] end # 把伪表头 td classheader 改成真正的 th css(td.header).each do |node| node.name th end # 去除代码高亮把 pre 内的 HTML 展开为纯文本 css(pre).each do |node| node.content node.content end doc end end end end编写要点文档明确强调空元素无需手动删除——管线后续的核心CleanTextFilter会自动清理空节点。目标是得到干净页面但修改次数应尽量少以便维护。页面样式归一化优先用自定义 CSS 完成隐藏内容永远通过删除标记实现而非 CSS。尽量为过滤器的每个行为尤其是只影响部分页面的修改写注释这会显著降低后续文档升级时的维护成本。仓库中的真实案例 lib/docs/filters/async/clean_html.rb 展示了更多技巧用node.before(node.children).remove把section/header/article等容器“解包”成子节点、用正则node.name.sub(/\d/) { |i| i.to_i - 1 }把 h3–h5 整体降级为 h2–h4、把ddul列表合并成纯文本、并为pre注入data-language属性。4.2EntriesFilter提取页面元数据EntriesFilter负责提取页面的元数据由一组entries表示每个 entry 包含名称name、类型type和路径path三个属性。底层使用两个模型均在 lib/docs/core/models 目录Entry(name, type, path)索引中的最小条目。构造时校验 name/path/type 均非空根条目除外其path indexas_json输出{name, path, type}供前端索引使用。Type(name, slug, count)条目的类型分组。slug由name.parameterize生成as_json会合并 slug。每个 scraper 必须通过继承Docs::EntriesFilter实现自己的 entries 过滤器。基类已实现call方法lib/docs/filters/core/entries.rb合并默认 entry 与 additional entries逐个构造Entry对象后写入result[:entries]。子类只需覆盖以下四个方法可覆盖方法类型说明默认值get_nameString默认 entry 的名称即页面名通常从slug或 HTML 标记中推断slug的变体下划线换成空格、斜杠换成点get_typeString默认 entry 的类型。无类型的 entry 可以被搜索到但不会出现在应用侧边栏除非没有其他带类型的 entrynilinclude_default_entry?Boolean是否包含默认 entry。用于页面只有附加 entries 而无自身名称/类型时或把整页从索引中移除此时页面不会被写入本地文件系统指向它的链接会断裂——这也是保持:skip/:skip_patterns选项体积可控、或处理无法从别处到达的链接页面的手段trueadditional_entriesArray附加 entries 列表[]additional_entries的数组格式每个元素是三个属性的数组[name, fragment, type]——名称、锚点标识、类型。锚点标识指向 HTML 元素的id通常是标题它会与页面路径拼接成 entry 的完整路径缺省或为nil时用页面路径类型缺省或为nil时用默认 type。例如[ [One], [Two, id], [Three, nil, type] ]表示三个附加 entries名 One默认路径、默认类型、名 Two路径带#id锚点、默认类型、名 Three默认路径、类型为 type。该列表通常通过遍历标记构造特定页面的例外也可以硬编码。以下访问器已实现、但禁止覆盖它们提供了 memoized 版本避免重复计算nameget_name的 memoized 结果根页面为nil。源码实现见 lib/docs/filters/core/entries.rb注意root_page?时直接返回nil而不调用get_name。typeget_type的 memoized 结果根页面为nil。关键注意事项文档逐条强调name 与 type 的首尾空白会被自动去除Entry#name/#type内部调用strip。name 在整个文档库中必须唯一且尽可能短理想小于 30 字符。方法尽量用()后缀与属性区分实例方法与类方法尽量遵循Class#method或object.method约定。可以在get_type里调用name或在get_name里调用type但二者同时调用会栈溢出只能单向推导。不要直接调用get_name/get_type因为它们的值没有被 memoized。根页面没有 name 和 type均为nil此时get_name、get_type不会被调用但additional_entries会。Docs::EntriesFilter是HTML 过滤器必须加入 scraper 的html_filters堆栈。尽量为代码尤其是特殊分支写注释便于后续更新维护。完整示例文档提供module Docs class MyScraper class EntriesFilter Docs::EntriesFilter def get_name node at_css(h1) result node.content.strip result event if type Events result () if node[class].try(:include?, function) result end def get_type object, method *slug.split(/) method ? object : Miscellaneous end def additional_entries return [] if root_page? css(h2).map do |node| [node.content, node[id]] end end def include_default_entry? !at_css(.obsolete) end end end end这个例子几乎覆盖了所有典型手法get_name从h1取标题并按type追加 event 后缀、按class追加()get_type从slug的路径段推断类型两级路径视为对象/方法单级归为 Miscellaneousadditional_entries遍历所有h2生成带锚点的条目include_default_entry?在页面含.obsolete元素时排除默认 entry。仓库中 lib/docs/filters/async/entries.rb 是一个真实范例它遍历.nav.methods li导航节点遇到toc-header就切换当前类型type其余节点则把名称、id从a[href]去掉#、当前类型组合成三元组加入 entries——完整演示了类型分组 锚点条目的常见模式。五、把过滤器接到 scraper 上端到端流程要理解过滤器如何工作还需要看它在 scraper 中的完整调用链lib/docs/core/scraper.rb爬取build_pages从initial_urls根 URL initial_paths出发递归抓取每次发现新的internal_urls就加入队列继续scraper.rb。解析process_response用Parser把响应体解析为 HTML 片段与标题构建context options.merge(url: response.url)scraper.rb。过管道pipeline.call(html, context, data)依次执行 HTML 过滤器堆栈 → 文本过滤器堆栈所有过滤器共享同一个context各自向data即result写入键值。产出handle_response返回包含:path、:store_path、:entries、:internal_urls等键的数据最终页面写入本地文件系统entries 汇入 JSON 索引。值得注意的是options在进入管道前会被freezescraper.rb因此过滤器只能读取context不能修改——这保证了同一 scraper 处理多个页面时的确定性。六、编写过滤器的最佳实践清单综合 docs/filter-reference.md 与源码实现编写高质量的过滤器应遵循以下原则保持最小修改能用 CSS 归一化的样式问题不要用过滤器处理需要隐藏的内容一律通过移除标记完成。严格遵守过滤器类型边界HTML 过滤器只动doc并返回doc文本过滤器只动html并返回html。这是防止反复解析的关键也是文档强调的核心约束。entries 命名规范化name 全库唯一、尽量短30 字符、用()/Class#method约定区分方法、实例方法与类方法。善用 memoized 访问器在get_type中用name、或在get_name中用type做单向推导但切忌双向互推导致栈溢出。充分注释尤其对只影响部分页面的修改和硬编码例外让后续文档更新者能快速理解意图。验证真实调用链新过滤器加入堆栈前先确认它在默认管道apply_base_url→container→clean_html→normalize_urls→internal_urls→normalize_paths→parse_cf_email→images→inner_html→clean_text→attribution中的位置是否合理——例如EntriesFilter必须在internal_urls之后依赖result[:path]、ImagesFilter必须在所有 HTML 过滤器之后运行。至此从 HTML::Pipeline 的管道模型、Filter 基类的每个实例方法、13 个核心过滤器的内部实现到自定义CleanHtmlFilter/EntriesFilter的完整编写范式你已经掌握了 devdocs 文档抓取体系中最核心的过滤器机制。结合 docs/scraper-reference.md 中的配置项与堆栈操作说明即可为任意目标文档站点编写出自己的 scraper。赞分享文档开发工具后端前端【免费下载链接】devdocsAPI Documentation Browser项目地址https://gitcode.com/GitHub_Trending/de/devdocs点击查看免费下载相关推荐大麦网抢票 Python 脚本完整教程从环境搭建到确认出票大麦网抢票 Python 脚本完整教程从环境搭建到确认出票 热门演出往往在开售几秒内就售空纯靠手动点击很难跟上。开源项目 Automatic_ticket_网页爬虫工作流自动化思源笔记网页剪藏3步上手把带图网页原样存进本地笔记思源笔记网页剪藏3步上手把带图网页原样存进本地笔记 写周报时想保存一篇行业分析复制进笔记工具后排版全乱图片是外链过一个月点开全挂了。思源笔记SiYua知识管理知识库QMK 机械键盘固件实操指南从编译自己的键位到 DFU 刷写只需 4 步QMK 机械键盘固件实操指南从编译自己的键位到 DFU 刷写只需 4 步 花几百块收来的机械键盘厂配固件的键位却处处反人类想换一组快捷键要进驱动想加个层嵌入式固件驱动开发硬件开发上一篇如何用FontForge征服AR/VR字体设计的终极挑战5个关键技巧下一篇Statping高可用架构5大设计策略避免监控服务自身单点故障创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考