前言
日常办公和开发里,真正天天用得到的往往不是复杂的框架,而是那种十几行就能解决一个小问题的脚本。它们不追求工程化,只追求「复制过去改两个路径就能用」。本文选的四个场景都很朴素:批量重命名文件、统计目录占用、文本按行去重、从名单里随机抽签。
需要先说明一个常见误解:很多人把「脚本」理解成「能自动干活的程序」,于是把注意力放在功能上,忽略了脚本同样要处理边界情况——文件被占用、编码不是 UTF-8、路径里有空格、抽签数量超过名单长度。这些才是脚本跑一半报错的主要原因。
下文的示例都以 Python 3 为基准,全部是标准库,不依赖任何第三方包。涉及较新语法的地方会注明版本。每个例子都保持十几行的规模,方便直接拼进你自己的工具箱。
另外要强调:本文只讲正当的本地文件与文本处理。凡是涉及探测、规避、攻击他人系统的脚本都不属于「常用脚本」的范畴,本文也不会提供。
一、批量重命名文件
最常见的需求是给一批文件统一加前缀或去掉冗余前缀。用pathlib比os.listdir更直观,因为它把路径当作对象,取名字、改名字都是方法调用。
# 适用于 Python 3.9+(用到了 str.removeprefix)
from pathlib import Path
def rename_add_prefix(folder, prefix):
"""给目录下所有文件加统一前缀(已加过的不重复加)"""
for p in sorted(Path(folder).iterdir()):
if p.is_file() and not p.name.startswith(prefix):
p.rename(p.with_name(prefix + p.name))
def rename_strip_prefix(folder, prefix):
"""去掉统一前缀"""
for p in sorted(Path(folder).iterdir()):
if p.is_file() and p.name.startswith(prefix):
p.rename(p.with_name(p.name.removeprefix(prefix)))str.removeprefix()是 Python 3.9 引入的。如果你还在 3.8,要写成p.name[len(prefix):],但那会无条件截断,前缀不匹配时反而会切错,所以更稳妥的旧写法是:
# 适用于 Python 3.8
from pathlib import Path
def rename_strip_prefix_38(folder, prefix):
for p in sorted(Path(folder).iterdir()):
if p.is_file() and p.name.startswith(prefix):
new_name = p.name[len(prefix):]
p.rename(p.with_name(new_name))Path.rename()是原地改名,返回改名后的新Path对象,原对象不会自动更新。改名要一个一个来,批量改名不存在「一次性原子提交」,所以脚本中途失败时,目录会处于改了一半的状态。稳妥的做法是先用一个「试运行」开关只打印不执行,确认无误再真正跑。
二、统计目录占用与文件类型分布
想知道某个目录里哪些类型的文件最多、总共占了多少空间,用rglob("*")递归遍历即可。注意rglob会同时返回目录,必须用is_file()过滤。
# 适用于 Python 3.8+
from pathlib import Path
from collections import Counter
def dir_stats(root):
counter = Counter()
total_bytes = 0
for p in Path(root).rglob("*"):
if p.is_file():
counter[p.suffix.lower() or "(无扩展名)"] += 1
total_bytes += p.stat().st_size
return counter, total_bytes
if __name__ == "__main__":
kinds, total = dir_stats(".")
for ext, n in kinds.most_common(10):
print(f"{ext:>12} {n:>6} 个")
print(f"合计 {total / 1024 / 1024:.2f} MB")Counter.most_common(n)返回出现次数最多的前 n 项,返回的是列表,元素是(键, 计数)元组。p.stat().st_size是字节数,按 1024 进制换算成 MB 只是便于阅读。
这里有个容易忽略的点:rglob遇到没有权限的子目录会抛PermissionError。如果你要扫描的目录有这种情况,应当在循环里try/except PermissionError跳过,而不是让整个脚本崩掉。
三、文本按行去重
处理日志、导出名单时,经常需要把重复行删掉。关键决策是「按什么算重复」:整行原样比较,还是去掉首尾空白后比较。两种语义不同,必须在函数名或参数里说清。
# 适用于 Python 3.8+
def dedup_lines(src, dst, strip_blank=True):
"""按行去重;strip_blank=True 时忽略首尾空白后再比较,
且丢弃空行。保持原有顺序。"""
seen = set()
with open(src, encoding="utf-8") as fin, \
open(dst, "w", encoding="utf-8", newline="\n") as fout:
for line in fin:
key = line.strip() if strip_blank else line.rstrip("\n")
if strip_blank and not key:
continue
if key in seen:
continue
seen.add(key)
fout.write(line if line.endswith("\n") else line + "\n")要点有三个。第一,open()必须显式写encoding="utf-8",否则在 Windows 上默认用本地代码页解码,遇到 UTF-8 文件里的中文会直接UnicodeDecodeError;这是跨平台脚本最常见的乱码来源。第二,用set判重是 O(1) 的,比每次遍历列表快得多,且随着行数增长不会退化。第三,输出时统一用newline="\n",避免 Windows 上被自动写成\r\n。
如果文件特别大(比如几 GB),把seen全放内存里不现实,这时应当改成先做外部排序再相邻去重——但那是另一个量级的需求,十几行的脚本不承担这个责任。
四、从名单里随机抽签
抽奖、分组、随机派单都用到它。标准库的random.sample(population, k)是无放回抽样,返回长度为 k 的新列表,原列表不变。
# 适用于 Python 3.8+
import random
def draw_winners(names, k, seed=None):
"""从 names 中无放回抽取 k 个"""
if k < 0:
raise ValueError("抽取数量不能为负")
if k > len(names):
raise ValueError(
f"要抽 {k} 个,但名单只有 {len(names)} 个"
)
rng = random.Random(seed)
return rng.sample(names, k)用random.Random(seed)建一个独立实例,而不是直接用模块级函数,好处是随机状态互不干扰:同一个 seed 必然得到同一结果,便于复现和留痕;同时不会影响程序里别处正在用的全局随机序列。
必须点明一个边界:random模块不是密码学安全的,它的输出可被预测,绝不能用来生成令牌、密码、验证码或抽奖中的真金白银凭据。凡是需要「防作弊」的随机,要用secrets模块(secrets.choice、secrets.randbelow等),它以操作系统提供的密码学安全随机源为基础。选错了模块,抽出来的奖可能被人反推。
常见坑点
- ❌ 直接
open("a.txt")不写encoding,在 Windows 上读 UTF-8 中文文件报UnicodeDecodeError。
✅ 一律写open("a.txt", encoding="utf-8"),写文件时再按需加newline="\n"。
- ❌ 用
for p in Path(folder).rglob("*")后直接对p调stat(),目录没权限时整脚本崩溃。
✅ 先if p.is_file()过滤,并对PermissionError单独捕获跳过。
- ❌ 边遍历
os.listdir()边对同一目录做改名/删除,结果漏处理或重复处理。
✅ 先把列表转成list(...)再循环,或改用sorted(...)生成静态快照。
- ❌ 用
random.random()或random.shuffle()生成密码、令牌、抽奖凭据。
✅ 安全场景用secrets模块;random只用于可复现的模拟与演示。
- ❌ 以为
list.sort()返回排好序的新列表,写出data = data.sort(),结果data变成None。
✅list.sort()是原地排序返回None;需要新列表用sorted(data)。
- ❌ 把
str.removeprefix()当 3.8 也能用,脚本在旧环境直接AttributeError。
✅ 3.9 起才有removeprefix/removesuffix;3.8 环境用切片或显式startswith判后再截。
- ❌ 去重时用
key = line却期望忽略大小写和空白,去重结果不符合预期。
✅ 明确去重口径:是否strip()、是否lower(),并写进函数参数名或文档字符串。
总结
| 脚本 | 关键 API | 最需要注意的边界 |
|---|
| 批量重命名 | Path.iterdir、Path.rename、str.removeprefix | 中途失败会改一半,先试运行 |
| 目录统计 | Path.rglob、Counter.most_common、stat().st_size | 无权限目录抛PermissionError |
| 文本去重 | set、open(encoding=...) | 编码必须显式指定,去重口径要明确 |
| 随机抽签 | random.Random.sample | random不安全,凭据要用secrets |
写工具箱脚本的诀窍不是把功能堆得多,而是把每个小脚本的边界想清楚:输入从哪来、编码是什么、失败时会不会污染现场。四个脚本加起来不到一百行,但把「显式编码、过滤目录、安全随机」这几条守住,它们就能长期稳定地被复用。