4、Python运维基础:Python环境搭建、常用内置模块(os、sys、subprocess、shutil)

说实话,很多运维同学刚开始接触Python时,最头疼的不是语法本身,而是环境搞不定。我见过太多人卡在第一步——装了个Python,结果跑脚本时各种报错,最后怀疑人生。其实没那么复杂,咱们一步步来。

4.1 Python环境搭建

我个人习惯用pyenv来管理多版本Python。为什么?因为生产环境里,你很可能同时维护着Python 2.7的老脚本和Python 3.10的新项目。用系统自带的Python?那简直是给自己挖坑。

核心原则:永远不要动系统自带的Python。用虚拟环境或版本管理工具隔离。

4.1.1 安装Python

Linux下我推荐两种方式:

  • 源码编译:适合对版本有严格要求的场景
  • 包管理器:适合快速部署

举个例子,在CentOS上:

# 安装依赖
yum install -y gcc openssl-devel bzip2-devel libffi-devel

# 下载Python 3.9.7
wget https://www.python.org/ftp/python/3.9.7/Python-3.9.7.tgz
tar -xf Python-3.9.7.tgz
cd Python-3.9.7

# 编译安装
./configure --prefix=/usr/local/python3.9
make && make install

# 建立软链接
ln -s /usr/local/python3.9/bin/python3.9 /usr/local/bin/python3
ln -s /usr/local/python3.9/bin/pip3.9 /usr/local/bin/pip3

小技巧:编译时加上--enable-optimizations参数,虽然编译时间会长一点,但运行性能能提升10%-15%。我在压测环境里实测过,效果明显。

4.1.2 虚拟环境

虚拟环境这东西,说白了就是给每个项目一个独立的Python“小房间”。你在这个房间里装什么包,都不会影响到外面的世界。

# 创建虚拟环境
python3 -m venv myproject_env

# 激活环境
source myproject_env/bin/activate

# 退出环境
deactivate

我曾经遇到过一个问题:两个项目都依赖requests库,但一个要2.25版本,另一个要2.26版本。如果没有虚拟环境,你只能二选一。有了虚拟环境,各玩各的,互不干扰。

4.2 常用内置模块

Python之所以适合运维,很大程度上是因为它的内置模块太强大了。你想想看,不用装任何第三方包,就能操作文件、执行系统命令、处理路径——这不就是运维的日常吗?

4.2.1 os模块——操作系统的瑞士军刀

os模块是我用得最多的模块之一。它让你能用Python代码直接跟操作系统打交道。

函数 作用 我在项目中的用法
os.getcwd() 获取当前工作目录 日志采集脚本中确认运行路径
os.chdir() 切换目录 批量处理不同目录下的配置文件
os.listdir() 列出目录内容 扫描日志目录,找出过期文件
os.mkdir() 创建目录 自动创建备份目录结构
os.remove() 删除文件 清理临时文件
os.path.join() 拼接路径 跨平台路径处理,避免硬编码斜杠

看个实际例子:

import os

def clean_old_logs(log_dir, days=7):
    """清理指定目录下超过N天的日志文件"""
    import time
    
    now = time.time()
    cutoff = now - days * 86400
    
    for filename in os.listdir(log_dir):
        filepath = os.path.join(log_dir, filename)
        if os.path.isfile(filepath):
            mtime = os.path.getmtime(filepath)
            if mtime < cutoff:
                os.remove(filepath)
                print(f"已删除: {filepath}")

# 使用
clean_old_logs("/var/log/myapp", days=3)

注意:os.remove()删除文件时,不会进回收站。我曾经手滑写错过路径,删掉了不该删的东西。从那以后,我养成了习惯——删除前先打印日志,确认无误再执行。

4.2.2 sys模块——跟Python解释器对话

sys模块让你能获取和修改Python解释器的运行时信息。说白了,就是让你知道“Python自己现在是什么状态”。

最常用的几个:

  • sys.argv:获取命令行参数。写脚本工具时必备。
  • sys.exit():退出程序并返回状态码。0表示成功,非0表示出错。
  • sys.path:模块搜索路径。当你import报错时,先看看这个。
  • sys.platform:判断操作系统。跨平台脚本的利器。
import sys

def check_environment():
    """检查运行环境是否满足要求"""
    print(f"Python版本: {sys.version}")
    print(f"操作系统: {sys.platform}")
    print(f"模块搜索路径: {sys.path}")
    
    # 检查Python版本
    if sys.version_info.major < 3:
        print("错误: 需要Python 3.x")
        sys.exit(1)
    
    # 解析命令行参数
    if len(sys.argv) > 1:
        print(f"传入的参数: {sys.argv[1:]}")
    else:
        print("没有传入额外参数")

if __name__ == "__main__":
    check_environment()

嗯,这里要注意一点:sys.argv[0]是脚本本身的文件名,从sys.argv[1]开始才是真正的参数。我刚开始写脚本时经常搞混这个索引。

4.2.3 subprocess模块——执行系统命令的正确姿势

以前很多运维老哥喜欢用os.system()来执行命令。但说实话,那玩意儿太简陋了。你拿不到输出结果,也控制不了错误处理。后来Python引入了subprocess模块,这才是执行系统命令的正确姿势。

我个人最常用的是subprocess.run()

import subprocess

# 执行命令,获取输出
result = subprocess.run(
    ["df", "-h"],
    capture_output=True,
    text=True,
    check=False
)

