20个超实用Python脚本,真的是我从日常办公里一点点攒出来的。你没看错,不是那种教学用的"Hello World",而是直接能改善工作流、省下大量重复劳动的东西。不管你是刚入门的Python新手,还是已经在用脚本处理数据的职场老手,这套脚本列表都值得花几分钟看看。把"整理文件、批量改格式、清理重复数据、监控文件夹变动"这类活,从手动点点点变成双击或者敲一行命令就能完成,这就是标题里"有如神助"的真正含义。我下面会把这20个脚本分类拆开讲,还会挑几个代表性的给出完整代码和踩坑记录,方便你直接抄走改改就能用。
1. 这批脚本是怎么攒出来的:需求盘点与分类思路
1.1 为什么是20个,不是10个也不是30个
说实话,20这个数字不是一开始就定好的。我最初只是把平时在命令行里反复敲的Python片段收集起来,发现数量很快就超过了20个。于是我做了一次减法:凡是"一年用不到两次"的,删掉;凡是"需要大量人工干预才能跑通"的,再改改直到真正能一键执行;凡是"和已有脚本功能高度重叠"的,合并掉。最终留下的这20个,都是高频场景、低维护成本的小工具。
选20个还有一个实际原因:数量太少覆盖不了日常遇到的重复劳动,数量太多又会让维护成本变高,脚本这东西一旦不维护,三个月后再看就跟天书一样。我当时的思路是,每个脚本尽量只做一件事,把功能收敛到"输入什么、输出什么"都极度清晰的程度。这样即使在半年后重新翻出来用,也不需要回忆太多背景。
1.2 脚本分成了哪几类,每类解决什么问题
我把这20个脚本分成了5个大类,这样梳理起来更有条理,也方便你在需要的时候按图索骥:
- 文件管理类(4个):批量重命名、重复文件查找、大文件扫描、按类型归档。
- 数据表格类(4个):CSV合并与拆分、Excel工作表拆分、JSON与表格互转、日志解析汇总。
- 文本与剪贴板类(4个):剪贴板历史记录、词频统计、批量文本替换、随机密码生成。
- 批量处理类(4个):图片批量压缩、图片格式转换、PDF合并拆分、批量发送邮件。
- 系统与监控类(4个):目录变动监控、定时清理旧文件、端口连通性检测、文本排版清理。
这个分类逻辑其实很直接:你日常用电脑遇到的"重复活",无非就是文件、数据、文本这三个对象。文件乱了要整理,数据碎了要合并,文本脏了要清洗,再加上"重复发送、重复转换、重复检查"这一类批量动作,基本就覆盖了80%的办公场景。
1.3 一类脚本背后共同的代码弹药库
虽然脚本功能各不相同,但它们共用的底层工具高度一致。我在动手写之前,先把自己最常用的几个标准库过了一遍:
pathlib:处理路径的现代方式,比os.path好用太多,尤其适合做文件遍历和重命名。shutil:复制、移动、压缩文件,归档脚本离不开它。hashlib:计算文件哈希(MD5),重复文件查找的底层依据。re:正则表达式,日志解析和文本清洗都靠它。json与csv:数据格式转换的基础。sys与argparse:读取命令行参数,让脚本可以复用而不是写死。
我的建议是,你要想把这20个脚本玩得转,不需要系统性学习Python的全部知识,只要掌握上面这些标准库的常用API,再配合一点列表推导式和with语句,就能覆盖绝大部分场景。另外有几个第三方库也会用到,比如pandas处理大型CSV、PIL处理图片、pypdf处理PDF、watchdog监控文件系统,这些我在下一章会专门说明选型原因。
2. 动手前的关键准备:环境、依赖和脚本骨架
2.1 Python版本与依赖库怎么选
先说版本,这直接影响你后面会不会踩坑。我自己主力环境用的是Python 3.9到3.12不等,不同机器上版本会有差异。如果你是新装环境,直接装3.10以上的稳定版就好,这几个和脚本相关的标准库在3.8以上行为基本一致,但3.8以下有些语法(比如海象运算符)不支持,旧环境跑新代码容易报错。
第三方库的选择上,我的原则是"能用标准库就用标准库,标准库不够再引第三方"。像CSV合并,如果文件不大,纯标准库的csv模块就够了;但如果遇到几个GB的大文件,我会切换到pandas,因为它的分块读取和向量化操作明显快得多。图片压缩、格式转换用Pillow;PDF相关操作我用pypdf,它在处理合并、拆分、加密这些基础需求时API很稳定;目录监控用watchdog,因为它的事件驱动模式比while True轮询要可靠得多。
另外强烈建议使用虚拟环境,哪怕你只是在本机写个工具脚本。我用python -m venv建一个专门的scripts_env,把需要用到的第三方库都装进去,这样不会污染系统的全局Python环境,换电脑迁移时也可以pip freeze > requirements.txt一键复现。
2.2 一个适合所有脚本的通用模板
脚本写多了以后,我逐渐形成了一个固定的骨架。不管脚本功能是什么,开头都长这样:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 功能说明:一句话说清楚这个脚本是干什么的 用法示例:python script.py <input_path> [options] """ import argparse import sys from pathlib import Path def parse_args(): parser = argparse.ArgumentParser(description="脚本功能说明") parser.add_argument("input_path", nargs="?", default=".", help="输入路径") parser.add_argument("--output", "-o", default="output", help="输出目录") return parser.parse_args() def main(): args = parse_args() # 核心逻辑写在这里 pass if __name__ == "__main__": main()为什么坚持用这个骨架?因为我踩过太多"没有接口、参数写死"的脚本的坑。比如你写了个批量重命名脚本,直接把目录路径写在代码里,今天在这个目录跑没问题,明天换一个目录就得改代码,改完还可能引入笔误。用argparse之后,每次只需要在命令行指定目录就行,脚本变成了一个真正可复用的"工具",而不是一段一次性代码。
2.3 命令行参数处理的三种姿势
简单脚本可以只用sys.argv,比如sys.argv[1]就是第一个参数,但一旦参数多了就容易乱。我推荐的进阶姿势是argparse,不仅自动生成帮助文档,还能做类型校验。比如定义一个--days参数并指定type=int,用户不小心传了字母时程序会直接提示错误,而不是运行到一半才崩溃。
第三个姿势是用os.environ读取环境变量,这主要用在定时任务里。比如备份脚本的保留天数、备份目录等,通过环境变量传入,这样把脚本部署到不同机器时就不用改代码逻辑了。这三种姿势没有绝对的优劣,关键是根据脚本的使用频率来决定:自己随手用的小工具用sys.argv最快,要分享给别人用的一定要上argparse,跑在服务器定时任务里的考虑环境变量。
3. 20个脚本逐个看:核心功能与关键逻辑
3.1 文件管理类(脚本1-4):重命名、去重、归档、扫描
第1个脚本是批量重命名。最典型的场景是相机照片导出来全是IMG_20240101_142503.jpg这种文件名,你希望按日期加序号重命名成2024-01-01_001.jpg。核心逻辑就是用pathlib遍历目录里的文件,从每个文件的元信息里取出日期,用f-string重新组合名字,再用os.rename改掉。这里有个容易忽略的点:重命名前要先检查目标文件名是否已经存在,否则会覆盖旧文件,后来我改用Path.rename之前加上if not target.exists()判断。
第2个脚本是重复文件查找。原理不复杂:遍历目录,对每个文件计算MD5值,把哈希值相同的文件放在一起。这里有个效率优化技巧:先按文件大小分组,只有大小完全一致的文件才去算哈希,因为MD5计算在大文件上很耗时,先按大小筛掉大部分候选文件可以把耗时减少一个数量级。
第3个脚本是大文件扫描。它的用处是当电脑磁盘空间不够时,快速找出哪些目录里躺着"吃空间的大户"。我用的是os.walk遍历加getsize累加,再把结果按大小排序输出Top 20。实际上tkinter还可以给它做个简单的图形界面,但我一般不建议给这种排查类脚本加UI,命令行输出反而更快。
第4个脚本是按类型归档。下载目录里总是堆着各种PDF、图片、压缩包、安装程序,这个脚本做的事就是按扩展名把它们分门别类移动到对应子目录。比如Downloads/build/、Downloads/images/、Downloads/archives/。关键逻辑是维护一个扩展名到目录名的映射表,遇到不认识的扩展名统一放到misc/。我每周都会运行一次,几个月下来下载目录从"像垃圾场"变成了"随时能找到东西"。
3.2 数据表格类(脚本5-8):CSV、Excel、JSON、日志
第5个脚本是CSV合并与拆分。把同一个目录下几十个结构相同的CSV文件合并成一个,或者把一个大CSV按行数拆成多个小文件。用pandas的话,合并就是pd.concat一行代码的事;拆分则要配合chunksize按块读取,避免一次性加载到内存导致机器卡死。我做数据对账时经常要用它,尤其是从不同系统导出的明细表,格式一样但数据量超大。
第6个脚本是Excel工作表拆分。比如一个总表里有几十个销售团队的数据,你想按"团队名"这一列把所有行拆分成独立的Excel文件。用pandas处理很简单,groupby按关键列分组后,to_excel逐组写出去。这里要注意两点:第一,openpyxl作为Excel写入引擎必须装好;第二,写入时不要循环里反复打开工作簿,应该先收集好所有数据再批量写入,速度提升明显。
第7个脚本是JSON与表格互转。在对接接口时经常要处理嵌套非常深的JSON,我习惯先把JSON转成"拍平"的表格便于查看,再把修改后的表格转回JSON去调接口。pandas的json_normalize可以解开嵌套结构,反过来用to_json(orient="records")就能转回数组格式。对于特别深的嵌套结构,我会先把每一层路径拼成字段名再保存,这个脚本处理起来非常顺手。
第8个脚本是日志解析汇总。系统运行日志动辄几十万行,光靠Ctrl+F找错误很浪费时间。我写了一个脚本,用正则表达式匹配常见的错误关键字(ERROR、Exception、Traceback等),然后collections.Counter统计每个错误类型出现的次数和时间分布,最后输出一个汇总报告。这个脚本救过我好几次,线上问题排查时几秒钟就能定位到大致的故障模块。
3.3 文本与剪贴板类(脚本9-12):常用但要细心
第9个脚本是剪贴板历史记录。系统自带剪贴板只能保存最后一次复制的内容,但我写代码或者写报告时经常需要连续复制好几段内容。我用pyperclip库循环读取剪贴板内容,每次检测到变化就往一个本地文本文件里追加一条记录,并带上时间戳。要注意的是,这个脚本需要一直后台运行,我一般搭配开机自启来用,Windows下可以用任务计划程序,macOS下可以用launchd。
第10个脚本是词频统计。做文本分析、写总结报告时,我想快速知道一段文字里哪些词出现得最频繁。先分词,然后Counter.most_common取前20个词,配合jieba库就能处理中文文本。这里有个心得:统计之前要做好小写化、去除标点和停用词的处理,否则看到的结果会被"的、了、在"这类虚词刷屏。
第11个脚本是批量文本替换。比如一个项目里几十个文件都把旧术语"customer"写成了"client",需要全部替换过来。用pathlib递归找到所有目标文件,逐个以UTF-8编码读取,执行replace后再写回。这个脚本看起来简单,但危险性也高:如果替换词写错,可能导致一批文件改错。所以在写回之前,我总会先把匹配数量统计出来,人工看一眼再决定是否真正执行写回,千万别一步到位。
第12个脚本是随机密码生成。它可以生成包含大小写字母、数字、特殊字符的指定长度密码,并支持一次生成多条。我用它来生成服务器密码和数据库密码,生成的密码默认还会排除掉容易混淆的字符(比如0和O、1和l),这样打印出来被人拿在手上抄也不容易看错。
3.4 批量处理类(脚本13-16):图片、PDF、邮件
第13个脚本是图片批量压缩。运营同事经常需要把一批商品图压缩到300KB以内再上传,我直接用Pillow打开图片,指定质量参数后重新保存到输出目录。这里的关键参数是quality,一般70左右肉眼几乎看不出画质损失,但文件大小能缩小一半以上。如果原图尺寸很大,还可以配合thumbnail先等比缩放,进一步压缩体积。
第14个脚本是图片批量格式转换。从PNG转JPG、JPG转WebP这类转换需求很常见。用Pillow十几行代码就能实现,注意PNG转JPG时需要先把带透明通道的图片合成到白色背景上,否则输出会在透明区域变成黑色块,这是个非常容易翻车的细节。
第15个脚本是PDF合并与拆分。合并就是把多个PDF按顺序拼接成一个;拆分则是指定页码范围抽取出来生成新文件。我用pypdf来实现,核心就是PdfReader读取、PdfWriter写入。这个脚本在处理扫描件、合同材料时几乎成了办公室的公共工具,同事经常把好几份PDF丢给我合并。拆分时,页码参数建议做成--pages 1-3,5,8-10这种格式,用起来很灵活。
第16个脚本是批量发送邮件。年终汇报时经常要给分布在不同部门的人发不同的附件,如果一个个打开邮箱操作,一下午就没了。我用email和smtplib写了一个脚本:读取一个Excel或CSV,里面每一行包含收件人、主题、正文模板、附件路径;脚本用字符串模板的占位符把每封邮件个性化,再通过SMTP服务器发出。这里最需要注意的是密码或授权码安全,不要在代码里硬编码,建议从环境变量或独立的配置文件中读取。
3.5 系统与监控类(脚本17-20):自动化的前端哨兵
第17个脚本是目录变动监控。比如我有个"待处理文件"目录,新文件一放进来,脚本立刻自动执行处理流程(转格式、归档、入Excel)。原理是watchdog库的Observer监听文件系统的创建、修改、删除事件,在回调函数里写业务逻辑。这个脚本把"人盯文件夹"变成"程序盯文件夹",大幅提升了工作的响应速度。
第18个脚本是定时清理旧文件。按天为单位把某个目录下超过保留期限的文件移动或删除,避免日志、临时文件、导出的报表把磁盘塞满。脚本通过--days参数设置保留天数,默认30天。它的核心就是比较文件修改时间和当前时间的差值,配合crontab或Windows计划任务使用。我强烈建议:清理类脚本不要直接删除,而是先移动到trash/目录,等运行稳定后再改成真正删除,防止误删重要文件。
第19个脚本是端口连通性检测。排查网络问题、验证服务是否正常启动时,用telnet太原始,用专业工具又太重。我写了一个轻量脚本:传入主机名和端口列表,脚本用socket去逐一探测连接,输出每个端口的通断状态和响应耗时。这个脚本我在部署服务时几乎必用,几台机器的端口状态一眼就能看完。
第20个脚本是文本排版清理。复制来的内容经常带着乱七八糟的全角半角标点、多余空格、换行符。这个脚本能自动统一引号、转全半角、去掉行首行尾空格、合并连续空行。写公众号文章、整理会议纪要时非常实用。实现上就是一组正则替换规则,规则写得多之后,整个脚本会演化成一个小的"文本清洁工"。
4. 从需求到落地:四个代表性脚本的完整实现
4.1 脚本一:下载目录自动归档
这个脚本是我用得最频繁的,代码相对简单但非常能说明问题:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """按扩展名自动归档下载目录中的文件""" import argparse import shutil from pathlib import Path EXT_MAP = { ".jpg": "images", ".png": "images", ".gif": "images", ".webp": "images", ".doc": "docs", ".docx": "docs", ".pdf": "docs", ".txt": "docs", ".zip": "archives", ".rar": "archives", ".7z": "archives", ".tar.gz": "archives", ".exe": "apps", ".msi": "apps", } def main(): parser = argparse.ArgumentParser(description="自动归档下载目录") parser.add_argument("source", nargs="?", default="~/Downloads", help="要整理的目录") parser.add_argument("--dry-run", action="store_true", help="只预览,不移动文件") args = parser.parse_args() source = Path(args.source).expanduser() if not source.is_dir(): print(f"目录不存在: {source}") return for item in source.iterdir(): if item.is_dir(): # 不移动子目录,只处理文件 continue ext = "".join(item.suffixes) # 保留 .tar.gz 这类复合后缀 target_dir = EXT_MAP.get(ext.lower(), "misc") target = source / target_dir target.mkdir(exist_ok=True) if args.dry_run: print(f"[预览] {item.name} -> {target_dir}/{item.name}") else: shutil.move(str(item), str(target / item.name)) print(f"[移动] {item.name} -> {target_dir}/{item.name}") if __name__ == "__main__": main()这个脚本验证了一个关键细节:iterdir()只处理当前目录下的文件,不会递归到子目录。这符合"整理下载目录"的预期,因为下载目录里本身不应该有太深的层级。"".join(item.suffixes)则处理了xxx.tar.gz这种双后缀文件,避免它被归到misc里。
--dry-run参数是我后来加上的。加之前我吃过一次亏:有个文件已经存在于目标目录,shutil.move直接覆盖了同名文件,导致我一份旧版本文件被新文件覆盖了。后来每次正式移动前,我都会先跑一遍--dry-run核对一遍列表,确认无误再真正执行。
4.2 脚本二:CSV大文件拆分
工作里经常遇到财务或数据分析同事丢过来一个几百万行的CSV,说"帮忙按日期拆一下"。我实现了一个按行数拆分的脚本:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """把超大CSV按固定行数拆分成多个小文件""" import csv import argparse from pathlib import Path def main(): parser = argparse.ArgumentParser(description="CSV按行数拆分") parser.add_argument("csv_path", help="CSV文件路径") parser.add_argument("--rows", "-r", type=int, default=100000, help="每个文件多少行") parser.add_argument("--output", "-o", default="split_output", help="输出目录") args = parser.parse_args() src = Path(args.csv_path) out_dir = Path(args.output) out_dir.mkdir(exist_ok=True) with open(src, "r", encoding="utf-8", newline="") as f: reader = csv.reader(f) header = next(reader) # 保留表头 file_count = 1 current_rows = [] for row in reader: current_rows.append(row) if len(current_rows) >= args.rows: write_part(out_dir, header, current_rows, file_count, src.stem) file_count += 1 current_rows = [] if current_rows: write_part(out_dir, header, current_rows, file_count, src.stem) def write_part(out_dir, header, rows, index, stem): out_path = out_dir / f"{stem}_part_{index}.csv" with open(out_path, "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(header) writer.writerows(rows) print(f"已生成: {out_path}({len(rows)} 行)") if __name__ == "__main__": main()这里我没有用pandas,因为对大文件来说pandas一次性读入内存会有风险,而csv模块按行迭代读取,内存占用非常稳定。虽然逐行读写速度不算最快,但胜在可靠。要拆分时按行数而不是按某个字段值,因为按字段拆分用pandas groupby更自然,但如果字段值分布极度不均匀,还是按行数拆分更可控。
4.3 脚本三:日志文件错误汇总
当年排查一个线上服务问题时,我打开日志文件发现里面有几十万行,手动翻看完全不可能。后来写出的这个错误汇总脚本帮我快速定位了问题:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """扫描日志文件,统计各类错误出现的次数并按时间汇总""" import re from collections import Counter, defaultdict from pathlib import Path # 匹配常见错误形式的正则 ERROR_PATTERNS = [ r"ERROR.*", r".*Exception.*", r"Traceback \(most recent call last\):", r"Failed to .*", r"Timeout.*", ] def main(log_path, top_n=15): log_file = Path(log_path) counter = Counter() time_bucket = defaultdict(Counter) line_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(\w+)\s+(.*)") with open(log_file, "r", encoding="utf-8", errors="ignore") as f: for line in f: match = line_pattern.search(line) if not match: continue timestamp, level, message = match.groups() if level in ("ERROR", "WARN", "CRITICAL"): # 提取核心信息,去掉具体参数值,方便聚合 key_msg = re.sub(r"\d+", "#", message)[:80] counter[key_msg] += 1 time_bucket[timestamp[:13]].update([key_msg]) # 按小时聚合 print(f"日志文件: {log_file}") print(f"共发现错误/警告类型 {len(counter)} 种,Top {top_n} 如下:") for msg, cnt in counter.most_common(top_n): print(f"{cnt:>6} 次 {msg}") print("\n按小时分布(前5个最忙时段):") for hour, cnt in time_bucket.most_common(5): total = sum(cnt.values()) print(f"{hour}:00 共 {total} 条") if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "app.log")这个脚本里有个很关键的思路:re.sub(r"\d+", "#", message)把数字统一替换成#。为什么?因为日志里常见的错误是"连接超时、第3次重试失败、请求xxx失败",如果把每一次的具体参数值都当成独立错误,聚合后会出现几十条几乎长得一样的记录,根本不是"同一类错误"。把数字归一化之后,才能真正看到"同类错误到底发生了多少次"。
4.4 脚本四:重复文件清理
平时备份、解压、传输文件,很容易在硬盘里积累大量完全一样的文件。我写的去重脚本是这样工作的:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """查找目录中的重复文件(按文件大小 + MD5 哈希)""" import hashlib import os from collections import defaultdict from pathlib import Path def file_md5(path, chunk_size=8192): h = hashlib.md5() with open(path, "rb") as f: while chunk := f.read(chunk_size): h.update(chunk) return h.hexdigest() def main(root_dir): root = Path(root_dir) size_map = defaultdict(list) # 第一轮:按文件大小分组,跳过小于1KB的文件 for file_path in root.rglob("*"): if file_path.is_file(): size = file_path.stat().st_size if size >= 1024: size_map[size].append(file_path) # 第二轮:同尺寸文件再计算MD5 duplicates = defaultdict(list) for size, files in size_map.items(): if len(files) < 2: continue for f in files: md5 = file_md5(f) duplicates[md5].append(str(f)) # 输出 count = 0 for md5, paths in duplicates.items(): if len(paths) < 2: continue count += 1 print(f"MD5: {md5}") for p in paths: print(f" {p}") print(f"共发现 {count} 组重复文件组,请人工确认后删除多余副本") if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else ".")我特意没有在脚本里加自动删除功能,这是刻意的。自动删除的风险非常大,你以为重复文件可以删,但有时候文件虽然内容一样,其中一个位于另一个程序的硬连接引用路径里,删除后可能影响程序运行。所以我的方案是:脚本只负责"找出来",删除这一步永远由人脑决策。
4.5 把这些脚本串成"自动化组合拳"
单个脚本好用,但真正让工作"有如神助"的是把它们组合起来。比如我每天的早间例行流程:
- 第一步:下载目录归档脚本把昨晚下载的杂七杂八文件分类。
- 第二步:重复文件清理脚本识别和昨天重复的下载。
- 第三步:日志汇总脚本把昨晚运行的服务日志生成一个错误摘要。
- 第四步:端口检测脚本确认所有关键服务还活着。
- 第五步:如果一切正常,系统自动给我发一封汇总邮件。
在Windows上,我用任务计划程序把这些脚本按时间串起来,比如每天8点先跑归档,8点5分跑日志汇总,8点10分发邮件。在Linux或macOS上,写一个cron表达式就行,例如:
0 8 * * * cd /path/to/scripts && python download_archiver.py 5 8 * * * cd /path/to/scripts && python log_summary.py组合起来之后,原本需要半小时的人工操作变成了开机后去看一眼邮件即可。这种感觉才是标题里"如虎添翼"的真实体验。
5. 实测过程记录:这三个坑几乎人人都会踩
5.1 编码问题:中文文件名与UTF-8的坑
我印象最深的一次是给公司同事写批量重命名脚本,运行到一半突然抛了UnicodeEncodeError。原因是Windows控制台默认编码可能是GBK,当脚本打印中文路径时控制台无法正确编码就崩溃了。后来我的处理方案是两件事同时做:第一,在脚本文件头部明确# -*- coding: utf-8 -*-;第二,在读取文件列表时指定encoding="utf-8"并加上errors="ignore"兜底。具体到不同平台上,Windows还可能需要sys.stdout.reconfigure(encoding="utf-8")才能正确输出中文。
另外一个隐蔽问题出现在CSV写入上:Excel打开CSV时默认按ANSI读取,直接用Python写入的UTF-8中文会被显示成乱码。解决方法是写入时加BOM头,或者统一转成UTF-8-SIG编码。这是我做CSV拆分脚本时被领导亲自"教育"过一次的经历,从那以后凡是给非技术同事用的CSV脚本,我都会默认用utf-8-sig编码。
5.2 路径与权限问题:Windows和macOS表现差很多
在Windows上跑文件遍历脚本时,经常遇到PermissionError或者FileNotFoundError。有一次脚本处理一个被临时占用的文件,读取时报"另一个程序正在使用此文件",整个脚本断掉了。后来我学会了把所有文件操作都包在try/except里,并打印出一条日志说明跳过了哪个文件,而不是直接让脚本崩溃。
macOS和Linux上遇到的则是另一类问题:目录里存在符号链接,会让rglob在遍历时陷入循环(比如链接指向上一层目录)。我的脚本普遍增加了一个约定:rglob("*")之后立刻过滤掉符号链接,用file_path.is_file() and not file_path.is_symlink()来双保险。
还有一个权限相关的心得:清理类脚本不要用rm,在Windows上尤其不要直接调os.remove来删只读文件,否则会报错。我会先尝试chmod去除只读属性,再执行删除,但如果脚本要服务很多非技术同事,最简单的方式是让脚本把待删文件列表输出到Excel,让用户自己手动删。
5.3 依赖库版本:为什么脚本今天能跑,明天就不能了
项目里某个脚本依赖pandas,某天同事升级了一下环境里的pandas,同一个脚本开始报AttributeError。问题出在pandas的一个API在1.5之后改了行为:append方法被弃用,我用的concat方式也受到了其他影响。从那以后,我给每一个重要脚本都准备了一个requirements.txt文件,里面锁定了用到的第三方库版本。
不要小看这个动作。假设你写了个脚本用了pypdf==3.17.4,半年后pypdf升级到4.x,可能某些内部方法名就变了,脚本直接无法导入。锁定版本之后,部署到新机器时执行pip install -r requirements.txt就能原样复现当时能工作的环境。对于常更新的脚本,我还会在文件顶部注释里写清楚"最后验证日期"和"依赖列表",隔几个月回头看一下,成本极低但收益很大。
6. 常见问题排错速查表
我在用脚本的过程中积累了不少排错经验,整理成下面这个速查表,按"现象-原因-排查思路"的格式来写,碰到问题可以先翻这一节:
| 现象 | 可能原因 | 排查与处理建议 |
|---|---|---|
运行Python脚本提示ModuleNotFoundError | 第三方库没装,或装到了别的虚拟环境 | 先pip list查看已装包,再pip install 包名 |
| 中文文件名乱码或打印报错 | 控制台编码与文件编码不一致 | Windows下先执行chcp 65001,代码中指定utf-8 |
| Excel打开CSV中文乱码 | 没有写入UTF-8 BOM | 写入编码换成utf-8-sig |
| 批量重命名时文件被覆盖 | 没检查目标文件是否存在 | 重命名前必须if target.exists()判断 |
| 大文件处理时内存飙升 | 一次性读入全部内容 | 改用分块读取(chunksize或for line in f) |
| 扫描目录时陷入死循环 | 有符号链接指向父目录 | 遍历时过滤is_symlink() |
| 脚本弹文件占用错误 | 文件正被其他程序打开 | 对单文件操作加try/except,失败则跳过 |
| 定时任务里脚本运行失败 | 工作目录不是脚本所在目录 | 脚本内部用绝对路径,不要依赖相对路径 |
| 日志脚本统计出错 | 日志格式不是预期格式 | 先打印前几行原始日志,核对正则表达式 |
| 邮件脚本发送失败 | 身份认证、SMTP端口被封 | 检查授权码,换587端口并启用SMTP_SSL |
除了表格里的这些,我再补充一个"独家"排查技巧:如果脚本逻辑复杂、不确定哪里出错了,就在关键节点加print或logging打印中间变量,不要直接删代码重写。打印一行日志的成本比重新调试低得多,而且保留打印语句还能为以后排错留个线索。等确认一切正常,再决定是否保留这些调试输出。
很多人还会问:脚本运行慢怎么办?我的经验是优先优化IO,而不是优化逻辑。比如合并CSV时,多次writer.writerow逐行写入肯定比一次性writerows慢;图片压缩时,文件IO和压缩算法耗时往往比Python自身代码慢得多。先用time模块测量哪一段耗时长,再针对性地优化,不要一上来就上多线程或改复杂算法。实际上很多办公脚本,几十兆数据用普通写法也就几秒钟,完全够用。
7. 最后分享两个我越用越顺手的小方法
第一,我为每一类脚本都建了别名,放在Shell配置文件里。比如Windows下我用doskey,macOS和Linux下我用alias。archive就是下载目录归档,dupfind就是查重复文件,logs就是日志汇总。这样我不需要每次打python /path/to/script.py那么长一串命令,敲几个字母加个路径参数就完事了。脚本的"使用门槛"越低,你越愿意用它,也越能感受到效率提升。
第二,我维护了一个"脚本的脚本":一个README.md文件和一个requirements.txt文件。README.md里按分类记录了所有脚本的功能、用法示例、依赖和已知限制,requirements.txt记录了所有第三方库。坚持做这件事之后,哪怕是半年之后换电脑、重新搭环境,我也能一天之内把所有工具恢复到位,不会出现"脚本还在但忘了怎么用"的尴尬。
如果你打算照着这套思路搭建自己的脚本库,我的建议是从最痛的一个场景切入:看你哪项重复劳动最频繁,就先写那一个脚本。不要试图一次搞定所有事情,先把一个场景打通,跑顺了再往下扩展。等到手里攒了几个能稳定运行的脚本之后,你会发现自己对Python的兴趣会随之涨起来,后面再写更大的工具也就不成问题了。