代理格式详解:每个工具要哪种写法
代理格式一次讲清:IP:端口:用户名:密码、user:pass@host:port 和完整 URL,各工具认哪种,附经过测试的 Python/JavaScript 转换代码和特殊字符处理。
代理格式,就是把主机、端口、用户名、密码这四个值写成一行字符串时的顺序。常见的有 HOST:PORT、HOST:PORT:USERNAME:PASSWORD(也就是常说的 IP:端口:用户名:密码)、USERNAME:PASSWORD@HOST:PORT,以及完整 URL http://USERNAME:PASSWORD@HOST:PORT。写代码用的库要完整 URL;指纹浏览器和批量导入框一般要冒号分隔的那一行。
不管哪种格式,四个值本身不变,变的只是它们之间的符号。一个正确的代理,按错误的格式粘进去,和一个错误的代理表现得一模一样,所以值得花十分钟把格式弄清楚。本文列出你会遇到的几种格式、各个工具接受哪一种,并给出一段 Python 和 JavaScript 的转换代码,免得你再手动改列表。
所有示例都用 HOST、PORT、USERNAME 和 PASSWORD 作占位符。你自己的值在控制台里每项服务的页面上,没有一个所有人共用、可以从博客里直接抄的网关地址。
你会遇到的五种代理格式
| 格式 | 示例 | 常见场合 |
|---|---|---|
| 主机加端口 | HOST:PORT |
浏览器和系统代理设置、已加白名单的连接 |
| 冒号行,主机在前 | HOST:PORT:USERNAME:PASSWORD |
服务商导出的列表、指纹浏览器导入 |
| 冒号行,用户名在前 | USERNAME:PASSWORD:HOST:PORT |
部分其他服务商的导出和老工具 |
| 登录信息在主机前 | USERNAME:PASSWORD@HOST:PORT |
curl、一些机器人程序和配置文件 |
| 完整 URL | http://USERNAME:PASSWORD@HOST:PORT |
Python、Node.js、Go、PHP、环境变量 |
建议把完整 URL 当作你的“原件”保存。它带协议头,是标准写法,其他格式都能从它推出来,不用猜。
开头的 http:// 是什么意思
协议头说明的是你的程序怎么和代理通信,而不是怎么和网站通信。http:// 表示普通的 HTTP 代理,照样能访问 https:// 网站:客户端请代理打开一条 CONNECT 隧道,加密在隧道里端到端进行。如果在代理前面写 https://,意思是要求和代理本身建立加密连接,这是另一回事。我们把 requests 指向一个普通 HTTP 代理、却写了 https://,它的报错说得很直白:
ProxyError('Unable to connect to proxy. Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP. ...')
每个产品都可以用 http://。想用 SOCKS5,就改写成 socks5h://:住宅代理的端口两种都接受,ISP 和数据中心代理在控制台里切换协议。
各工具认哪种代理字符串格式
我们把每种格式分别交给 curl 8.5、requests 2.34 和 undici 8.11(Node.js 的 fetch 库),连接一个本地的、需要登录的测试代理。结果如下。
curl 最宽容。-x "http://USERNAME:PASSWORD@HOST:PORT" 可以,不带协议头的 -x USERNAME:PASSWORD@HOST:PORT 也可以,curl 会默认按 http:// 处理。你也可以用 -U 把登录信息从 URL 里拿出来:
curl -x HOST:PORT -U "USERNAME:PASSWORD" https://api.ipify.org
冒号行在发出任何请求之前就会被拒绝:curl: (5) Unsupported proxy syntax in 'HOST:PORT:USERNAME:PASSWORD': Port number was not a decimal number between 0 and 65535。curl 的其他代理选项见 cURL 教程。
Python requests 只要带登录信息,就要完整 URL。不带 http:// 的 USERNAME:PASSWORD@HOST:PORT 会报 InvalidProxyURL: Please check proxy URL. It is malformed and could be missing the host.,冒号行会报 InvalidURL: Failed to parse。单独的 HOST:PORT 可以用,requests 会自动补上协议头。
Node.js 最严格。undici 的 ProxyAgent 遇到 HOST:PORT 会报 TypeError: Invalid URL,遇到 USERNAME:PASSWORD@HOST:PORT 会报 Invalid URL protocol: the URL must start with `http:` or `https:`.。每次都给它完整 URL,和 Node.js 教程里的写法一样。
浏览器用一组输入框填主机和端口,代理第一次要求登录时再弹窗让你输用户名和密码。Chrome 和 Edge 把整件事交给操作系统。浏览器教程讲了 Firefox 自带的设置面板,以及带正规登录字段的 FoxyProxy 扩展。如果改完设置 Chrome 什么都打不开,Chrome 代理报错把每个错误代码对应到了原因。
指纹浏览器两种都支持:分开的主机、端口、用户名、密码字段,以及一个按 HOST:PORT:USERNAME:PASSWORD 每行一个代理的粘贴或导入框。GoLogin、AdsPower 和 BitBrowser 的教程分别说明了这个框藏在哪里。
Proxifier 所有值都分开填。协议选 HTTPS,这是 Proxifier 对“能打开 CONNECT 隧道的 HTTP 代理”的叫法;Proxifier 教程有完整步骤。
控制台导出的 IP:端口:用户名:密码 格式
控制台里的住宅代理页面按 HOST:PORT:USERNAME:PASSWORD 的顺序导出代理,和 GoLogin、BitBrowser 的导入顺序相同。ISP 和数据中心服务会把每个静态 IP 连同它自己的端口和登录信息列出来,这四个值可以填进上面任何一种格式。
使用住宅代理时,先在控制台里选好 Randomize IP 或 Sticky IP,再去复制。选 Sticky IP 时,会话选项附在密码末尾,所以复制出来的密码比普通密码长。请按显示的样子整串复制。粘性密码如果在某个符号处被截断,就会得到 407;什么时候需要这个模式,见轮换代理与粘性代理。
特殊字符:user:pass@host:port 为什么会坏
在 URL 里,有些字符有自己的职责:@ 表示登录信息结束,: 分隔用户名和密码、主机和端口,/、#、?、% 也各有含义。密码里只要有这些字符,在 URL 格式中就必须做百分号编码。我们用密码 p@ss:w/rd#1%41 做了测试:
- 原样放进 URL:curl 报
Unsupported proxy syntax,requests 报InvalidURL,undici 报Invalid URL。 - 编码成
p%40ss%3Aw%2Frd%231%2541后,三者都连上了。 - 用 curl 的
-U时,@、:、/、#可以不编码,但%41仍然被解码成了A,所以那里的%也要写成%25。
冒号行的规则正好相反:它从不编码,因为工具是按冒号切分它,而不是当作 URL 解析。这也让冒号行有自己的脆弱之处:密码里带冒号时,只有工具知道字段顺序、并把第三个冒号之后的内容全部当作密码,才能切对。登录信息在去往代理的路上还有哪些被弄坏的方式,见 407 错误怎么解决。
IPv6 代理要加方括号
IPv6 地址里全是冒号,所以写进 URL 时要放进方括号:http://[2001:db8::10]:PORT,登录信息照常放在前面。不加方括号,curl 会报同样的 “Port number was not a decimal number”,requests 报 Failed to parse。加了方括号,curl、requests 和 undici 都连上了我们在 [::1] 上的测试代理。冒号行里也要保留方括号,写成 [2001:db8::10]:PORT:USERNAME:PASSWORD,否则没有工具分得清地址在哪里结束。
Python 代理格式转换代码
下面这段能读入五种格式中的任意一种,输出你需要的那种。保存为 proxyfmt.py:
import re
from urllib.parse import quote, unquote
HOST_RE = r"(?P<host>\[[^\]]+\]|[^:@\[\]]+)"
PORT_RE = r"(?P<port>[^:@/]+)"
URL_FORM = rf"^(?:(?P<scheme>[a-z0-9]+):\/\/)?(?:(?P<user>[^:@]*):(?P<password>.*)@)?{HOST_RE}:{PORT_RE}/?$"
COLON_FORMS = {
"host_first": rf"^{HOST_RE}:{PORT_RE}:(?P<user>[^:]*):(?P<password>.*)$",
"user_first": rf"^(?P<user>[^:]*):(?P<password>.*):{HOST_RE}:{PORT_RE}$",
}
def parse_proxy(line, order="host_first"):
line = line.strip()
match = re.match(URL_FORM, line)
decode = unquote
if not match:
match = re.match(COLON_FORMS[order], line)
decode = str
if not match:
raise ValueError(f"not a proxy line I recognise: {line!r}")
p = match.groupdict()
return {
"scheme": p.get("scheme") or "http",
"host": p["host"],
"port": p["port"],
"user": decode(p["user"]) if p["user"] is not None else None,
"password": decode(p["password"]) if p["password"] is not None else None,
}
def to_url(p):
auth = ""
if p["user"] is not None:
auth = quote(p["user"], safe="") + ":" + quote(p["password"], safe="") + "@"
return p["scheme"] + "://" + auth + p["host"] + ":" + p["port"]
def to_colon(p):
parts = [p["host"], p["port"], p["user"], p["password"]]
return ":".join(part for part in parts if part is not None)
if __name__ == "__main__":
for line in [
"HOST:PORT",
"HOST:PORT:USERNAME:PASSWORD",
"USERNAME:PASSWORD@HOST:PORT",
"http://USERNAME:PASSWORD@HOST:PORT",
]:
print(to_url(parse_proxy(line)))
print(to_colon(parse_proxy("USERNAME:PASSWORD:HOST:PORT", order="user_first")))
两种冒号行从外表上看不出区别,所以 parse_proxy 不去猜。它默认主机在前,也就是控制台的顺序;来自其他地方的列表,传入 order="user_first"。URL 格式在读入时解码、输出时编码,所以满是符号的密码转一圈也不会变。用 requests 读取保存好的导出文件:
import requests
from proxyfmt import parse_proxy, to_url
with open("proxies.txt") as f:
proxies = [to_url(parse_proxy(line)) for line in f if line.strip()]
for proxy in proxies:
r = requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy}, timeout=30)
print(r.status_code, r.text)
JavaScript 版转换代码
proxyfmt.mjs,适用于 Node.js 22 及以上:
const HOST_RE = String.raw`(?<host>\[[^\]]+\]|[^:@\[\]]+)`;
const PORT_RE = String.raw`(?<port>[^:@/]+)`;
const URL_FORM = new RegExp(String.raw`^(?:(?<scheme>[a-z0-9]+):\/\/)?(?:(?<user>[^:@]*):(?<password>.*)@)?${HOST_RE}:${PORT_RE}\/?$`);
const COLON_FORMS = {
hostFirst: new RegExp(String.raw`^${HOST_RE}:${PORT_RE}:(?<user>[^:]*):(?<password>.*)$`),
userFirst: new RegExp(String.raw`^(?<user>[^:]*):(?<password>.*):${HOST_RE}:${PORT_RE}$`),
};
export function parseProxy(line, order = 'hostFirst') {
line = line.trim();
let match = line.match(URL_FORM);
let decode = decodeURIComponent;
if (!match) {
match = line.match(COLON_FORMS[order]);
decode = (s) => s;
}
if (!match) throw new Error(`not a proxy line I recognise: ${line}`);
const { scheme, host, port, user, password } = match.groups;
return {
scheme: scheme ?? 'http',
host,
port,
user: user === undefined ? undefined : decode(user),
password: password === undefined ? undefined : decode(password),
};
}
export function toUrl(p) {
const auth = p.user === undefined ? '' : encodeURIComponent(p.user) + ':' + encodeURIComponent(p.password) + '@';
return p.scheme + '://' + auth + p.host + ':' + p.port;
}
export function toColon(p) {
return [p.host, p.port, p.user, p.password].filter((part) => part !== undefined).join(':');
}
配合 undici 使用(npm i undici):
import { fetch, ProxyAgent } from 'undici';
import { parseProxy, toUrl } from './proxyfmt.mjs';
const proxy = toUrl(parseProxy('HOST:PORT:USERNAME:PASSWORD'));
const response = await fetch('https://api.ipify.org', { dispatcher: new ProxyAgent(proxy) });
console.log(response.status, await response.text());
两个版本我们都在本地测试代理上跑过:普通密码、上面那个满是符号的密码、带方括号的 IPv6 地址,以及用户名在前的行。每一行都连上了,转成 URL 再转回来,四个值也完全一致。
什么时候只要 HOST:PORT 就够了
如果你在控制台里把自己连接时用的 IP 地址加进服务的 IP 白名单,代理就会放这个地址进来,不再要求登录。之后 HOST:PORT(Node.js 里写 http://HOST:PORT)就是完整的代理字符串,也没人需要把密码存在配置文件里。这适合公网地址固定的服务器,不适合在不同网络之间来回切换的笔记本电脑,因为白名单只认你填进去的地址。
常见问题
IP:端口:用户名:密码 是什么格式? 就是主机在前的冒号行:HOST:PORT:USERNAME:PASSWORD。控制台的住宅代理导出用的就是它,大多数指纹浏览器导入的也是它。
user:pass@host:port 和 URL 格式一样吗? 差不多,只是前面少了 http://。curl 可以直接用;Python requests 和 Node.js 需要补上协议头。
代理 URL 应该以 http 还是 https 开头? http://,访问 https 网站时也一样。网站的加密在隧道里进行。
为什么我的代理列表在一个工具里能用,换一个就不行? 字段顺序或编码不同。用上面的代码转换一下,再看看密码里有没有符号。
下一步
从服务页面复制一行,用转换代码跑一遍,然后向 https://api.ipify.org 发一个请求。返回的地址不是你自己的,格式就对了。第一个请求的更多细节见代理怎么用;如果列表还是导不进去,把报错(密码打码)贴到 Discord。