第22章 文本处理

构建下载、正文提取、去标签和normalization流水线,再扩展simple_grep的次数、片段、高亮与可变前后文。

学习目标

  • 能解释下载文件与获取正文在“第22章 文本处理”中的责任边界
  • 能围绕“怎样把下载、解码、正文提取、匹配和上下文展示做成可复核文本管线?”运行正常与故障轨迹并定位首个分岔
  • 能用“第22章 文本处理的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”

来源、版次与运行边界

“第22章 文本处理”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单练习答案勘误只作为公开支持材料,不被冒充为原书全文。

对“第22章 文本处理”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。

“第22章 文本处理”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。

围绕“怎样把下载、解码、正文提取、匹配和上下文展示做成可复核文本管线?”,本页验收“第22章 文本处理的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“用正则删除 HTML 标签后把残缺文本当成可靠正文”;若无法定位第一处状态分岔,就拒绝当前解释。

从“下载HTML后gsub标签”开始纠正

Text processing不是一个Regexp串联,而是bytes acquisition、decode、document parse、content selection、text extraction、normalization和search。防止后一步无法解释结果来源。

先预测:html.gsub(/<[^>]+>/, "")能否可靠去标签?不能处理>出现在attribute、script/style、entity、comments和malformed HTML,也会把段落粘连。HTML用parser,Regexp只处理已提取plain text。

准备文本:下载文件

下载器接受allowlisted URI,设置connect/read timeout、redirect、status/content-type和最大bytes,保存raw bytes与response metadata。避免SSRF和unbounded memory。

require "open-uri"
 
def download(uri, max_bytes: 2 * 1024 * 1024)
  data = URI.open(uri, read_timeout: 5).read(max_bytes + 1)
  raise "response too large" if data.bytesize > max_bytes
  data
end

这是教学简化,不足以安全接收untrusted URL;production显式HTTP client验证DNS/IP、redirect每跳、TLS与status,并stream cap。Encoding来自Content-Type/BOM/site contract,进入第19章strict decode流程。

获取正文

先用HTML parser构造DOM,再通过site-specific selector选择article/main/body,删除nav/aside/script/style。不能把整个body视为正文。

Parser library/version是dependency;malformed input、巨大DOM和entity处理需限制。Selector失效要报错/metrics,不应静默返回空文本并被当“文章无内容”。

删除标签

DOM text extraction解码entities并应在block/paragraph/list之间插入separator。。

# Pseudocode around an HTML parser API:
article.css("script, style, nav, aside").remove
paragraphs = article.css("p, h1, h2, li").map { |node| node.text.strip }
text = paragraphs.reject(&:empty?).join("\n")

不要直接渲染提取text为HTML,输出target仍需escape。若search result要回链source offsets,DOM extraction/normalization必须建立mapping;否则只声明offset属于normalized text。

扩展simple_grep.rb:显示匹配次数

Count必须定义是match occurrences、matching lines还是files;Regexp zero-length match还要保证scan progress。。

def count_matches(text, pattern)
  count = 0
  text.scan(pattern) { count += 1 }
  count
end
 
p count_matches("Ruby ruby RUBY", /ruby/i)

Scan默认non-overlapping;overlapping需求使用lookahead或手动position并测试zero-width。Large text用line/chunk streaming,但跨chunk pattern需carry overlap buffer或限定line-local grammar。

显示匹配的部分

MatchData提供full match、captures和begin/end offsets。让caller无需重跑Regexp。

def spans(text, pattern)
  result = []
  text.to_enum(:scan, pattern).each do
    match = Regexp.last_match
    result << {
      start: match.begin(0),
      finish: match.end(0),
      text: match[0],
    }
  end
  result
end

Global last match容易被嵌套Regexp覆盖;在loop首行保存local match。Offset在String character/byte语义按Ruby版本/encoding验证;一旦normalize,source coordinate已改变。

突出匹配到的位置

