1. 为什么需要坚不可摧的自动化脚本
在macOS环境下使用Python开发自动化脚本时,我们经常会遇到各种意外情况导致脚本中断。想象一下:你精心编写的爬虫脚本运行了8小时后突然因为网络波动而崩溃,或者定时备份脚本因为临时文件权限问题而失败。这些场景正是我们需要构建"坚不可摧"脚本的原因。
Python的try机制为我们提供了优雅的错误处理方式,但大多数开发者仅仅停留在基础的try-except使用层面。实际上,结合macOS终端特性,我们可以打造真正健壮的自动化方案。这种脚本应该具备以下核心能力:
- 能够捕获并记录所有类型的异常
- 在非预期退出时自动重试关键操作
- 清理临时文件和资源
- 提供详细的运行日志
- 在终端中给出明确的状态反馈
2. 基础防护:理解Python异常处理机制
2.1 try-except的基础与进阶用法
最基本的异常处理结构大家都很熟悉:
python复制try:
risky_operation()
except Exception as e:
print(f"操作失败: {e}")
但在自动化脚本中,我们需要更精细的控制。考虑以下进阶模式:
python复制import sys
import traceback
def main_operation():
# 你的主要逻辑代码
pass
try:
main_operation()
except KeyboardInterrupt:
print("\n用户中断执行")
sys.exit(0)
except ConnectionError as ce:
print(f"网络连接错误: {ce}")
# 可以在这里添加重试逻辑
except ValueError as ve:
print(f"值错误: {ve}")
# 特定错误的处理
except Exception as e:
print(f"未预期的错误: {e}")
traceback.print_exc() # 打印完整调用栈
sys.exit(1)
finally:
print("清理工作...")
# 无论成功失败都会执行的清理代码
2.2 异常层级与捕获策略
Python的异常是分层级的,理解这一点对构建健壮脚本至关重要:
code复制BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── StopIteration
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ImportError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
├── OSError
│ ├── BlockingIOError
│ ├── ChildProcessError
│ ├── ConnectionError
│ │ ├── BrokenPipeError
│ │ ├── ConnectionAbortedError
│ │ ├── ConnectionRefusedError
│ │ └── ConnectionResetError
│ ├── FileExistsError
│ ├── FileNotFoundError
│ ├── InterruptedError
│ ├── IsADirectoryError
│ ├── NotADirectoryError
│ ├── PermissionError
│ ├── ProcessLookupError
│ └── TimeoutError
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ └── RecursionError
├── SyntaxError
├── SystemError
├── TypeError
└── ValueError
在自动化脚本中,我建议采用"从具体到一般"的捕获顺序,先处理最可能发生的特定异常,最后用Exception兜底。
3. macOS终端集成技巧
3.1 让脚本在终端中表现更专业
macOS终端提供了许多可以增强脚本表现力的特性:
python复制import sys
import time
class TerminalColors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def print_success(message):
print(f"{TerminalColors.OKGREEN}✓ {message}{TerminalColors.ENDC}")
def print_failure(message):
print(f"{TerminalColors.FAIL}✗ {message}{TerminalColors.ENDC}")
def print_progress(iteration, total, prefix='', suffix='', length=50):
percent = ("{0:.1f}").format(100 * (iteration / float(total)))
filled_length = int(length * iteration // total)
bar = '█' * filled_length + '-' * (length - filled_length)
print(f'\r{prefix} |{bar}| {percent}% {suffix}', end='\r')
if iteration == total:
print()
3.2 处理.command脚本的终端行为
参考网络热词中提到的技巧,我们可以优化脚本的终端行为:
python复制import subprocess
import os
def ensure_termination():
"""确保脚本执行完毕后终端窗口会正确关闭"""
if os.name == 'posix' and 'TERM_PROGRAM' in os.environ:
# 如果是macOS终端环境
subprocess.run(['osascript', '-e', 'tell application "Terminal" to close front window'])
4. 构建完整的自动化脚本框架
4.1 日志记录与错误报告
一个健壮的自动化脚本需要完善的日志系统:
python复制import logging
from logging.handlers import RotatingFileHandler
def setup_logging(log_file='automation.log'):
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# 控制台输出
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
# 文件输出
file_handler = RotatingFileHandler(
log_file, maxBytes=1024*1024, backupCount=5
)
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(file_formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
4.2 重试机制实现
对于可能失败的临时性操作,实现智能重试:
python复制import time
import random
from functools import wraps
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return f(*args, **kwargs)
except exceptions as e:
attempts += 1
if attempts == max_attempts:
raise
sleep_time = delay * (backoff ** (attempts - 1)) * (1 + random.random() * 0.1)
time.sleep(sleep_time)
return wrapper
return decorator
5. 实战案例:网页抓取自动化脚本
让我们把这些技术应用到一个实际的网页抓取脚本中:
python复制import requests
from bs4 import BeautifulSoup
import logging
from urllib.parse import urljoin
logger = setup_logging()
class WebScraper:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
@retry(max_attempts=5, delay=2, exceptions=(requests.RequestException,))
def fetch_page(self, url):
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.text
except requests.HTTPError as http_err:
logger.error(f"HTTP错误: {http_err}")
raise
except requests.Timeout:
logger.warning("请求超时,将重试...")
raise
except requests.RequestException as err:
logger.error(f"请求异常: {err}")
raise
def parse_links(self, html):
soup = BeautifulSoup(html, 'html.parser')
for link in soup.find_all('a', href=True):
yield urljoin(self.base_url, link['href'])
def run(self, start_url):
try:
visited = set()
queue = [start_url]
while queue:
url = queue.pop(0)
if url in visited:
continue
logger.info(f"正在处理: {url}")
try:
html = self.fetch_page(url)
visited.add(url)
# 处理页面内容...
# 这里添加你的业务逻辑
# 收集新链接
for new_url in self.parse_links(html):
if new_url not in visited:
queue.append(new_url)
except Exception as e:
logger.error(f"处理 {url} 时出错: {e}")
continue
except KeyboardInterrupt:
logger.info("用户中断执行")
except Exception as e:
logger.critical(f"脚本严重错误: {e}")
raise
finally:
self.session.close()
logger.info("爬取完成")
if __name__ == "__main__":
try:
scraper = WebScraper("https://example.com")
scraper.run("https://example.com/start")
except Exception as e:
logger.critical(f"脚本执行失败: {e}")
sys.exit(1)
finally:
if 'TERM_PROGRAM' in os.environ:
ensure_termination()
6. 高级防护技巧
6.1 资源管理与上下文协议
使用上下文管理器确保资源正确释放:
python复制import contextlib
@contextlib.contextmanager
def temp_file_handler(filename):
try:
# 创建临时文件
with open(filename, 'w') as f:
yield f
finally:
# 确保文件被删除
try:
os.unlink(filename)
except OSError:
pass
# 使用示例
with temp_file_handler('temp.txt') as f:
f.write('临时数据')
# 离开with块后文件会自动删除
6.2 信号处理与优雅退出
处理系统信号实现优雅退出:
python复制import signal
import sys
class GracefulExiter:
def __init__(self):
self.shutdown = False
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
print(f"\n接收到信号 {signum}, 准备优雅退出...")
self.shutdown = True
# 使用示例
exiter = GracefulExiter()
while not exiter.shutdown:
try:
# 主循环工作
time.sleep(1)
except Exception as e:
print(f"错误: {e}")
continue
6.3 内存与性能监控
监控脚本资源使用情况:
python复制import resource
import psutil # 需要安装: pip install psutil
def monitor_resources():
"""记录内存和CPU使用情况"""
process = psutil.Process()
mem_info = process.memory_info()
cpu_percent = process.cpu_percent(interval=1)
logger.info(
f"内存使用: RSS={mem_info.rss/1024/1024:.2f}MB "
f"VMS={mem_info.vms/1024/1024:.2f}MB | "
f"CPU使用: {cpu_percent}%"
)
# 设置内存限制(100MB)
soft, hard = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (100 * 1024 * 1024, hard))
7. 打包与分发技巧
7.1 将脚本转换为macOS应用
使用PyInstaller创建独立的macOS应用:
bash复制# 安装PyInstaller
pip install pyinstaller
# 打包脚本
pyinstaller --onefile --windowed --name "MyAutomation" your_script.py
# 添加图标(需要.icns文件)
pyinstaller --onefile --windowed --name "MyAutomation" --icon=app_icon.icns your_script.py
7.2 创建.command启动器
参考网络热词中的技巧,创建更友好的启动方式:
bash复制#!/bin/bash
# 切换到脚本所在目录
cd "$(dirname "$0")"
# 设置Python虚拟环境(可选)
source venv/bin/activate
# 执行Python脚本
python3 your_script.py
# 根据退出状态决定是否关闭终端
if [ $? -eq 0 ]; then
osascript -e 'tell application "Terminal" to close front window'
else
echo "脚本执行失败,终端窗口保持打开以便调试"
fi
记得给.command文件添加执行权限:
bash复制chmod +x your_script.command
