第10章 10.5 终极实战:CLI 工具开发
前置章节:第10章 10.4 性能分析与优化
上一章我们学会了用 cProfile 找出程序的性能瓶颈,用各种招数让代码跑得更快。但跑得快只是「内功」,这一章我们要练「外功」——学会把自己写的 Python 程序,打包成别人电脑上也能一键运行的命令行工具。
你有没有遇到过这种情况:
- 写了一个特别有用的 Python 脚本,但每次运行都要
python script.py,还得告诉别人「先装 Python、再 pip install 依赖…」 - 想把自己的工具发布到 GitHub,让别人
pip install就能用,但不知道怎么做 - 用过
click或argparse,但不太清楚怎么写出专业的命令行界面
这一章,就是来解决这些问题的。学完之后,你写的 Python 代码可以直接变成别人电脑上的「小软件」,一行命令就能跑。
🎯 1. 开场 3 分钟:为什么要学 CLI 工具开发?
真实场景:你的「小工具」被别人用上了
假设你写了一个 Python 脚本,帮自己自动整理桌面上的文件(把图片移到 图片 文件夹、把文档移到 文档 文件夹)。你自己用着特别爽,然后发到群里说「大家拿去用啊」。
结果:
- 小李问:「怎么用?」
- 你说:「先安装 Python,然后 pip install watchdog…」
- 小李:「算了,太麻烦了」
如果你把这个脚本做成一个 CLI 工具,小李只需要:
pip install your-awesome-tool
file-organizer --watch ~/Desktop
一行命令,直接跑。这就是 CLI 工具的魅力。
什么是 CLI 工具?
CLI = Command Line Interface,就是命令行界面。你写的程序通过终端/命令提示符和人交互,而不是图形界面(点按钮那种)。
类比一下:
- 图形界面 = 餐厅里的服务员(你点菜,他上菜)
- 命令行界面 = 自助餐台(你自己拿,但效率高)
CLI 工具特别适合:
- 程序员自己用(自动化重复任务)
- 给其他程序员用(不需要图形界面,服务器也能跑)
- 做数据处理、文件转换、批量操作这类任务
这一章你能做出什么?
学完这一章,你能独立做出这样的工具:
# 别人安装你的工具
pip install my-task-manager
# 你的工具能做这些事
task add 写周报 # 添加待办
task list # 查看所有待办
task done 1 # 标记完成
task export --format csv # 导出为 CSV
这就是一个完整的 CLI 工具,有子命令、有参数、有输出。准备好了吗?我们开始。
🧱 2. 基础 25 分钟:核心概念
2.1 什么是 argparse?什么是 click?
在 Python 里,写 CLI 工具主要有两种方式:
| 库 | 特点 | 适合场景 |
|---|---|---|
argparse |
Python 标准库,不需要安装 | 简单的命令行参数 |
click |
更优雅的 API,自动生成帮助信息 | 复杂的 CLI 工具 |
生活类比:
argparse= 手动的点菜单(你自己定义每个参数)click= 自助点餐机(它帮你做好界面,你只管定义功能)
这一章我们两个都学,但重点放在 click 上,因为它写出来更好看、功能更强大。
2.2 先玩 argparse:最简单的参数接收
# greeter.py
import argparse
# 创建一个解析器
parser = argparse.ArgumentParser(description='向人打招呼的工具')
# 添加一个参数
parser.add_argument('name', help='要打招呼的人的名字')
parser.add_argument('--greeting', default='你好', help='用什么词打招呼')
# 解析参数
args = parser.parse_args()
# 使用参数
print(f"{args.greeting},{args.name}!")
运行一下:
python greeter.py 小明
# 输出:你好,小明!
python greeter.py 小红 --greeting "早上好"
# 输出:早上好,小红!
看!你的程序可以接收参数了。但 argparse 的参数定义比较啰嗦,每个参数都要单独写一行。接下来看 click。
2.3 click 入门:一个命令搞定
# greeter_click.py
import click
@click.command()
@click.argument('name')
@click.option('--greeting', default='你好', help='用什么词打招呼')
def greet(name, greeting):
"""向人打招呼的简单工具"""
click.echo(f"{greeting},{name}!")
if __name__ == '__main__':
greet()
运行:
python greeter_click.py 小明
# 输出:你好,小明!
python greeter_click.py 小红 --greeting "早上好"
# 输出:早上好,小红!
解释一下:
@click.command()把下面的函数变成一个命令@click.argument('name')定义一个「位置参数」(必须传的参数)@click.option('--greeting')定义一个「选项参数」(可选的,用--开头)click.echo()是打印的升级版,能自动处理 Windows/Unix 差异

