十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

删除 txt 指定行:sed、awk、Python 与编码避坑

删除 txt 指定行:sed、awk、Python 与编码避坑 上周同事丢过来一个 380MB 的日志文件说里面有六万多行是某个模块重复刷的错误堆栈要把这些行连同它后面紧跟着的两行一起删掉问有没有不写代码的办法。我先让他把文件复制了一份然后用 sed 按区间删干净前后花了不到一分钟。他挺意外说以前遇到这种事都是打开文本编辑器一行一行框选文件一大就卡到没法动。其实删除 txt 等文本文档中指定行这个需求几乎每个和数据打交道的岗位都会碰到——运维清日志、爬虫洗数据、测试改配置、翻译校对去重、小说文本整理章节标记场景不同但动作是一样的从文件的几千行几万行里精准地拿掉一部分行剩下的原样保留。麻烦的地方从来不是删而是怎么定位那一行、用什么工具删、删完文件会不会坏。这篇就把这三件事拆开讲清楚从完全不写代码的可视化操作到命令行一行命令再到能批量复用的 Python 脚本最后把编码和换行符这两个最容易让人翻车的坑单独拎出来说。1. 先把删哪一行这件事拆开工具选型才有依据很多人问怎么删指定行其实这句话里藏着四种完全不同的需求用错工具就会白折腾。你要删的确定是第 100 行还是内容里含 error 的那一行是所有以 # 开头的行还是所有空行定位方式决定了工具按行号删命令行最省事按内容删编辑器的正则标记最快按复杂规则删只能上脚本按状态删去重、去空行大部分工具都有现成参数。下面把这四种情况分开说你可以直接对号入座。1.1 按行号定位位置固定但内容会漂移按行号删除是最直觉的方式比如第 3 行是表头垃圾删掉命令写起来也最短。但这里有个容易被忽略的前提行号必须是稳定的。如果你在删除之前已经对文件做过别的改动或者文件本身是由别的程序边写边生成的行号随时会变这时候按行号删就是给自己挖坑。我一般建议的做法是先确认文件处于最终状态、不再被写入再执行按行号的操作操作前用wc -l或者findstr /N /C:之类的方式把总行数打出来核一遍。另外一个细节是行号从 1 开始还是从 0 开始绝大多数命令行工具sed、awk 的 NR、Python 的 enumerate 加一都是 1 起始但 Python 的列表下标是 0 起始混用的时候错一行可能就把相邻内容删了。这个坑我踩过一次删一个配置文件的第 12 行结果删到了第 13 行那个文件后面还有效验逻辑直接报错排查了半天才意识到是下标问题。1.2 按内容定位精确相等和包含匹配是两回事删掉内容等于---的那一行和删掉所有包含---的行是两种完全不同的效果。前者是精确匹配后者是子串匹配。很多工具默认走的是子串比如 sed 的/pattern/d、PowerShell 的-match它们只要行里出现这段字符就命中。这在处理分隔符、注释标记时特别危险因为---可能出现在正文里也可能出现在----分割线----这种行中。要精确匹配整行sed 得写成/^---$/dPowerShell 用-eq而不是-matchNotepad 的正则要加^和$锚点。还有一个隐蔽的陷阱是行尾的空白字符一个看起来是空行的地方可能藏着两个空格或者一个制表符用/^$/匹配不上得用/^[[:space:]]*$/才行。我的习惯是处理之前先做一次探查——统计一下目标模式命中了多少行跟预期的数量对上了再真删。1.3 按模式定位正则能解决什么不能解决什么正则表达式是处理有规律但不固定的内容的利器比如删掉所有以时间戳开头的行^\[20[0-9]{2}-删掉所有只有数字和逗号的行^[0-9,]$。但正则也有明确的能力边界最典型的就是它不会计数。你没法用一条正则表达式的某一个匹配表达第 5 次匹配也就意味着删除第 N 个命中行这种需求光靠正则是做不到的必须借助工具的行号能力或者脚本里维护一个计数器。同理删除每个段落的第一行这种依赖上下文的操作正则也很难单独完成因为它需要记住上一个匹配的位置。遇到这类需求别硬磕正则直接上 awk 或者 Python用几行代码反而更清楚。正则适合的是每一行独立判断的场景一旦需要跨行状态就该换思路了。1.4 按状态定位空行、重复行、首尾行这一类不是按具体内容删而是按行的状态删。最常见的三种删空行、去重复行、删首行表头或尾行汇总。它们的共同特点是工具基本都给了现成方案硬写正则属于自找麻烦。删空行GNU sed 的/^[[:space:]]*$/d、awk 的NF字段数为 0 即空行都能一行搞定去重复行awk 的!seen[$0]是教科书级写法保留首次出现、丢弃后续重复删首行tail -n 2最优雅删尾行head -n -1GNU或者 sed 的$d。需要注意的是去重默认是按整行内容比较的行尾多一个空格就算不同如果数据来源不干净去重前最好先统一去掉行尾空白否则你会发现去重效果远不如预期。这一点在从不同系统拼接出来的 csv 或 txt 里特别常见。定位方式典型需求最省事的工具主要风险按行号删第 3 行、删 5 到 8 行sed、PowerShell行号漂移、下标从 0 还是 1 起按内容删含某关键词的行Notepad 标记、sed子串误命中、行尾空白按模式删有规律的行脚本、awk、编辑器正则正则无法计数、跨行状态按状态去重、删空行、删首尾awk、tail、head行尾空白导致去重失效2. Notepad 的标记删除不写代码时最稳的一条路如果你只是偶尔处理一个几兆、几十兆的文本装个 Notepad 就够了它的标记功能是整个编辑器里被低估得最厉害的一个。很多人一提到 Notepad 删行第一反应是查找替换把要删的内容替换成空但这样做有个前辈留下的老毛病替换成空以后那一行往往只剩下一个空行你还得再删一遍空行两轮操作下来容易出错。正确姿势是走标记这条路。2.1 为什么建议用标记行 删除标记行而不是直接替换在 Notepad 里按CtrlF打开查找窗口切到标记这个选项卡输入查找内容、勾上标记行点查找全部你会看到所有命中的行在编辑区被高亮标上了颜色右下角还会提示标记了多少行。这时候你先别急着删先看一眼这个数字——这就是你的第一道校验。如果预期删 200 行却标记了 2000 行说明正则写宽了立刻停下改表达式。确认无误后菜单栏选搜索 → 删除标记行所有被标记的行连同它的换行符一起消失一行都不残留。对比之下用替换为空的方式正则得写成^.*关键词.*\r?\n才能真正连行带换行删掉写漏了\r?\n就会留空行写错了.*的范围可能把整行内容连同不该删的部分一起吞掉。标记删除天然是整行粒度的这个语义和删除指定行是一致的。2.2 六个可以照抄的正则表达式下面这几条我在日常里用得最多直接填进查找框查找模式选正则表达式不要勾.匹配换行即可删除空行含只有空格和制表符的行^[ \t]*$删除以#开头的注释行^#.*$删除包含特定关键词的行^.*关键词.*$删除整行内容恰好等于分隔符的行^---$删除行首为数字编号的列表行^[0-9][\.、].*$删除行尾带特定标记的行^.*\t$行尾是制表符注意 Notepad 的正则引擎在多行模式下$匹配的是行尾位置换行符之前所以^.*$这种写法和标记整行的效果是一致的你不需要手动去补\r?\n。反而是如果你在替换模式下用了\r\n在某些 CRLF 文件里会出问题这也是我推荐标记模式的原因之一。另外提醒一句.*在默认模式下不匹配换行所以它不会跨行贪吃这点可以放心但如果你手滑勾了.匹配换行那.*会一路吃到文件末尾第一行标记就把整个文件标红了发现这个情况赶紧取消勾选。2.3 Notepad 在大文件和批量场景下的短板Notepad 有几个硬限制得提前知道。第一它对超大文件的支持有上限官方定位是大文件模式Settings → Preferences → MISC 里有个开启大文件访问的选项但即便开了几百 MB 级别的文件加载和标记操作也会明显卡顿上 GB 的基本就别指望了内存和时间都吃不消。第二它一次只能处理一个文件没有内置的批量能力你有 500 个 txt 要删同样的行只能一个个开。第三标记删除是不可撤销的整批操作虽然可以 CtrlZ 撤销但大文件撤销本身也慢操作前务必备份原文件我的习惯是同目录复制一份改名成xxx.bak.txt再动手。第四它默认按当前文件的编码读写如果文件是 GBK 而你手动改了编码设置保存时可能出现乱码这一点在第 6 节会详细讲。总结下来Notepad 适合小文件、临时处理、需要肉眼确认的场景量大或者要重复执行还是得往下看命令行。3. Windows 命令行findstr 与 PowerShell 各自的活在 Windows 上不装任何软件也能干这件事系统自带的 findstr 和 PowerShell 就够用。关键是要搞清楚它俩各自擅长什么别拿 findstr 去干正则替换的活也别用 PowerShell 去做它不擅长的超大数据流处理。3.1 findstr /V 的用法与它的正则局限findstr 最有用的一招是/V参数它的含义是输出不匹配的行也就是把你要删的那些行过滤掉剩下的保存成新文件等于变相完成删除。用法findstr /V /C:要删除的内容 input.txt output.txt/C:表示把后面的字符串当成一个整体的字面量来匹配而不是拆成多个关键词。如果你不加/C:直接写findstr /V error input.txtfindstr 会把 error 当成一个词来找行为上看着差不多但遇到带空格的模式就崩了。如果要按正则匹配用/R /C:findstr /V /R /C:^\[20[0-9][0-9]- input.txt output.txtfindstr 的正则能力非常有限只支持^ $ . * []这几个元字符没有、?、\d、{}这些现代语法写复杂的模式会很别扭。还有两个坑一是它默认对大小写敏感要忽略大小写得加/I二是它和重定向配合时输出文件的编码跟着当前命令行代码页走中文环境下常常变成 GBK如果你要的是 UTF-8需要先chcp 65001再执行或者干脆用 PowerShell。另外/V不能原地修改必须重定向到新文件然后再手动改名覆盖。3.2 PowerShell 里按行号删的三段写法PowerShell 的表达能力强很多但写法上有个分水岭是流式管道还是先读进数组。这两者在处理大文件时差别巨大。按内容删用流式管道最短内存占用低Get-Content .\input.txt | Where-Object { $_ -notmatch 关键词 } | Set-Content .\output.txt -Encoding UTF8按行号删如果行号靠后先读进数组再按下标切最直观$lines Get-Content .\input.txt # 假设要删第 3 行下标 2 $new $lines[0..1] $lines[3..($lines.Count - 1)] $new | Set-Content .\output.txt -Encoding UTF8如果要删首行和尾行用Select-Object更省事Get-Content .\input.txt | Select-Object -Skip 1 -SkipLast 1 | Set-Content .\output.txt -Encoding UTF8这里有三个容易翻车的点。第一-SkipLast和-Skip同时用的时候管道会把整个集合缓存下来等于全量读入内存文件大的话内存直接起飞这一点在官方文档里只是一句轻描淡写的话实测非常关键。第二Get-Content不带-Raw时是逐行读、每行去掉换行符存成字符串数组写回时会统一使用Set-Content的换行风格原本的 CRLF/LF 混排会被抹平。第三-Skip和数组下标混用时索引越界不会像 Python 那样报一个清晰的错误而是静默地少输出内容所以对账步骤不能省。3.3 存成 .bat 或 .ps1 复用时最容易翻车的编码问题很多人把命令存成 .bat 想一键执行结果发现脚本里的中文路径或者中文关键词变成乱码。原因是 .bat 文件本身有编码cmd 解释它时用的是当前代码页。稳妥的做法是把 .bat 存成 ANSI即中文环境的 GBK脚本第一行加chcp 936显式声明代码页或者把 .bat 存成 UTF-8 但第一行加chcp 65001不过要注意加了 65001 之后某些老程序的输出会异常需要实测。相比之下 .ps1 更推荐但 PowerShell 5.1 和 PowerShell 7 的默认编码不一样5.1 下Set-Content默认写 ANSI-Encoding UTF8写的是带 BOM 的 UTF-8PowerShell 7 下默认就是无 BOM 的 UTF-8。这个差异会让同一个脚本在两台机器上产出不同的文件如果下游程序对 BOM 敏感就会莫名其妙报错。我的做法是脚本里显式声明不用默认值$enc New-Object System.Text.UTF8Encoding($false) # 不带 BOM [System.IO.File]::WriteAllLines(.\output.txt, $new, $enc)4. Linux 与 macOSsed 按位置删、awk 按条件留到了 Linux 和 macOS 这边sed 和 awk 是绕不过去的两个工具而且它们的分工非常清晰sed 的思维是删掉匹配的awk 的思维是只输出要留的。理解了这句话用哪个就不会纠结了。4.1 sed 的地址表达式行号、区间、步长sed 的删除靠d命令前面跟地址来决定删哪些行sed -i 3d file.txt # 删除第 3 行 sed -i 3,5d file.txt # 删除第 3 到第 5 行 sed -i $d file.txt # 删除最后一行 sed -i 1d;$d file.txt # 删除首行和末行 sed -i 2~3d file.txt # 从第 2 行起每隔 3 行删一行 sed -i /关键词/d file.txt # 删除所有含关键词的行 sed -i /关键词/,2d file.txt # 删除命中行及其后 2 行 sed -i /^$/d file.txt # 删除空行 sed -i /^[[:space:]]*$/d file.txt # 删除空行含纯空白行 sed -i /关键词/!d file.txt # 反向只保留含关键词的行这里有个特别实用的技巧/关键词/,2d这种命中行 后续 N 行的区间删除正好对应我开头说的那个日志场景——错误堆栈是三行一组第一行有特征词后面两行是堆栈内容。如果每组的行数固定这个写法一次就能清干净。如果组长度不固定堆栈嵌套深度不一就得换 awk 维护状态。2~3d那个步长语法是 GNU sed 独有的macOS 自带的 BSD sed 不支持用之前先sed --version确认一下。4.2 sed 按内容删时的转义与分隔符处理sed 用/当默认分隔符如果你的模式里本身就带/比如要删 URL 相关的行就得转义成\/或者更省事的做法是换一个分隔符sed 允许你把第一个字符换成别的sed -i \#http://#d file.txt # 用 # 当分隔符避免转义 /模式里带.、*、[、]、^、$这些正则元字符时也要小心。sed 的地址默认走的是基本正则BRE、?、{}、()这些需要加反斜杠才是元字符跟大多数现代语言用的扩展正则ERE不一样加-E参数才切到 ERE。我见过不少人从别的工具搬来一条正则在 sed 里就是匹配不上原因就是 BRE 和 ERE 的差异。另外\d在 GNU sed 里可以得到支持作为[0-9]的简写但 BSD sed 不认跨平台脚本里统一写成[0-9]最保险。4.3 awk 的逆向思路不删只输出要保留的awk 天生就是按条件筛选并输出的语言所以用它删行时代码里根本不会出现删除这个动作而是写不匹配的行才打印awk NR!3 file.txt out.txt # 保留除第 3 行以外的所有行 awk !/关键词/ file.txt out.txt # 保留不含关键词的行 awk NF file.txt out.txt # 保留非空行NF 是字段数 awk !seen[$0] file.txt out.txt # 去重保留首次出现 awk NR1 NR100 file.txt out.txt # 只保留第 2 到 99 行去重那行!seen[$0]值得单独说seen是一个关联数组$0是整行内容当 key。awk 求值seen[$0]时先取当前值未定义按 0 处理作为表达式的值再自增。所以第一次遇到某行时!0为真打印第二次遇到时值已经是 1!1为假不打印。一行代码完成去重还顺带保留原始顺序比 sort | uniq 更符合保持原顺序的需求uniq 只去相邻重复sort 会打乱顺序。awk 里还有一个常用的判空写法是length($0) 0它和NF有细微差别NF在只有空白字符的行上为 0而length只看字符数空白行长度不为 0。要删全是空白的行用NF更准。4.4 -i 原地修改与备份后缀的实际用法sed 的-i是原地修改看起来很方便但它是直接覆盖原文件写错一个字符可能就回不去了。GNU sed 支持在-i后面跟一个后缀自动给原文件留备份sed -i.bak 3,5d file.txt # 原文件存为 file.txt.bak修改写回 file.txtmacOS 的 BSD sed 语法不一样它要求-i后面的后缀必须作为独立参数哪怕是空字符串也得写出来sed -i 3,5d file.txt # macOS 下这样写不加 会报错这个差异在跨平台脚本里是个经典的坑写Linux 上跑得好好的脚本拷到 Mac 就报错。awk 没有原地修改的选项得靠重定向写临时文件再 mv 回去awk !/关键词/ file.txt file.txt.tmp mv file.txt.tmp file.txt注意那个它的意思是前一步成功才执行后一步。如果 awk 因为文件不存在之类的原因失败了输出文件可能是空的或者不完整的这时候如果再无条件 mv 覆盖你的原文件就被毁掉了。加上至少能保证 awk 正常结束才替换。更谨慎的做法是先 tmp再手动检查 tmp 的行数确认没问题再 mv。5. 用 Python 把一次操作变成可复用的小工具当需求变成每周处理一批文件、规则还可能变的时候命令行的一次性命令就不够用了该上脚本了。Python 在这个场景里几乎没有对手不是因为别的语言做不到而是它对文本编码、异常处理、目录遍历的支持最贴合日常杂活。下面这份代码可以直接拿去改。5.1 逐行流式处理与一次性读入内存的分界线写文本处理脚本第一件要想清楚的事是文件有多大。10MB 以内随便你怎么读readlines()全进内存也没事。100MB 到 1GB 之间就该用逐行迭代for line in fPython 会把文件对象当迭代器内部有缓冲内存占用基本恒定。上 GB 甚至几 GB 的时候连逐行迭代都要注意——虽然内存不大但耗时可能到分钟级这时候要考虑的是能不能不重写整个文件比如只从命中位置开始计偏移用二进制模式做局部改写这个复杂得多一般不值得。判断标准其实很简单你的文件能不能全部装进内存还留出足够余量。装得下就用最直观的写法装不下就老老实实流式。别一上来就追求高效为了省内存把代码写得晦涩最后自己都看不懂出错了改不动反而更亏。5.2 一份可以直接跑的通用过滤函数import re from pathlib import Path def filter_lines(src, dst, pattern, encodingutf-8, keepFalse, backupTrue): src: 源文件路径 dst: 输出文件路径可与 src 相同函数内部用临时文件保证安全 pattern: 正则表达式字符串 keep: False 删除匹配行True 只保留匹配行 backup: 原地覆盖时是否保留 .bak 备份 rx re.compile(pattern) src_path Path(src) tmp_path src_path.with_suffix(src_path.suffix .tmp) total 0 removed 0 # newline 关闭通用换行转换原样保留每行的 CRLF 或 LF with open(src_path, r, encodingencoding, newline) as fin, \ open(tmp_path, w, encodingencoding, newline) as fout: for line in fin: total 1 hit bool(rx.search(line)) if hit ! keep: removed 1 continue fout.write(line) if backup and src_path Path(dst): src_path.replace(src_path.with_suffix(src_path.suffix .bak)) tmp_path.replace(dst) return total, removed这个函数有几个设计上的取舍值得说明。第一先写临时文件再替换而不是直接在原文件上操作这样即使中途程序崩了、断电了原文件也还是完整的最多留一个 .tmp 垃圾。第二newline这个参数很多人不知道它的作用是关闭 Python 的自动换行转换读进来是什么行尾、写出去还是什么行尾避免一个 CRLF 文件被无声无息改成全 LF。第三hit ! keep这行逻辑看着绕其实是在做删除模式/保留模式的统一keepFalse 时命中就跳过keepTrue 时命中才保留一行代码覆盖两种需求。第四函数返回总行数和删除行数方便调用方做对账——这个返回值是我最看重的部分没有它你根本不知道脚本到底干了什么。5.3 批量处理整个目录时的文件筛选单个文件处理完批量就简单了遍历目录加过滤条件root Path(rD:\data\txt) for p in sorted(root.glob(*.txt)): if p.name.endswith(.bak.txt): continue if p.stat().st_size 0: print(f跳过空文件: {p.name}) continue total, removed filter_lines(p, p, r^\s*$, encodingutf-8) print(f{p.name}: 共 {total} 行, 删除 {removed} 行)这里我加了两道过滤。一是跳过已经备份出来的.bak.txt否则第二次运行会把备份文件也处理一遍来回几次磁盘上就多出一堆无用文件。二是跳过零字节文件因为对空文件做正则匹配没有意义白白浪费一次 I/O。另外sorted()是为了让输出顺序可预测方便你对照日志排查问题不加的话目录遍历顺序依赖文件系统每次可能不一样。真要处理几万个文件还可以加一个进度提示或者用concurrent.futures做并发不过说实话文本处理本身受磁盘 I/O 限制并发几个线程提升有限还要考虑写入冲突我个人一般不上并发宁愿让它慢慢跑。5.4 删完之后的行数对账脚本跑完不算完得核对。最直接的办法是比较处理前后的行数def count_lines(path, encodingutf-8): n 0 with open(path, r, encodingencoding, newline) as f: for _ in f: n 1 return n然后在处理前记下旧行数处理后用这个函数数一遍理论上满足旧行数 - 删除行数 新行数。这个等式不成立只有两种可能一是函数本身有 bug二是文件在处理过程中被别的进程改动了。两种情况都必须报警不能忽略。我见过一次事故某个同事的清洗脚本因为编码判断错误把 GBK 文件按 UTF-8 读抛异常被吞掉了输出文件是空的但他看日志只看了处理完成几个字就交差了下游拿着空文件跑了一整天。如果当时有对账这一步这个问题在第一时间就会暴露。6. 编码和换行符不处理这一步删对了行文件也是坏的前面所有章节都在讲怎么删,但真正让人抓狂的往往是删完之后文件是变了可打开一看全是方块或者问号或者下游程序直接报格式错误。这不是删除逻辑的问题是编码和换行符的问题。6.1 UTF-8、UTF-8 BOM、GBK 怎么判断同样一个中文 txt可能是 UTF-8 无 BOM、UTF-8 带 BOM、GBK 三种编码之一而它们的字节序列完全不同。程序的悲哀在于它不会读心术你不告诉它编码它只能猜猜错就乱码。判断方法有几种一是看文件头三个字节是不是EF BB BF是的话就是 UTF-8 带 BOMPython 读的时候用encodingutf-8-sig会自动去掉 BOM用utf-8则会把 BOM 当成一个不可见字符留在第一行开头导致第一行匹配失败——这是中文文件里最常见的删不掉第一行的原因。二是用 Python 的chardet或charset-normalizer库自动检测但它们对小文件、短文本的判断经常不准短文件几百字节最好不要依赖自动检测宁可让人工确认。三是看能否用 GBK 解码成功中文 Windows 生成的 txt 大概率是 GBK尤其是老系统上导出的。编码特征Python 读取写法常见来源UTF-8 无 BOM无特殊头中文 3 字节encodingutf-8现代编辑器、Web 导出UTF-8 带 BOM开头EF BB BFencodingutf-8-sigWindows 记事本另存为、Excel 导出GBK / GB2312中文多为 2 字节encodinggbk老系统、中文 Windows 程序UTF-16 LE开头FF FEencodingutf-16部分日志、注册表导出6.2 CRLF 与 LF 混用的后果行尾符看起来无害实际上它是 sed、awk、Python 里一堆诡异现象的根源。Linux 用 LF\nWindows 用 CRLF\r\n。如果你在 Windows 上用 Python 以默认模式不给newline读一个 LF 文件再写出去它会自动转成 CRLF行数没变但文件大小变了反过来也一样。更麻烦的是在 sed 里从 Windows 拷过来的文件行尾带\r你写/关键词$/会匹配不上因为$前面其实还站着一个\r/^$/也匹配不上因为那行不是空的里面有个\r。解决办法是处理前先统一sed -i s/\r$// file.txt把所有 CRLF 转成 LF或者用dos2unix工具。我现在的习惯是凡是来自 Windows 环境的文本第一步就是归一化行尾再谈别的处理能省掉至少一半的奇怪 bug。6.3 复现一个中文文件被删坏的过程举个我实际遇到过的例子。一个 GBK 编码的配置 txt内容是几行中文注释加参数要删掉中间某几行。用 Python 写了encodingutf-8去读结果在读到第一个中文字符时就抛 UnicodeDecodeError脚本挂了。如果代码里图省事写了errorsignore那就更糟——它会把所有无法解码的字节直接丢掉中文字符消失参数行的内容也被吃掉一部分最后写出去的文件看着处理成功实际内容已经残缺。还有一个更隐蔽的情况用errorssurrogateescape它能把无法解码的字节原样保留下来写回时如果还用同样的编码和同样的错误处理器文件能无损往返但一旦中间有个环节用了别的编码就会残留一堆代理字符看起来像乱码。所以我的原则很明确先确认编码再处理确认不了就停下来问绝不带着errorsignore上生产。宁可多花五分钟问清楚也不要用一个看起来跑通的脚本毁掉原始数据。7. 删完才发现的问题一份踩坑记录上面讲的是应该怎么做这一节讲我当年是怎么做错的。这些坑绝大多数不是逻辑错误而是操作习惯上的疏忽但每一个都能让你白干半天。7.1 正则贪婪匹配把不该删的行一起带走了最经典的一次我要删掉一行以name开头的配置写了正则^name.*$就跑了。看起来没问题但那个文件里还有usernamexxx、filenameyyy这样的行^name只匹配行首是 name 的行所以那两条其实没事——真正出问题的是我后来改成了.*name.*想匹配包含 name的所有行结果username、filename全中招一次删了几十行。教训是正则里的锚点^和$不是可选项尤其是按内容删行时能加锚点就一定要加。如果确实需要匹配行中间那也得先用标记或者先统计命中数确认范围别直接删。还有一种是.*的贪婪特性^a.*b.*$会尽可能多地吃字符如果你以为它匹配的是a 开头 b 结尾的短串实际可能吃掉整行。这时候用非贪婪.*?更符合直觉但要注意非贪婪只在支持它的引擎里有效Python、PCRE 支持sed 的 BRE 不支持。7.2 边读边写文件被清空这个错误我犯过一次而且是最蠢的那种。用 Python 写了个脚本想着就地处理于是写成open(path, r)读同时open(path, w)写。运行完文件变成 0 字节。原因很简单w模式打开文件的瞬间就会把文件截断为 0此时你的读句柄再往下读后面已经是空的——读的时候还没读到内容写操作已经把文件清空了。正确的做法永远是先写临时文件全部写完、确认无误再用os.replace()或mv替换原文件。这个先写临时再替换的模式是所有文件处理脚本的标配宁可多一个步骤也不要冒原地覆盖的风险。同理shell 里用重定向时也要注意sort file.txt file.txt同样会把源文件清空必须用sort file.txt tmp mv tmp file.txt。7.3 只读属性、占用与权限有时候脚本跑不起来报的是拒绝访问或者你需要权限才能执行此操作。这通常有三种原因按出现频率排序一是文件有只读属性Windows 下用attrib -r 文件取消Linux/macOS 下用chmod w 文件二是文件正被别的程序打开占用比如还开着记事本、Excel 或者某个后台进程在读它这种情况只能先关掉占用程序Windows 上可以用资源监视器查看关联的句柄三是文件位于需要更高权限的目录里这种情况要看你有没有对应目录的写权限而不是去折腾权限本身。顺带说一句处理前先检查文件是否可写比事后处理报错要主动得多Python 里可以用os.access(path, os.W_OK)做一个前置判断不可写就跳过并打印提示批量处理时这一条尤其有用否则脚本跑到一半挂掉前面处理过的文件状态就乱了。7.4 千万行级别文件的处理顺序文件大到一定程度操作的顺序会直接影响总耗时。我的经验是先做最便宜的过滤再做最贵的操作。比如要删空行、去重、还要删某个正则匹配的行那就先删空行比较字符数很快再去重需要读整行建字典最后才是正则匹配。反过来先做正则白白处理了一堆马上要被删掉的空行浪费时间。另外如果文件真的很大比如几千万行建议分两步先用wc -l或者快速扫描确认行数和特征分布评估一下要删的比例如果比例很小比如只删 100 行可以考虑用grep -n定位行号再用 sed 按行号精准删除比全文读写一遍再写回要快得多。还有一种情况是文件大到根本无法一次性加载这时候 awk 和 sed 的流式优势就体现出来了它们是真的边读边处理内存占用恒定几百 MB 到几 GB 都能扛。最后分享一个我用了很多年的小习惯任何批量删除操作之前先拿文件的头 1000 行做一次试跑。命令是head -n 1000 file.txt sample.txt然后对 sample.txt 执行你准备好的那套规则看看结果对不对、删了多少行、编码有没有变。确认无误再上全量。这个动作花不了三十秒但它救过我至少三次——有一次是一条正则在小样本上表现正常上了全量后因为文件里夹了几行特殊格式的内容把不该删的区块连带删了幸好我留了备份。文本处理这件事写代码的成本很低翻车的成本很高多一次试跑、多一份备份永远不亏。
返回列表