key 返回什么就按什么排。要多级排序,让 key 返回一个元组:先比第一项,相等再比第二项。
try 有四个块:try、except、else、finally。
else 是没出错才走,finally 是不管出不出错都走。
调外部程序要用列表,不要拼字符串。而且「命令不存在」和「命令跑失败」是两回事,得分开处理。
这是第 5 章最后一篇,也是全书第一个「完整的命令行工具」收尾。
教材第 5 章最后几节是【数据排序】【使用 try except 捕捉例外状况】
还有【使用 FFmpeg 转换多媒体文件格式】和【自定义程序模块】。
四件事凑在一起,是因为一个真能干活的下载器四样都得有:按画质排、出错不崩、调 FFmpeg 合流、
把公用函数抽成模块。
本篇有个容易忽略的细节,在演示第四段:
ffmpeg 没装的时候 Python 抛的是 FileNotFoundError,不是返回非零退出码。
这两种情况要分开处理。
一、一列按什么排:key 与多级排序
第 3.3 篇用过 sorted(items, key=lambda t: t[1])。这里把它讲透。
key 是一个函数,sorted 对每个元素调一次它,按返回值排序。你返回什么,它就按什么排。
演示第一段有四个文件,按分辨率降序排。
注意两个都是 1080 的(教程 A 和教程 C),
单按分辨率排时谁在前取决于原始顺序——这叫排序是稳定的。
稳定有什么用?意味着「先按名字排一遍,再按分辨率排一遍」,同分辨率的那些还能保持名字的顺序。
要多级排序,让 key 返回一个元组:key=lambda t: (-t[1], t[2])。
元组比较是先比第一项,相等再比第二项,以此类推。
降序怎么办?数字前面加负号最简单(-t[1])。负数比正数小,排出来就是大的在前。
字符串不能加负号,得用 reverse=True。
演示里加了第二级条件之后,教程 C 排到了教程 A 前面——因为它的文件更小。 这就是多级排序的意义:第一个条件打平时,用第二个决定。
二、try 的四个块各干什么
| 块 | 什么时候执行 |
|---|---|
try |
总是先执行这里面 |
except |
try 里抛出对应异常时 |
else |
try 里没出错时 |
finally |
不管出不出错,最后都执行 |
演示第二段:输入 '120',走 try 成功、else 执行、finally 执行;
输入 '12O'(字母 O),except 抓到 ValueError、finally 照样执行。
else 很容易被忘掉。它的用处是:把「可能出错的代码」和「出错之后才该走的代码」分开。
比如 try: 打开文件 / except: 报错 / else: 读文件内容。
读文件不该放在 try 里,否则读取时的错误也会被当成「打开失败」。
finally 是收尾用的。关文件、关数据库连接、删临时文件,放这里才不会漏。
哪怕 try 里 return 了,finally 也会执行。
Python 还有个 with 语句能自动关文件,比手写 finally 更省事。第 7 章会用到。
但 finally 适用的场景比 with 广得多,得会。
三、四种常见异常长什么样
| 代码 | 异常 | 意思 |
|---|---|---|
1 / 0 |
ZeroDivisionError |
除以零 |
int('abc') |
ValueError |
值不对,转不了 |
{'a':1}['b'] |
KeyError |
字典没这个键 |
open('没有这个文件') |
FileNotFoundError |
文件不存在 |
[1][9] |
IndexError |
下标越界 |
'a' + 1 |
TypeError |
类型不匹配 |
这六个名字背下来,看报错就不用猜了。它们都是 Exception 的子类。
所以 except Exception: 能一网打尽——但也别什么都抓。
抓太宽会把真正的 bug 一起吞掉。
比如你打错一个变量名,NameError 被吞了,程序继续往下跑,最后报一个完全不相干的错,
排查时间翻十倍。
原则是: 能预料到的错误精确抓,预料不到的就让它炸。 炸出来的报错信息比你自己写的「出错了」有用得多。
四、调用外部程序:列表不是字符串
subprocess.run(['ffmpeg', '-i', src, dst])——注意参数是列表,不是拼好的字符串。
为什么不能拼字符串?两个原因。
一是路径里有空格就散架了:'ffmpeg -i C:\我的 视频.mp4' 会被当成两个参数。
二是安全。拼字符串时,用户输入里如果有特殊字符,可能被当成命令执行。
列表形式让系统自己去处理引号和转义,两个问题一起解决。
演示第四段有个关键细节:调 python 成功了,
调 ffmpeg 抛了 FileNotFoundError——这台机器没装 ffmpeg。
报错形式不一样。 命令压根不存在时,Python 抛 FileNotFoundError;
命令存在但跑失败了,才体现在返回码上。
这两种情况必须分开处理。
只判返回码的话,没装 ffmpeg 的机器会直接崩在调用那一行,而且报的是一个跟 ffmpeg 无关的错。
标准写法是外面套一层 try:
try: subprocess.run(...) except FileNotFoundError: print('没装 ffmpeg,请先安装')。
这样用户看到的是人话,不是堆栈。
五、转格式的那条命令
subprocess.run(['ffmpeg', '-i', src, '-vn', '-acodec', 'libmp3lame', '-q:a', '2', dst])。
逐段读:-i 输入文件,-vn 不要视频(video no),-acodec 音频编码器选 libmp3lame,
-q:a 2 音质(数字越小越好),最后是输出文件。
-vn 这个参数是关键:
不写它,ffmpeg 会想办法把视频也塞进 mp3,然后失败。因为 mp3 这个容器根本装不了视频。
还有一个参数必须加:check=True。它的意思是「出错就抛异常」。
不加会怎样?
ffmpeg 失败了只体现在返回码上,脚本以为成功了,继续往下走,最后交给你一个 0 字节的文件。
这一类 bug 最难查:程序不报错,文件也生成了,就是打不开。
所以调外部程序,check=True 应该是默认动作,除非你有理由不写。
六、把自己的函数拆成模块
程序写到几百行,就该拆了。
同目录下的 .py 文件,import 名字 就能用,不用装、不用改 sys.path。
演示第六段:main.py 里 import helper,拿到的 helper 就是同目录那个文件。
文件里定义的函数直接 helper.函数名() 调。
拆的原则很简单:「到处都要用的小函数」抽出去,「只在这一处用的」留在原地。
抽完之后主程序只剩流程:读参数 → 下视频 → 转格式 → 存盘。
读代码的人一眼能看懂它在干什么,不用陷进细节里。
这也解释了第 4.3 篇为什么要写 if __name__ == '__main__':拆出去的 helper.py 既想能单独跑测试,
又想被 main.py import 时不乱跑。
再往后一步就是打包成包(带 __init__.py 的文件夹)。
那是另一个话题,本书第 9 章建 Flask 项目时会用到目录结构,但不会深讲打包。
七、动手 Demo:排序、出错、调用外部程序
先看单级排序和多级排序的差别,注意那两个都是 1080 的文件顺序变了。 再把 try 的四个块跑一遍,看哪个块什么时候执行。 然后调两个外部程序:一个存在、一个不存在,看报错形式差在哪。 最后验证同目录的 .py 能不能直接 import。
import os, subprocess, sys, tempfile
print('① 一列按什么排')
files = [('教程A.mp4', 1080, 900), ('教程B.mp4', 720, 400),
('教程C.mp4', 1080, 100), ('教程D.mp4', 480, 200)]
print(' 原顺序', [f[0] for f in files])
print(' 按分辨率降序 ', [f[0] for f in sorted(files, key=lambda t: -t[1])])
print(' 分辨率降序 + 大小升序 ', [f[0] for f in sorted(files, key=lambda t: (-t[1], t[2]))])
print('② try 的四个块各干什么')
def parse(s):
try:
n = int(s)
except ValueError:
print(' except 抓到 ValueError;finally 一定执行')
return 0
else:
print(' try 成功;else 执行(没出错才走);finally 一定执行')
return n
finally:
pass
print(" 输入 '120' → 结果", parse('120'))
print(" 输入 '12O' → 结果", parse('12O'))
print('③ 四种常见异常长什么样')
for tag, fn in (('1 / 0', lambda: 1 / 0),
("int('abc')", lambda: int('abc')),
("{'a':1}['b']", lambda: {'a': 1}['b']),
('open(没有这个文件)', lambda: open('没有这个文件'))):
try:
fn()
except Exception as e:
print(' %-18s → %s: %s' % (tag, type(e).__name__, str(e)[:36]))
print('④ 调用外部程序')
P = sys.executable
r = subprocess.run([P, '-c', 'import sys; print("子进程跑起来了, 参数", sys.argv[1:])',
'a', 'b'], capture_output=True, text=True)
print(' 调 python -c:', r.stdout.strip())
r2 = subprocess.run([P, '-c', 'import sys; sys.exit(2)'])
print(' 子进程退出码 %d(假失败)' % r2.returncode)
try:
subprocess.run(['ffmpeg', '-version'], capture_output=True)
except FileNotFoundError:
print(' 调 ffmpeg → FileNotFoundError(这台机器没装)')
print('⑤ 转格式的那条命令长什么样')
print(' [平台原样 · 本沙箱不执行]')
print(' | # 本书第 5-9 节的样子(平台原样,本沙箱不执行)')
print(' | subprocess.run([\'ffmpeg\', \'-i\', src, \'-vn\', \'-acodec\', \'libmp3lame\',')
print(' | \'-q:a\', \'2\', dst], check=True)')
print(' |')
print(' | # -i 输入 -vn 不要视频 -acodec 音频编码 -q:a 2 音质(越小越好)')
print(' check=True 的意思是「出错就抛异常」,不然失败了也是静悄悄的')
print('⑥ 把自己的函数拆成模块')
d = tempfile.mkdtemp()
open(os.path.join(d, 'helper.py'), 'w').write(
'def human(n):\n'
' return "%.1f KB" % (n / 1024)\n'
'def ext(name):\n'
' return name.rsplit(".", 1)[-1]\n')
open(os.path.join(d, 'main.py'), 'w').write(
'import helper\n'
'print(" ", helper.human(1536))\n'
'print(" ", helper.ext("a.b.c.mp4"))\n')
r = subprocess.run([P, 'main.py'], capture_output=True, text=True, cwd=d)
print(' main.py 里 import helper:')
for ln in r.stdout.strip().splitlines():
print(' ' + ln)
print(' 同目录下的 .py 文件,import 名字就能用,不用装、不用改 sys.path。')
实跑输出:
--------------------------------------------------------------------
排序、出错、调用外部程序
--------------------------------------------------------------------
① 一列按什么排
--------------------------------------------------------------------
原顺序 ['教程A.mp4', '教程B.mp4', '教程C.mp4', '教程D.mp4']
按分辨率降序 ['教程A.mp4', '教程C.mp4', '教程B.mp4', '教程D.mp4']
分辨率降序 + 大小升序 ['教程C.mp4', '教程A.mp4', '教程B.mp4', '教程D.mp4']
两个都是 1080 的,单按分辨率排时谁在前取决于原始顺序(排序是稳定的);
加上第二级条件,文件小的就排到前面去了。
key 返回什么就按什么排。要多级排序,让 key 返回一个元组:元组先比第一项,相等再比第二项。
想降序就在数字前加负号。
② try 的四个块各干什么
--------------------------------------------------------------------
输入 '120' → 结果 120 try 成功;else 执行(没出错才走);finally 一定执行
输入 '12O' → 结果 0 except 抓到 ValueError;finally 一定执行
else 是「try 里没出错」才走,finally 是「不管出不出错」都走。
关文件、关数据库连接这类收尾动作放 finally,才不会漏。
③ 四种常见异常长什么样
--------------------------------------------------------------------
1 / 0 → ZeroDivisionError: division by zero
int('abc') → ValueError: invalid literal for int() with
{'a':1}['b'] → KeyError: 'b'
open('没有这个文件') → FileNotFoundError: [Errno 2] No such file or dire
记住这几个名字就够用:除以零是 ZeroDivisionError,转换失败是 ValueError,
字典没这个键是 KeyError,文件没找到是 FileNotFoundError。
它们都是 Exception 的子类,所以 except Exception 能一网打尽——但也别什么都抓,
抓太宽会把真正的 bug 一起吞掉。
④ 调用外部程序
--------------------------------------------------------------------
调 python -c:子进程跑起来了, 参数 ['a', 'b']
子进程退出码 2(假失败)
调 ffmpeg → FileNotFoundError(这台机器没装)
subprocess.run 把参数列成列表,不拼字符串。拼字符串遇到带空格的路径就散架,而且有注入风险。
列表形式系统自己处理引号。
注意这次的报错形式:命令压根不存在时,Python 抛 FileNotFoundError;
命令存在但跑失败了,才体现在返回码上。
这两种情况要分开处理——只判返回码的话,没装 ffmpeg 的机器会直接崩在调用那一行。
⑤ 转格式的那条命令长什么样
--------------------------------------------------------------------
[平台原样 · 本沙箱不执行]
| # 本书第 5-9 节的样子(平台原样,本沙箱不执行)
| subprocess.run(['ffmpeg', '-i', src, '-vn', '-acodec', 'libmp3lame',
| '-q:a', '2', dst], check=True)
|
| # -i 输入 -vn 不要视频 -acodec 音频编码 -q:a 2 音质(越小越好)
check=True 的意思是「出错就抛异常」,不然失败了也是静悄悄的
check=True 很关键。
不加的话 ffmpeg 失败只体现在返回码上,脚本会以为成功了,继续往下走,最后交出一个 0 字节的文件。
⑥ 把自己的函数拆成模块
--------------------------------------------------------------------
main.py 里 import helper:
1.5 KB
a_b_c_.mp4
同目录下的 .py 文件,import 名字就能用,不用装、不用改 sys.path。
把「到处都要用的几个小函数」抽成模块,主程序就只剩流程,读起来清楚得多。
第一段两张排序结果不一样,原因在教程 A 和教程 C 都是 1080。 单按分辨率排时它们谁在前取决于原顺序(排序是稳定的);加上第二级条件,文件小的那个就排到前面了。 第二段那两句打印是关键:输入合法时走 else,不合法的走 except,而 finally 两条路都走。 else 容易被忘掉,它解决的是「把可能出错的代码和后续代码分开」。 第三段六个异常名字里,TypeError 和 IndexError 没在演示里跑,但都在正文那张表里列着。 记住类型名比背错误原文有用。 第四段是本篇最该记住的一处:调 ffmpeg 时抛的是 FileNotFoundError,不是返回码。 命令不存在和命令跑失败,是两种完全不同的情况,得分开处理。 第六段说明拆模块有多简单:同目录的 .py,import 名字就能用。 这也是第 4.3 篇那个 if name 判断真正派上用场的地方。
30 秒实验
- 把演示第一段的
-t[1]改成t[1],看排序结果反过来没有。 - 把演示第二段的
except ValueError改成except TypeError,再输'12O', 看程序会发生什么(提示:异常没被接住,会往上抛)。 - 把演示第四段调 ffmpeg 的那一行改成传字符串而不是列表,看它能不能跑,再想想为什么不该这么写。
常见误区
- 「
try只用来接错误,else、finally是多余的。 」else把「可能出错的」和「出错后才该走的」分开,finally保证收尾一定执行——哪怕 try 里 return 了。 - 「
except Exception最保险,全都抓。」它会把真正的 bug 一起吞掉。 你打错一个变量名,NameError被吞,程序继续跑,最后报一个不相干的错。 - 「调外部程序看不出区别,反正失败就看返回码。
」命令不存在时抛的是
FileNotFoundError,根本不返回。 只判返回码的代码会在没装工具时崩在调用那一行。
自测三问(点开对答案)
1. 要让 `sorted` 先按分辨率降序、分辨率相同再按文件大小升序,key 该怎么写? —— `key=lambda t: (-t[1], t[2])`。返回元组,比较时先比第一项;第一项相等再比第二项。 降序靠在第一项前加负号,因为负数比正数小,排出来就是大的在前。 2. `try` / `except` / `else` / `finally` 四个块,哪个在「没出错」的时候执行? —— `try` 和 `else` 会在没出错时执行,`finally` 不管出不出错都执行。`except` 只在出错时执行。 `else` 的价值是把「可能出错的代码」和「出错之后才该走的代码」分开。 3. 为什么调外部程序要用列表 `['ffmpeg', '-i', src]` 而不是拼一个字符串? —— 两个原因。一是路径里有空格时字符串会被拆成多个参数,直接散架; 二是安全,用户输入里的特殊字符可能被当成命令执行。 列表形式让系统自己处理引号和转义,两个问题一起解决。第 5 章结束。下一篇进第 6 章:网页到底是什么,怎么用程序去读它。
- ☐ (本篇完,下一篇 6.1 HTML 与 CSS:网页是什么)