强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

HTTP 协议详解教程 / 第 3 章:URL 与 URI

第 3 章:URL 与 URI

URL 是我们每天都在使用的概念,但其背后的语法规范和编码规则往往被忽视。本章将带你彻底理解 URL 的方方面面。


3.1 URI、URL 和 URN 的关系

这三个术语经常被混用,但它们有明确的区别:

URI (Uniform Resource Identifier)  — 统一资源标识符
├── URL (Uniform Resource Locator) — 统一资源定位符(标识 + 定位)
└── URN (Uniform Resource Name)    — 统一资源名称(仅标识)
术语定义示例
URI标识资源的字符串的超集https://example.com/pageurn:isbn:978-7-111-63666-6
URL通过位置标识资源https://example.com/api/users/1
URN通过名称标识资源,与位置无关urn:uuid:550e8400-e29b-41d4-a716-446655440000

📝 注意:实际工作中,URL 是使用最广泛的概念,日常说 “URI” 通常指的是 URL。


3.2 URL 的语法结构

根据 RFC 3986,URL 的通用语法为:

  scheme://[userinfo@]host[:port]/path[?query][#fragment]
  ──┬──   ────┬────  ─┬─  ─┬─  ─┬─    ──┬──    ──┬───
    │         │       │    │    │        │        │
  协议      用户信息  主机  端口  路径    查询参数  片段

各组成部分详解

https://alice:p4ss@api.example.com:8443/v2/users?role=admin&page=1#section-2
部分说明
schemehttps协议方案
userinfoalice:p4ss用户名和密码(已不推荐使用)
hostapi.example.com主机名(域名或 IP)
port8443端口号(默认端口可省略)
path/v2/users资源路径
queryrole=admin&page=1查询参数
fragmentsection-2片段标识(不会发送到服务器)

Python 解析 URL

from urllib.parse import urlparse, urlunparse

url = "https://alice:p4ss@api.example.com:8443/v2/users?role=admin&page=1#section-2"
parsed = urlparse(url)

print(f"scheme:   {parsed.scheme}")      # https
print(f"netloc:   {parsed.netloc}")      # alice:p4ss@api.example.com:8443
print(f"hostname: {parsed.hostname}")    # api.example.com
print(f"port:     {parsed.port}")        # 8443
print(f"username: {parsed.username}")    # alice
print(f"password: {parsed.password}")    # p4ss
print(f"path:     {parsed.path}")        # /v2/users
print(f"query:    {parsed.query}")       # role=admin&page=1
print(f"fragment: {parsed.fragment}")    # section-2

# 重新构建 URL
new_url = urlunparse((
    'https',                    # scheme
    'api.example.com',          # netloc
    '/v3/users',                # path
    '',                         # params (路径参数)
    'page=2&limit=20',          # query
    'top'                       # fragment
))
print(new_url)  # https://api.example.com/v3/users?page=2&limit=20#top

3.3 常见的 Scheme(协议方案)

Scheme名称默认端口说明
http超文本传输协议80明文传输
https安全超文本传输协议443TLS 加密
ftp文件传输协议21文件传输
wsWebSocket80WebSocket 连接
wss安全 WebSocket443加密 WebSocket
file本地文件访问本地文件系统
data数据内联数据
mailto邮件电子邮件地址
# data URL 示例
data_url = "data:text/html,<h1>Hello</h1>"
data_url_b64 = "data:text/html;base64,PGgxPkhlbGxvPC9oMT4="

# 在 JavaScript 中
# window.location = "mailto:alice@example.com?subject=Hello"

3.4 URL 编码(Percent-Encoding)

URL 中只能包含 ASCII 可打印字符(不包含空格和特殊字符)。其他字符必须进行 百分号编码(Percent-Encoding)

编码规则

  1. 保留字符有特殊含义,作为普通数据使用时必须编码
  2. 非 ASCII 字符(中文、日文等)必须编码
  3. 编码格式:% + 字符的两位十六进制 ASCII 码

保留字符

字符含义编码
:分隔 scheme/port%3A
/路径分隔%2F
?查询参数开始%3F
#片段开始%23
[ ]IP 地址%5B %5D
@用户信息分隔%40
! * ' ( )子分隔符各自编码
+空格(在查询中)%2B
=参数键值分隔%3D
&参数分隔%26
空格%20+

Python URL 编码

from urllib.parse import quote, unquote, urlencode

# 编码路径
path = "/文档/报告 2024.pdf"
encoded_path = quote(path, safe='/')
print(encoded_path)  # /%E6%96%87%E6%A1%A3/%E6%8A%A5%E5%91%8A%202024.pdf

# 解码
print(unquote(encoded_path))  # /文档/报告 2024.pdf

# 编码查询参数
params = {
    "q": "HTTP 协议",
    "lang": "zh",
    "page": "1"
}
query_string = urlencode(params, quote_via=quote)
print(query_string)
# q=HTTP%20%E5%8D%8F%E8%AE%AE&lang=zh&page=1
// JavaScript URL 编码
const keyword = "HTTP 协议 & 安全";

// encodeURI — 编码整个 URL(不编码 scheme、host 等)
console.log(encodeURI(keyword));
// HTTP%20%E5%8D%8F%E8%AE%AE%20%26%20%E5%AE%89%E5%85%A8

// encodeURIComponent — 编码 URL 组件(编码更多字符)
console.log(encodeURIComponent(keyword));
// HTTP%20%E5%8D%8F%E8%AE%AE%20%26%20%E5%AE%89%E5%85%A8

