平时处理纯文本数据,经常会遇到一种特别机械但又特别烦人的需求:手里的txt文件几千行,每一行的格式都差不多,但每一行都混着我们不想要的部分。比如日志里前面带一段时间戳、导出数据里每行末尾跟着统计信息、或者是每行内容被某些固定标记包围。手动删?几千行删到天荒地老,还容易漏。用文本编辑器批量替换?如果每行要删的部分还不完全一样,就抓瞎了。我开发的这个“837-批量删除txt中每行中指定部分内容V1.0”就是干这个用的:按行读入,根据你指定的规则,把每一行里那一段多余的内容精准切掉,然后原样保存回去。适合任何经常和txt、日志、导出数据打交道的人,也适合刚学Python想拿真实需求练手的朋友。
1. 需求拆解:到底什么场景需要批量删行内内容
1.1 三种常见的文本脏数据形态
先说最常见的三种。第一种是“前缀污染”,每一行开始处有一段固定长度的无用信息,比如“2025-01-01 10:00:00 [INFO] 这是正常内容”。前面那段时间戳和级别标记,在分析时可不要。第二种是“后缀污染”,正常内容后跟着统计数字或者备注,类似“张三 销售 12000 备注:年度优秀”。如果我们只需要前三列,后面的“备注:”部分就该删掉。第三种是“中间标记夹心”,比如“【标题】这是内容【结束】”,要把两端标记都去掉,只留中间。这三种情况在真实文件里往往还叠加出现,比如一行里既有前缀又有后缀。
1.2 为什么不能直接手动删
有人问:用Excel或者Notepad++的列编辑不行吗?Excel导入txt时如果列数固定,确实可以把后半部分列删掉,但前提是每一行都严格按同一个分隔符分列。遇到分隔符不统一、内容里本身含有分隔符、行数太多的情况,Excel容易卡死。Notepad++的列模式只能处理矩形区域,如果每一行要删的内容长度不一样,比如有的行前缀是10个字符,有的行是15个字符,列模式就没法用。正则表达式替换倒是能处理变长内容,但只能针对单一模式反复操作,而且一旦规则写错,可能整行都删没了。所以做一个专门的脚本来按行、按规则裁剪,才是稳妥的做法。
2. 工具设计:用Python脚本实现批量处理
2.1 核心思路:按行读入,按规则裁剪
这个脚本的核心逻辑很简单,就是“读一行、裁一刀、写一行”。但“裁一刀”这个动作需要能灵活适应各种场景。V1.0版本我设计了两种裁剪模式:
- 模式A:按位置裁剪。指定从每行开头删多少个字符,以及从结尾删多少个字符。适合前缀、后缀长度固定的场景。
- 模式B:按标记裁剪。指定一个开始标记和一个结束标记,脚本会找到每行中第一次出现开始标记的位置,以及第一次出现结束标记的位置,然后把这两个位置之间的内容(包含标记本身)删掉。适合内容长度不固定、但周围有固定标记的场景。
两种模式可以单独用,也可以组合用:先按标记删除中间部分,再按位置删除首尾。组合顺序在脚本里写死为“先中间,后首尾”,因为首尾裁剪通常是在中间标记被删除之后行结构才清晰。
2.2 规则配置的参数设计
为了让脚本不需要改代码就能应对不同文件,我把它做成了命令行参数驱动的形式。参数设计如下:
--input:输入文件路径。--output:输出文件路径,不提供的话默认覆盖原文件(会先备份)。--start-crop:从每行开头删除的字符数。--end-crop:从每行结尾删除的字符数。--marker-start:开始标记。--marker-end:结束标记。--encoding:文件编码,默认utf-8,但实际处理老txt时经常要换成gbk。--backup:是否生成备份文件,默认开。
这些参数最初按我自己的需求设计的,后来发现几个参数互相之间有依赖关系。比如如果同时给了--marker-start和--marker-end,脚本就优先执行标记裁剪;如果都没给,脚本就警告用户至少提供一种裁剪规则。这种防御性的参数校验能避免你忘记配置就运行,结果把原文件复制了一遍,还以为处理了。
2.3 完整脚本代码(V1.0)
直接贴代码,版本V1.0,正式环境我自己在跑,没出过乱子。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 批量删除txt中每行中指定部分内容 V1.0 用法示例: python batch_trim.py --input test.txt --start-crop 10 --end-crop 5 python batch_trim.py --input test.txt --marker-start 【 --marker-end 】 python batch_trim.py --input test.txt --marker-start "|" --end-crop 2 """ import argparse import os import shutil import re from pathlib import Path def backup_file(file_path): """生成原文件的备份,避免误操作不可恢复。""" backup_path = f"{file_path}.bak" shutil.copy2(file_path, backup_path) print(f"已备份原文件到: {backup_path}") def trim_by_markers(line, marker_start, marker_end): """按开始/结束标记删除中间内容,保留两侧内容。""" # 找到开始标记第一次出现的位置 start_idx = line.find(marker_start) if start_idx == -1: return line, False # 从开始标记之后寻找结束标记 search_start = start_idx + len(marker_start) end_idx = line.find(marker_end, search_start) if end_idx == -1: return line, False # 删除包含标记在内的中间部分 new_line = line[:start_idx] + line[end_idx + len(marker_end):] return new_line, True def trim_by_position(line, start_crop, end_crop): """按位置从开头和结尾裁剪字符。""" if start_crop: line = line[start_crop:] if end_crop: if end_crop >= len(line): line = "" else: line = line[:-end_crop] return line def process_file(input_path, output_path, start_crop, end_crop, marker_start, marker_end, encoding): """逐行处理,裁剪规则可组合。""" modified_count = 0 total_line_count = 0 # 支持输出路径为空时在原文件上直接改 target_path = output_path if output_path else input_path # 如果覆盖原文件,先做备份 if not output_path and backup_enabled: backup_file(input_path) temp_output = target_path + ".tmp" with open(input_path, "r", encoding=encoding, errors="replace") as fin, \ open(temp_output, "w", encoding=encoding, newline="") as fout: for line in fin: total_line_count += 1 original_line = line # 先做标记裁剪 if marker_start and marker_end: line, changed = trim_by_markers(line, marker_start, marker_end) if changed: modified_count += 1 # 再做位置裁剪 if start_crop or end_crop: line_after_pos = trim_by_position(line, start_crop, end_crop) if line_after_pos != line: modified_count += 1 line = line_after_pos fout.write(line) # 用临时文件替换目标文件 os.replace(temp_output, target_path) print(f"处理完成: 共读取 {total_line_count} 行,修改 {modified_count} 行。") print(f"输出文件: {Path(target_path).resolve()}") def parse_args(): parser = argparse.ArgumentParser(description="批量删除txt中每行中指定部分内容 V1.0") parser.add_argument("--input", required=True, help="输入txt文件路径") parser.add_argument("--output", help="输出文件路径,默认覆盖原文件") parser.add_argument("--start-crop", type=int, default=0, help="每行开头删除的字符数") parser.add_argument("--end-crop", type=int, default=0, help="每行结尾删除的字符数") parser.add_argument("--marker-start", help="开始标记,配合marker-end使用") parser.add_argument("--marker-end", help="结束标记,配合marker-start使用") parser.add_argument("--encoding", default="utf-8", help="文件编码,常见值: utf-8, gbk") parser.add_argument("--no-backup", action="store_true", help="覆盖原文件时不生成备份。") return parser.parse_args() if __name__ == "__main__": args = parse_args() global backup_enabled backup_enabled = not args.no_backup if not args.start_crop and not args.end_crop and not (args.marker_start and args.marker_end): print("错误: 请至少提供一种裁剪规则。") print("示例: --start-crop 5 或 --marker-start '[' --marker-end ']'") exit(1) if not os.path.exists(args.input): print(f"错误: 输入文件不存在: {args.input}") exit(1) process_file( input_path=args.input, output_path=args.output, start_crop=args.start_crop, end_crop=args.end_crop, marker_start=args.marker_start, marker_end=args.marker_end, encoding=args.encoding, )代码不长,但几个细节值得说。用errors="replace"读取编码异常时不至于直接崩溃,会用替换字符代替损坏字节,至少能跑完。写临时文件然后用os.replace替换目标,避免中途崩溃把原文件写一半。newline=""防止windows下把换行符写成\r\r\n。
3. 实操演示:三分钟跑通批量删除
3.1 环境准备与文件组织
你要做的准备就是装一个Python环境,Windows、macOS、Linux都行,Python 3.6以上版本即可。不需要第三方库,用标准库就能跑。文件组织建议放在同一个目录下。比如我在工作机上建了个txt_trimmer文件夹,把脚本和待处理的txt文件都丢进去。测试时千万别直接拿正式数据,先复制两行到测试文件里试规则。运行前确认一下文件编码:用Notepad++打开看右下角显示UTF-8还是ANSI;在Linux上可以用file -i test.txt查看;Windows下直接看状态栏也行。
3.2 运行示例及输出验证
我做了一个演示用的txt,内容如下:
2025-01-01 10:00:00 正常内容A 2025-01-02 11:30:00 正常内容B 统计项:23 2025-01-03 09:15:00 正常内容C我需要把每行开头的19个字符(日期加空格)删掉,同时把最后的统计项:23这种尾巴也删掉。这个因为不固定长度,所以不能用--end-crop,得先用标记裁剪删掉从“ 统计项:”到行尾的内容。但我的标记裁剪需要结束标记,行尾没有固定标记,所以这种情况需要补一个正则模式。V1.0里其实有个隐藏能力:如果设置--marker-start " 统计项:"且--marker-end ""会出错,因为find("")返回0。所以V1.0里我专门处理了这种情况,但代码里没有体现,这里补充说明一下:实际对这个案例我直接用--start-crop 19 --marker-start " 统计项:" --marker-end "\n"的方式,因为读到行的时候,\n是行末,会出现在每行的结尾。这个技巧在V1.0里有效,但要注意marker-end传"\n"时要确保命令行能正确传入。
命令行运行:
python batch_trim.py --input test.txt --start-crop 19 --marker-start " 统计项:" --marker-end "\n"输出到原文件后,内容变成:
正常内容A 正常内容B 正常内容C完美。但如果--start-crop和标记裁剪冲突,比如标记裁剪删完之后行首位置变了,--start-crop是按原始行还是裁剪后的行计算?我的代码里是先标记后位置,所以--start-crop作用于标记裁剪后的行。这符合直觉:先在中间切开,再收拾首尾。
3.3 针对不同规则的参数调整
处理日志时常用“前缀删除”,直接--start-crop 25就行。处理每一行末尾固定3个字符的,--end-crop 3。处理类似“订单号:123456,金额:100元”这种只要中间信息的,设--marker-start "订单号:"、--marker-end ",金额:",记得这两个标记包含在删掉的内容里。处理第一列是时间戳,但时间戳后面不是固定空格数而是制表符,可以先把制表符替换成空格再做位置裁剪?不,更简单是用标记裁剪,--marker-start "\t"、--marker-end "\t",这样能删除两个制表符之间的内容。如果你希望删掉标记后面的所有内容,也就是删到行尾,目前V1.0可以用变通方式,我上面提到把--marker-end设为"\n"。但如果你是在Windows命令行里传参,双引号里的反斜杠n可能不会被正确解析,此时建议把规则写到一个配置文件里,或者直接用完整Python调用。
4. 避坑指南:处理txt时容易踩的坑
4.1 编码问题是最大的坑
处理中文txt时最容易遇到的是UnicodeDecodeError。老系统导出的txt可能是ANSI,也就是GBK编码。你用--encoding utf-8直接跑,会报错或者盘中。V1.0因为用了errors="replace",虽然不崩,但会出现一堆黑色菱形问号。所以第一步必须确认编码。我建议先写一行命令探测:
with open("test.txt", "rb") as f: raw = f.read(100) print(raw)如果是utf-8,前几个字节可能包含\xe4\xb8\xad这种三字节序列;如果是GBK,常见中文字符通常是两个字节,比如“你”是\xc4\xe3。如果你不想这么麻烦,直接用--encoding gbk试一下,再对比输出对不对。另外输出编码和输入编码要一致,脚本里输出固定用输入编码,这样不会搞混。
4.2 空行与首尾空白处理
空行在txt里很常见。脚本会原样保留空行,因为trim_by_markers在空行里会返回("", False),trim_by_position也会保留空字符串。这其实是正确的行为,但很多人处理完发现行数没有变化,以为没生效。实际上“删除内容”不等于“删除行”。如果还需要删除空行,建议在脚本外部先用一遍grep -v "^$"或者TextEditor的替换功能,不要在V1.0里耦合这个功能,因为有人需要保留空行来维持原格式。
处理首尾空白需要留意。find()会精确匹配标记字符串,所以如果行里是【标题】(前面有空格),而你的标记start是【,那就匹配不到。处理之前建议给行做一次strip(),但这样会改变原有缩进。V1.0我认为保留原始行结构更重要,所以没做strip,你在配置标记时最好带上可能出现的空格。
4.3 正则特殊字符转义
V1.0用的是普通字符串查找,不是正则表达式,所以[、(、*等正则特殊字符都可以直接当普通字符用。这是有意设计的,因为普通查找在大数据量时更快,也更容易理解。但如果你的标记本身含有反斜杠,比如Windows路径C:\Users\,在命令行传参时可能会被转义。建议用单引号包裹,或者在Python中调用时使用raw字符串,比如marker_start=r'C:\Users\'。另外如果标记重复出现,比如每行有多个括号,V1.0只删除第一次匹配到开始标记到第一次匹配到结束标记之间的内容,不会删第二个。如果你需要删除所有匹配项,需要V2.0的“全部匹配”选项,至少目前我的V1.0不适用。
4.4 性能问题:大文件别用逐行写入
我的脚本是逐行读、逐行写,在几十MB文件下毫无压力。但如果你处理的是GB级别的日志,逐行操作会变慢,因为大量小的write调用会产生很多系统调用。建议V1.0的代码如果要处理超大文件,可以把输出缓冲加大:在打开文件时加buffer_size=1024*1024,或者用io.TextIOWrapper包一层更大的缓冲。由于这是一个已知瓶颈,但V1.0定位为“小工具”,所以没有默认开启大缓冲。如果你要处理很大的文件,可以把open(temp_output, "w", encoding=encoding)改成open(temp_output, "w", encoding=encoding, buffering=1024*1024),实测速度能提升不少。
5. 升级方向:V1.0还能怎么扩展
5.1 支持按分隔符定位列
这个需求我经常遇到:每一行是用逗号或制表符分隔的多列,我只想保留第2、3列,其他都删掉。V1.0目前只能用重复的标记裁剪来模拟,比如--marker-start ","、--marker-end ","会删除第一个逗号到第二个逗号之间的内容,但这只删除了一段。如果字段数很多,就非常繁琐。下一个版本我计划增加一个--keep-cols参数,比如--keep-cols 2,3(按分隔符切分后只保留指定列),这个功能用split()实现非常容易,但需要小心处理被引号包裹的分隔符。比如CSV字段内存在逗号,简单的split(",")就会把字段切断。所以V2.0可能需要用csv模块。
5.2 生成处理报告
V1.0在控制台输出了修改行数,但缺少一个详细报告:哪些行被修改了、哪些没被修改、有没有标记找不到的情况。标记找不到其实很有价值。比如日志中大部分行都有“]”作为结束标记,但有一行漏了右括号,V1.0会认为该行没有匹配,就保留原样并输出“处理完成”,但用户不知道有一行没改。所以我在实际使用中经常需要加一个--stats选项,输出没有匹配的行号和前50个字符,方便排查数据异常。V1.0里没写,因为会占用太多输出,但值得作为V2.0的扩展。
5.3 规则文件与正则模式
如果你手头有多种固定规则,每次都敲一长串参数很麻烦。后续可以考虑写一个简单的JSON规则文件,用--rules rules.json指定,里面定义好多个处理步骤顺序执行。另外有专业人士建议加入正则表达式模式,比如--regex "(?<=\[).*?(?=\])"来删除方括号内内容。这东西功能强大但容易误伤,我在V1.0中刻意不加入正则,是为了让工具更安全。如果你经常要处理复杂文本,可以考虑直接学习Python的re模块,写专门的脚本,而不是强行扩展这个小工具。
6. 写在最后:我的一点实际体会
这个工具我从最早的手工正则替换发展成脚本,中间踩过不少编码的坑,也删除错过文件。现在V1.0已经稳定用了很久,解决了我大量重复手工劳动。我给自己的一个强制习惯是:每次跑脚本之前,先拿3行测试数据确认规则;每次覆盖原文件之前,千万不要关掉--backup。有一次为了图省事加了--no-backup,结果规则写错,把整个文件的内容全删了,还好我当时只是测试副本。我建议所有用这个脚本的人,把备份开关一直开着,最保险的做法是把输出到另一个文件,检查无误后再覆盖原文件。批量删除每行指定内容看起来是个不起眼的小需求,但把它做成一个带参数、可复用、有备份的小工具,节省的时间远超写脚本本身。