陈小涛的技术学习笔记

Python 脚本自动化入门记录

我最早学 Python 不是为了写什么大项目,动机特别朴素:每天有一堆手动重复的破事,烦。整理文件、改名字、统计表格里的数、定时备份——这些活单次做只要几分钟,但每天都做,一年下来就是几百个小时。这篇文章用一个我真实用过的例子,把"从手动到自动"这条路完整走一遍。我会把中间踩的坑也写上,因为那些坑才是真正卡住人的地方。

批量处理干跑验证参数化定时任务

一、自动化的本质:把重复动作变成循环

想通这一层之后,写脚本这件事的门槛一下子降下来了。任何"对一堆东西做同一件事"的工作,结构都是一样的三步,没有例外:

① 找出所有目标 遍历 ② 对每个做同一件事 处理 ③ 把结果写出去 落盘或打印 把这三步想清楚,代码基本就成型了
自动化不需要聪明,需要的是把步骤拆干净

我第一次意识到这点,是在手动改了两百多个文件名之后。当时我的做法是:全选、右键重命名、输入新名字、确定,然后发现自己漏了几十个,又重新来一遍。做完那一刻我就在想——这件事机器做只要一秒钟,我为什么要花四十分钟?

二、第一个真正用上的脚本

需求很简单:把一批 photo_001.jpg 这样的文件,统一加上日期前缀,让它按时间顺序排得整整齐齐。

import os

folder = "./photos"
prefix = "20260925_"

for name in os.listdir(folder):
    old = os.path.join(folder, name)
    if not os.path.isfile(old):
        continue                        # 只处理文件,跳过子目录
    new = os.path.join(folder, prefix + name)
    os.rename(old, new)
    print(f"已重命名: {name} -> {prefix + name}")

十行代码,但是第一次跑的时候我没敢直接跑。我先把它改成了只打印不执行:

# 第一遍:只打印,不动文件
for name in os.listdir(folder):
    print(f"将要重命名: {name}  ->  {prefix + name}")

# 确认打印出来的东西就是我想的那样之后,
# 再把上面的 print 换成真正的 os.rename
坑点:这条"先打印、再执行"的习惯,是我用一次代价换来的。当年路径多写了一层,脚本把文件挪到了完全不相干的目录里,找回来花了半小时。从那以后凡是涉及改名、移动、删除的脚本,第一遍一律只打印不执行。

三、从"能跑"到"敢用",要补的四件事

上面那个十行脚本能解决一次问题,但你不会想用它第二次——因为路径和前缀都写死在代码里,每次用都要改代码。想让脚本真正变成能反复用的东西,要补四件事:

  1. 把会变的东西提到外面。路径、前缀、日期格式这些每次都可能不同的值,改成从命令行参数拿,而不是写死在代码里。改参数不改代码,这是脚本和一次性代码的分界线。
  2. 加一个干跑开关。给它一个 --dry-run 参数,打开就只打印计划、不真的动手。这样你每次用它之前都能先看一眼它打算干什么,安心。
  3. 让它留下记录。每次跑完往一个日志文件里追加一行:什么时间、处理了多少个、有没有失败。出问题的时候,这份记录比你的记忆可靠得多。
  4. 把可能的意外接住。某个文件被占用、权限不够、编码不是 UTF-8——这些都会让脚本中途崩掉。用 try/except 把单个文件的失败接住、记下来、继续处理下一个,别让一个坏文件毁掉整批任务。
我的判断标准:如果这个脚本我这辈子只会用一次,那就怎么快怎么来,十行搞定;如果它会被用第二次,上面四件事就得补上。补这四件事花的时间,通常在第三次使用时就赚回来了。

四、一个更完整的版本

把上面四条都加进去之后,脚本大概长这样。代码长了一倍,但从此我就再也不用打开它改代码了:

import os
import sys
import argparse
from datetime import datetime

def main():
    p = argparse.ArgumentParser(description="给文件批量加日期前缀")
    p.add_argument("folder", help="要处理的目录")
    p.add_argument("--prefix", default=datetime.now().strftime("%Y%m%d_"),
                   help="前缀,默认是今天的日期")
    p.add_argument("--dry-run", action="store_true", help="只打印,不真改")
    args = p.parse_args()

    if not os.path.isdir(args.folder):
        print(f"目录不存在: {args.folder}")
        sys.exit(1)

    ok, fail = 0, 0
    for name in sorted(os.listdir(args.folder)):
        old = os.path.join(args.folder, name)
        if not os.path.isfile(old):
            continue
        new = os.path.join(args.folder, args.prefix + name)
        if os.path.exists(new):
            print(f"跳过(目标已存在): {name}")
            fail += 1
            continue
        try:
            if args.dry_run:
                print(f"[干跑] {name} -> {args.prefix + name}")
            else:
                os.rename(old, new)
                print(f"已处理: {name}")
            ok += 1
        except OSError as e:
            print(f"失败: {name} ({e})")
            fail += 1

    print(f"\n完成:成功 {ok} 个,跳过/失败 {fail} 个")

if __name__ == "__main__":
    main()

用法就很舒服了:

# 先看它打算干嘛
python rename.py ./photos --dry-run

# 确认没问题再真跑
python rename.py ./photos

# 想换前缀就传参数,不用改代码
python rename.py ./photos --prefix "旅行_"

里面有两点值得单独说:一是 if os.path.exists(new) 那个判断,避免把已经处理过的文件再套一层前缀;二是 if __name__ == "__main__" 这个写法,它让这个文件既能直接跑,也能被别的脚本导入复用。

五、让它自己按时跑

脚本写好了,接下来是不用自己记得点它。Linux 上交给 cron:

# 打开当前用户的定时任务表
crontab -e

# 每天凌晨 3 点跑一次
0 3 * * * /usr/bin/python3 /opt/scripts/backup.py >> /var/log/backup.log 2>&1

# 工作日每 2 小时跑一次
0 */2 * * 1-5 /usr/bin/python3 /opt/scripts/sync.py

五个位置的顺序是:分 时 日 月 周,后面接要执行的命令。Windows 上对应的是"任务计划程序",思路完全一样——定时间、指脚本、设好工作目录。

坑点(这个我吃过):定时任务里的环境和你登录后的终端不是同一个。PATH 更短、环境变量更少,所以"我在终端里跑得好好的脚本,放进定时任务就报找不到命令"。解决办法有两个:命令一律写绝对路径;需要环境变量的话在脚本里显式读配置文件,别依赖当前终端的设置。

另一个建议:定时任务里跑的命令,一定要把输出重定向到日志文件(就是上面 >> 和 2>&1 那部分)。不重定向的话,脚本半夜失败了你是不会知道的——什么都没有发生,和"跑成功了"看起来一模一样。

六、我给自己定的几条规矩

会做的

  • 先手动跑通逻辑,再套循环
  • 涉及改删的先干跑一遍
  • 文件开头写清用途和用法
  • 把路径当参数传进去
  • 跑完打印一行摘要

不去做的

  • 一上来就写全自动的复杂流程
  • 在脚本里硬编码一堆路径
  • 不留任何输出,失败了还不知道
  • 为了"优雅"把简单事情写复杂
  • 在没有备份的目录上直接试

小结

Python 让我最舒服的一点是:它允许我用几十行解决一个具体问题,而不强迫我为了解决方案去搭一整套工程。一个脚本只要解决了你真实的麻烦,它就有价值,跟它有多少行、用了多高级的技巧完全无关。我现在判断一个脚本好不好的标准只有一条——下次遇到同样的活,我会不会想起它、愿不愿意用它。