跳到主要内容

自己动手搭建 uv python 智能代理站

· 阅读需 13 分钟

之前有介绍在国内使用 uv 如何设置代理加速python 包的下载,在安装python 时,当时介绍了国内目前只有一个南京大学镜像站,https://mirror.nju.edu.cn/github-release/indygreg/python-build-standalone/, 这个下载站只镜像了 uv 最新的release 文件,所以对于之前月份的文件,如果使用南京大学下载站就会下载失败,本文介绍一种使用海外云服务搭建uv python 的下载代理站实现可以正常下载安装 uv python 的实现方案。

前言:为什么我们需要这个方案?

作为一名 Python 开发者,你一定遇到过这样的烦恼:想用超快的 uv 工具安装 Python,却发现国内访问 GitHub 慢如蜗牛,之前介绍过国内只有南京大学镜像站可用于加速下载,但该镜像站仅镜像了 uv 最新的 release 文件。别担心!今天我要分享的解决方案,能让你像使用国内镜像站一样流畅地安装任意版本的 uv Python!本文将详细介绍一种使用海外云服务搭建 uv Python 下载代理站的实现方案,让你轻松解决下载难题

[!warning] 本文仅限技术交流,请勿用于任何违法用途

uv 下载安装python 的逻辑

uv 在安装python 时默认从 github 上下载,基础下载路径地址如下

https://github.com/astral-sh/python-build-standalone/releases/download

如安装mac 下的python3.10.17, 下载地址为 https://github.com/astral-sh``/python-build-standalone/releases/download/20250409/cpython-3.10.17+20250409-x86_64-apple-darwin-debug-full.tar.zst

它是将/20250409 之后的内容拼接到基础下载路径,但是这个github 地址国内访问又经常失败,uv 提供了以个 --mirror 的参数,以及镜像环境变量 UV_PYTHON_INSTALL_MIRROR , uv 当检测到有这个值的时候,就将文件下载地址拼接到对应的基础路径,我们需要搭建一个代理服务,实现以下功能

  1. 先检查南京大学镜像站是否有文件,如果有就直接302到南京大学下载站

  2. 如果没有就从github 上下载,并流式的代理给客户端。

🚀 方案概览:智能代理的魔法

我们的方案就像一个聪明的快递小哥:

  1. 先敲门问南京大学:"你有这个版本的 Python 吗?"
  2. 如果有:直接告诉你去南京大学取货(302 跳转)
  3. 如果没有:小哥亲自去 GitHub 帮你取货,然后送到你手上
请求 → 校验 → 镜像检查 → GitHub回源 → 流式传输
│ │
└─ 302跳转 └─ 代理下载

🔧 技术选型:为什么是 OpenResty?

目前有两种方案可以实施:

  1. 使用 OpenResty 配合 Lua 脚本,检测南京大学是否有相应文件,并配合做相应的跳转与代理。

  2. 使用开源的 gh-proxy 项目做代理。

我比较偏向于第一种方案,因为可以不用开发服务端的代码,且 OpenResty 是基于 Nginx 的,本身性能比较高。gh-proxy 是使用 Python Flask 框架开发的下载代理项目,也有很多 GitHub 下载代理站使用,当然如果对于 Python 很熟悉的话,这个项目部署起来也很方便。

OpenResty 三大优势让你无法拒绝

  1. 性能怪兽 🚗💨

    • 基于 Nginx,轻松应对高并发
    • LuaJIT 让脚本运行快如闪电
  2. 零代码开发 🧑‍💻

    • 只需配置 + Lua 小脚本
    • 不用写复杂的后端服务
  3. 灵活如猫 🐱

    • 动态决定跳转或代理
    • 轻松添加新规则
方案优点缺点
OpenResty高性能、灵活、零代码依赖需学习 Lua 语法
gh-proxy开箱即用,无需开发功能固定,无法自定义逻辑(如优先镜像站)

🛠️ 手把手搭建指南

第一步:安装 OpenResty

这里我写了一个一键安装脚本

#!/bin/bash

# 检查是否是root用户
if [ "$(id -u)" -ne 0 ]; then
echo "必须以root用户运行脚本!"
exit 1
fi

echo "开始安装 OpenResty 和 lua-resty-http..."

# 1. 安装 EPEL 和必要工具
echo "安装必要的工具..."
yum install -y yum-utils wget curl