// 构建带参数的 URL
const base = "https://api.example.com/search";
const params = new URLSearchParams({ q: keyword, page: 1 });
console.log(`${base}?${params}`);
// https://api.example.com/search?q=HTTP+%E5%8D%8F%E8%AE%AE+%26+%E5%AE%89%E5%85%A8&page=1

3.5 查询参数(Query Parameters)

查询参数以 ? 开头,键值对之间用 & 分隔。

语法

?key1=value1&key2=value2&key3=value3

复杂参数的编码方案

场景方案示例
数组重复键ids=1&ids=2&ids=3
数组方括号ids[]=1&ids[]=2&ids[]=3
对象方括号filter[name]=alice&filter[age]=25
嵌套方括号链filter[address][city]=beijing
空值省略值debugdebug=
from urllib.parse import parse_qs, urlencode

# 解析重复键参数
query = "ids=1&ids=2&ids=3&name=alice"
params = parse_qs(query, keep_blank_values=True)
print(params)
# {'ids': ['1', '2', '3'], 'name': ['alice']}

# 方括号风格
query2 = "ids[]=1&ids[]=2&filter[name]=alice&filter[age]=25"
# 这种格式需要手动解析或使用框架解析
// JavaScript URLSearchParams
const params = new URLSearchParams();
params.append('ids', '1');
params.append('ids', '2');
params.append('ids', '3');
params.set('page', '1');
params.sort();  // 可排序

console.log(params.toString()); // ids=1&ids=2&ids=3&page=1
console.log(params.getAll('ids')); // ['1', '2', '3']
console.log(params.get('page'));  // '1'

// 从当前 URL 解析
const url = new URL('https://example.com/search?q=hello&page=1');
console.log(url.searchParams.get('q'));    // 'hello'
console.log(url.searchParams.get('page')); // '1'

3.6 片段标识(Fragment)

片段(Fragment)以 # 开头,不会发送到服务器,仅在客户端(浏览器)使用。

https://example.com/page#section-2
// 浏览器中操作 Fragment
console.log(window.location.hash); // #section-2

window.location.hash = '#section-3';  // 修改片段,不发送请求

// 监听片段变化
window.addEventListener('hashchange', (event) => {
    console.log('旧片段:', event.oldURL);
    console.log('新片段:', event.newURL);
});

常见用途

用途说明
页面内锚点跳转到页面特定位置
SPA 路由单页应用的前端路由(hash 模式)
媒体片段视频的特定时间段 #t=10,20
文字片段高亮页面中的特定文本 #:~:text=hello

3.7 相对 URL 与绝对 URL

HTML 中可以使用相对 URL,浏览器会基于当前页面 URL 解析。

<!-- 绝对 URL -->
<a href="https://example.com/page">绝对链接</a>

<!-- 相对 URL -->
<a href="page.html">同目录下的 page.html</a>
<a href="./page.html">同目录下的 page.html(显式)</a>
<a href="../other/page.html">上级目录的 other/page.html</a>
<a href="//cdn.example.com/file.js">协议相对 URL</a>
from urllib.parse import urljoin

base = "https://example.com/dir/page.html"

print(urljoin(base, "other.html"))
# https://example.com/dir/other.html

print(urljoin(base, "../other.html"))
# https://example.com/other.html

print(urljoin(base, "/root.html"))
# https://example.com/root.html

print(urljoin(base, "https://other.com/page"))
# https://other.com/page

print(urljoin(base, "//cdn.example.com/file.js"))
# https://cdn.example.com/file.js

3.8 业务场景:搜索引擎 URL 设计

一个典型的搜索 API URL 设计:

基础:  /api/v1/search
文本:  /api/v1/search?q=HTTP+协议
分页:  /api/v1/search?q=HTTP+协议&page=2&limit=20
排序:  /api/v1/search?q=HTTP+协议&sort=date&order=desc
过滤:  /api/v1/search?q=HTTP+协议&tag=tutorial&lang=zh
// 构建搜索 URL 的工具函数
class SearchURLBuilder {
    constructor(base) {
        this.url = new URL(base);
    }

    query(q) { this.url.searchParams.set('q', q); return this; }
    page(p) { this.url.searchParams.set('page', p); return this; }
    limit(l) { this.url.searchParams.set('limit', l); return this; }
    sort(field, order = 'asc') {
        this.url.searchParams.set('sort', field);
        this.url.searchParams.set('order', order);
        return this;
    }
    filter(key, value) {
        this.url.searchParams.set(key, value);
        return this;
    }
    build() { return this.url.toString(); }
}

const url = new SearchURLBuilder('https://api.example.com/search')
    .query('HTTP 协议')
    .page(2)
    .limit(20)
    .sort('date', 'desc')
    .filter('lang', 'zh')
    .build();

console.log(url);
// https://api.example.com/search?q=HTTP+%E5%8D%8F%E8%AE%AE&page=2&limit=20&sort=date&order=desc&lang=zh

⚠️ 注意事项

  1. 不要在 URL 中放置敏感信息:URL 会被记录在日志、浏览器历史、Referer 头中
  2. Fragment 不发送到服务器:不能依赖 Fragment 做服务端逻辑
  3. 注意编码一致性:客户端和服务端使用相同的编码方式
  4. URL 长度限制:虽然规范没有限制,但浏览器通常限制在 2000-8000 字符
  5. 避免使用 userinfouser:password@host 格式已被废弃,存在安全风险

🔗 扩展阅读


下一章第 4 章:请求方法详解 — GET/POST/PUT/DELETE/PATCH/OPTIONS/HEAD 语义与选型