🐍 第10章 部署网站到云端空间
📄 一句话概括

项目 A 要 flask 2.x,项目 B 要 flask 3.x。 全局只有一个 site-packages,装哪个都会把另一个顶掉。 虚拟环境就是给每个项目单独一份 site-packages,再配一个指向同一个解释器的快捷方式。 内网穿透的原理是「反过来连」:你的机器主动往外连,路由器不但不拦,还替你记住这条连接。

📝 本章和前面几章的关系

前九章都在自己电脑上折腾。这一章要把它搬到别人能访问的地方。 教材第 10 章开头两节是【建立虚拟环境】和【使用 Serveo 与 Ngrok 向外界发布本机网站】。 两件事放一起,是因为它们都服务于同一个目的: 让别人能跑起来、能访问到。虚拟环境保证别人装得出一样的环境, 内网穿透让别人的请求能到你机器上。 虚拟环境是真建的:python -m venv 跑了 7.3 秒,目录结构和 pyvenv.cfg 都是真的。 内网穿透那两行命令标了「平台原样」,因为要对外注册子域名。

一、为什么要隔离

你手上两个项目:A 用的是老版 Flask 2.x,B 用的是新版 Flask 3.x。两个都要装,装到哪儿? 全局只有一个 site-packages 目录。装 2.x 就把 3.x 顶掉,装 3.x 就把 2.x 顶掉。 同一时刻只能存在一个版本。 虚拟环境解决的就是这件事: 给每个项目单独一份 site-packages,再配一个指向同一个解释器的快捷方式。

所以它不复制 Python 本身(那是几百兆),只建一个几十兆的骨架。 演示里建一次 7.3 秒,里面主要是复制 pip 和几个基础包。 关键的一句:它不复制解释器。 下一节看 pyvenv.cfg 就明白了。

二、真建一个,看里面有什么

python -m venv venv 一句就建好。 注意前面那个 python -m——第 5.1 篇说过,这样才能保证用的是你指定的那个解释器。 建出来的目录里有五样东西:Scripts(Windows; Linux 上是 bin)、Lib(装包的地方)、Include、pyvenv.cfg、.gitignore。 pyvenv.cfg 是这个环境的身证。 里面写着 home 指向哪个 Python、include-system-site-packages = false(不共享系统的包)、 以及版本号。

home 是关键:它指向「真正在干活的那个 Python」。 venv 自己不复制解释器,只是记了个位置。 这就是为什么虚拟环境建起来那么快。 它建的是一个「壳」,里面那个 python.exe 只有几十 KB,实际干活的还是 home 指向的那个。 激活它:Windows 上敲 venv\Scripts\activate,Linux 和 macOS 上敲 source venv/bin/activate。 激活之后命令提示符前面会多个 (venv)。

三、进了虚拟环境,sys.prefix 会变

sys.prefix 是「当前这套标准库和 site-packages 在哪儿」。 sys.base_prefix 是「底层那个 Python 在哪儿」。 两个不相等,就说明现在在虚拟环境里。 这是判断「我在不在 venv 里」最直接的办法。 演示第三段打了两组:系统解释器的 prefix 是一个路径,虚拟环境里的 prefix 是另一个, 但两者的 base_prefix 完全一样——因为它们共用同一个底层 Python。

很多工具就是靠这个判断「我该往哪个 site-packages 装东西」。pip 也是。 「我明明装了 requests 却 import 失败」,最常见的两个原因:装到了系统的 Python 里、 或者当前 shell 激活的是另一个 venv。 排查只要一行:python -c "import sys; print(sys.prefix)"。 看清楚现在用的是哪个环境,比乱试快十倍。

四、内网穿透在解决什么问题

你家里的电脑在路由器后面,没有公网 IP。外面的人知道你的网址,也找不到你家这台机器。

路子 要不要公网 IP 难度
申请固定公网 IP 要 找运营商,不一定给
路由器上做端口映射 要 要能登录路由器
内网穿透(Serveo/ngrok) 不要 一行命令

前两条都要公网 IP。 家用的宽带绝大多数是动态分配的内网 IP,就算有也是别人共用的,所以端口映射根本没意义。 内网穿透的原理是「反过来连」:你的机器主动连到一台有公网地址的服务器,把自己的连接挂在那儿; 外面的人访问那台服务器,服务器顺着你早就连好的通道把请求转给你。 因为是你主动往外连,路由器不但不拦,还会替你记住这条连接。 这正是 NAT 的工作方式—— 它只拦「从外面主动进来的」,不拦「从里面出去的」。

