4.12 Python 编写运维自动化脚本
预计阅读时间:15 分钟
📖 目录
学习目标
- 掌握 Python 运维脚本的核心库:argparse、subprocess、requests、paramiko、psutil、logging
- 理解何时该从 Shell 切换到 Python,以及两者的权衡
- 能用 Python 编写实用的运维自动化脚本(命令包装、SSH 批量操作、系统健康检查)
- 学会使用 virtualenv / Poetry 管理项目依赖和环境隔离
核心知识
| 库 / 工具 | 用途 | 关联知识 |
|---|---|---|
| argparse | 命令行参数解析 | 对应 2.1:Shell 脚本入门 $1/getopts |
| subprocess | 执行系统命令、捕获输出 | 对应 2.1:Shell 脚本入门 命令执行、2.8:Shell 脚本进阶 管道组合 |
| requests | HTTP API 调用 | 对应 4.5:网络故障排查 curl/nc 网络排查 |
| paramiko / Fabric | SSH 远程执行与控制 | 对应 2.11:SSH 深入 SSH 深入、2.8:Shell 脚本进阶 批量脚本 |
| psutil | 系统资源采集(CPU/内存/磁盘) | 对应 3.12:系统监控与告警 系统监控、2.7:日志与故障排查 故障排查 |
| logging | 结构化日志输出 | 对应 2.7:日志与故障排查 syslog/logger |
| virtualenv / Poetry | Python 环境隔离与依赖管理 | 对应 ch8 软件包管理思路 |
常用库速查表
| 库 | 关键函数/对象 | 一句话用法 |
|---|---|---|
| os | os.path.join / os.walk / os.makedirs / os.listdir | os.walk(root) 递归遍历目录树 |
| shutil | shutil.move / copy2 / rmtree / disk_usage | shutil.disk_usage("/") 直接取磁盘总量/已用 |
| subprocess | run / Popen / CalledProcessError | subprocess.run(cmd, capture_output=True, check=True) |
| re | compile / match / search / findall / sub | re.findall(r"\d{4}-\d{2}-\d{2}", text) 提取日期 |
| json | dumps / loads / dump / load | json.dumps(data, ensure_ascii=False, indent=2) 中文友好输出 |
| requests | get / post / Session / Timeout | requests.Session() 复用连接池,必带 timeout |
| datetime | now / strptime / timedelta | datetime.now() - timedelta(days=7) 算 7 天前的时刻 |
| concurrent.futures | ThreadPoolExecutor | 并发跑 SSH/HTTP 批量任务,IO 密集型提速显著 |
知识关联
- 前置知识:2.1:Shell 脚本入门 Shell 脚本入门、2.8:Shell 脚本进阶 Shell 脚本进阶、2.11:SSH 深入 SSH 深入(paramiko 依赖 SSH 协议理解)
- 后续影响:4.1:Ansible 自动化 Ansible 自动化运维(Python 是 Ansible 底层语言)、4.9:CI/CD 持续部署 CI/CD 持续部署(Python 编写流水线脚本)
- 配套技术:Python + Fabric + Invoke 替代 Shell 做远程运维,Poetry/virtualenv 管理项目依赖隔离
原理讲解
当 Shell 不够用时——何时该换 Python 写运维脚本?
Shell 脚本(2.1:Shell 脚本入门、2.8:Shell 脚本进阶)擅长文件操作、管道组合和快速原型,但在以下场景力不从心:
- 复杂逻辑:多层循环、条件分支、数据结构(字典/列表)
- 异常处理:网络超时、SSH 断开、JSON/XML 解析失败
- 跨平台:Shell 命令因发行版/系统而异,Python 标准库统一
- 依赖管理:多脚本共享函数库时,Shell 的 source 机制脆弱
- 性能:大量文本解析或 HTTP 调用时 Python 更高效稳定
决策原则:单次手动的任务用 Shell;需要重试、告警、持久化、结构化数据的自动化任务用 Python。
Python 运维场景清单
实践中哪些任务值得用 Python 而不是 Shell?按出现频率排序的运维场景清单:
| 场景 | 典型输入 | Python 优势 |
|---|---|---|
| 日志分析 | GB 级 access.log / 应用日志 | 正则 + 计数器单次扫描,内存可控 |
| 文件批处理 | 批量重命名、归档、清理过期文件 | os/shutil 跨平台,异常可控可回滚 |
| 自动化巡检 | 多台服务器指标采集 | psutil + 线程池并发,输出结构化 JSON |
| API 调用 | 云厂商/监控/告警平台 REST API | requests 处理认证、重试、分页 |
| 数据清洗汇总 | 导出的 CSV/JSON 生成报表 | csv/json 标准库直接结构化处理 |
| 故障自愈编排 | 多步骤回滚、条件分支决策 | 完整编程语言,逻辑可写单元测试 |
Python vs Shell 选型对比
| 维度 | Shell | Python |
|---|---|---|
| 启动速度 | 毫秒级 | 百毫秒级(解释器启动开销) |
| 文本管道处理 | 极佳(awk/sed/grep 组合) | 好(re 模块 + 正则) |
| 数据结构 | 只有字符串/数组,复杂逻辑痛苦 | 字典/列表/集合/类,天然适合 |
| 异常处理 | 无原生 try/catch,靠 exit code | try/except/finally 完整 |
| 第三方生态 | 依赖系统命令是否安装 | pip 安装即用(requests/psutil/paramiko) |
| 跨平台 | 命令差异大(如 sed -i 各发行版不同) | 标准库行为统一 |
| 维护性 | 超过 100 行难以维护 | 函数/类组织,可写测试 |
| 适合任务 | 单次手工操作、简单管道 | 定时自动化、需重试/告警/持久化 |
经验法则:脚本 30 行以内且一次性使用 → Shell;需要长期维护、会重试/告警/解析结构化数据 → Python。两者也常组合:Shell 管道里用 python3 -c 处理中间数据。
subprocess 详解:run / Popen / shell 安全
subprocess 是 Python 运维脚本的"shell",掌握三个层次即可覆盖绝大多数场景:
| 函数 | 适用场景 | 关键参数 |
|---|---|---|
subprocess.run() | 执行命令、等待结束、取结果(首选) | capture_output, text, check, timeout |
subprocess.Popen() | 持续交互、实时读输出流 | stdout=PIPE, stderr=STDOUT, stdin=PIPE |
subprocess.check_output() | 只要标准输出,出错即抛异常(Python 3.5+ 推荐用 subprocess.run(capture_output=True) 替代) | stderr=subprocess.STDOUT |
import subprocess
# run():一次性执行(90% 的场景用这个)
r = subprocess.run(["systemctl", "is-active", "nginx"],
capture_output=True, text=True)
print(r.returncode, r.stdout.strip()) # 输出: 0 active
# Popen():实时读日志流(等价于 tail -f 并过滤)
p = subprocess.Popen(["journalctl", "-f", "-u", "nginx"],
stdout=subprocess.PIPE, text=True)
for line in p.stdout:
if "error" in line:
print("发现错误行:", line.strip())
break
p.terminate()
# shell=True 的危害演示:命令注入
filename = "foo; rm -rf /tmp/evil"
subprocess.run(f"ls {filename}", shell=True) # 危险!文件名里的分号会被 shell 执行
subprocess.run(["ls", filename]) # 安全:列表参数不经 shell 解释
shell=True 安全警告:一旦用了 shell=True,字符串里的 ;、|、>、$() 都会被 shell 解释。脚本输入来自外部(文件名、URL 参数、API 返回值)时,必须用列表参数形式让 Python 直接 exec。真需要管道时用 Popen(stdout=PIPE) 串联两个进程,而不是拼 shell 字符串——后者既是安全漏洞也是引号地狱。
为什么用 Python 而不是 Bash 写运维脚本
Bash 脚本在简单任务上无可替代——启动快、管道组合优雅、系统命令直接调用。但当脚本超过 100 行或需要处理复杂逻辑时,Bash 的局限暴露:数据结构缺失——只有字符串和数组,没有字典/集合/对象,解析 JSON 需要 jq 等外部工具;错误处理脆弱——set -e 只能捕获命令返回码,无法区分"命令不存在"和"命令执行失败";跨平台差异——sed -i 在 macOS 和 Linux 上语法不同,readlink -f 在 BSD 上不存在;可测试性差——Bash 函数难以单元测试。Python 标准库统一了这些差异,subprocess 提供安全的命令调用,try/except 提供完整的异常处理链,argparse 自动生成帮助文档。经验法则:脚本 30 行以内且一次性 → Bash;需要长期维护、会重试/告警/解析结构化数据 → Python。
为什么 paramiko 而不是直接 subprocess 调用 ssh
用 subprocess.run(["ssh", "host", "cmd"]) 调用 SSH 有几个问题:输出解析困难——SSH 的连接信息、警告、错误混在 stdout/stderr 中,难以程序化分离;连接复用困难——每次 subprocess 调用都是独立的 SSH 连接,批量操作 100 台服务器需要建立 100 次 TCP 连接+密钥交换;认证管理复杂——密码认证需要交互式输入或 Expect 脚本,密钥认证需要管理 agent 转发。paramiko 作为纯 Python SSH 库解决了所有问题:原生 API 控制连接生命周期、支持连接复用(SSHClient.get_transport())、内置多种认证方式(密钥/密码/agent)、输出以 file-like 对象返回便于程序化处理。Fabric 是 paramiko 的上层封装,进一步简化了批量操作语法。
示例代码
示例 1:subprocess 命令包装器
#!/usr/bin/env python3
"""命令安全执行包装器 —— 含超时、日志、重试"""
import subprocess, sys, logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
def run_cmd(cmd, timeout=30, retries=2):
for attempt in range(1, retries + 2):
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=True)
logging.info("OK: %s", " ".join(cmd))
return r.stdout.strip()
except subprocess.TimeoutExpired:
logging.warning("超时 (attempt %d): %s", attempt, cmd)
except subprocess.CalledProcessError as e:
logging.error("失败 (attempt %d): %s\n%s", attempt, e, e.stderr)
if attempt <= retries:
import time; time.sleep(2 ** attempt)
sys.exit(1)
if __name__ == "__main__":
print(run_cmd(["df", "-h"])) # 输出: Filesystem Size Used Avail Use% Mounted on ...
示例 2:paramiko SSH 批量命令
#!/usr/bin/env python3
"""批量 SSH 执行命令,收集结果"""
import paramiko, argparse
from concurrent.futures import ThreadPoolExecutor
def ssh_run(host, cmd, key_path="~/.ssh/id_ed25519"):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, username="root", key_filename=key_path, timeout=10)
_, stdout, stderr = client.exec_command(cmd, timeout=15)
rc = stdout.channel.recv_exit_status()
out = stdout.read().decode().strip()
err = stderr.read().decode().strip()
client.close()
return host, rc, out, err
def main():
ap = argparse.ArgumentParser(description="批量 SSH 执行命令")
ap.add_argument("hosts", nargs="+", help="目标主机列表")
ap.add_argument("-c", "--cmd", default="uptime", help="要执行的命令")
ap.add_argument("-k", "--key", default="~/.ssh/id_ed25519", help="SSH 密钥路径")
args = ap.parse_args()
with ThreadPoolExecutor(max_workers=8) as pool:
futures = [pool.submit(ssh_run, h, args.cmd, args.key) for h in args.hosts]
for f in futures:
host, rc, out, err = f.result()
status = "OK" if rc == 0 else "FAIL"
print(f"[{status}] {host}: {out or err}") # 输出: [OK] 10.0.0.1: 09:30:00 up 10 days
if __name__ == "__main__":
main()
示例 3:psutil 系统健康检查与告警
#!/usr/bin/env python3
"""采集系统指标,超阈值时输出 JSON 告警"""
import psutil, json, socket, logging
from datetime import datetime
logging.basicConfig(level=logging.WARNING, format="%(message)s")
alerts = []
host = socket.gethostname()
now = datetime.now().isoformat()
def check(metric, value, warn, crit):
if value >= crit:
alerts.append({"level": "CRITICAL", "host": host, "metric": metric, "value": value, "threshold": crit, "time": now})
elif value >= warn:
alerts.append({"level": "WARNING", "host": host, "metric": metric, "value": value, "threshold": warn, "time": now})
check("cpu_percent", psutil.cpu_percent(interval=2), 70, 90)
check("mem_percent", psutil.virtual_memory().percent, 80, 95)
check("disk_root_percent", psutil.disk_usage("/").percent, 80, 92)
check("disk_data_percent", psutil.disk_usage("/data").percent, 85, 95)
if alerts:
for a in alerts:
logging.warning(json.dumps(a)) # 输出: {"level": "WARNING", "host": "...", "metric": "cpu_percent", ...}
else:
print(json.dumps({"level": "OK", "host": host, "time": now})) # 输出: {"level": "OK", "host": "...", "time": "..."}
示例 4:日志分析脚本(正则 + 统计)
#!/usr/bin/env python3
"""Nginx 访问日志分析:状态码分布、TOP IP、总请求量"""
import re, sys, collections
LOG_PAT = re.compile(
r'^(?P<ip>[\d.]+) .*? \[(?P<time>[^\]]+)\] '
r'"(?P<req>[^"]*)" (?P<status>\d{3}) (?P<size>\d+) '
r'"(?P<ref>[^"]*)" "(?P<ua>[^"]*)"'
)
def analyze(path):
statuses, ips = collections.Counter(), collections.Counter()
total = 0
with open(path, errors="replace") as f: # errors="replace" 容忍日志中的乱码字节
for line in f:
m = LOG_PAT.match(line)
if not m:
continue
total += 1
statuses[m.group("status")] += 1
ips[m.group("ip")] += 1
print(f"总请求数: {total}")
print("状态码分布:", dict(statuses))
print("TOP 5 IP:")
for ip, c in ips.most_common(5):
print(f" {ip}: {c}")
if __name__ == "__main__":
analyze(sys.argv[1] if len(sys.argv) > 1 else "/var/log/nginx/access.log")
# 输出: 总请求数: 123456
# 输出: 状态码分布: {'200': 118000, '404': 3456, '500': 12, ...}
# 输出: TOP 5 IP: 1.2.3.4: 89012 ...
示例 5:批量重命名脚本(干跑模式)
#!/usr/bin/env python3
"""批量重命名:backup_20260101.tar.gz → 20260101.tar.gz
--dry-run 先预览,确认无误再去掉参数真正执行"""
import os, re, sys, argparse
def main():
ap = argparse.ArgumentParser(description="批量重命名备份文件")
ap.add_argument("dir", help="目标目录")
ap.add_argument("--dry-run", action="store_true", help="只预览不执行")
ap.add_argument("--pattern", default=r"^backup_(\d{8})\.tar\.gz$",
help="匹配正则,捕获组用于拼新文件名")
args = ap.parse_args()
renamed = 0
for name in sorted(os.listdir(args.dir)):
m = re.match(args.pattern, name)
if not m:
continue
new_name = f"{m.group(1)}.tar.gz"
old, new = os.path.join(args.dir, name), os.path.join(args.dir, new_name)
if os.path.exists(new):
print(f"跳过(目标已存在): {name} -> {new_name}")
continue
if args.dry_run:
print(f"[DRY-RUN] {name} -> {new_name}")
else:
os.rename(old, new)
print(f"已重命名: {name} -> {new_name}")
renamed += 1
print(f"共处理 {renamed} 个文件")
if __name__ == "__main__":
main()
# 用法: ./rename.py /data/backups --dry-run
# 输出: [DRY-RUN] backup_20260101.tar.gz -> 20260101.tar.gz ...
示例 6:服务器健康检查脚本(巡检 + 告警推送)
#!/usr/bin/env python3
"""巡检脚本:检查服务、端口、磁盘、负载,异常时推送到企业微信 webhook"""
import json, subprocess, socket, datetime
import psutil, requests
WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=XXXX"
HOST = socket.gethostname()
def check_service(name):
r = subprocess.run(["systemctl", "is-active", name],
capture_output=True, text=True)
return r.stdout.strip() == "active"
def check_port(port, host="127.0.0.1"):
with socket.socket() as s:
s.settimeout(2)
return s.connect_ex((host, port)) == 0
def main():
problems = []
for svc in ["nginx", "mysql", "redis-server"]:
if not check_service(svc):
problems.append(f"服务 {svc} 不在运行")
if not check_port(6379):
problems.append("Redis 端口 6379 不通")
load = psutil.getloadavg()
if load[0] > 4:
problems.append(f"负载过高: {load[0]:.2f}")
disk = psutil.disk_usage("/")
if disk.percent > 85:
problems.append(f"磁盘 / 使用率 {disk.percent}%")
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if problems:
msg = {"msgtype": "text",
"text": {"content": f"[{HOST}] {now}\n" + "\n".join(problems)}}
requests.post(WEBHOOK, json=msg, timeout=5) # 输出: 异常项推送成功
print("异常项已推送:", problems)
else:
print(f"[{now}] {HOST} 健康检查通过") # 输出: [2026-07-31 09:00:00] web-01 健康检查通过
if __name__ == "__main__":
main()
定时运行:cron 与 systemd timer
巡检脚本要定时跑,两个坑必须先处理:cron 的 PATH 不包含 /usr/local/bin,python3 必须写绝对路径;输出必须重定向,否则脚本里 print 的告警无人看到。
# crontab -e
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 每 5 分钟健康检查,输出追加到日志
*/5 * * * * /usr/bin/python3 /opt/scripts/health_check.py >> /var/log/health_check.log 2>&1
# 每天 3 点跑日志分析
0 3 * * * /usr/bin/python3 /opt/scripts/log_analyzer.py >> /var/log/log_analyzer.log 2>&1
# 更推荐的 systemd timer 方式(输出进 journalctl,天然统一管理)
# /etc/systemd/system/health-check.service
[Unit]
Description=Health Check Script
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/scripts/health_check.py
# /etc/systemd/system/health-check.timer
[Unit]
Description=Run health check every 5 minutes
[Timer]
OnCalendar=*:0/5
Persistent=true
[Install]
WantedBy=timers.target
# 启用并验证
sudo systemctl daemon-reload
sudo systemctl enable --now health-check.timer
# 输出: Created symlink /etc/systemd/system/timers.target.wants/health-check.timer
sudo systemctl list-timers health-check # 输出: NEXT 09:05:00 5min left health-check.timer
journalctl -u health-check.service -S "10 min ago" # 查看上次执行输出
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
FileNotFoundError | subprocess 调了不存在的命令(如 shell=True 但 PATH 不对) | 用 shutil.which() 检查或写全路径 |
AuthenticationException | paramiko 密钥路径不对或未加载 agent | 显式指定 key_filename,或先 ssh-add |
TimeoutExpired | 远程命令执行太久未返回 | 设置 timeout 参数,配合重试逻辑 |
PermissionError | psutil 查某些进程信息需 root | 检查是否用 sudo 运行 |
ModuleNotFoundError | 未安装依赖或未激活虚拟环境 | 先 pip install -r requirements.txt 或激活 venv |
json.decoder.JSONDecodeError | API 返回非 JSON(如 502/503) | 先检查 resp.status_code,再 resp.json() |
最佳实践
| 实践 | 说明 |
|---|---|
始终用 capture_output=True, text=True | subprocess 默认输出 bytes,转文本统一处理 |
优先 check=True 或主动检查 returncode | 避免静默失败 |
SSH 连接放入 with 或 try/finally | 确保用完关闭,防止连接泄漏 |
敏感信息走环境变量或 python-dotenv | 切忌密码/密钥硬编码 |
每个脚本提供 --help(argparse) | 降低使用门槛 |
结构化日志用 logging 而非 print | 便于后续对接日志中心 |
| 用 Poetry / virtualenv 隔离环境 | 防止全局 pip 污染,方便 CI/CD 复现 |
脚本入口用 if __name__ == "__main__" | 让模块可测试、可 import |
练习题
- 日志清理器:用
subprocess找到/var/log下 7 天前的*.log文件并压缩(gzip),保留原始文件列表到 JSON。 - 端口扫描器:用
socket+argparse批量扫描指定主机的 22/80/443/3306/6379 端口,输出开放列表。 - 批量 SSH 磁盘检查:用 paramiko 对一批服务器执行
df -h,解析结果,打印出磁盘使用率超过 85% 的挂载点。 - 进程树观察者:用 psutil 每 10 秒采集 CPU top-5 进程,当某个进程连续 3 次 CPU > 80% 时发出警告。
- Poetry 项目打包:用
poetry new myops创建项目,将练习 3 的脚本放入,加依赖 paramiko,编写对应单元测试。
点击查看答案
subprocess.run(["find", "/var/log", "-name", "*.log", "-mtime", "+7", "-type", "f"], capture_output=True, text=True)获取文件列表,subprocess.run(["gzip", f])逐个压缩,将原始文件路径写入 JSON。- 用
argparse定义主机和端口参数;socket.connect_ex()返回 0 表示端口开放。批量扫描后打印host:port OPEN列表。 - paramiko 连接每台主机执行
df -h,解析输出中 Use% 列,打印超过 85% 的挂载点:if int(line.split()[-2].strip('%')) > 85: print(...)。 - psutil 每 10 秒取
psutil.process_iter()按 CPU 排序取 top 5。用 deque 记录每个进程最近 3 次 CPU%,若均 > 80% 则logging.warning()发出告警。 poetry new myops && cd myops && poetry add paramiko。将批量 SSH 脚本放入myops/目录,编写tests/test_ssh.py用 mock 测试 SSH 连接逻辑。运行poetry run pytest验证。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 Python 在运维自动化中的优势 | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 Python 脚本编写、库安装、调试执行 | 在终端实际执行 |
| 原理掌握 | 能说出 Python 的异常处理、文件操作、进程管理机制 | 画出流程图 |
| 故障排查 | 能独立排查 Python 脚本语法错误、依赖问题、权限错误 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么需要为 Python 脚本添加日志记录和配置管理 | 对比不同方案 |
本章总结
速查表
| 任务类型 | 推荐库 | 一句话用法 |
|---|---|---|
| 命令行参数 | argparse | ap.add_argument() → ap.parse_args() |
| 执行系统命令 | subprocess | subprocess.run(cmd, capture_output=True, check=True) |
| HTTP API | requests | requests.get/post(url, headers=..., json=...) |
| SSH 远程 | paramiko / Fabric | SSHClient().connect() → exec_command(cmd) |
| 系统指标采集 | psutil | psutil.cpu_percent() / virtual_memory() / disk_usage('/') |
| 日志输出 | logging | logging.basicConfig() → logging.info/warning/error() |
| 环境隔离 | Poetry / venv | poetry add <pkg> / python -m venv .venv |
学习路径:2.1:Shell 脚本入门/2.8:Shell 脚本进阶 Shell 基础 → 本章 Python 运维脚本 → 4.1:Ansible 自动化 Ansible(更高层抽象)→ 4.9:CI/CD 持续部署 CI/CD 集成。
延伸阅读
- Python subprocess 官方文档
- Paramiko 官方文档
- psutil 官方文档
- requests 官方文档
- Poetry 官方文档
- Fabric 官方文档
- 2.1:Shell 脚本入门 Shell 脚本入门 · 2.8:Shell 脚本进阶 Shell 脚本进阶 · 3.12:系统监控与告警 系统监控方案 · 4.1:Ansible 自动化 Ansible 自动化运维 · 2.11:SSH 深入 SSH 深入