# 2. 添加 OpenResty YUM 仓库
echo "添加 OpenResty 仓库..."
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

# 3. 安装 OpenResty
echo "安装 OpenResty..."
yum install -y openresty

# 4. 启动 OpenResty
echo "启动 OpenResty..."
systemctl start openresty
systemctl enable openresty

# 5. 安装 OPM(OpenResty Package Manager)
echo "安装 OpenResty 包管理器 OPM..."
yum install -y openresty-opm

# 6. 安装 lua-resty-http
echo "安装 lua-resty-http..."
/usr/local/openresty/bin/opm get ledgetech/lua-resty-http

# 7. 检查 OpenResty 是否安装成功
echo "检查 OpenResty 版本..."
openresty -v

# 8. 检查 lua-resty-http 是否安装成功
echo "检查 lua-resty-http 是否安装成功..."
if [ -f "/usr/local/openresty/site/lualib/resty/http.lua" ]; then
echo "lua-resty-http 安装成功!"
else
echo "lua-resty-http 安装失败!"
exit 1
fi


# 10. 重启 OpenResty
echo "重启 OpenResty..."
systemctl restart openresty

echo "安装完成!"

由于是国外的服务器,下载安装还是很快的。

第二步:配置 Nginx 核心

nginx 核心配置文件路径为 /usr/local/openresty/nginx/conf/nginx.conf 修改这个文件如下:

#user  nobody;
worker_processes 1;

error_log logs/error.log info;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
resolver 8.8.8.8 8.8.4.4 ipv6=off;
include mime.types;
default_type application/octet-stream;

