4、脚本基础语法(二):条件判断、循环控制与异常处理

好,咱们接着聊脚本语法。上一章我们把变量、数据类型和运算符过了一遍,那些是脚本的「砖块」。这一章要讲的,是让砖块动起来的「水泥」——条件判断、循环控制和异常处理。

说白了,没有这些控制结构,你的脚本就只能从上往下一条条执行,跟流水账似的。遇到不同情况做不同处理?重复执行某个操作?程序出错了怎么办?这些都得靠今天的内容来解决。

本章核心三要素:
  • 条件判断:让脚本会「思考」,根据条件走不同分支
  • 循环控制:让脚本会「重复」,批量处理数据
  • 异常处理:让脚本会「兜底」,出错了也不崩
脚本控制结构知识图谱 控制结构 条件判断 循环控制 异常处理 if / elif / else 嵌套判断 for 循环 while 循环 try / except finally 三大控制结构是脚本逻辑的骨架 条件分支 + 循环重复 + 异常兜底 = 健壮的自动化脚本

4.1 条件判断:让脚本学会「看情况办事」

条件判断是脚本里最常用的控制结构。没有它,你的脚本就是个「一根筋」——不管输入什么,都走同一条路。

4.1.1 if / elif / else 基础结构

Python 的条件判断写法很直观。我刚开始写脚本时,最常犯的错误就是忘了冒号,或者缩进不对。记住:冒号开头,缩进控制代码块。

# 基础 if 结构
status = "running"

if status == "running":
    print("服务运行正常")
elif status == "stopped":
    print("服务已停止,需要启动")
elif status == "error":
    print("服务异常,请检查日志")
else:
    print("未知状态,请人工确认")
我的习惯:写条件判断时,我一般会把最可能发生的情况放在最前面。这样脚本执行时能更快命中,少做无谓的比较。尤其是在处理大量数据时,这个习惯能省不少时间。

4.1.2 条件表达式与逻辑运算符

实际项目中,条件往往不是单一的。比如检查服务器状态,既要看进程是否存活,又要看端口是否监听。这时候就需要组合条件。

运算符 含义 示例 说明
and 与 a > 0 and b < 10 两个条件都成立才为真
or 或 a == 0 or b == 0 至少一个条件成立即为真
not 非 not a > 0 取反操作
in 成员判断 "error" in log 检查是否包含某个元素
is 身份判断 a is None 检查是否为同一个对象
# 组合条件实战
cpu_usage = 85
memory_usage = 92
disk_usage = 78

# 检查是否需要告警
if cpu_usage > 90 and memory_usage > 85:
    print("⚠️ 严重告警:CPU和内存同时高负载")
elif cpu_usage > 80 or memory_usage > 80:
    print("⚠️ 警告:CPU或内存负载偏高")
elif not disk_usage > 90:
    print("✅ 磁盘空间充足,无需处理")
else:
    print("✅ 系统运行正常")
我曾经踩过的坑:用 == 判断浮点数相等。比如 0.1 + 0.2 == 0.3 返回的是 False!因为浮点数精度问题。后来我改用 abs(a - b) < 0.0001 这种方式来判断,再也没出过问题。

4.2 循环控制:让脚本学会「重复劳动」

运维脚本里,循环用得特别多。批量检查服务器、遍历日志文件、重试失败的操作……这些场景都离不开循环。

4.2.1 for 循环:遍历利器

for 循环适合遍历已知范围的数据。我个人觉得,能用 for 解决的问题,尽量别用 while——代码更清晰,也不容易写出死循环。

# 遍历列表
servers = ["web-01", "web-02", "db-01", "cache-01"]
for server in servers:
    print(f"正在检查 {server} 的状态...")
    # 这里可以调用检查函数

# 遍历字典
config = {"host": "192.168.1.1", "port": 8080, "timeout": 30}
for key, value in config.items():
    print(f"{key} = {value}")

# 使用 range() 生成序列
for i in range(1, 6):
    print(f"第 {i} 次重试...")

4.2.2 while 循环:条件驱动

