🐍 第5章 建立命令行工具:下载 YouTube 影片
📄 一句话概括

pip 不是 Python 语法,它是个独立程序,只是刚好用 Python 写的。 所以要用 python -m pip 叫它,保证用的是同一个解释器。 pip list 看到的是包名,import 用的是模块名,两个名字经常不一样。 opencv-python-headless 装完 import 的是 cv2,Pillow 装完 import 的是 PIL。 MP4 是盒子,H.264 是盒子里装的东西。同一个盒子可以装不同编码,所以「下个 mp4」这句话并不精确。

📝 本章和前面几章的关系

前四章只用标准库,一个第三方包装都没碰。这一章开始装东西。 教材第 5 章开头两节是【使用 pip 安装 Python 套件】和【YouTube 影音的 Codec 与下载模块】。 两节看着不搭界,其实是一件事的两步:先学会装工具, 再搞清楚这个工具要处理的「视频格式」到底是什么。 本篇的演示全部真跑,包括 pip list 和那条失败的 import。 只有 yt-dlp 的格式串标了「平台原样 · 本沙箱不执行」——那台机器上没装它。

一、pip 不是语法,是一个程序

import 是 Python 语法,pip 不是。pip 是一个独立的程序,只是碰巧用 Python 写的。 所以你可以在命令行敲 pip install 包名,也可以敲 python -m pip install 包名。 两条命令做的事情一样,但后者更安全。 为什么?因为一台机器上可能装了好几个 Python(系统自带一个、你自己装的又一个、虚拟环境里还有)。 pip 这个词可能指向其中一个,python -m pip 明确说「用我指定的这个解释器去跑它自己的 pip」。

装了多个版本时,这是「明明装了却 import 不到」的头号原因。第 10 章讲虚拟环境会再碰到这件事。

二、包名和模块名,经常不是一回事

pip list 打出来的是包名,import 写的是模块名。 大部分时候它们一样,但不一样的情况非常多。

pip 装的包名 import 写的模块名
opencv-python-headless cv2
Pillow PIL
beautifulsoup4 bs4
scikit-learn sklearn
PyYAML yaml

演示第二段把本机装了的四个包列出来了:flask 3.1.3、requests 2.34.2、numpy 2.5.2、 opencv-python-headless 4.10.0.84。 最后一个就是典型:装的时候叫 opencv-python-headless,用的时候写 import cv2。 名字对不上的时候,去看包的文档第一页,那儿一定写了「怎么 import」。 pip show 包名 也能帮上忙,它会打出包的位置和它提供的模块。 实在找不到,import 一下看报什么错,报错信息里会写清楚找不到的是哪个名字。

三、没装会怎样:ModuleNotFoundError

演示第三段 import yt_dlp 直接抛 ModuleNotFoundError: No module named 'yt_dlp'。 这个名字值得记:它是 ImportError 的子类,专门表示「这个名字我压根找不到」。 看到这个错,九成是没装,剩下一成是装到了另一个解释器里。

「装到了另一个解释器」是什么情况?你在 A 环境 pip install 了,但跑代码用的是 B 环境。 症状跟没装一模一样。 排查办法:python -c "import sys; print(sys.executable)" 看当前用的是哪个解释器, python -m pip list 看这个解释器里有什么。两个对上了,问题就找到了。

四、解释器去哪儿找模块

sys.path 是一个列表,import 的时候从上往下找,找到就停。演示第四段把前五条打出来了。 第一条通常是当前目录(或者脚本所在目录,取决于你怎么启动的——第 1.2 篇讲过)。 最后几条是标准库和 site-packages。 「从上往下找,找到就停」这条规则解释了两种怪事。

第一种:你自己写了个 random.py,放在脚本旁边。 结果 import random 拿到的是你的文件,不是标准库那个——因为当前目录排在前面。 这是新手非常容易踩的坑,而且报错很莫名其妙。 第二种:装了新版本却 import 到旧的。多半是旧版本还在前面某个目录里躺着。 pip show 看位置,sys.path 看顺序,一比就明白。 所以别用标准库的名字给你的文件起名。random.py、json.py、email.py 都是雷区。

五、容器和编码是两回事

要下载视频,得先搞清楚「格式」这个词到底指什么。它其实是两层。

容器(后缀) 常见视频编码 常见音频编码 典型用途
MP4(.mp4) H.264/H.265 AAC 兼容性最好
WebM(.webm) VP9/AV1 Opus 网页用,体积小
MKV(.mkv) 几乎都行 几乎都行 收藏,能装多字幕
MP3(.mp3) 没有视频 MP3 纯音频

MP4 是盒子,H.264 是盒子里装的东西。 同一个盒子可以装不同的编码。 所以「下个 mp4」这句话并不精确。 你想要的多半是「H.264 的 mp4」——因为老设备、老电视、某些播放器只认这一个组合。 这就是下载工具为什么非要让你选格式:它得知道你要哪个盒子、里面装哪种编码、什么分辨率。 三件事,一个都不能少。

六、格式选择串长什么样