proxy_buffer_size 16k;
proxy_buffers 8 128k; # 8 × 128k = 1MB 缓冲池
proxy_busy_buffers_size 768k; # 约占75%,提高发送速度
proxy_connect_timeout 5s;
proxy_read_timeout 600s;
send_timeout 600s;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;
include /usr/local/openresty/nginx/conf/default.d/*.conf;
server {
listen 80;
server_name localhost;


error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

修改几个buffer 配置,github 返回的header 头信息还是挺多挺大的

proxy_buffer_size 16k;
proxy_buffers 8 128k;
proxy_busy_buffers_size 768k;

添加 dns,屏蔽掉ipv6 配置

resolver 8.8.8.8 8.8.4.4 ipv6=off;

添加网站配置文件目录, 表示将一下目录所有以 .conf 文件加载进来。

include /usr/local/openresty/nginx/conf/default.d/*.conf;

第三步:编写智能代理逻辑

/usr/local/openresty/nginx/conf/default.d/ 创建一个uvproxy.conf 文件,写入以下内容

server {
listen 8081;
set $target_backend "";
set $redirect_target "";
location / {
content_by_lua_block {
local remote_ip = ngx.var.remote_addr
-- 校验日期是否合法的函数
local function is_valid_date(date_str)
if not date_str or #date_str ~= 8 then
ngx.log(ngx.INFO, "date " .. date_str .. " is not a valid date")
return false
end
local year = tonumber(string.sub(date_str, 1, 4))
local month = tonumber(string.sub(date_str, 5, 6))
local day = tonumber(string.sub(date_str, 7, 8))
ngx.log(ngx.INFO, year .." ".. month.." "..day)

if not (year and month and day) then
return false
end

if month < 1 or month > 12 then
ngx.log(ngx.INFO, "month is not valid month:" .. month)
return false
end

local days_in_month = {
31, (year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)) and 29 or 28,
31, 30, 31, 30,
31, 31, 30, 31, 30, 31
}

if day < 1 or day > days_in_month[month] then
ngx.log(ngx.INFO, "day is not a valid day: ".. day)
return false
end

return true
end

local http = require "resty.http"
local uri = ngx.var.request_uri
-- 检查是否以 /YYYYMMDD 开头
local m = ngx.re.match(uri, [[^/(\d{8})(/.*)?$]])
if not m then
ngx.log(ngx.INFO, "data is not valid data: ".. uri)
ngx.status = ngx.HTTP_FORBIDDEN
ngx.say("403 Forbidden: Invalid URI start")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end

local date_part = m[1]
local rest_path = m[2] or ""

-- 检查日期是否合法
if not is_valid_date(date_part) then
ngx.log(ngx.INFO, "data not valid data: ".. uri)
ngx.status = ngx.HTTP_FORBIDDEN
ngx.say("403 Forbidden: Invalid date format")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end

-- 检查扩展名
local allowed_exts = {[".sha256"]=true, [".gz"]=true, [".zst"]=true, [""]=true}
local ext = ""

do
local ext_m = ngx.re.match(rest_path, [[\.([a-z0-9]+)$]], "ijo")
if ext_m then
ext = "." .. ext_m[1]
end
end

if not allowed_exts[ext] then
ngx.log(ngx.INFO, "ext not valid:"..ext)
ngx.status = ngx.HTTP_FORBIDDEN
ngx.say("403 Forbidden: Invalid file extension")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end


local httpc = http.new()

-- 先请求 南京大学
local nju_base = "https://mirror.nju.edu.cn/github-release/indygreg/python-build-standalone"
local github_base = "https://github.com/astral-sh/python-build-standalone/releases/download"

-- 构造请求的完整 URL
local nju_url = nju_base .. uri
local githuburl = github_base .. uri
ngx.log(ngx.INFO, "check nju URL: " .. nju_url)
local res, err = httpc:request_uri(nju_url, {
method = "HEAD",
ssl_verify = false,
headers = {
["Host"] = "mirror.nju.edu.cn",
}
})
if res and res.status == 200 then
-- 返回200
-- 打印请求的 URL 到 error.log
ngx.log(ngx.INFO, "use nju URL: " .. nju_base)
ngx.redirect(nju_url, 302)
else
-- 使用github 地址
res, err = httpc:request_uri(githuburl, {
method = "HEAD",
ssl_verify = false,
})
if res.status == 302 then
local location = res.headers["Location"]
if location then
ngx.log(ngx.INFO, "302 redirect detected, proxying to: " .. location)
-- 解析完整 URL(返回 scheme、host、port、path、query)
local function parse_url(url)
local parsed = {}

parsed.scheme, parsed.rest = url:match("^(https?)://(.+)$")
if not parsed.scheme then return nil, "invalid URL" end

parsed.host, parsed.port, parsed.path = parsed.rest:match("^([^:/]+):?(%d*)(/?.*)$")
parsed.port = tonumber(parsed.port)
if parsed.path == "" then parsed.path = "/" end

-- 提取查询字符串
if parsed.path:find("?") then
parsed.path, parsed.query = parsed.path:match("^([^?]+)%??(.*)$")
end

return parsed
end
-- 流式下载函数
local function stream_proxy(location)
local parsed_url, err = parse_url(location)
if not parsed_url then
ngx.log(ngx.ERR, "URL parse failed: ", err)
return ngx.exit(ngx.HTTP_NOT_FOUND)
end


local scheme = parsed_url.scheme or "https"
local host = parsed_url.host
local port = tonumber(parsed_url.port) or (scheme == "https" and 443 or 80)
local path = parsed_url.path or "/"
if parsed_url.query then
path = path .. "?" .. parsed_url.query
end

local httpc = http.new()
httpc:set_timeout(10000)
local ok, err = httpc:connect(host, port)
if not ok then
ngx.log(ngx.ERR, "connect failed: ", err)
return ngx.exit(ngx.HTTP_NOT_FOUND)
end

if scheme == "https" then
local ok, err = httpc:ssl_handshake(nil, host, false)
if not ok then
ngx.log(ngx.ERR, "ssl handshake failed: ", err)
return ngx.exit(ngx.HTTP_NOT_FOUND)
end
end

local res, err = httpc:request({
method = "GET",
path = path,
headers = {
["Host"] = host,
["User-Agent"] = "Mozilla/5.0"
}
})

if not res then
ngx.log(ngx.ERR, "proxy request failed: ", err)
return ngx.exit(ngx.HTTP_NOT_FOUND)
end

-- 转发响应头
for k, v in pairs(res.headers) do
if k:lower() ~= "transfer-encoding" and k:lower() ~= "connection" then
ngx.header[k] = v
end
end

ngx.status = res.status

-- 流式传输内容
while true do
local chunk, err = res.body_reader()
if err then
ngx.log(ngx.ERR, "read error: ", err)
break
end
if not chunk then break end
ngx.print(chunk)
ngx.flush(true)
end

httpc:set_keepalive()
return ngx.exit(res.status)
end
return stream_proxy(location)
else
ngx.log(ngx.ERR, "302 received but no Location header")
-- 返回失败
return ngx.exit(ngx.HTTP_NOT_FOUND)
end

else
-- 这里也直接返回失败
ngx.log(ngx.ERR, "github status unknow:" .. res.status)
return ngx.exit(ngx.HTTP_NOT_FOUND)
end
end
}
}
}

这个脚本比较长,简单介绍一下实现的过程

为了避免一些不必要的请求,会对下载地址做一些校验,对于非法的请求直接返回403,所以有以下规则

  1. 由于 uv python 的下载路径是以 /20250409 这样的日期格式开头的路径,所以这里先校验了一下格式是否正确,同时这里定义了一个is_valid_date函数,校验日期是否合法。

  2. 检查下载路径是否是 .sha256, .gz, .zst 或者空

  3. 之后拼接南京大学的下载地址,请求南京大学镜像站,注意这里使用的是HEAD 请求,并不会真正的下载文件,只是获取一下头信息,这个头信息很重要,它告诉客户端是否服务器上是否有该请求资源,如果是需要302跳转的,会将跳转地址写到响应头信息中

  4. 如果南京大学镜像站有该文件,即http返回200,nginx 直接返回302,告诉客户端去这个地址下载

  5. 如果没有,则拼接github 下载url, 再次以HEAD 方式请求访问github 资源,注意,如果github 有这个资源,一般情况下是会返回一个302 下载地址,因为github 会将文件存储在cdn 网络上。

  6. 当github 返回302 时,注意这里不能像南京大学那样,也用302将文件下载地址返回给客户端,因为github 返回的302 下载地址一般国内也是下载不到了,所以这里需要使用这台代理服务器下载该文件,并返回给客户端

  7. 之后代理服务器以流式的方式获取文件,并且以流式的方式返回给客户端

  8. 如果github 返回404,则直接返回给客户端404

💡 核心技术揭秘

1. 为什么不能用简单的 proxy_pass?

GitHub 返回的是带签名的临时 S3 地址:

https://objects.githubusercontent.com/...?X-Amz-Algorithm=...&X-Amz-Expires=300...

这些参数如果使用proxy_pass 会被 Nginx 截断,导致签名失效 → 404 错误!

2. github 返回的地址为什么不能直接302给客户端

南京大学镜像站如果文件,我们是直接302跳转的,但是github 却不行,国内请求github 文件之所以慢,主要是由于github 返回的cdn 地址也在国外,国内下载是很慢或者下载不到的,如果github 下载地址也返回302,客户端将直接访问这个国外的cdn地址,那么就和没有使用代理一样。

3. 为什么要使用流式传输 ✨

代理下载github 文件有两种方式

  1. 代理服务器先将文件全部下载下来,然后再返回给客户端(传统方式)

  2. 代理服务器一边下载一边将内容返回给客户端

两种方式的比较如下:

传统方式我们的方式
全部下载完再发送像水管一样边下载边传输
内存占用高几乎不占额外内存
用户等待时间长用户立即开始下载
所以这里我采用了边下边播的方式
-- 流式传输内容
while true do
local chunk, err = res.body_reader()
if err then
ngx.log(ngx.ERR, "read error: ", err)
break
end
if not chunk then break end
ngx.print(chunk)
ngx.flush(true)
end

🎯 效果测试

假设你的服务器IP是 1.1.1.1,测试命令:

UV_PYTHON_INSTALL_MIRROR=http://1.1.1.1:8001 uv python install 3.10

观察日志,你会看到:

  1. 优先尝试南京大学镜像
  2. 找不到时自动切换 GitHub 代理
  3. 大文件流畅下载不卡顿

📦 资源获取

所有配置文件和脚本已开源,有兴趣可以参考一下: 👉 https://github.com/kevinkelin/uvproxy

通过这个方案,你现在可以: ✅ 享受国内镜像站的速度
✅ 访问 GitHub 上的所有历史版本
✅ 完全掌控自己的开发环境

是不是有种"原来如此"的畅快感?赶紧动手试试吧!遇到问题欢迎留言交流~

⚠️ 最后的重要提醒

[!danger] 重要提醒

  1. 技术无罪,用法有责!请严格遵守法律法规,仅将本方案用于合法合规的技术研究和个人使用。
  2. 海外服务器要遵守当地法律法规
  3. 大流量使用请注意服务器带宽
wp