自动化脚本与 CLI 工具
用 Python 自动化一切重复工作——从文件批处理到专业 CLI 工具,Typer 让命令行开发变得简单。
CLI 开发生态
graph TD
CLI[Python CLI 开发] --> BUILTIN[内置方案]
CLI --> LIB[第三方库]
BUILTIN --> ARGPARSE[argparse]
BUILTIN --> SYS["sys.argv"]
LIB --> TYPER[Typer 推荐]
LIB --> CLICK[Click]
LIB --> RICH[Rich 美化]
TYPER --> T1[类型注解驱动]
TYPER --> T2[自动帮助文档]
TYPER --> T3[Tab 补全]
RICH --> R1[颜色输出]
RICH --> R2[进度条]
RICH --> R3[表格]
style CLI fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
style TYPER fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
Typer CLI 框架
"""
Typer:用类型注解构建 CLI
pip install typer[all] rich
"""
import typer
from pathlib import Path
from typing import Optional
from rich.console import Console
from rich.table import Table
app = typer.Typer(help="文件管理工具")
console = Console()
@app.command()
def find(
path: Path = typer.Argument(..., help="搜索目录"),
pattern: str = typer.Option("*", "--pattern", "-p", help="文件名模式"),
min_size: int = typer.Option(0, "--min-size", help="最小文件大小(KB)"),
recursive: bool = typer.Option(True, "--recursive/--no-recursive", help="递归搜索"),
):
"""搜索文件"""
if not path.exists():
console.print(f"[red]路径不存在: {path}[/red]")
raise typer.Exit(1)
glob_fn = path.rglob if recursive else path.glob
files = []
for f in glob_fn(pattern):
if f.is_file() and f.stat().st_size >= min_size * 1024:
files.append(f)
# Rich 表格输出
table = Table(title=f"搜索结果 ({len(files)} 个文件)")
table.add_column("文件名", style="cyan")
table.add_column("大小", justify="right")
table.add_column("修改时间")
for f in sorted(files)[:50]:
size_kb = f.stat().st_size / 1024
mtime = f.stat().st_mtime
from datetime import datetime
dt = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M")
table.add_row(str(f.relative_to(path)), f"{size_kb:.1f} KB", dt)
console.print(table)
@app.command()
def rename(
path: Path = typer.Argument(..., help="目标目录"),
prefix: str = typer.Option("", "--prefix", help="添加前缀"),
suffix: str = typer.Option("", "--suffix", help="添加后缀"),
dry_run: bool = typer.Option(True, "--dry-run/--execute", help="预览模式"),
):
"""批量重命名文件"""
files = sorted(path.iterdir())
changes = []
for f in files:
if f.is_file():
new_name = f"{prefix}{f.stem}{suffix}{f.suffix}"
new_path = f.parent / new_name
changes.append((f, new_path))
for old, new in changes:
status = "[yellow]预览[/yellow]" if dry_run else "[green]完成[/green]"
console.print(f" {status} {old.name} → {new.name}")
if not dry_run:
old.rename(new)
console.print(f"\n{'预览' if dry_run else '完成'} {len(changes)} 个文件")
if __name__ == "__main__":
app()
# 使用方式:
# python cli.py find ./data --pattern "*.csv" --min-size 10
# python cli.py rename ./photos --prefix "2024_" --execute
自动化脚本模式
"""
实用自动化脚本模板
"""
from pathlib import Path
import shutil
import json
# === 批量文件整理 ===
def organize_by_extension(source_dir: str):
"""按文件扩展名整理文件到子目录"""
src = Path(source_dir)
moved = 0
for f in src.iterdir():
if f.is_file() and not f.name.startswith("."):
ext_dir = src / f.suffix.lstrip(".").upper()
ext_dir.mkdir(exist_ok=True)
shutil.move(str(f), str(ext_dir / f.name))
moved += 1
print(f"整理完成: {moved} 个文件")
# === 日志分析 ===
def analyze_log(log_path: str, pattern: str = "ERROR") -> dict:
"""分析日志文件"""
from collections import Counter
error_types = Counter()
total = 0
with open(log_path, encoding="utf-8") as f:
for line in f:
total += 1
if pattern in line:
# 提取错误类型
parts = line.split(pattern, 1)
if len(parts) > 1:
error_msg = parts[1].strip()[:50]
error_types[error_msg] += 1
return {
"total_lines": total,
"error_count": sum(error_types.values()),
"top_errors": error_types.most_common(10),
}
# === 配置文件批量更新 ===
def update_configs(config_dir: str, updates: dict):
"""批量更新 JSON 配置文件"""
path = Path(config_dir)
for config_file in path.glob("*.json"):
with open(config_file, encoding="utf-8") as f:
config = json.load(f)
# 合并更新
config.update(updates)
with open(config_file, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
print(f"更新: {config_file.name}")
# === 监控目录变化 ===
def watch_directory(path: str, callback):
"""简单的目录变化监控"""
from time import sleep
watch_path = Path(path)
known_files = set(watch_path.rglob("*"))
print(f"监控: {path}")
while True:
current_files = set(watch_path.rglob("*"))
new_files = current_files - known_files
deleted_files = known_files - current_files
for f in new_files:
callback("新增", f)
for f in deleted_files:
callback("删除", f)
known_files = current_files
sleep(1)
Rich 美化输出
"""
Rich:美化终端输出
"""
from rich.console import Console
from rich.progress import track
from rich.panel import Panel
from rich.table import Table
import time
console = Console()
# 颜色文本
console.print("[bold green]成功![/bold green] 任务完成")
console.print("[red]错误:[/red] 文件不存在")
# 进度条
for _ in track(range(100), description="处理中..."):
time.sleep(0.01)
# 面板
console.print(Panel(
"[bold]系统状态[/bold]\nCPU: 45%\n内存: 2.1GB\n磁盘: 60%",
title="监控",
border_style="green",
))
# 表格
table = Table(title="依赖检查")
table.add_column("包名", style="cyan")
table.add_column("当前版本")
table.add_column("最新版本")
table.add_column("状态")
table.add_row("fastapi", "0.109", "0.110", "[yellow]可更新[/yellow]")
table.add_row("pydantic", "2.6.0", "2.6.0", "[green]最新[/green]")
table.add_row("requests", "2.28", "2.31", "[red]过时[/red]")
console.print(table)
CLI 工具对比
| 特性 | argparse | Click | Typer |
|---|---|---|---|
| 学习曲线 | 低 | 中 | 低 |
| 类型安全 | 否 | 否 | 是 |
| 自动补全 | 否 | 是 | 是 |
| 帮助文档 | 基础 | 好 | 好 |
| 依赖 | 无 | click | typer+click |
| 推荐 | 简单脚本 | 复杂 CLI | 新项目首选 |
本章小结
| 知识点 | 要点 |
|---|---|
| Typer | 类型注解驱动、自动帮助 |
| Rich | 颜色、进度条、表格美化 |
| 自动化 | 文件整理、日志分析、配置更新 |
| 目录监控 | 简单轮询或 watchdog |
| 最佳实践 | dry-run 模式、错误处理 |
延伸阅读:将 CLI 工具打包为 PyPI 包,让更多人使用你的工具。