这个思路叫作反向隧道。理解了它,你就明白为什么「不用改路由器、不用公网 IP」也能做到。

五、那条通道长什么样

本质上就是两条已经连通的管道,两端各读写一头。 演示里用 socketpair 在本机建了一对:一端发「请求:GET / HTTP/1.1」,另一端收到并回话。 socketpair 这一对在内存里直接连通,没走网卡。 所以它只是演示「两端各有一个 socket、 数据从一头灌进另一头出来」这个结构,不是真的穿透。 真正的隧道是同一个结构,只是两端隔着一个公网服务器:你那头连上去, 外面那台服务器负责把公网请求塞进这条已经连好的管道。

命令是 ssh -R 80:localhost:5000 serveo.net 或者 ngrok http 5000。 跑完会给你一个公网网址,外面的人访问它就等于访问你本机的 5000 端口。 -R 这个参数的意思是「远程转发」:在远程那台机器上开一个端口,把收到的流量转回本地。 方向跟平时 ssh 正好相反。 这两行在演示里标了「平台原样 · 本沙箱不执行」——要对外注册子域名,沙箱里做不了。

六、把服务暴露出去之后

注意这是把本机的服务暴露到公网上。 任何人拿到那个网址都能访问,不需要密码, 不需要你在旁边同意。 演示里的 Flask 应用没有登录、没有鉴权、数据库里就几行测试数据。 这种东西挂在公网上一天,第二天可能就被人扫到并留了后门。 所以规矩是:测试完立刻关掉。 关掉命令行窗口,那条隧道就断了,网址也就失效了。

要长期对外提供服务,正确做法是第 10.2 篇讲的部署——把程序放到一台专门对外服务的机器上, 而不是把家里的电脑挖个洞。 两者的区别不只是「稳不稳」: 家里的电脑关机、断网、换 IP,服务就没了;而且你家整个局域网都跟这台机器连着。

七、动手 Demo:虚拟环境与内网穿透

真建一个虚拟环境,看它建出什么东西、pyvenv.cfg 里写了什么。 再对比系统解释器和虚拟环境里的 sys.prefix。 最后用 socketpair 演示隧道那个结构——两端各一个 socket,数据从一头灌进另一头出来。

import os, socket, subprocess, sys, tempfile, threading, time

P = sys.executable

print('① 为什么要隔离')
print('  项目A 要 flask 2.x,项目B 要 flask 3.x')
print('  全局只有一个 site-packages 目录,装哪个都会把另一个顶掉')
print('  虚拟环境就是给每个项目单独一份 site-packages,再配一个指向同一个解释器的快捷方式。')

print('② 真建一个,看里面有什么')
d = tempfile.mkdtemp()
venv = os.path.join(d, 'venv')
t0 = time.time()
r = subprocess.run([P, '-m', 'venv', venv], capture_output=True, text=True)
cost = time.time() - t0
print('  python -m venv venv   退出码 %d,耗时 %.1f 秒' % (r.returncode, cost))
for name in sorted(os.listdir(venv)):
    p = os.path.join(venv, name)
    print('    %-14s %s' % (name, '目录' if os.path.isdir(p) else '文件'))
cfg = open(os.path.join(venv, 'pyvenv.cfg'), encoding='utf-8').read()
print('  pyvenv.cfg 里写着:')
for ln in cfg.strip().splitlines():
    if ln.startswith('command'):
        ln = ln[:60] + '…'
    print('    ' + ln)
print('  最要紧的是 pyvenv.cfg 里那行 home,它指向「真正在干活的那个 Python」。')
print('  venv 自己不复制解释器,只是记了个位置。')

print('③ 进了虚拟环境,sys.prefix 会变')
PROBE = 'import sys; print(sys.prefix); print(sys.base_prefix); print(sys.prefix != sys.base_prefix)'
for tag, exe in (('【系统解释器】', P),
                 ('【虚拟环境里的解释器】', os.path.join(venv, 'Scripts', 'python.exe'))):
    out = subprocess.run([exe, '-c', PROBE], capture_output=True, text=True).stdout.splitlines()
    print('  ' + tag)
    print('    prefix      ' + out[0])
    print('    base_prefix ' + out[1])
    print('    相等吗      ' + out[2])