2.4 多个子命令:做一个任务管理器
真正的 CLI 工具通常有多个子命令,比如 git commit、git push…click 里用 @click.group() 实现这个:
# task_manager.py
import click
import json
from pathlib import Path
# 数据文件存在这里
DATA_FILE = Path('tasks.json')
def load_tasks():
"""加载任务列表"""
if not DATA_FILE.exists():
return []
return json.loads(DATA_FILE.read_text())
def save_tasks(tasks):
"""保存任务列表"""
DATA_FILE.write_text(json.dumps(tasks, ensure_ascii=False, indent=2))
@click.group()
def cli():
"""任务管理器:帮你追踪待办事项"""
pass
@cli.command()
@click.argument('task')
def add(task):
"""添加一个新任务"""
tasks = load_tasks()
task_id = len(tasks) + 1
tasks.append({'id': task_id, '内容': task, '完成': False})
save_tasks(tasks)
click.echo(f"✅ 已添加任务 {task_id}:{task}")
@cli.command()
def list():
"""列出所有任务"""
tasks = load_tasks()
if not tasks:
click.echo("📝 暂无任务,添加一个吧:task add \"写周报\"")
return
for t in tasks:
status = "✅" if t['完成'] else "⬜"
click.echo(f"{status} [{t['id']}] {t['内容']}")
@cli.command()
@click.argument('task_id', type=int)
def done(task_id):
"""标记任务为完成"""
tasks = load_tasks()
for t in tasks:
if t['id'] == task_id:
t['完成'] = True
save_tasks(tasks)
click.echo(f"🎉 任务 {task_id} 已完成!")
return
click.echo(f"❌ 未找到任务 {task_id}")
if __name__ == '__main__':
cli()
运行试试:
python task_manager.py add "写周报"
# 输出:✅ 已添加任务 1:写周报
python task_manager.py add "整理桌面"
# 输出:✅ 已添加任务 2:整理桌面
python task_manager.py list
# 输出:
# ⬜ [1] 写周报
# ⬜ [2] 整理桌面
python task_manager.py done 1
# 输出:🎉 任务 1 已完成!
python task_manager.py list
# 输出:
# ✅ [1] 写周报
# ⬜ [2] 整理桌面
看!一个完整的小工具就做好了。现在它已经有模有样了:可以添加、查看、标记完成。