while 循环适合你不知道要循环多少次,但知道什么时候该停的场景。比如等待某个服务启动完成。

while 循环的核心原则:一定要确保循环条件能在某次迭代后变为 False,否则就是死循环。我见过太多线上事故是因为 while 条件写错了,脚本跑了一整夜没停。
# 等待服务启动
import time

service_ready = False
retry_count = 0
max_retries = 10

while not service_ready and retry_count < max_retries:
    print(f"检查服务状态... 第 {retry_count + 1} 次")
    # 模拟检查服务
    if retry_count >= 3:  # 假设第4次检查成功
        service_ready = True
        print("服务已就绪!")
    else:
        print("服务尚未就绪,等待 2 秒...")
        time.sleep(2)
        retry_count += 1

if not service_ready:
    print("服务启动超时,请手动检查")

4.2.3 循环控制:break、continue 与 else

这三个关键字是循环的「方向盘」。用好了,代码会非常优雅;用不好,逻辑会乱成一团。

关键字 作用 使用场景
break 立即跳出整个循环 找到目标后提前结束
continue 跳过本次循环,进入下一次 过滤掉不需要处理的数据
else 循环正常结束(未被 break)时执行 检查是否找到了目标
# break 示例:找到第一个错误日志就停止
logs = ["info: 正常", "info: 正常", "error: 连接超时", "info: 正常", "error: 权限拒绝"]
for log in logs:
    if "error" in log:
        print(f"发现错误:{log}")
        break  # 找到第一个错误就停止
else:
    print("未发现任何错误")  # 只有循环正常结束才会执行

# continue 示例:只处理 error 级别的日志
for log in logs:
    if "info" in log:
        continue  # 跳过 info 日志
    print(f"处理错误日志:{log}")
我的经验:循环中的 else 子句很多人不知道。它特别适合「查找-确认」场景——如果循环里用 break 找到了目标,else 就不执行;如果找了一圈都没找到,else 就会触发。比设个标志变量优雅多了。

4.3 异常处理:让脚本学会「扛得住」

写脚本时,最怕的就是「未捕获的异常」。脚本跑到一半突然崩了,前面的工作全白费。异常处理就是给你的脚本穿上「防弹衣」。

4.3.1 try / except 基础

你想想看,运维脚本要处理各种不确定因素:网络超时、文件不存在、权限不足……这些都得靠异常处理来兜底。

# 基础异常处理
try:
    # 可能会出错的代码
    with open("/etc/config.ini", "r") as f:
        content = f.read()
        print("配置文件读取成功")
except FileNotFoundError:
    print("配置文件不存在,使用默认配置")
except PermissionError:
    print("权限不足,请检查文件权限")
except Exception as e:
    print(f"未知错误:{e}")

4.3.2 捕获多个异常与异常链

实际项目中,一个操作可能引发多种异常。我习惯把能预见的异常单独处理,最后留一个兜底的 Exception。

# 多异常捕获
import requests

try:
    response = requests.get("http://example.com/api/status", timeout=5)
    data = response.json()
    print(f"API 返回:{data}")
except requests.ConnectionError:
    print("网络连接失败,请检查网络")
except requests.Timeout:
    print("请求超时,服务可能不可用")
except ValueError as e:
    print(f"JSON 解析失败:{e}")
except Exception as e:
    print(f"未预期的错误:{e}")
    # 记录日志,方便排查
我曾经犯过的错:空 except 子句。早期写脚本时,为了省事,直接写 except: pass,结果线上出了问题,脚本静默吞掉了所有异常,连日志都没留下。排查了整整一天才发现是某个接口改了返回格式。现在我的原则是:要么明确捕获,要么记录日志,绝不静默吞异常。

4.3.3 finally:无论如何都要执行的代码

finally 子句是异常处理里的「保险」。不管有没有异常,不管有没有 return,finally 里的代码一定会执行。资源清理、关闭连接、释放锁……这些操作放 finally 里最合适。