print('  两个 prefix 不相等,就说明现在在虚拟环境里。')
print('  「我明明装了 requests 却 import 失败」,最常见的两个原因:装到了系统的 Python 里、')
print('  或者当前 shell 激活的是另一个 venv。')

print('④ 内网穿透在解决什么问题')
print('  你家里的电脑在路由器后面,没有公网 IP')
print('  外面的人知道你的网址,也找不到你家这台机器')
print('| 路子 | 要不要公网IP | 难度 |')
print('|---|:---:|---|')
print('| 申请固定公网 IP | 要 | 找运营商,不一定给 |')
print('| 路由器上做端口映射 | 要 | 要能登录路由器 |')
print('| 内网穿透(Serveo/ngrok) | 不要 | 一行命令 |')
print('  原理是「反过来连」:你的机器主动连到一台有公网地址的服务器,把自己的连接挂在那儿。')

print('⑤ 那条通道长什么样')
a, b = socket.socketpair()
def sender():
    a.sendall('请求:GET / HTTP/1.1'.encode())
    a.close()
threading.Thread(target=sender).start()
print('  本机起一对连通的 socket,一端发:%s' % b.recv(1024).decode())
b.sendall('回话:HTTP/1.1 200 OK'.encode())
print('  另一端回:%s' % b.recv(1024).decode())
b.close()
print('  socketpair 这一对在本机内存里直接连通,没走网卡。')
print('  内网穿透的隧道也是这个意思:两端各有一个 socket,')
print('  外面那台服务器负责把公网请求塞进这条已经连好的管道。')

print('  [平台原样 · 本沙箱不执行]')
print('  | # 本书第 10-2 节(平台原样,本沙箱不执行:要对外注册子域名)')
print('  | ssh -R 80:localhost:5000 serveo.net')
print('  | ngrok http 5000')
print('  跑完会给你一个公网网址,外面的人访问它就等于访问你本机的 5000 端口')

实跑输出:

--------------------------------------------------------------------
虚拟环境与内网穿透
--------------------------------------------------------------------

  ① 为什么要隔离
  --------------------------------------------------------------------
    项目A 要 flask 2.x,项目B 要 flask 3.x
    全局只有一个 site-packages 目录,装哪个都会把另一个顶掉
  虚拟环境就是给每个项目单独一份 site-packages,再配一个指向同一个解释器的快捷方式。互不干扰。

  ② 真建一个,看里面有什么
  --------------------------------------------------------------------
    python -m venv venv   退出码 0,耗时 7.3 秒
      .gitignore   文件
      Include      目录
      Lib          目录
      Scripts      目录
      pyvenv.cfg   文件
    pyvenv.cfg 里写着:
      home = C:\Users\admin\.workbuddy\binaries\python\versions\3.13.12
      include-system-site-packages = false
      version = 3.13.14
      executable = C:\Users\admin\.workbuddy\binaries\python\envs\default\Scripts\python.exe
      command = C:\Users\admin\.workbuddy\binaries\python\envs\default\Scripts\python.exe -m venv I:\Desktop\自我教学\.workbuddy\scripts\ch58_dev\_tmp58\m02\venv
  最要紧的是 pyvenv.cfg 里那行 home,它指向「真正在干活的那个 Python」。
  venv 自己不复制解释器,只是记了个位置。

  ③ 进了虚拟环境,sys.prefix 会变
  --------------------------------------------------------------------
    【系统解释器】
      prefix      C:\Users\admin\.workbuddy\binaries\python\envs\default
      base_prefix C:\Users\admin\.workbuddy\binaries\python\versions\3.13.12
      相等吗      False
    【虚拟环境里的解释器】
      prefix      I:\Desktop\自我教学\.workbuddy\scripts\ch58_dev\_tmp58\m02\venv
      base_prefix C:\Users\admin\.workbuddy\binaries\python\versions\3.13.12
      相等吗      False
  两个 prefix 不相等,就说明现在在虚拟环境里。
  很多工具就是靠这个判断「我该往哪个 site-packages 装东西」。
  「我明明装了 requests 却 import 失败」,最常见的两个原因:装到了系统的 Python 里、
  或者当前 shell 激活的是另一个 venv。先打一行 sys.prefix 看清楚,比乱试快。

  ④ 内网穿透在解决什么问题
  --------------------------------------------------------------------
    你家里的电脑在路由器后面,没有公网 IP
    外面的人知道你的网址,也找不到你家这台机器