2.5 进阶技巧:带参数的子命令
有时候子命令也需要参数。比如导出任务时指定格式:
@cli.command()
@click.option('--format', 'fmt', default='text', help='导出格式:text、json、csv')
def export(fmt):
"""导出所有任务"""
tasks = load_tasks()
if fmt == 'json':
click.echo(json.dumps(tasks, ensure_ascii=False, indent=2))
elif fmt == 'csv':
click.echo("id,内容,完成")
for t in tasks:
click.echo(f"{t['id']},{t['内容']},{t['完成']}")
else:
for t in tasks:
status = "✅" if t['完成'] else "⬜"
click.echo(f"{status} {t['内容']}")
运行:
python task_manager.py export --format json
# 输出 JSON 格式的任务列表
🔥 3. 实战 35 分钟:3 个递进的小项目
📦 项目 1(5 分钟):温度转换器
目标:写一个命令行工具,把摄氏度和华氏度互相转换。
# temp_converter.py
import click
@click.command()
@click.argument('temp', type=float)
@click.option('--to', 'direction', type=click.Choice(['f', 'c']), default='f',
help='转换为 f(华氏度)或 c(摄氏度)')
def convert(temp, direction):
"""温度转换器:默认华氏度"""
if direction == 'f':
result = temp * 9/5 + 32
click.echo(f"{temp}°C = {result:.2f}°F")
else:
result = (temp - 32) * 5/9
click.echo(f"{temp}°F = {result:.2f}°C")
if __name__ == '__main__':
convert()
运行:
python temp_converter.py 100 --to c
# 输出:100.0°F = 37.78°C
python temp_converter.py 37.78 --to f
# 输出:37.78°C = 100.0°F
一句话解释:用 @click.argument 接收温度值,用 --to 选项指定转换方向。
📊 项目 2(15 分钟):CSV 数据处理器
目标:读取一个 CSV 文件,统计里面的数据。
假设你有一个 sales.csv 文件:
日期,商品,销量,单价
2024-01-01,苹果,100,5.5
2024-01-01,香蕉,80,3.2
2024-01-02,苹果,120,5.5
2024-01-02,香蕉,90,3.2
现在写一个工具来分析它:
# sales_analyzer.py
import click
import csv
from pathlib import Path
from collections import defaultdict
@click.command()
@click.argument('file', type=click.Path(exists=True))
@click.option('--sum-by', help='按什么字段汇总(如:商品)')
def analyze(file, sum_by):
"""分析 CSV 销售数据"""
with open(file, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
rows = list(reader)
if not rows:
click.echo("❌ CSV 文件是空的")
return
headers = rows[0].keys()
click.echo(f"📊 共 {len(rows)} 条记录")
click.echo(f"📋 字段:{', '.join(headers)}")
if sum_by:
if sum_by not in rows[0]:
click.echo(f"❌ 没有 '{sum_by}' 这个字段")
return
totals = defaultdict(int)
for row in rows:
totals[row[sum_by]] += int(row['销量'])
click.echo(f"\n📈 按 {sum_by} 汇总的销量:")
for key, total in sorted(totals.items(), key=lambda x: -x[1]):
click.echo(f" {key}: {total}")
if __name__ == '__main__':
analyze()
运行:
python sales_analyzer.py sales.csv
# 输出:
# 📊 共 4 条记录
# 📋 字段:日期, 商品, 销量, 单价
python sales_analyzer.py sales.csv --sum-by 商品
# 输出:
# 📈 按 商品 汇总的销量:
# 苹果: 220
# 香蕉: 170
一句话解释:用 csv.DictReader 读取 CSV,然后用 defaultdict 做汇总统计。
🚀 项目 3(15 分钟):个人支出追踪器
目标:综合运用前面的知识,做一个有点真实用途的小工具——追踪每天的支出。
# expense_tracker.py
import click
import json
from pathlib import Path
from datetime import datetime
from collections import defaultdict
DATA_FILE = Path('expenses.json')
def load_expenses():
if not DATA_FILE.exists():
return []
return json.loads(DATA_FILE.read_text())
def save_expenses(expenses):
DATA_FILE.write_text(json.dumps(expenses, ensure_ascii=False, indent=2))
@click.group()
def wallet():
"""💰 个人支出追踪器 - 记录你的每一笔花销"""
pass
@wallet.command()
@click.argument('amount', type=float)
@click.argument('description')
@click.option('--category', '-c', default='其他', help='支出类别')
def add(amount, description, category):
"""添加一笔支出"""
expenses = load_expenses()
expense_id = len(expenses) + 1
expense = {
'id': expense_id,
'日期': datetime.now().strftime('%Y-%m-%d'),
'金额': amount,
'描述': description,
'类别': category
}
expenses.append(expense)
save_expenses(expenses)
click.echo(f"✅ 已记录:{amount}元 - {description} [{category}]")
@wallet.command()
def list():
"""查看所有支出"""
expenses = load_expenses()
if not expenses:
click.echo("📝 暂无支出记录")
return
total = 0
for e in expenses:
total += e['金额']
click.echo(f"[{e['id']}] {e['日期']} {e['金额']}元 - {e['描述']} [{e['类别']}]")
click.echo(f"\n💵 总支出:{total:.2f} 元")
@wallet.command()
@click.option('--category', '-c', help='只看某个类别')
def summary(category):
"""按类别汇总支出"""
expenses = load_expenses()
if not expenses:
click.echo("📝 暂无支出记录")
return
if category:
expenses = [e for e in expenses if e['类别'] == category]
if not expenses:
click.echo(f"❌ 没有 '{category}' 类别的支出")
return
by_category = defaultdict(float)
for e in expenses:
by_category[e['类别']] += e['金额']
click.echo(f"\n📊 {'所有' if not category else category}支出汇总:")
for cat, total in sorted(by_category.items(), key=lambda x: -x[1]):
click.echo(f" {cat}: {total:.2f} 元")
@wallet.command()
@click.argument('expense_id', type=int)
def delete(expense_id):
"""删除一笔支出"""
expenses = load_expenses()
new_expenses = [e for e in expenses if e['id'] != expense_id]
if len(new_expenses) == len(expenses):
click.echo(f"❌ 未找到支出 {expense_id}")
return
save_expenses(new_expenses)
click.echo(f"🗑️ 已删除支出 {expense_id}")
if __name__ == '__main__':
wallet()
运行:
python expense_tracker.py add 25.5 --category 餐饮 "午餐"
# 输出:✅ 已记录:25.5元 - 午餐 [餐饮]
python expense_tracker.py add 120 --category 交通 "打车"
# 输出:✅ 已记录:120.0元 - 打车 [交通]
python expense_tracker.py list
# 输出:
# [1] 2024-01-15 25.5元 - 午餐 [餐饮]
# [2] 2024-01-15 120.0元 - 打车 [交通]
#
# 💵 总支出:145.50 元
python expense_tracker.py summary
# 输出:
# 📊 所有支出汇总:
# 交通: 120.00 元
# 餐饮: 25.50 元
一句话解释:综合了文件存储、子命令、选项参数,做成了一个完整的支出追踪 CLI 工具。
💪 4. 进阶 20 分钟:常见坑 + 性能小贴士
4.1 坑 1:click.echo vs print
❌ 错误:
print("正在处理...") # Windows 下可能不显示
✅ 正确:
click.echo("正在处理...") # 跨平台兼容
4.2 坑 2:参数类型不匹配
❌ 错误:
@click.argument('count')
def process(count):
total = count * 100 # count 是字符串,不是数字!
✅ 正确:
@click.argument('count', type=int)
def process(count):
total = count * 100 # count 已经是整数了
4.3 坑 3:中文编码问题
❌ 错误:
with open('data.txt', 'w') as f:
f.write("中文内容")
✅ 正确:
with open('data.txt', 'w', encoding='utf-8') as f:
f.write("中文内容")
4.4 坑 4:忘记调用 parse_args()
❌ 错误(argparse):
parser = argparse.ArgumentParser()
parser.add_argument('--name')
# 没有 parse_args(),args 永远是 None
print(args.name) # 报错!
✅ 正确:
parser = argparse.ArgumentParser()
parser.add_argument('--name')
args = parser.parse_args() # 关键这行!
print(args.name)
4.5 坑 5:click.group() 没有正确调用子命令
❌ 错误:
@click.group()
def cli():
pass
@cli.command()
def sub():
pass
cli.sub() # ❌ 这样调用不会工作
✅ 正确:
if __name__ == '__main__':
cli() # ✅ 直接调用 group 本身
4.6 性能小贴士:文件操作优化
如果你要频繁读写 JSON 文件,可以用 orjson 库加速:
pip install orjson
import orjson
# 比 json.dumps 快 10 倍
data = orjson.dumps({"key": "value"})
4.7 调试技巧:用 click.echo 调试
@cli.command()
@click.argument('filename')
def process(filename):
click.echo(f"[DEBUG] 收到文件:{filename}") # 调试信息
click.echo(f"[DEBUG] 文件存在:{Path(filename).exists()}")
# 继续处理...
✏️ 5. 练习题 + 作业题
练习题(10 分钟)
练习 1(1 分钟):温度转换
- 输入:python temp_converter.py 0 --to c
- 预期输出:0.0°F = -17.78°C
- 提示:直接运行项目 1 的代码
练习 2(2 分钟):添加类别选项
- 输入:在温度转换器中添加 --category 选项
- 预期输出:运行 python temp_converter.py 100 --to c --category 测试
- 提示:参考 @click.option 的写法
练习 3(3 分钟):处理新 CSV
- 输入:创建一个 test.csv,包含 姓名,分数 两列,用分析器处理
- 预期输出:显示记录数和字段
- 提示:CSV 要有表头
练习 4(4 分钟):组合项目 2 和 3
- 输入:用项目 2 的 CSV 分析功能,导出项目 3 的支出数据
- 预期输出:把支出数据转成 CSV 格式导出
- 提示:参考 export 命令的实现
练习 5(挑战题):修复报错
- 输入:运行以下代码,说出什么错误:
import click
@click.command()
@click.argument('num', type=int)
def double(num):
click.echo(f"两倍是:{num * 2}")
if __name__ == '__main__':
double()
运行 python script.py abc
- 预期输出:应该报错,解释为什么
- 提示:看 type=int 做了什么
作业题(30 分钟 - 2 小时)
作业:做一个「个人阅读追踪器」CLI 工具
需求描述:
做一个命令行工具,追踪你读过的书和文章。
功能点:
1. add 命令:添加一本书(书名、作者、页数、阅读日期)
2. list 命令:列出所有阅读记录
3. stats 命令:统计阅读量(总数量、总页数、按月份统计)
4. search 命令:搜索书名或作者
加分项:
1. 支持 delete 删除记录
2. 支持导出为 CSV 格式
验收标准:
- 能添加和查看阅读记录
- 统计数据正确
- 搜索能正常工作
- 代码有中文注释
提交方式:
评论区贴代码或 GitHub 链接
📚 6. 总结 + 资源
这一章学到的 3 个核心点
argparsevsclick:标准库 argparse 适合简单场景,click 更适合生产级 CLI 工具- 子命令设计:用
@click.group()实现多个子命令(add/list/done),让工具更像「系统」 - 数据持久化:结合 JSON 文件存储,让工具能记住历史数据
延伸学习资源
- Click 官方文档 - 官方出品,最权威
- Python 标准库 argparse 教程 - 官方文档
- 《Python CLI 工具实战》- 推荐在 Real Python 网站上搜索相关教程
互动钩子
🎉 恭喜你! 你已经完成了 Python 从入门到精通全系列 45 章的学习!
你现在掌握了 Python 的:
- 基础语法(变量、循环、函数)
- 面向对象编程
- 异常处理
- 文件操作
- 数据库连接
- Web 开发
- API 开发
- 测试驱动开发
- 性能优化
- CLI 工具开发
下一步学什么?
- 🔥 自动化脚本:用 Python 自动化你的日常工作(文件整理、邮件处理、Excel 报表)
- 🌐 Web 全栈开发:用 Flask/Django 做个自己的博客或管理后台
- 📊 数据分析:用 Pandas/Matplotlib 分析数据,做可视化报表
- 🤖 机器学习入门:用 scikit-learn 跑第一个预测模型
你在哪个场景最想用 Python? 评论区聊聊,老粉优先回复!👇
前置章节:第10章 10.4 性能分析与优化 | 相关资源:Python 官方教程

评论(0)