比对两个版本的文本,以及在上线前验证正则
站内负责「检查」的一半是两个页面:文本比对 看两版内容差在哪,正则测试 看一条模式究竟匹配到什么。它们对小输入都很可靠,对某些大输入则会安静地误导你,所以值得知道每个工具的假设划在哪条线上。
比对只比行,不比行内的任何东西
左边贴旧版、右边贴新版,按 Compare。工具先找出两版中都出现的最长公共行序列,其余一律算作新增或删除。结果是两栏,每一步一行,某侧在该位置没有内容时补一个空白占位行——因此两侧的行号各自连续,纵向又能对齐。
「只按行」这个模型有四个后果:
- 没有「已修改」这个状态。 一个字符的改动会算成一行删除加一行新增,摘要里报出来的就是这两个数。标题处不会告诉你有多少行是被编辑而不是被移动,所以要看两栏的内容本身,而不是看数字。
- 行内不做比较。
title = "旧"和title = "新"显示成两行整行变化。行越长,真正的差异越可能藏在中间而看不见。 - 调整顺序会被当成改写。 把一个函数移到文件顶部,中间的每一行都会同时出现在新增和删除里。
- 同一处替换可能落在不同侧。 当两种对齐方式得分相同,算法偏向读作「新增」的那一支。差异集合是一样的,但某一行被归到左边还是右边,可能和别的工具不一致。
采信结果之前先检查这一个坑
工具按 \n 切分,并且不做换行符归一化。把 Windows 的 CRLF 文件贴进左框、把同样的 Unix 版本贴进右框,结果是每一行都不同——因为左侧每行末尾都带一个看不见的 \r。
症状是:两份看着一模一样的文件比出满屏红绿,新增行数与删除行数都等于总行数。这不是内容差异,是换行符差异。把两侧从同一来源重新贴一次,或者先过一遍文本格式化的 Trim Lines。
相关的细节还有一个:比对前每侧只丢弃一个结尾空行。如果文件以两个换行结尾,剩下那个空行就会真的参与比对。
长行会把两栏的对齐搞乱
行高由内容决定,长行是折行而不是横向滚动。由于两栏是两个独立的列,某一行在一侧折行后那一行变高,它下面的每一行就整体错位,左右两栏不再肉眼对应。
出现这种情况时没有任何提示。如果发现两栏对不上,就滚到第一处长行再往下读;保持行短是维持对应的办法。
比对的代价随两侧行数的乘积增长
算法要铺一张覆盖「左侧每行 × 右侧每行」的完整表格。在浏览器里实测 3,000 行对 3,000 行大约五分之一秒。两侧同时翻倍,计算量和内存都是四倍——6,000 × 6,000 就是 3,600 万个单元格。
这里没有规模上限、没有后台线程、也没有取消按钮。超过某个规模后页面会长时间失去响应,而不是给你一个错误。还有一点:编辑输入不会自动重算,屏幕上永远是你上一次按 Compare 时的结果——改过内容却忘了重按,读到的就是过期差异。
几千行以内的文件和改动审阅适合用它;文件规模再大,请交给能流式处理的命令行 diff。
正则:一边打字一边执行,错误也一样
模式框和测试文本框都是逐键重跑。这让它适合探索,也正是坏模式带走整个页面的机制所在。
含嵌套量词的模式——(a+)+b、(\w*\s*)*——在判定「匹配不上」之前可能需要穷举指数级的切分方式。而这里没有超时,所以这类输入不会报错,它会把页面卡住,唯一的恢复办法是刷新或关掉标签页。试探陌生输入时先把量词收紧({0,10}),一定要试不限量词的模式,就先贴一段很短的文本。
六个标志各自改变什么
g(默认开着)——匹配全部而不是只找第一处。关掉它,高亮结果和统计都只有单个匹配,而且 Replace 只替换第一处。i——忽略大小写。m——让^和$锚定每一行的开头结尾,而不是整段文本的开头结尾。这条最要紧:测试页下方那 10 条参考模式在点选时只填模式、不动标志,所以^[\u4e00-\u9fa5]+$(仅中文)在多行示例里点下去是不匹配的,必须自己勾上m。s——让.也能匹配换行符。u——Unicode 模式:对转义更严格,把代理对当一个字符。y——sticky:匹配必须正好从引擎当前游标处开始。和g叠加时,游标只在命中后才前移,于是一条在第 0 个字符处匹配不上的模式会报出完全没有匹配。这个组合几乎不是你想要的。
模式语法出错时不做包装:直接把引擎原始报错带个警告图标显示出来,而且用的是浏览器报告的语言——同一条错误在 Chrome、Firefox、Safari 里的措辞并不相同。
统计行的三个数字
框下面报三样:匹配多少个、总共匹配到多少字符、以及(仅当第一个匹配含捕获组时)捕获组个数。
关于这些数字要知道两点:
- 组数只取自第一个匹配。 如果后续匹配会产生不同数量的组,这里不会反映;页面也不展示每个组到底抓到了什么。所以这一块的作用是「我的模式是否如预期匹配」,不是捕获组检视器。
- 空模式是合法模式。 什么都没填时,正则在每个位置都匹配到空串——这就是首次打开页面、下面那段示例文本会得到一百多个匹配而 Total matched characters: 0 的原因。匹配数本身不能证明模式有效;零长度就是那个该盯住的破绽。
替换:就是标准的字符串替换,包括 $ 的规矩
填入替换内容后按 Replace。替换结果不会随打字刷新(那个实时处理函数是空的),所以输出永远来自上一次点击。
替换框遵循 JavaScript 的通常规则,也就是说 $ 不是普通字符:
| 你写 | 实际替换成 |
|---|---|
$& |
本次匹配到的内容 |
$` / $' |
匹配之前 / 之后的全部文本 |
$1、$2 … |
第 1、2 … 个捕获组 |
$$ |
一个字面意义的 $ |
于是 $1 可以重排匹配结果——(\d{4})-(\d{2})-(\d{2}) 配 $3/$2/$1 就能改写日期格式;而替换内容里本来就要带美元符号时(比如金额),必须写成 $$12.50。如果你脑子里想的规则不是这一套,会咬人的地方就在这。
你的内容会去哪儿
两个页面都只在本地对你贴进去的东西运算:比对和匹配都不发出任何请求,也都不做保存——刷新之后两个框回到内置示例。