先说清楚这件东西是什么。2018 年 8 月 4 日,我用易语言写了一个 批量搜索修改文件内文本.e,51,699 字节,两个窗口程序集加起来约 11.5 KB 文本,是这一批旧工程里最大的一个实用工具——其他几个都是玩具,这个是当时真拿来干活的。窗口标题叫"批量搜索修改文件内文本小工具",署名 Lpyax(我当年混聊天室用的 ID),工程元数据里的作者邮箱带了我自己的旧 QQ 号,容器里已经等长替换成 XXXXXXXXXX@qq.com,文件总字节数不变。
它的替换单位是区域,不是字符串
普通 sed 是"找到 A 换成 B"。这个工具不是。界面上是一句完整的话:
寻找以【文本开头】开始的文本,并且以【文本结束】结束的文本,将中间改为……
真正被操作的是两个锚点之间夹出来的那段内容。锚点本身的字节原样保留,中间的段被取出来(取文本中间),按组合框里选的八种方式之一加工,再拼回文件。八种方式是:加文本于前、加文本于后、减字节、加、乘、除以、取__的左边、取__的右边。前面还有一个"原有/无"开关——选"无"的时候中间内容整个丢弃,只留新文本;所以界面标签自己提示"在 无 模式下,加文本于前 于 加文本于后 实现的效果是一样的"。这套结构的适用场景很具体:不是改错别字,是改字段值——比如 <name>xxx</name> 这种有前导标记和 trailing 标记的结构化文本,锚点定位天然比整行替换准。
循环次数那栏默认填的是 9999999,配合循环体里"锚点找不到(寻找文本 返回 -1)就跳出循环"的判断,实际语义是"有多少处改多少处,改到没有为止"。这个写法今天看依然顺手:给一个不可能的上限加提前退出,比让用户填"全部"省事。大小写开关是有的,默认勾选"区分大小写",选中状态取反后直接喂给 寻找文本 的比较参数。
这个找锚点的循环本身,是这份代码里最值得抄走的一段。它维护两个游标而不是一个:搜寻位置[1] 盯着开头锚点,搜寻位置[2] 盯着结束锚点。规则是——如果新一轮找到的开头落在旧结尾之后,结束锚点就从新开头往后重找;否则从旧结尾的位置继续往后找。两条分支合起来管住一件事:已消费过的文本不再参与下一次配对,嵌套或重叠的结构不会被一个开头配到别人的结尾。更关键的是替换之后那行:下一次搜索的起点要加上"改后的长度减去改前的长度"这个差值——搜寻位置[2] = 搜寻位置[2] + … + (取文本长度(修改后) - 临时)。任何在字符串内部边找边改的循环都躲不开这个偏移量修正,2018 年我不懂什么叫测试覆盖,但这行没写丢,程序才跑得起来。
界面还有个细节配得上说一句。红字标签建议"对搜索范围进行缩小,例如在前面加上制表符",而它下面放了一个内容就是一个真实制表符的小输入框,标题"制表符复制"。逻辑是:锚点越短越容易撞车,把缩进之类的不可见字符摁进锚点里,匹配的独占性立刻上升;但不可见字符没法教用户打,于是在 UI 里内嵌一个可复制的原件。“要用户输入一个他描述不出来的字符"这个难题,六行界面就解掉了,今天做工具依然成立。
编码:整个程序里找不到一个编码相关的判断
文件处理就三行:文件内容 = 到文本 (读入文件 (路径)),内存里改,最后 写到文件 (路径, 到字节集 (文件内容))。没有 BOM 检测,没有编码参数,没有任何分支。易语言的"文本型"本身就是 ANSI 字节串,在中文 Windows 上就是 GBK——所以这个工具对文件的整个理解方式是:按 GBK 双字节规则切的字节流。
这不是我的猜测,程序自己承认了。选了"减字节"模式时,标签会显示:“注意:中文汉字、符号为2字符,英文、英文符号为1字符,空格为1字符,回车为2字符”。这是在向用户解释 取文本长度 数出来的"字符"其实是 GBK 字节。也就是说这套工具里所有的截断、长度差值、位置计算,全在字节坐标上进行,用户必须用字节思维下指令——这是 2018 年没有 UTF-8 意识年代的诚实设计,字节层面自洽。
由此推出的行为,分两半说,说死的部分和说不死的分清楚。能说死的是:纯 ASCII 文件随便改,搜索串也在 ASCII 范围内才可靠;GBK 文件是它的原生主场,中文搜索串直接匹配。不好下结论的是别的编码:UTF-8 文件里中文是三字节序列,界面输入框里的中文搜索串按 GBK 编码,两者字节对不上,中文模式匹配必然失败,这个能从代码断言;但纯 ASCII 内容的 UTF-8 文件,替换动作本身够得着。至于一个文件里如果存在凑不成合法 GBK 双字节的序列(比如 UTF-8 尾字节挨着 ASCII),到文本/到字节集 这趟往返会不会改变那几个字节——我从代码看不出来,程序也没做任何防护或验证,这份归档里没有一个真实用例能作证。这是这个 2018 工具留白最诚实的一处。
对今天写批量文件改写工具的人,这套代码给的是反面教材加一条底线方法:读字节、猜编码、转内部文本、改完再转字节覆盖,这条链子上每一环都可能悄悄改坏你不打算碰的部分。要么全程在字节数组上做正则,要么检测编码后显式往返并拿"改动前的字节"和"没改过的内容"做一次回读比对再落盘——后者是这个工具没有、但一个循环结构就能补上的东西。
覆盖是原地的,防护是佛系的
再看写回:直接 写到文件 覆盖原路径。没有临时文件改名,没有保留原件。这意味着数据丢失风险在动手那一刻就存在,而它设计的对冲只有一层——可选的自动备份。勾了"执行时备份”,它把路径按 \ 切开、拼出上级目录,建一个 自动备份目录,复制一份叫 备份_原文件名。同名旧备份会被覆盖,只保最后一代。这是从代码看得明明白白的全部保护。
没勾呢?有一个提醒框:“您没有勾选自动备份,是否现在勾选?"。关键在这个提醒只存在一次:无论点是还是点否,代码紧跟着把"不提示勾选自动备份"这个勾选框自己置真。第二次执行开始,无备份直接覆盖,静默。一个认真想过数据安全的程序不会这么写——它会每次都问,或者干脆拒绝执行。2018 年的我选择的是"提醒义务尽到,责任归你”。文件夹模式执行前还有一句原话,更能代表当时的心态:“是否要开始执行?执行过程中不可取消,否则后果自负。“运行中窗口被 禁止 属性锁死,用户唯一能"取消"的方式是杀进程,而那时当前文件可能正处在"旧内容已被读进内存、还没写回或写到一半"的窗口里——后果自负四个字是有指涉的,具体怎么个死法代码没写,我也不替它补。
还有个容易忽略的细节:一处都没匹配到的时候,文件照样被整写回一遍。循环跳出之后 写到文件 是无条件执行的。“没改动"和"没落盘"在这个程序里不是一回事,上一节说的编码往返风险,对每一个被"路过"的文件都全额存在。这大概是这份归档给所有批处理工具作者的最有用一鞭:改不动就原样写回,等于把手伸进了一个本来不需要碰的文件。
批量:一层目录,明确不递归
文件夹模式(第二个窗口)的做法:把主窗口目标文件的后缀抄下来,用 寻找文件 按 文件夹\*.后缀 把一层目录里的同后缀文件收集成数组——窗口上的标签直接写明"本模式不会搜索子文件夹”——然后逐个把路径塞回主窗口的文件框、调用主窗口的"开始执行"子程序。复用单文件引擎,一行改文件逻辑都不写。这个结构值得留一句:批量的正确姿势是给单文件处理器套循环,而不是复制一份批量版处理器——它这个窗口程序集 2 总共才 60 来行,全靠这条。
枚举文件那个循环也值得一抄,因为它是 Windows 目录枚举原生 API 的裸形态:第一次 寻找文件 带通配符(相当于 FindFirstFile),之后在 循环判断 里每次传空参数继续取下一个(FindNextFile),直到返回空串停止,最后还要手工删掉数组尾部那个空成员。今天多数人只见过"一行拿到文件列表"的封装库,不知道底层就是这个"首次带模式、后续带空、空串收工"的三步协议——在没有任何列表库的运行时里写批量工具,这个形态照样能用。窗口互斥也是同一种朴素:设置窗一创建就把主窗口的"开始执行"按钮禁用,窗口销毁再恢复,防止两个入口同时改同一个文件框。
顺带一提备份的落点:它把目标路径按 \ 切开、把除最后一段外的部分重新拼回去,得到上级目录再挂 自动备份目录。没有路径库,字符串分割就是 dirname。不递归的选择,窗口里那句"请将非需要修改的文件移动到其他文件夹"说明它把"圈定作用域"的责任推给了用户的目录整理。今天看这不是偷懒是什么?是。但它也没有假装自己处理不了的东西。另外主窗口的红字标签写着:“文件夹模式目前为测试阶段,稳定性不知,请谨慎使用”——2018 年就会在 UI 上写 disclaimer,这个习惯可以。
报错没有日志,进度就是标题栏
工具没有控制台,没有日志文件。进度全程写窗口标题:”–正在执行操作,请不要关闭程序”、"–自动备份完成,即将开始操作"、"–操作进度 n/总数"、"–操作完成!共计修改了n个内容"。出错走消息框,标题用 警告:、提示:、Error:(在原始容器里扫到的 “Error:” 字样就是这些弹窗标题,比如空路径时的"搜索进程意外终止,请重试。")。标题栏当进度条,是 GUI 脚本工具最便宜的方案,零控件零成本;代价是关掉窗口一切归零,事后无法追查哪些文件被动过。对一个会原地覆盖文件的工具,我认为今天再做至少要写一行一个文件的日志,这不是奢侈。
另外两处诚实的毛边,我不修,也明说:空"修改为"的警告框弹完之后没有终止执行,代码继续往下走;循环提前跳出时标题报的"共计修改了 n 个"会比实际多 1 个(那轮只用来发现没有下一处了)。前者算 bug,后者算无伤大雅的口径差。
归档
仓库:https://cnb.cool/t1mmoc/tspace/eyuyan/epl-batch-text-replacer,公开。里面是脱敏后的原始 .e(唯一改动是那 10 位 QQ 号等长换成 X,总字节数 51699 不变)和机器反解出的可读文本:两个窗口程序集的代码、两张窗体定义 XML(控件名、标题、默认值都在里面)。注意这些文本带着两处易语言 IDE 里并不存在的提取器痕迹:字符串里的 #e2txt_text# 标记和 krnln:: 前缀,读的时候当它们不存在就行。.e 本身没有 IDE 打不开,仓库里放它只为存档,不为复现。