项目 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 秒实验
- 在本机建一个 venv,激活它,然后
pip install requests, 再python -c "import sys; print(sys.prefix)",看路径指到哪。 - 把演示第二段建的 venv 目录用
shutil.disk_usage算一下大小,再跟底层 Python 的目录大小比一比, 看差多少倍。 - 把演示第五段的
socket.socketpair()换成socket.socket()加手动bind和connect, 看走真实网卡会不会慢一些。
常见误区
- 「虚拟环境会复制一份 Python。」不复制。它只建一个壳,
pyvenv.cfg里记着底层 Python 在哪。 所以建起来只要几秒,不是几百兆。 - 「
python -m venv和venv命令一样。」在多版本机器上不一样。python -m venv明确说「用这个解释器建」,不容易建错版本。 - 「内网穿透就是把防火墙关掉。」完全不是。 它是你的机器主动往外连,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 和防火墙只拦「从外面主动进来的连接」,你主动往外连它不但不拦,还会记住这条连接, 好让回包能回来。外面的请求就顺着这条已经连好的管道转给你。下一篇讲真正的部署:代码怎么进版本库、怎么推到一台一直开着的机器上。
- ☐ (本篇完,下一篇 10.2 Git 与 Heroku 部署)