if result.returncode == 0:
    print("磁盘使用情况:")
    print(result.stdout)
else:
    print(f"命令执行失败: {result.stderr}")

几个关键参数说明:

参数 作用 使用场景
capture_output=True 捕获标准输出和错误输出 需要分析命令结果时
text=True 以文本形式返回(而非字节) 处理人类可读的输出
check=True 命令失败时抛出异常 关键步骤,失败必须中断
shell=True 通过shell执行(慎用!) 需要管道、通配符等shell特性

安全警告:尽量不要用shell=True,尤其是当命令中包含用户输入时。我曾经见过一个同事因为用了shell=True,导致脚本被注入了恶意命令。如果你必须用,记得先做参数转义。

再看一个实际点的例子——批量检查服务器连通性:

import subprocess
import sys

def ping_host(host, count=3):
    """检查主机是否可达"""
    # 根据操作系统选择参数
    param = "-n" if sys.platform == "win32" else "-c"
    
    try:
        result = subprocess.run(
            ["ping", param, str(count), host],
            capture_output=True,
            text=True,
            timeout=10,
            check=False
        )
        return result.returncode == 0
    except subprocess.TimeoutExpired:
        print(f"ping {host} 超时")
        return False

# 批量检查
hosts = ["192.168.1.1", "192.168.1.2", "google.com"]
for host in hosts:
    status = "可达" if ping_host(host) else "不可达"
    print(f"{host}: {status}")

4.2.4 shutil模块——高级文件操作

shutil模块是os模块的升级版。它提供了更高级的文件操作,比如复制整个目录树、移动文件、归档压缩等。

我最常用的几个函数:

  • shutil.copy():复制文件
  • shutil.copytree():递归复制整个目录
  • shutil.move():移动文件或目录
  • shutil.rmtree():递归删除目录(比os.remove强大得多)
  • shutil.make_archive():创建压缩归档
import shutil
import os

def backup_project(src_dir, backup_dir):
    """备份项目目录"""
    # 创建备份目录
    os.makedirs(backup_dir, exist_ok=True)
    
    # 生成备份文件名(带时间戳)
    from datetime import datetime
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_name = f"backup_{timestamp}"
    
    # 创建压缩归档
    archive_path = shutil.make_archive(
        base_name=os.path.join(backup_dir, backup_name),
        format='gztar',
        root_dir=src_dir
    )
    
    print(f"备份完成: {archive_path}")
    return archive_path

# 使用
backup_project("/opt/myapp", "/backup")

避坑指南:我曾经用shutil.rmtree()删除一个临时目录,结果因为路径拼接错误,差点把整个/tmp给删了。从那以后,我每次用rmtree之前,都会先打印一下要删除的路径,确认无误再执行。

4.3 综合实战:日志分析小工具

把今天学的知识串起来,写一个实用的日志分析工具:

import os
import sys
import subprocess
import shutil
from datetime import datetime, timedelta

def analyze_logs(log_dir, days=1):
    """
    分析指定目录下的日志文件
    1. 找出最近N天的日志
    2. 统计错误数量
    3. 压缩归档旧日志
    """
    if not os.path.exists(log_dir):
        print(f"错误: 目录 {log_dir} 不存在")
        sys.exit(1)
    
    now = datetime.now()
    cutoff = now - timedelta(days=days)
    
    error_count = 0
    recent_logs = []
    
    for filename in os.listdir(log_dir):
        filepath = os.path.join(log_dir, filename)
        if not os.path.isfile(filepath):
            continue
        
        # 检查文件修改时间
        mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
        if mtime > cutoff:
            recent_logs.append(filepath)
            
            # 用grep统计错误
            result = subprocess.run(
                ["grep", "-c", "ERROR", filepath],
                capture_output=True,
                text=True,
                check=False
            )
            if result.returncode == 0 and result.stdout.strip():
                error_count += int(result.stdout.strip())
    
    print(f"最近{days}天的日志文件: {len(recent_logs)}个")
    print(f"错误总数: {error_count}次")
    
    # 归档7天前的日志
    archive_cutoff = now - timedelta(days=7)
    old_logs = []
    
    for filename in os.listdir(log_dir):
        filepath = os.path.join(log_dir, filename)
        if os.path.isfile(filepath):
            mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
            if mtime < archive_cutoff:
                old_logs.append(filepath)
    
    if old_logs:
        archive_dir = os.path.join(log_dir, "archive")
        os.makedirs(archive_dir, exist_ok=True)
        
        archive_path = shutil.make_archive(
            base_name=os.path.join(archive_dir, f"logs_{now.strftime('%Y%m%d')}"),
            format='gztar',
            root_dir=log_dir,
            base_dir='.'
        )
        
        # 删除已归档的旧日志
        for filepath in old_logs:
            os.remove(filepath)
        
        print(f"已归档 {len(old_logs)} 个旧日志文件到: {archive_path}")
    
    return error_count

if __name__ == "__main__":
    log_dir = sys.argv[1] if len(sys.argv) > 1 else "/var/log/myapp"
    analyze_logs(log_dir)

这个工具虽然简单,但涵盖了今天讲的大部分知识点。你想想看,用不到50行代码,就实现了一个能自动分析、统计、归档的日志工具。这就是Python运维的魅力所在。

最后说一句:这些内置模块只是冰山一角。等你真正用起来,会发现Python的标准库几乎覆盖了运维的方方面面。多写、多用、多踩坑,慢慢就熟练了。