yt-dlp 用一串自己的小语言来描述「我要什么」。它不是 Python 语法,是那个工具自己定的规则。 bv*[height<=1080]+ba/b[height<=1080] 这么一串,拆开看是四段:bv* 要最好的视频流、 [height<=1080] 高度不超过 1080、+ba 再配一条最好的音频流、/ 后面是「前面不成,就试这个」。 最后那半段 b[height<=1080] 是兜底:单个文件、高度不超过 1080。两个都拿不到就算了。

为什么要视频音频分开下? 因为 YouTube 的高清视频流里通常没有声音,声音是单独一条。 分开下完,还得用 FFmpeg 把两条合成一个文件。 这就是这一章后半段要装 FFmpeg 的原因。不是可选步骤,是必需步骤。

七、动手 Demo:pip 装的是包,import 进的是模块

先看 pip 自己是谁、装了什么、装在哪。 再故意 import 一个没装的东西,看报错长什么样。 然后把 sys.path 打出来,理解「从上往下找」这条规则。

import subprocess, sys

P = sys.executable
def sh(args):
    r = subprocess.run([P, '-m'] + args, capture_output=True, text=True)
    return (r.stdout + r.stderr).strip()

print('① pip 自己是个程序')
print('  python -m pip --version')
print('    ' + sh(['pip', '--version']))

print('② 已装了什么、装在哪')
out = sh(['pip', 'list'])
want = ('flask', 'requests', 'numpy', 'opencv-python-headless')
for ln in out.splitlines():
    low = ln.lower()
    if any(low.startswith(w) for w in want):
        print('    ' + ln)
print('  flask 装在哪:' + sh(['pip', 'show', 'flask']).split('Location: ')[1].splitlines()[0])

print('③ 装了才能 import,没装会怎样')
try:
    import yt_dlp
except ModuleNotFoundError as e:
    print('  import yt_dlp  → ModuleNotFoundError:', e)
import builtins
print('  ModuleNotFoundError 是 ImportError 的子类:',
      issubclass(ModuleNotFoundError, ImportError))

print('④ 解释器去哪儿找模块')
for p in sys.path[:5]:
    print('    ' + p)
print('  sys.path 是一个列表,import 时从上往下找,找到就停。')

print('⑤ 视频的 Codec:容器和编码是两回事')
rows = [('MP4(.mp4)', 'H.264 / H.265', 'AAC', '兼容性最好,手机网页都认'),
        ('WebM(.webm)', 'VP9 / AV1', 'Opus', '网页用,体积小'),
        ('MKV(.mkv)', '几乎都行', '几乎都行', '收藏用,能装多条字幕'),
        ('MP3(.mp3)', '没有视频', 'MP3', '纯音频')]
print('| 容器(文件后缀) | 常见视频编码 | 常见音频编码 | 典型用途 |')
print('|---|---|---|---|')
for r in rows:
    print('| %s | %s | %s | %s |' % r)

print('⑥ 格式选择串长什么样')
print('  [平台原样 · 本沙箱不执行]')
print('  | # yt-dlp 的格式选择串(平台原样,本沙箱不执行)')
print('  | yt-dlp -f "bv*[height<=1080]+ba/b[height<=1080]" URL')
print('  |')
print('  | # 拆开看:')
print('  | #   bv*   最好的视频流(* 表示可以是纯视频)')
print('  | #   [height<=1080]  高度不超过 1080')
print('  | #   +ba   再配一条最好的音频流')
print('  | #   /     前面不成,就试后面这个')
print('  | #   b[height<=1080]  退而求其次:单个文件,高度不超过 1080')

实跑输出:

--------------------------------------------------------------------
pip 装的是包,import 进的是模块
--------------------------------------------------------------------

  ① pip 自己是个程序
  --------------------------------------------------------------------
    python -m pip --version
      pip 26.1.2 from C:\Users\admin\.workbuddy\binaries\python\envs\default\Lib\site-packages\pip (python 3.13)
  pip 不是 Python 语法的一部分,它是个独立程序,只是刚好用 Python 写的。
  所以要用 python -m pip 这种方式叫它,保证用的是同一个解释器。

  ② 已装了什么、装在哪
  --------------------------------------------------------------------
    flask                    3.1.3
    requests                 2.34.2
    numpy                    2.5.2
    opencv-python-headless   4.10.0.84
    flask 装在哪:es\python\envs\default\Lib\site-packages\flask
  pip list 看到的是一堆包名,import 用的是模块名。
  两个名字不一样是常事:opencv-python-headless 装完 import 的是 cv2,Pillow 装完 import 的是 PIL。
  查不到就去看包的文档。

  ③ 装了才能 import,没装会怎样
  --------------------------------------------------------------------
    import yt_dlp  → ModuleNotFoundError: No module named 'yt_dlp'
  ModuleNotFoundError 是 ImportError 的子类。
  看到这个错,九成是没装,剩下一成是装到了另一个解释器里。

  ④ 解释器去哪儿找模块
  --------------------------------------------------------------------
    I:\Desktop\自我教学\.workbuddy\scripts\ch58_dev
    D:\WorkBuddy\resources\app.asar.unpacked\cli\vendor\shim
    C:\Users\admin\.workbuddy\binaries\python\versions\3.13.12\python313.zip
    C:\Users\admin\.workbuddy\binaries\python\versions\3.13.12\DLLs
    C:\Users\admin\.workbuddy\binaries\python\versions\3.13.12\Lib
  sys.path 是一个列表,import 时从上往下找,找到就停。
  所以同名的模块,排在前面的目录会赢——这也是「装了却 import 到旧版本」的常见原因。

  ⑤ 视频的 Codec:容器和编码是两回事
  --------------------------------------------------------------------