不要直接gsub插ANSI/HTML后继续使用旧offset;先基于original spans切segments,再由renderer输出。。

def highlight_terminal(text, span)
  before = text[0...span[:start]]
  matched = text[span[:start]...span[:finish]]
  after = text[span[:finish]..-1]
  "#{before}\e[31m#{matched}\e[0m#{after}"
end

Untrusted text含ANSI escape时terminal injection,先strip/escape control codes或提供plain mode;HTML必须escape all text segments后加markup。Multiple spans按sorted non-overlap一次构造,避免offset drift。

显示前后各10个字符

Window start为max(match_start - 10, 0),finish为min(match_end + 10, text.length)。防negative range从尾部取值。

def context_for(text, span, radius: 10)
  start_at = [span[:start] - radius, 0].max
  finish_at = [span[:finish] + radius, text.length].min
 
  {
    text: text[start_at...finish_at],
    leading_truncated: start_at > 0,
    trailing_truncated: finish_at < text.length,
  }
end

“字符”若面向用户需grapheme segmentation;String index可能是codepoint-oriented。Newline context要选择保持、折叠或标记,不能让输出协议破行。

让前后的字符数可变更

CLI解析non-negative Integer并限制max radius和total output。避免N×matches爆炸。

Nearby windows可merge为一个snippet并在内含多个spans;或逐match输出但重复context,policy需稳定。Streaming input用ring buffer保留before,match后继续读足够after;跨chunk Regexp和multibyte boundary需encoding-safe decoder。

工具整合与验收

CLI adapter处理URL/file/stdin、pattern/options/radius与output format;pipeline返回structured results,renderer不重跑search。支持测试与扩展。

Exit status可区分found、not found、invalid invocation、partial source failure。Golden tests固定normalized text与spans,property tests验证span bounds/non-overlap,security tests覆盖URL/HTML/ANSI/Regexp resource limits。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。

对象与状态模型

从输入、接收者到可观察证据

怎样把下载、解码、正文提取、匹配和上下文展示做成可复核文本管线?

输入与接收者

固定下载文件所需的原始值、Ruby 版本和调用入口。

状态变化

在执行前记录接收者身份,并声明获取正文的允许状态。

观察证据

保存第22章 文本处理的初值、参数、编码或资源位置。

正式节点:下载文件、获取正文、删除标签、显示匹配次数、显示匹配的部分、突出匹配到的位置、显示前后各10个字符、让前后的字符数可变更

控制与消息轨迹

在相同初值下定位首个分岔

  1. 01固定下载文件的输入和接收者
  2. 02执行获取正文并记录状态
  3. 03观察删除标签的返回或副作用
  4. 04用让前后的字符数可变更核对不变量并复位

运行不变量:原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。

边界故障探针

一次只破坏一个前提

基线满足:原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。

本章回顾:先保存证据,再做展示

  1. Remote bytes按network/size/encoding门禁下载,DOM parser选正文并结构化去标签。
  2. Normalization是loss stage;需要source offset时维护mapping,否则明确coordinate属于normalized text。
  3. Count定义occurrence/line/file与overlap,spans保存offset/text/capture,避免重跑Regexp。
  4. Highlight由target-aware renderer escape并切segments,不在原文gsub后继续用旧offset。
  5. Context clamp边界、限制radius/total output,overlap与streaming策略明确。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样把下载、解码、正文提取、匹配和上下文展示做成可复核文本管线?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“用正则删除 HTML 标签后把残缺文本当成可靠正文”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联下载文件、获取正文、删除标签、显示匹配次数、显示匹配的部分、突出匹配到的位置、显示前后各10个字符、让前后的字符数可变更,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

下载文件

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

获取正文

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

删除标签

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

显示匹配次数

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

显示匹配的部分

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

突出匹配到的位置

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

显示前后各10个字符

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

让前后的字符数可变更

“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

本章以Ruby 基础教程第 5 版作者支持资料合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…