| 路子 | 要不要公网IP | 难度 |
|---|:---:|---|
| 申请固定公网 IP | 要 | 找运营商,不一定给 |
| 路由器上做端口映射 | 要 | 要能登录路由器 |
| 内网穿透(Serveo/ngrok) | 不要 | 一行命令 |

  内网穿透的原理是「反过来连」:你的机器主动连到一台有公网地址的服务器,把自己的连接挂在那儿;
  外面的人访问那台服务器,服务器顺着你早就连好的这条通道把请求转给你。
  因为是你主动往外连,路由器不但不拦,还会替你记住这条连接——这正是 NAT 的工作方式。

  ⑤ 那条通道长什么样
  --------------------------------------------------------------------
    本机起一对连通的 socket,一端发:请求:GET / HTTP/1.1
    另一端回:回话:HTTP/1.1 200 OK
  socketpair 这一对在本机内存里直接连通,没走网卡。
  内网穿透的隧道也是这个意思:两端各有一个 socket,
  外面那台服务器负责把公网请求塞进这条已经连好的管道。


  [平台原样 · 本沙箱不执行]
  | # 本书第 10-2 节(平台原样,本沙箱不执行:要对外注册子域名)
  | ssh -R 80:localhost:5000 serveo.net
  | ngrok http 5000
  跑完会给你一个公网网址,外面的人访问它就等于访问你本机的 5000 端口

  注意这是把本机的服务暴露到公网上。测试完记得关掉,别让一个没有密码的调试接口在公网上挂一整天。

第二段那五样东西要认:Scripts 里放可执行文件,Lib 里装包,pyvenv.cfg 是身证。 注意 venv 目录整个只有几十兆,因为它没复制 Python 本身。 pyvenv.cfg 里 home 那行是关键:它指向真正干活的解释器。 所以虚拟环境是「壳」,不是「另一份 Python」。 第三段两个 prefix 的对照是本篇最实用的排查手段。装不上、找不到包,先打这一行,比乱试快十倍。 第五段那个 socketpair 只是演示结构:两端各一个 socket、数据从一头灌进另一头出来。 它在本机内存里连通,没走网卡,所以不是真的穿透。 最后那段命令标了平台原样。真跑会给你一个公网网址,但也意味着你的本机服务暴露在公网上——测完记得关。

30 秒实验

  1. 在本机建一个 venv,激活它,然后 pip install requests, 再 python -c "import sys; print(sys.prefix)",看路径指到哪。
  2. 把演示第二段建的 venv 目录用 shutil.disk_usage 算一下大小,再跟底层 Python 的目录大小比一比, 看差多少倍。
  3. 把演示第五段的 socket.socketpair() 换成 socket.socket() 加手动 bind 和 connect, 看走真实网卡会不会慢一些。

常见误区

  1. 「虚拟环境会复制一份 Python。」不复制。它只建一个壳,pyvenv.cfg 里记着底层 Python 在哪。 所以建起来只要几秒,不是几百兆。
  2. 「python -m venv 和 venv 命令一样。」在多版本机器上不一样。 python -m venv 明确说「用这个解释器建」,不容易建错版本。
  3. 「内网穿透就是把防火墙关掉。」完全不是。 它是你的机器主动往外连,NAT 只拦从外面主动进来的,不拦从里面出去的。 防火墙一个字都不用改。
自测三问(点开对答案) 1. 为什么需要虚拟环境?全局装包不行吗? —— 因为全局只有一个 `site-packages`,同一时刻只能存在一个版本。 两个项目分别要 Flask 2.x 和 3.x 就冲突了。 虚拟环境给每个项目一份独立的 `site-packages`,互不干扰, 而且换台机器按 `requirements.txt` 能装出一模一样的环境。 2. 怎么快速判断当前用的是不是虚拟环境? —— 打一行 `python -c "import sys; print(sys.prefix, sys.base_prefix)"`。 两个值不相等就说明在虚拟环境里;相等就是在用系统那个。 3. 内网穿透为什么不需要公网 IP、也不用改路由器? —— 因为方向反过来:是你的机器主动连到一台有公网地址的服务器,把连接挂在那儿。 NAT 和防火墙只拦「从外面主动进来的连接」,你主动往外连它不但不拦,还会记住这条连接, 好让回包能回来。外面的请求就顺着这条已经连好的管道转给你。

下一篇讲真正的部署:代码怎么进版本库、怎么推到一台一直开着的机器上。

相关笔记