| 容器(文件后缀) | 常见视频编码 | 常见音频编码 | 典型用途 |
|---|---|---|---|
| MP4(.mp4) | H.264 / H.265 | AAC | 兼容性最好,手机网页都认 |
| WebM(.webm) | VP9 / AV1 | Opus | 网页用,体积小 |
| MKV(.mkv) | 几乎都行 | 几乎都行 | 收藏用,能装多条字幕 |
| MP3(.mp3) | 没有视频 | MP3 | 纯音频 |

  MP4 是盒子,H.264 是盒子里装的东西。
  同一个盒子可以装不同编码,所以「下个 mp4」这句话并不精确——你想要的是「H.264 的 mp4」,
  因为老设备只认这个。
  这就是下载工具为什么要让你选格式:它得知道你要哪个盒子、里面装哪种编码、什么分辨率。

  ⑥ 格式选择串长什么样
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | # yt-dlp 的格式选择串(平台原样,本沙箱不执行)
  | yt-dlp -f "bv*[height<=1080]+ba/b[height<=1080]" URL
  |
  | # 拆开看:
  | #   bv*   最好的视频流(* 表示可以是纯视频)
  | #   [height<=1080]  高度不超过 1080
  | #   +ba   再配一条最好的音频流
  | #   /     前面不成,就试后面这个
  | #   b[height<=1080]  退而求其次:单个文件,高度不超过 1080
  这一串是 yt-dlp 自己的小语言,不是 Python 语法

  为什么要视频音频分开下:YouTube 的高清视频流里通常没有声音,声音单独一条。
  下载器下完还得用 FFmpeg 把两条合起来——这就是第 5-8 节要装 FFmpeg 的原因。

第一段那行版本信息里有三样东西:pip 自己的版本、它装在哪、跟着哪个 Python。 第三样最重要——它保证你装的东西进的是同一个解释器。 第二段注意最后一行:包名叫 opencv-python-headless,但代码里得写 import cv2。 这种不一致非常常见,不是你的错,去看文档第一页就行。 第三段那句 issubclass 为 True,说明 ModuleNotFoundError 是 ImportError 的一种。 所以写 except ImportError 也能抓到它。 第四段前五条路径要看顺序:当前目录排在最前面。 这就是为什么你自己写个 random.py 会顶掉标准库那个——同一个名字,前面的赢。 第六段那串是 yt-dlp 自己的小语言,不是 Python。 斜杠的意思是「前面不成试后面」,加号的意思是「两个都要,下完合起来」。

30 秒实验

  1. 敲 python -m pip install requests,看输出里「already satisfied」这行说明什么。
  2. 在演示第四段后面加一行 print(len(sys.path)),看你这台机器上一共几条。
  3. 在脚本旁边建一个空文件叫 random.py,然后 import random 再 print(random.__file__), 看它指向哪个文件——看完记得删掉。

常见误区

  1. 「pip 装的包名就是 import 的名字。」经常不是。 opencv-python-headless 装完 import cv2,Pillow 装完 import PIL。 名字对不上看文档。
  2. 「ModuleNotFoundError 一定是没装。」也可能是装到了另一个解释器里。 用 sys.executable 看当前解释器,用 python -m pip list 看它里面有什么。
  3. 「mp4 就是一种格式。」mp4 是容器,里面装什么编码是另一回事。 同一个 mp4 里可能是 H.264,也可能是 H.265。 老设备只认前者。
自测三问(点开对答案) 1. 为什么推荐写 `python -m pip install 包名`,而不是直接 `pip install 包名`? —— 因为一台机器上可能有多个 Python,`pip` 这个词可能指向其中一个。 `python -m pip` 明确说「用这个解释器跑它自己的 pip」,装的东西一定进这个环境。 多个版本共存时,这是「装了却 import 不到」的主因。 2. `pip list` 里明明有 opencv-python-headless,`import opencv` 却报错,为什么? —— 因为包名和模块名不是一回事。这个包装完之后模块名叫 `cv2`,得写 `import cv2`。 `pip show 包名` 能看到它提供的模块,文档第一页也会写。 3. MP4 和 H.264 是什么关系? —— MP4 是容器(盒子),H.264 是编码(盒子里装的东西)。 同一个 MP4 文件里可以是 H.264,也可以是 H.265。 所以「下个 mp4」这种说法不精确,得说清「哪个盒子装哪种编码」。

下一篇给这个下载器做规划:默认存哪、选项怎么去重、用户没给的参数怎么补。

相关笔记