# finally 实战:确保文件关闭
def read_config(file_path):
    f = None
    try:
        f = open(file_path, "r")
        return f.read()
    except FileNotFoundError:
        print("文件不存在")
        return None
    finally:
        if f:
            f.close()  # 确保文件被关闭
            print("文件已关闭")

# 即使 try 里有 return,finally 也会执行
result = read_config("/etc/config.ini")
print(f"读取结果:{result}")

4.3.4 自定义异常与 raise

有时候,内置的异常类型不够用。比如你想定义「磁盘空间不足」这种业务异常。这时候可以自定义异常类。

# 自定义异常
class DiskFullError(Exception):
    """磁盘空间不足异常"""
    def __init__(self, disk_path, usage_percent):
        self.disk_path = disk_path
        self.usage_percent = usage_percent
        super().__init__(f"磁盘 {disk_path} 使用率已达 {usage_percent}%")

# 使用自定义异常
def check_disk_space(path):
    # 模拟检查磁盘
    usage = 95
    if usage > 90:
        raise DiskFullError(path, usage)
    return True

try:
    check_disk_space("/data")
except DiskFullError as e:
    print(f"⚠️ {e}")
    # 触发告警或自动清理逻辑
异常处理的最佳实践:
  1. 精确捕获:能捕获具体异常就别用 Exception
  2. 保留异常链:使用 raise ... from ... 保留原始异常信息
  3. 不要滥用:能用条件判断解决的问题,别用异常处理
  4. 记录日志:捕获异常后一定要记录,方便事后排查

4.4 综合实战:一个完整的脚本片段

把今天学的三个知识点串起来,写一个实际能用的脚本。这个脚本会批量检查服务器状态,遇到错误会重试,重试失败会记录异常。

import time
import logging

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def check_server(server_ip, port=80):
    """检查服务器是否存活"""
    # 模拟检查逻辑
    import random
    return random.choice([True, False])

def batch_check_servers(server_list, max_retries=3):
    """批量检查服务器,支持重试"""
    results = {}
    
    for server in server_list:
        logging.info(f"开始检查服务器:{server}")
        success = False
        
        for attempt in range(1, max_retries + 1):
            try:
                logging.info(f"第 {attempt} 次尝试检查 {server}")
                status = check_server(server)
                
                if status:
                    results[server] = "正常"
                    success = True
                    logging.info(f"{server} 检查通过")
                    break  # 检查通过,跳出重试循环
                else:
                    logging.warning(f"{server} 返回异常状态")
                    
            except ConnectionError as e:
                logging.error(f"{server} 连接失败:{e}")
                if attempt < max_retries:
                    wait_time = attempt * 2  # 递增等待时间
                    logging.info(f"等待 {wait_time} 秒后重试...")
                    time.sleep(wait_time)
                else:
                    results[server] = f"连接失败(已重试{max_retries}次)"
                    
            except Exception as e:
                logging.error(f"{server} 发生未知错误:{e}")
                results[server] = f"未知错误:{str(e)}"
                break  # 未知错误不重试
                
        else:
            # 循环正常结束(未被 break),说明所有重试都失败了
            if server not in results:
                results[server] = f"检查失败(已重试{max_retries}次)"
    
    return results

# 执行批量检查
servers_to_check = ["192.168.1.1", "192.168.1.2", "192.168.1.3"]
check_results = batch_check_servers(servers_to_check)

# 输出结果
print("\n=== 检查结果汇总 ===")
for server, status in check_results.items():
    if "正常" in status:
        print(f"✅ {server}: {status}")
    else:
        print(f"❌ {server}: {status}")
关于这个脚本,我想说的:这个例子几乎用到了本章所有知识点——for 循环遍历服务器列表,if/else 判断检查结果,try/except 处理连接异常,break 在成功时跳出循环,else 子句处理所有重试都失败的情况。你看,把这些基础结构组合起来,就能写出很实用的运维脚本。

嗯,这一章的内容就到这里。条件判断、循环控制、异常处理,这三个东西是脚本逻辑的「三驾马车」。写脚本时多想想:什么情况走哪条路?哪些操作需要重复?出错了怎么兜底?想清楚了,脚本的质量自然就上去了。