1. 先聊清楚一件事:为什么要用Python做自动化
Python在运维和自动化领域火了很多年,但很多人一开始并不知道该从哪儿下手。网上教程一大把,今天教你爬虫、明天教你机器学习,转了半天还在语法层面打转。如果你就是想解决日常工作中那些重复、繁琐、容易出错的事情——批量改配置、定时跑数据统计、监控服务状态、自动发报表——那Python这套组合拳是性价比极高的选择。
自动化和运维开发的核心逻辑其实很简单:把以前靠手工点来点去的事情,变成脚本定时跑、自动跑、异常了自己处理。省下的时间拿去摸鱼也好,做更有价值的事也好,都值。
这篇文章不是我给你念一遍官方文档,而是基于我自己从入门到实际落地项目的完整路径,把最常用的核心库和使用方法拆开揉碎讲清楚。最后我会带你把三个真实场景完整过一遍:Webhook消息推送、服务器配置批量检查、日志自动分析并生成报表。这三个场景覆盖了自动化领域最典型的三大类需求:触发类、巡检类、分析类。学完你就能根据手头的任务自行组合,不再局限于某个单一示例。
不管你之前有没有写过代码,只要会一点基础语法(哪怕完全不会也没关系,边看边查),跟着这篇文章一步步操作,基本都能跑通。
2. 核心库选型:自动化运维的真需求
先解决“用哪些库”的问题。很多人喜欢收集库,装了一大堆但不知道干嘛用。按照真实使用频率和覆盖场景,核心其实就那么几个。
2.1 调系统命令:subprocess
自动化做的最多的一件事,就是在Python里去执行Shell命令。比如查看磁盘、杀进程、调接口、备份文件等等。subprocess就是干这个的标准库,不需要额外安装。
重点掌握两个基础用法。最简单的方式:
import subprocess result = subprocess.run(["df", "-h"], capture_output=True, text=True) print(result.stdout)复杂点的场景,比如程序可能会卡住,需要设置超时:
import subprocess try: result = subprocess.run( ["ping", "-c", "5", "某网站"], capture_output=True, text=True, timeout=10 ) except subprocess.TimeoutExpired: print("命令执行超时")这里有个细节值得注意,capture_output=True会把输出捕获到内存而不打印到终端,text=True表示输出按字符串处理。跑批处理脚本时,输出几百行是常事,捕获后可以直接进行筛选解析,方便继续做判断。我一般习惯直接写成stdout=subprocess.PIPE, stderr=subprocess.PIPE,在需要同时拿两个输出做判断时更明确。
2.2 HTTP请求:requests
自动化和运维里,和别的系统打交道是绕不开的。调接口、发告警通知、抓取页面数据,全都要发HTTP请求。requests虽然不是标准库,但基本上是默认标配。
常规GET和POST:
import requests # GET请求 resp = requests.get("http://某服务/health", timeout=5) print(resp.status_code) print(resp.json()) # POST请求,常用于接口调用 data = {"name": "某服务", "status": "running"} resp = requests.post("http://某系统/api/notify", json=data, timeout=10)有两个点大家很容易忽略。一是超时,很多人不写timeout,结果某个接口卡住,脚本一直挂着不动,整个自动化链路就堵死了。另一个是Session的用法,连续调用需要带登录态或头的接口时,用Session可以维持上下文。
import requests s = requests.Session() s.headers.update({"Authorization": "token xxx"}) # 之后的请求都自动带上token resp1 = s.get("http://某系统/api/list") resp2 = s.post("http://某系统/api/update", json={...})2.3 Shell命令替代方案:sh库或直接os模块
用subprocess写多了之后,你会发现大量代码在拼命令字符串,有点繁琐。这时可以试试sh这个库,它把Shell命令直接变成Python函数,代码看起来会更简洁。
import sh # 执行ls命令 print(sh.ls("-l")) # 连续执行,类似管道 iface = sh.ifconfig.eth0 print(iface)不过说实话,sh库在实际项目里属于“锦上添花”型,我个人建议核心掌握subprocess就够了。因为它是标准库,不会遇到环境缺依赖的问题。另外,很基础的文件操作、路径拼接、环境变量读取,直接用os、pathlib就好。pathlib的语法更现代,比如:
from pathlib import Path data_dir = Path("~/data").expanduser() data_dir.mkdir(exist_ok=True) # 遍历目录下的所有log文件 for log_file in data_dir.glob("*.log"): print(log_file.name)2.4 定时任务:schedule或APScheduler
写好的脚本,最终要挂在机器上按计划跑。方案有两种:一种直接用系统的crontab,把Python脚本加进计划任务;另一种是在Python内部启用定时调度。
如果脚本逻辑简单,只是每隔几分钟跑一次,用schedule库就够了:
import schedule import time def job(): print("执行巡检任务...") # 每天凌晨2点执行 schedule.every().day.at("02:00").do(job) # 每30分钟执行 schedule.every(30).minutes.do(job) while True: schedule.run_pending() time.sleep(1)调度逻辑复杂,比如需要跨进程任务、失败重试、持久化存储,那就用APScheduler。
2.5 日志处理:logging比print靠谱
很多人写脚本喜欢用print打日志,然后留存文件。小脚本没问题,但一旦跑的时间长、逻辑复杂,你会瞬间崩溃——找不到关键日志、没有时间戳、没法对日志分级。
标准库logging简单配置一下就能解决:
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler("task.log", encoding="utf-8"), logging.StreamHandler() ] ) logging.info("任务开始") logging.warning("内存占用率偏高") logging.error("接口调用失败")几点经验:level=logging.INFO是大部分日常任务的合理配置,设成DEBUG日志会太吵,设成WARNING又会漏掉正常执行记录。FileHandler加StreamHandler可以同时输出到文件和终端,排查问题时非常有用。如果脚本要在多台机器上部署,建议把日志路径放在一个独立目录,统一管理。
3. 动手前必须知道的设计套路
脚本写多了你会发现,自动化项目写得好不好,不在于用了多少高级语法,而在于设计思路对不对。这一节我先说通用套路,后面三个实战场景都会反复用到。
3.1 三个核心设计原则
自动化脚本设计绕不开三个原则:幂等、可重试、可观测。
幂等指同一操作执行一次和执行N次,结果是一样的。比如批量改配置这个场景,如果脚本每次启动都插入一条相同的配置,跑第二次就重复了。解决方式很简单,执行前先检查是否已存在同名配置。
可重试指任务失败后,再跑一次能恢复。比如发通知的接口网络抖动失败了,脚本尽量不把所有数据发一半就退出,最好是分批重试,或者失败的任务单独输出到文件,供后续重新跑。
可观测指每一步执行完,能清晰看到结果。日志、退出码、输出文件,都要有明确信息。出了问题,能一眼定位是网络故障、数据异常还是脚本自身Bug。
3.2 脚本结构模板
我写自动化脚本一般固定用一个模板,简单又清晰,你们直接参考就行:
# -*- coding: utf-8 -*- import logging import os import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__)) LOG_DIR = os.path.join(BASE_DIR, "logs") os.makedirs(LOG_DIR, exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler(os.path.join(LOG_DIR, "run.log"), encoding="utf-8"), logging.StreamHandler(), ], ) logger = logging.getLogger(__name__) def main(): logger.info("任务启动") # 核心逻辑写这里 logger.info("任务结束") if __name__ == "__main__": sys.exit(main())这个模板配合 crontab 或者 systemd timer 就能长期稳定运行。脚本写成if __name__ == "__main__"的形式还有个额外好处,当别的脚本想复用里面的函数时,直接import不会触发执行。
3.3 错误处理:别让脚本默默挂掉
自动化任务最怕的是脚本异常退出,然后还没人知道。所以每个可能出错的节点都要有明确的异常处理。
try: resp = requests.get("http://某服务/health", timeout=5) resp.raise_for_status() except requests.Timeout: logger.error("请求超时,服务端无响应") sys.exit(1) except requests.RequestException as e: logger.error(f"请求失败: {e}") sys.exit(1) else: logger.info(f"服务返回状态码: {resp.status_code}")注意sys.exit(1)这个方法,退出码对定时任务非常重要。在crontab里,只有脚本返回0才表示成功,非0退出码就能触发告警逻辑。
4. 实战场景一:Webhook消息推送与告警机器人
先来一个最简单但也最实用的场景:Webhook消息推送。不管是在服务器上接监控平台的告警,还是脚本执行完自动推送结果到聊天工具,都是很常见的需求。
4.1 场景分析和思路拆解
场景设定:某服务每天凌晨会执行一次全量备份,备份完成后需要推送一条消息到团队群,内容包括备份大小、耗时、是否成功。如果备份失败,不仅推送失败消息,还要在日志里标注错误码,方便后续排查。
这个任务的难点不在HTTP请求本身,而在于消息格式的组装和异常分支的覆盖。Webhook通常要求POST一个JSON,里面包含标题、内容、消息类型等字段。
4.2 核心实现步骤
第一,写一个发消息的基础函数。
import requests import json WEBHOOK_URL = "https://某系统/webhook/send" def send_webhook(title, content, msg_type="text"): payload = { "title": title, "content": content, "msgtype": msg_type } try: resp = requests.post(WEBHOOK_URL, json=payload, timeout=10) resp.raise_for_status() result = resp.json() if result.get("code") == 0: print(f"消息推送成功: {title}") return True else: print(f"消息推送返回异常: {result}") return False except Exception as e: print(f"消息推送失败: {e}") return False第二,在备份任务里调用这个函数。
def main(): import subprocess import time start_time = time.time() # 模拟备份命令 result = subprocess.run( ["du", "-sh", "/data/backup"], capture_output=True, text=True ) backup_size = result.stdout.strip().split("\t")[0] elapsed = int(time.time() - start_time) if result.returncode == 0: send_webhook( "备份任务成功", f"完成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n数据大小: {backup_size}\n耗时: {elapsed}秒" ) else: send_webhook( "备份任务失败", f"错误信息: {result.stderr}\n请登录服务器检查", msg_type="warning" )4.3 这个场景的实操心得与坑
第一个坑:Webhook地址里的密钥。很多团队把Webhook密钥直接硬编码在脚本里,然后丢进代码仓库。时间一长,密钥泄露却没人发现。建议把密钥放到环境变量或独立的配置文件里,脚本读取环境变量使用。
import os WEBHOOK_URL = os.getenv("WEBHOOK_URL", "")第二个坑:消息重试。Webhook服务偶尔不稳定,一次发送失败就直接忽略会漏消息。简单做法是失败后等几秒重试一次,连发三次都失败再记日志。我的实践一般是写个装饰器,专门处理重试逻辑:
import time from functools import wraps def retry(max_retries=3, delay=3): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_retries + 1): try: return func(*args, **kwargs) except Exception as e: print(f"第{attempt}次执行失败: {e}") time.sleep(delay) return None return wrapper return decorator @retry(max_retries=3, delay=2) def send_webhook_retry(title, content): return send_webhook(title, content)第三个坑:消息内容格式。不同聊天工具的Webhook消息格式差别挺大,有支持Markdown,有只支持纯文本,有需要特定的字段名。写的时候最好先手动构造一个JSON,用curl工具发到Webhook验证一下格式,再来写Python代码,能省不少调试时间。
5. 实战场景二:服务器配置批量检查与巡检
第二个场景是运维开发里最经典的需求:对一批服务器做定时巡检,检查磁盘使用率、内存占用、服务存活状态,最后生成一份巡检报告。
5.1 场景分析和思路拆解
假设现在有几十台服务器,分布在不同的IP段,每台机器的配置和用途不完全相同。传统方式是手动登录一台台看,费时且容易漏。脚本化之后,只要把所有服务器的IP列表存到配置文件里,固定时间跑一次,自动收集数据并比对阈值,超过阈值的自动标红。
这个场景的设计核心是:数据收集用远程命令,数据分析本地做,结果输出结构化。
服务器多的情况下,如果一台一台顺序跑,速度会很慢。运维上标配做法是用并行方式批量执行。Python里最简单的方式是concurrent.futures的线程池。
5.2 核心实现步骤
第一,准备服务器清单。使用一个简单的文本文件,一行一台机器:
192.168.1.10 192.168.1.11 192.168.1.12第二,定义巡检函数。
import subprocess import shlex from concurrent.futures import ThreadPoolExecutor, as_completed def check_disk_usage(host): cmd = f"ssh root@{host} 'df -h /'" result = subprocess.run( shlex.split(cmd), capture_output=True, text=True, timeout=10 ) if result.returncode != 0: return {"host": host, "error": result.stderr.strip()} lines = result.stdout.strip().splitlines() header = lines[0].split() usage_line = lines[1].split() usage_percent = usage_line[header.index("Use%")].replace("%", "") return {"host": host, "disk_usage": int(usage_percent)}注意这里用了shlex.split而不是简单split(),它能把带引号的命令正确解析,避免路径含空格时出错。
第三,批量跑。
def run_checks(hosts): result_list = [] with ThreadPoolExecutor(max_workers=10) as executor: future_to_host = { executor.submit(check_disk_usage, host): host for host in hosts } for future in as_completed(future_to_host): result = future.result() if result["disk_usage"] >= 85: result["level"] = "WARN" elif result["disk_usage"] >= 95: result["level"] = "ERROR" else: result["level"] = "OK" result_list.append(result) return result_list5.3 巡检报告输出与告警触发
巡检收集完数据后,最好把结果集中到一个目录,按日期管理。我用的是“按天建目录,报告落在当天目录里”的做法,配合日志转储很方便。
import datetime from pathlib import Path today = datetime.date.today().isoformat() report_dir = Path(f"/data/ops_reports/{today}") report_dir.mkdir(parents=True, exist_ok=True) report_file = report_dir / "summary.json" import json with open(report_file, "w", encoding="utf-8") as f: json.dump(result_list, f, ensure_ascii=False, indent=2)如果某个机器状态异常,同步触发Webhook告警,通知相关人员。
5.4 这个场景的关键注意事项
并行巡检虽然快,但要注意两点。一是SSH连接超时一定要设置,timeout参数不给的话,某台机器网络不通,线程就被卡住,整个脚本卡在那里等着。二是并发数不宜过高,我一般控制在max_workers=10,超过这个值,某些机器配置较低时SSH服务可能会拒绝连接。
这个小细节,传统文档不太会讲,但实际运维中很影响效率。跑过一次就知道了。
6. 实战场景三:日志自动分析与报表生成
第三个场景是数据提炼类需求,在自动化里特别有代表性:读取程序的日志文件,自动统计关键字、报错次数、耗时分布,生成一张整洁的报表。
6.1 场景分析和思路拆解
设想一个业务系统的日志,每天会记录大量操作行为。业务方想统计每天的错误率、某些接口调用次数、耗时超过三秒的慢请求数量。人工搜日志不可行,脚本去逐行解析最靠谱。
设计思路三步走:第一,按日志关键字过滤;第二,用正则提取关键字段;第三,聚合统计输出结果。
日志格式通常长这样:
2025-01-15 09:12:33,123 [ERROR] payment Service Timeout, cost=5321ms 2025-01-15 09:13:01,456 [INFO] order request success, cost=210ms 2025-01-15 09:14:22,789 [ERROR] payment Service Unavailable 2025-01-15 09:15:10,321 [INFO] user login success, cost=98ms6.2 核心实现步骤
第一,写一个按正则抽取日志字段的解析函数:
import re LOG_PATTERN = re.compile( r"(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\s+" r"\[(?P<level>INFO|WARN|ERROR)\]\s+" r"(?P<message>.+)" ) def parse_log_line(line): match = LOG_PATTERN.search(line) if not match: return None return match.groupdict()第二,统计逻辑:
from collections import Counter, defaultdict def analyze_log_file(log_path): level_counter = Counter() slow_requests = [] with open(log_path, "r", encoding="utf-8") as f: for line in f: parsed = parse_log_line(line) if not parsed: continue level_counter[parsed["level"]] += 1 # 找出耗时超过3000ms的请求 cost_match = re.search(r"cost=(\d+)ms", parsed["message"]) if cost_match: cost = int(cost_match.group(1)) if cost > 3000: slow_requests.append({ "time": parsed["timestamp"], "message": parsed["message"], "cost": cost }) return level_counter, slow_requests第三,生成报表。报表建议不要搞花里胡哨的格式,一个简单的HTML或者CSV就够了。我用得比较多的是直接生成CSV,方便在Excel里二次分析。
import csv def generate_report(level_counter, slow_requests, output_path): with open(output_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["日志级别", "数量"]) for level, count in level_counter.items(): writer.writerow([level, count]) writer.writerow([]) writer.writerow(["慢请求时间", "日志内容", "耗时(ms)"]) for item in slow_requests: writer.writerow([item["time"], item["message"], item["cost"]])这里用utf-8-sig而不是utf-8是个细节,Excel直接打开CSV时不会乱码。
6.3 日志分析的重难点补充
日志解析最大的难点不在正则本身,而是日志格式不统一。不同系统的日志时间格式、分隔符、字段顺序都不一样。建议不要写死一个通用解析器,而是针对每一类日志单独维护解析规则。配置化解析是一种好设计,把日志格式和提取字段放在配置文件里,脚本主体代码不变,改配置就能适配新日志。
另一个坑是大文件读取性能。几GB的日志文件,逐行readline()不会一次性占满内存,这没问题,前提是别使用f.read()或f.readlines()。另外,统计大量的Counter对象时,内存也会涨,建议按小时分片解析,每小时结果即时落盘,再清空计数器。
6.4 这个场景的运行方式建议
日志分析这类任务,建议直接丢到crontab里每天定时跑。凌晨收集昨天的日志,早上生成报表发送。调度可以这样写:
0 6 * * * cd /opt/log_analyzer && python run.py >> logs/cron.log 2>&1脚本开始前先判断当前日期,然后再找对应日期的日志文件。如果某天日志没生成,脚本要给出明确提示,而不是分析了一个空文件,输出一份全零报表——这种事我踩过,整个团队白白查了一上午问题。
7. 常见问题与排查技巧实录
自动化脚本跑多了,总会遇到各种稀奇古怪的问题。这里挑几个出现频率最高的,逐个说说排查思路。
7.1 脚本在终端能跑,放到crontab就废了
这个太常见了。原因八九不离十是环境变量问题。crontab里的环境是精简过的,没有加载你${HOME}/.bashrc里的变量,而且PATH目录很少。如果脚本里依赖了某个安装在自定义目录下的工具,直接跑不到。
解决办法,脚本开头显式设置环境:
import os os.environ["PATH"] = "/usr/local/bin:/usr/bin:/bin"或者crontab里指定:
0 2 * * * /usr/bin/env PATH=/usr/local/bin:/usr/bin:/bin /usr/bin/python /opt/my_script.py另外注意python还是python3,不同机器的默认版本不一样。脚本顶部用#!/usr/bin/env python3声明,crontab里调用时写绝对路径。
7.2 中文输出乱码
这个分两种情况。终端乱码是终端编码问题,设置export LANG=en_US.UTF-8能解决大半。文件写入乱码多半是没指定编码,Python默认编码在某些老系统上是ASCII,写中文会直接报错。统一规范就是,打开文件时都显式写encoding="utf-8",不要省。
7.3 脚本运行时间过长,卡死
检查是不是有网络请求没设置超时,或者是命令行执行没有timeout。用我前面代码里的timeout参数,基本能覆盖90%的卡死场景。另外,如果脚本里循环了无限次,也可能造成假死。排查时用py-spy dump或faulthandler能精准看出卡在哪一行。
7.4 定时任务重复执行
crontab设置失误,或者脚本执行时间超过调度周期,会导致上一个任务还没结束,下一个任务已经启动。这种情况加锁就能解决。
import os import sys LOCK_FILE = "/tmp/my_task.lock" if os.path.exists(LOCK_FILE): print("已有任务在执行,退出") sys.exit(0) with open(LOCK_FILE, "w") as f: f.write(str(os.getpid())) try: # 核心逻辑 pass finally: os.remove(LOCK_FILE)如果担心任务进程被强杀后锁文件残留,可以再进一步检查锁文件里的PID是否还存活:
import os def is_alive(pid): try: os.kill(pid, 0) return True except OSError: return False7.5 requests慢到怀疑人生
排查顺序:先确认是不是DNS解析问题,换用IP直连测试;再看目标服务的响应时间;然后用curl -w看看分阶段耗时。Python层面能做的优化是设置连接复用和调整超时。
session = requests.Session() adapter = requests.adapters.HTTPAdapter( pool_connections=10, pool_maxsize=20 ) session.mount("http://", adapter)8. 从脚本到小系统:下一步进阶方向
学会写这些独立的自动化脚本之后,很多人自然会有个疑问:每次手动跑脚本还是有点烦,能不能把这些能力整合成一个更完整的东西?这里说几个值得考虑的扩展方向,不涉及具体代码,但能给大家指个路。
方向一:给脚本加一个统一的调度管理入口。不满足于crontab,想统一管理脚本的启停、日志、监控、失败重试,那可以引入APScheduler,或者用一个轻量的任务调度系统。核心思想是:调度器和业务逻辑分开,调度配置集中管理。
方向二:把巡检结果沉淀成结构化的数据接口。巡检脚本跑出来的结果不只是一份报表,还能往监控系统或数据平台上报。这就涉及数据标准化的设计——每类采集数据定义好schema,上报时统一转换格式。
方向三:做成一个小型的运维平台。如果公司里有多个运维脚本,大家都靠手工执行,效率和安全性都是问题。把高频的运维操作封装成Web接口,配合简单的鉴权和审计,能很大程度减少误操作。这个阶段可以考虑用Flask或FastAPI搭一套轻量的API服务。
这三个方向没有谁先谁后的强制顺序,完全看实际需求和日常工作痛点。
9. 学习路径建议与资源避坑指南
最后说点学习路线上我踩过的坑。
很多人学Python自动化,容易钻进一个误区:先啃完一整本语法书再开始动手。实际根本不需要。边做边查是最有效的方式。我当年就是直接套着一个“服务器巡检”的需求开干,前两周整天在查列表推导式、字典怎么遍历、装饰器怎么写,但那两周学到的东西比看一个月书都多。
在工具选择上,IDE推荐直接用VS Code配Python插件,轻量好用。调试的时候多用pdb断点,比写一堆print然后猜测问题所在强太多。遇到不会的语法点,最快的方式是去交互式环境里敲两行代码试一下,眼见为实。
另外推荐一个非常基础的测试习惯:写脚本的时候,先用一小段样例数据跑通核心逻辑,再拿真实数据做验证。比如日志分析功能,先构造20行日志格式的样例,确认正则都能匹配之后再上服务器跑全量数据。这个习惯能避免99%的低级解析错误。
如果一开始没思路,可以给自己制造需求:给家里人做一个消费记账自动统计脚本,每天自动推送支出汇总到聊天工具;处理一份几千行的Excel表格,筛选出规律异常的数据。需求不用复杂,但必须是真实要用的。真实需求带给你的动力,远远超过跟着课程敲例子。
有一点也要提醒:不要指望一次就把代码写得完美。我自己现在回过头看早期写的脚本,逻辑混乱、变量命名随意、没有版本管理,但当时能跑、能解决实际问题就够了。代码是靠一次次迭代写好的,不是靠脑子里想好的。