第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
endGlobal 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}"
endUntrusted 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。
正式节点与章专属证据
- ↡在“第22章 文本处理”中,下载文件必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
- ↡在“第22章 文本处理”中,获取正文必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
- ↡在“第22章 文本处理”中,删除标签必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
- ↡在“第22章 文本处理”中,显示匹配次数必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
- ↡在“第22章 文本处理”中,显示匹配的部分必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
- ↡在“第22章 文本处理”中,突出匹配到的位置必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
- ↡在“第22章 文本处理”中,显示前后各10个字符必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
- ↡在“第22章 文本处理”中,让前后的字符数可变更必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样把下载、解码、正文提取、匹配和上下文展示做成可复核文本管线?
输入与接收者
固定下载文件所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明获取正文的允许状态。
观察证据
保存第22章 文本处理的初值、参数、编码或资源位置。
正式节点:下载文件、获取正文、删除标签、显示匹配次数、显示匹配的部分、突出匹配到的位置、显示前后各10个字符、让前后的字符数可变更
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定下载文件的输入和接收者
- 02执行获取正文并记录状态
- 03观察删除标签的返回或副作用
- 04用让前后的字符数可变更核对不变量并复位
运行不变量:原始材料保留,解析规则固定,匹配位置可回指且输出有界稳定。
边界故障探针
一次只破坏一个前提
本章回顾:先保存证据,再做展示
- Remote bytes按network/size/encoding门禁下载,DOM parser选正文并结构化去标签。
- Normalization是loss stage;需要source offset时维护mapping,否则明确coordinate属于normalized text。
- Count定义occurrence/line/file与overlap,spans保存offset/text/capture,避免重跑Regexp。
- Highlight由target-aware renderer escape并切segments,不在原文gsub后继续用旧offset。
- Context clamp边界、限制radius/total output,overlap与streaming策略明确。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样把下载、解码、正文提取、匹配和上下文展示做成可复核文本管线?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“用正则删除 HTML 标签后把残缺文本当成可靠正文”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联下载文件、获取正文、删除标签、显示匹配次数、显示匹配的部分、突出匹配到的位置、显示前后各10个字符、让前后的字符数可变更,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 下载文件
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 获取正文
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 删除标签
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 显示匹配次数
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 显示匹配的部分
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 突出匹配到的位置
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 显示前后各10个字符
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 让前后的字符数可变更
“第22章 文本处理”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。