Python urlsplit 的端口属性:地址已经拆分成功,为什么读取 port 才抛出异常

前天 3阅读

把urlsplit放进try里没有报错,就记录“网址校验成功”,后续代码访问parts.port却突然出现ValueError。这不矛盾:拆出URL的几个文本组件,与把端口转换成受范围约束的整数,不一定在同一个时点发生。检查函数需要覆盖真正要读取的属性。

本文在CPython 3.12.14实跑,只解析代码里的固定字符串,不发起网络请求或DNS查询。保存为demo.py,用python3 demo.py运行。example.invalid仅作为示例名称;后半段只演示一个固定HTTPS来源的窄检查,不是通用网址安全验证器。

Python urlsplit 的端口属性:地址已经拆分成功,为什么读取 port 才抛出异常

AI模型生成概念图:长带先经过组件分槽,再经过独立的关闭检查门,用于比喻拆分和端口检查发生在不同阶段;橙色记号不表示实际端口值。

把拆分与取端口分成两个观察点

前四个样本分别使用443、70000、nope,以及省略双斜杠的形态。每次先打印scheme与netloc,再在独立try内访问port。后面的origin_for_demo把属性读取放回同一个校验路径,另加本例自己的协议、主机、用户信息与端口约定。

完整可运行程序

from urllib.parse import urlsplit

samples = [
    "https://example.invalid:443/report",
    "https://example.invalid:70000/report",
    "https://example.invalid:nope/report",
    "example.invalid:443/report",
]
for value in samples:
    parts = urlsplit(value)
    print("split:", repr(parts.scheme), repr(parts.netloc))
    try:
        print("port:", parts.port)
    except ValueError:
        print("port:", "ValueError")

def origin_for_demo(value):
    if not isinstance(value, str) or any(c.isspace() or ord(c) < 32 for c in value):
        raise ValueError("whitespace or control character")
    parts = urlsplit(value)
    port = parts.port
    if parts.scheme != "https" or parts.hostname != "example.invalid":
        raise ValueError("unexpected scheme or host")
    if parts.username is not None or parts.password is not None:
        raise ValueError("userinfo is not allowed")
    if port not in (None, 443):
        raise ValueError("unexpected port")
    return parts.scheme, parts.hostname, 443 if port is None else port

assert origin_for_demo(samples[0]) == ("https", "example.invalid", 443)
bad_inputs = samples[1:] + [
    "https://user@example.invalid/report",
    "https://example.invalid:444/report",
    " https://example.invalid/report",
]
rejected = 0
for value in bad_inputs:
    try:
        origin_for_demo(value)
    except ValueError:
        rejected += 1
    else:
        raise AssertionError("unexpected acceptance")
assert rejected == 6
print("explicitly rejected:", rejected)

本次实际输出(以下为结果,不是程序)

split: 'https' 'example.invalid:443'
port: 443
split: 'https' 'example.invalid:70000'
port: ValueError
split: 'https' 'example.invalid:nope'
port: ValueError
split: 'example.invalid' ''
port: None
explicitly rejected: 6

返回ParseResult风格对象仍可能留有待检查属性

第一个地址拆出https与example.invalid:443,port成功得到整数443。第二、第三个地址同样成功拆分,netloc保留70000或nope,但访问port时都输出ValueError。捕获位置若只包住urlsplit这一行,就会漏掉真正触发失败的读取。

这里使用的实际返回类型是SplitResult,与urlparse的ParseResult相似但组件数量不同。不要为了“全面检查”把它强转成普通元组后就结束,元组里的netloc仍是原始组件字符串;只有按需求读取和验证派生属性,才能获得后续代码真正依赖的值。

缺少双斜杠可能改变组件的含义

第四个输入的scheme是example.invalid,netloc为空,port为None。冒号前的一段被当作协议形状解析,没有因为看起来像域名就自动补成HTTPS主机。此时None表示在解析出的组件中没有端口,不表示那个原始字符串已经是可访问的默认端口地址。

因此检查函数同时要求scheme为https、hostname为固定示例名。端口None在满足前述条件后才按业务约定归一成443,而不是对任意输入都自动补一个默认值。先保留结构差别,再实施归一化,能避免把缺失主机与省略端口混为一谈。

检查原字符串与解析结果各有作用

origin_for_demo首先拒绝空白与低位控制字符,然后拆分,并立即读取port。这样非法端口的异常仍会从这个入口传出;其余条件进一步拒绝意外协议或主机、含userinfo的形式以及非443端口。六个反例全部被拒绝,计数为6。

原字符串检查不是为了替代URL解析器,而是表达本接口不接受这些字符的约定。某些Python版本会在解析时移除特定空白或控制字符;如果业务希望它们一律报错,就应在解析前明确拒绝,而不是只观察规范化后的组件。规则是否需要更宽松,应由输入合同决定。

这个检查只回答固定范围的问题

函数返回协议、主机和归一端口,不校验路径业务含义,也不访问目标。它没有做域名解析、IP地址限制、重定向跟踪或授权判断。即使返回元组,也只说明这组字符串符合示例的窄条件,不能被写成“允许向任意此类地址发请求”的安全结论。

排查真实客户端时,把输入解析、属性读取、目标政策和请求阶段分别记录,便于知道失败发生在哪一层。Python对某些异常URL的处理会随版本调整,文章使用固定3.12文档与3.12.14输出;升级时保留这些反例再跑一次,比仅检查一个正常网址更有效。

参考资料

资料核验日期:2026年10月2日。以上输出来自文中固定输入的本地实跑,程序退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。