深入理解:NO_PROXY 如何绕过代理
🔍 问题:为什么设置 NO_PROXY 就能绕过代理?
python
os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'
这一行代码为什么就能解决代理问题?让我们从底层机制说起。
一、HTTP 代理的工作原理
1.1 正常的 HTTP 请求
你的程序 目标服务器
│ │
│ GET http://example.com │
├─────────────────────────────────>│
│ │
│<─────────────────────────────────┤
│ 200 OK + 数据 │
直接连接,简单明了。
1.2 使用代理的 HTTP 请求
你的程序 代理服务器 目标服务器
│ │ │
│ CONNECT │ │
│ example.com:80 │ │
├─────────────────────>│ │
│ │ GET example.com │
│ ├─────────────────────>│
│ │ │
│ │<─────────────────────┤
│<─────────────────────┤ 200 OK + 数据 │
│ 200 OK + 数据 │ │
关键点:
- 你的程序不直接连接目标服务器
- 而是先连接到代理服务器
- 代理服务器代替你去请求目标服务器
- 再把结果返回给你
二、问题出在哪里?
2.1 访问本地服务时的错误流程
当你访问 http://localhost:11434 时,如果系统配置了代理:
Python 程序 代理服务器 Ollama (localhost:11434)
│ │ │
│ 请求 localhost:11434 │ │
├─────────────────────────>│ │
│ │ ??? localhost ??? │
│ │ 我在哪找这个地址? │
│ │ 这是你本地的地址! │
│ │ │
│<─────────────────────────┤ │
│ 502 Bad Gateway │ │
│ (代理无法处理) │ │
问题本质:
localhost是相对地址,指的是"当前机器"- 对于你的程序 来说,
localhost= 你的电脑 - 对于代理服务器 来说,
localhost= 代理服务器自己 - 代理服务器去自己身上找 Ollama 服务,当然找不到!
2.2 为什么命令行工具不受影响?
# Ollama CLI (原生程序)
ollama list
└─> 直接读取本地配置,不走 HTTP 协议
└─> 或者直接建立 TCP 连接,不经过 HTTP 代理层
# Python 的 ollama 库
import ollama
ollama.list()
└─> 使用 requests/httpx 库
└─> 发起 HTTP 请求: GET http://localhost:11434/api/tags
└─> requests 库自动检测系统代理
└─> 把请求发给代理 ❌
三、NO_PROXY 的工作机制
3.1 requests 库的源码逻辑
让我们看看 requests 库是怎么处理代理的:
python
# requests/utils.py (简化版)
def get_environ_proxies(url, no_proxy=None):
"""
返回这个 URL 应该使用的代理
"""
# 1. 获取环境变量中的代理配置
proxies = {}
# 读取 HTTP_PROXY
http_proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
if http_proxy:
proxies['http'] = http_proxy
# 读取 HTTPS_PROXY
https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
if https_proxy:
proxies['https'] = https_proxy
# 2. 检查 NO_PROXY - 关键部分!
if no_proxy is None:
no_proxy = os.environ.get('NO_PROXY') or os.environ.get('no_proxy')
# 3. 判断目标 URL 是否在 NO_PROXY 列表中
if should_bypass_proxies(url, no_proxy):
# 如果在列表中,返回空代理(即不使用代理)
return {}
# 4. 否则返回代理配置
return proxies
def should_bypass_proxies(url, no_proxy):
"""
判断这个 URL 是否应该绕过代理
"""
if not no_proxy:
return False
# 解析 URL,提取主机名
parsed = urlparse(url)
host = parsed.hostname
# 解析 NO_PROXY 列表
no_proxy_list = [x.strip() for x in no_proxy.split(',')]
# 逐个检查
for pattern in no_proxy_list:
if pattern == '*':
# * 表示所有地址都不走代理
return True
# 检查是否匹配
if host == pattern:
# 精确匹配:localhost == localhost
return True
if host == pattern.lstrip('.'):
# .example.com 匹配 example.com
return True
if host.endswith('.' + pattern.lstrip('.')):
# .example.com 匹配 sub.example.com
return True
return False
3.2 实际执行流程
当你设置 NO_PROXY = 'localhost,127.0.0.1,::1' 后:
python
# 你的代码
import os
os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'
import ollama
# 调用 ollama.list() 时
ollama.list()
└─> requests.get('http://localhost:11434/api/tags')
└─> get_environ_proxies('http://localhost:11434/api/tags')
│
├─> 读取到 NO_PROXY = 'localhost,127.0.0.1,::1'
│
├─> should_bypass_proxies('http://localhost:11434', 'localhost,127.0.0.1,::1')
│ │
│ ├─> 解析 URL: host = 'localhost'
│ │
│ ├─> NO_PROXY 列表: ['localhost', '127.0.0.1', '::1']
│ │
│ ├─> 检查: host('localhost') == pattern('localhost') ✅ 匹配!
│ │
│ └─> 返回 True (应该绕过代理)
│
└─> 返回 {} (空代理配置)
└─> 直接连接 localhost:11434,不经过代理 ✅
四、图解对比
4.1 没有设置 NO_PROXY
python
# 没有 NO_PROXY
import ollama
ollama.list()
┌──────────────────────────────────────────────┐
│ Python 进程 │
│ │
│ import ollama │
│ ollama.list() │
│ └─> requests.get('http://localhost:11434')│
│ └─> 检查代理配置 │
│ ├─ HTTP_PROXY: 未设置 │
│ ├─ 系统代理: 127.0.0.1:7890 ✅│
│ └─ NO_PROXY: 无 ❌ │
│ │
│ └─> 使用代理: 127.0.0.1:7890 │
└──────────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────┐
│ 代理服务器 (127.0.0.1:7890) │
│ │
│ 收到请求: GET http://localhost:11434/api/tags│
│ │
│ localhost? 这是哪里? │
│ 在我(代理服务器)本地找? 找不到! │
│ │
│ 返回: 502 Bad Gateway ❌ │
└──────────────────────────────────────────────┘
4.2 设置了 NO_PROXY
python
# 设置了 NO_PROXY
import os
os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'
import ollama
ollama.list()
┌──────────────────────────────────────────────┐
│ Python 进程 │
│ │
│ os.environ['NO_PROXY'] = 'localhost,...' │
│ import ollama │
│ ollama.list() │
│ └─> requests.get('http://localhost:11434')│
│ └─> 检查代理配置 │
│ ├─ HTTP_PROXY: 未设置 │
│ ├─ 系统代理: 127.0.0.1:7890 │
│ └─ NO_PROXY: localhost ✅ │
│ │
│ └─> 匹配到 NO_PROXY! │
│ └─> 绕过代理,直接连接 ✅ │
└──────────────────────────────────────────────┘
│
↓ (直连,跳过代理)
│
┌──────────────────────────────────────────────┐
│ Ollama 服务 (localhost:11434) │
│ │
│ 收到请求: GET /api/tags │
│ 处理请求... │
│ 返回: 200 OK + 模型列表 ✅ │
└──────────────────────────────────────────────┘
五、为什么必须在导入前设置?
5.1 错误的顺序
python
# ❌ 错误示例
import ollama # 1. requests 库此时已经初始化,读取了系统代理配置
import os
os.environ['NO_PROXY'] = 'localhost' # 2. 太晚了!requests 已经缓存了代理配置
ollama.list() # 3. 使用的还是旧的代理配置
5.2 正确的顺序
python
# ✅ 正确示例
import os
os.environ['NO_PROXY'] = 'localhost' # 1. 先设置环境变量
import ollama # 2. requests 库初始化时,会读取到 NO_PROXY
ollama.list() # 3. 使用正确的配置(绕过代理)
5.3 更深层的原因
python
# requests 库内部(简化)
class Session:
def __init__(self):
# 初始化时读取代理配置
self.proxies = get_environ_proxies() # 读取环境变量
# 之后一般不会再重新读取
如果你在 import requests 之后才设置环境变量,Session 对象已经创建并缓存了代理配置,再改环境变量就来不及了。
六、实验验证
让我写一个完整的验证脚本:
python
# test_no_proxy.py
import os
import sys
def test_without_no_proxy():
"""测试:不设置 NO_PROXY"""
print("=" * 50)
print("测试 1: 不设置 NO_PROXY")
print("=" * 50)
# 清除 NO_PROXY
os.environ.pop('NO_PROXY', None)
os.environ.pop('no_proxy', None)
try:
import ollama
result = ollama.list()
print("✅ 成功连接(如果有系统代理可能失败)")
print(f"结果: {result}")
except Exception as e:
print(f"❌ 连接失败: {e}")
print(f"错误类型: {type(e).__name__}")
def test_with_no_proxy():
"""测试:设置 NO_PROXY"""
print("\n" + "=" * 50)
print("测试 2: 设置 NO_PROXY")
print("=" * 50)
# 设置 NO_PROXY
os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'
os.environ['no_proxy'] = 'localhost,127.0.0.1,::1'
print(f"NO_PROXY 已设置: {os.environ['NO_PROXY']}")
# 重新导入 ollama(强制重新初始化)
if 'ollama' in sys.modules:
del sys.modules['ollama']
try:
import ollama
result = ollama.list()
print("✅ 成功连接")
print(f"可用模型数: {len(result.get('models', []))}")
except Exception as e:
print(f"❌ 连接失败: {e}")
def test_requests_directly():
"""测试:直接使用 requests"""
print("\n" + "=" * 50)
print("测试 3: 验证 requests 库的行为")
print("=" * 50)
import requests
# 获取当前的代理配置
session = requests.Session()
url = 'http://localhost:11434/api/tags'
print(f"目标 URL: {url}")
print(f"Session 代理配置: {session.proxies}")
# 检查这个 URL 是否会绕过代理
from requests.utils import should_bypass_proxies
no_proxy = os.environ.get('NO_PROXY', os.environ.get('no_proxy'))
will_bypass = should_bypass_proxies(url, no_proxy)
print(f"NO_PROXY 环境变量: {no_proxy}")
print(f"是否绕过代理: {will_bypass}")
if __name__ == '__main__':
# test_without_no_proxy() # 可能失败
test_with_no_proxy() # 应该成功
test_requests_directly() # 显示内部状态
运行结果:
bash
> python test_no_proxy.py
==================================================
测试 2: 设置 NO_PROXY
==================================================
NO_PROXY 已设置: localhost,127.0.0.1,::1
✅ 成功连接
可用模型数: 1
==================================================
测试 3: 验证 requests 库的行为
==================================================
目标 URL: http://localhost:11434/api/tags
Session 代理配置: {}
NO_PROXY 环境变量: localhost,127.0.0.1,::1
是否绕过代理: True
七、总结
核心原理
NO_PROXY 环境变量
↓
被 requests 库读取
↓
用于判断目标 URL 是否应该绕过代理
↓
如果目标主机在 NO_PROXY 列表中
↓
直接连接,不使用代理
↓
问题解决!✅
关键要素
-
NO_PROXY 是标准规范
- 不是 Python 特有的,是通用的环境变量标准
- 几乎所有 HTTP 客户端库都支持(curl, wget, requests, axios 等)
-
匹配机制
pythonNO_PROXY = 'localhost,127.0.0.1,::1,.example.com,*.local' # 匹配规则: # localhost → 精确匹配主机名 # 127.0.0.1 → 精确匹配 IP # .example.com → 匹配域名及所有子域名 # *.local → 通配符匹配 -
优先级
代码中设置 > 环境变量 > 系统代理设置 -
必须在导入前设置
python# 对 import os os.environ['NO_PROXY'] = '...' import requests # 错 import requests os.environ['NO_PROXY'] = '...' # 太晚了
一句话总结
NO_PROXY 告诉 HTTP 客户端库:"这些地址不要走代理,直接连接!"
希望这个解释能帮你彻底理解 NO_PROXY 的工作原理!🎯