功能说明
不少 API 用 Link 响应头返回分页或相关资源链接,例如 next、prev、last。这个解析器把头部拆成若干个链接项,再提取目标 URL、rel 和可选的 title,最终以 rel 为键返回字典。
难点在于逗号既可能分隔链接,也可能出现在带引号的 title 或尖括号包裹的 URL 中。代码用一个简单的状态机区分两种上下文,只在两者之外才把逗号当作分隔符。
关键代码
split_parts 扫描每个字符,维护 in_quotes 和 in_angle 两个布尔状态;遇到不在引号、也不在尖括号内的逗号就切分。parse_link_header 从 <...> 中取出目标地址,再把剩余属性按分号拆分,用 partition("=") 得到键值并去掉包裹的引号。
main 使用包含逗号标题的示例,验证 next 的 title 没有被截断,同时确认 last 的目标地址完整。
使用注意
同名 rel 出现多次时,后出现的会覆盖先出现的;如果业务需要保留全部关系,应把值改成列表。属性值内部若含分号,当前实现会误切分,这是简化解析器的已知限制。RFC 允许一个 rel 包含多个以空格分隔的关系名,这里会把整串当作一个键。
返回的目标地址可能是相对路径,调用方需要结合请求 URL 解析。多个同名响应头应先用逗号连接再解析。解析器不验证 URL 协议,也不处理扩展属性中的转义引号。
验证方式
示例中 next 的目标以 page=2&limit=20 结尾,title 完整保留为 Next, page 2;last 的目标以 page=9&limit=20 结尾;空字符串返回空字典。可以再加入三个链接、其中一个 title 含逗号,确认拆分数量仍然正确。
代码示例
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Link:
target: str
rel: str
title: str | None = None
def split_parts(header: str) -> list[str]:
parts: list[str] = []
current: list[str] = []
in_quotes = False
in_angle = False
for char in header:
if char == '"':
in_quotes = not in_quotes
elif char == "<" and not in_quotes:
in_angle = True
elif char == ">" and not in_quotes:
in_angle = False
if char == "," and not in_quotes and not in_angle:
parts.append("".join(current).strip())
current = []
else:
current.append(char)
if "".join(current).strip():
parts.append("".join(current).strip())
return parts
def parse_link_header(header: str) -> dict[str, Link]:
links: dict[str, Link] = {}
for part in split_parts(header):
if not part.startswith("<") or ">" not in part:
continue
target, _, rest = part[1:].partition(">")
attributes: dict[str, str] = {}
for attribute in rest.split(";"):
attribute = attribute.strip()
if not attribute or "=" not in attribute:
continue
name, _, raw_value = attribute.partition("=")
attributes[name.strip().lower()] = raw_value.strip().strip('"')
rel = attributes.get("rel")
if rel:
links[rel] = Link(target=target, rel=rel, title=attributes.get("title"))
return links
def main() -> None:
header = (
'<https://api.example.com/items?page=2&limit=20>; rel="next"; '
'title="Next, page 2", '
'<https://api.example.com/items?page=9&limit=20>; rel="last"'
)
links = parse_link_header(header)
print(links["next"].target)
print(links["next"].title)
assert links["last"].target.endswith("page=9&limit=20")
assert parse_link_header("") == {}
if __name__ == "__main__":
main()