URL编码与解码完全指南

URL编码(URL Encoding)是Web开发中常见但容易出错的技术点。正确理解和使用URL编码,能够避免许多奇怪的问题和安全漏洞。

什么是URL编码

URL编码是一种编码机制,用于将URL中的不安全字符转换为%后跟两位十六进制数的形式。也叫百分号编码(Percent-encoding)。

为什么需要URL编码

URL只允许使用以下字符:

  • 字母:a-z, A-Z
  • 数字:0-9
  • 特殊字符:- _ . ~
  • 保留字符:! * ’ ( ) ; : @ & = + $ , / ? % # [ ]

其他字符(如中文、空格、特殊符号)都需要编码。

原始: 你好世界
编码后: %E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C

原始: hello world
编码后: hello%20world

URL编码规则

编码方式

字符 → % + 两位十六进制数

空格 " "  → %20
中文 "你"  → %E4%BD%A0 (UTF-8编码后每字节转为%XX)

不需要编码的字符

A-Z a-z 0-9 - _ . ~
! * ' ( ) ; : @ & = + $ , / ? % # [ ]

常见字符编码对照表

字符编码说明
空格%20最常见的编码字符
!%21感叹号
"%22双引号
#%23井号(URL片段标识符)
$%24美元符号
%%25百分号本身
&%26和号(URL参数分隔符)
+%2B加号
/%2F斜杠(URL路径分隔符)
:%3A冒号
=%3D等号(URL参数键值分隔符)
?%3F问号(URL查询字符串开始)
@%40艾特符号

URL编码实现

JavaScript实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// encodeURI(编码完整URL)
const url = 'https://example.com/search?q=你好世界&sort=date';
const encoded = encodeURI(url);
console.log(encoded);
// "https://example.com/search?q=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C&sort=date"

// encodeURIComponent(编码URL组件)
const component = '你好世界&sort=date';
const encodedComponent = encodeURIComponent(component);
console.log(encodedComponent);
// "%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C%26sort%3Ddate"

// decodeURI(解码完整URL)
const decoded = decodeURI(encoded);
console.log(decoded);
// "https://example.com/search?q=你好世界&sort=date"

// decodeURIComponent(解码URL组件)
const decodedComponent = decodeURIComponent(encodedComponent);
console.log(decodedComponent);
// "你好世界&sort=date"

encodeURI vs encodeURIComponent

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// encodeURI:不编码URL保留字符
const url1 = 'https://example.com/path?query=value#anchor';
console.log(encodeURI(url1));
// "https://example.com/path?query=value#anchor"

// encodeURIComponent:编码所有特殊字符
const component = 'https://example.com/path?query=value#anchor';
console.log(encodeURIComponent(component));
// "https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue%23anchor"

// 使用场景示例
const baseUrl = 'https://api.example.com/search';
const query = 'JavaScript 编程';
const fullUrl = `${baseUrl}?q=${encodeURIComponent(query)}`;
console.log(fullUrl);
// "https://api.example.com/search?q=JavaScript%20%E7%BC%96%E7%A8%8B"

Python实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from urllib.parse import quote, unquote, quote_plus, unquote_plus

# 编码
text = '你好世界'
encoded = quote(text)
print(encoded)  # '%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C'

# 编码(空格转为+)
encoded_plus = quote_plus(text)
print(encoded_plus)  # '%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C'

# 解码
decoded = unquote(encoded)
print(decoded)  # '你好世界'

# 解码(+转为空格)
decoded_plus = unquote_plus(encoded_plus)
print(decoded_plus)  # '你好世界'

Java实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.net.URLDecoder;

public class URLEncoding {
    public static void main(String[] args) throws UnsupportedEncodingException {
        // 编码
        String text = "你好世界";
        String encoded = URLEncoder.encode(text, "UTF-8");
        System.out.println(encoded);  // %E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C

        // 解码
        String decoded = URLDecoder.decode(encoded, "UTF-8");
        System.out.println(decoded);  // 你好世界
    }
}

应用场景

1. 表单提交

1
2
3
4
5
6
<!-- GET请求:自动URL编码 -->
<form action="/search" method="get">
  <input type="text" name="q" value="你好 世界">
  <button type="submit">搜索</button>
</form>
<!-- 提交后URL: /search?q=%E4%BD%A0%E5%A5%BD+%E4%B8%96%E7%95%8C -->
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 手动构建查询字符串
function buildQueryString(params) {
  return Object.entries(params)
    .map(([key, value]) =>
      `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
    )
    .join('&');
}

const params = {
  q: 'JavaScript 编程',
  page: 1,
  sort: 'date'
};

const queryString = buildQueryString(params);
console.log(queryString);
// "q=JavaScript%20%E7%BC%96%E7%A8%8B&page=1&sort=date"

2. AJAX请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用fetch API
async function search(query) {
  const url = `/api/search?q=${encodeURIComponent(query)}`;

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    }
  });

  return await response.json();
}

// 使用axios
axios.get('/api/search', {
  params: {
    q: '你好世界',
    page: 1
  }
});
// axios自动处理URL编码

3. URL参数解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 解析URL参数
function parseQueryString(url) {
  const queryString = url.split('?')[1];
  if (!queryString) return {};

  const pairs = queryString.split('&');
  const result = {};

  pairs.forEach(pair => {
    const [key, value] = pair.split('=');
    result[decodeURIComponent(key)] = decodeURIComponent(value || '');
  });

  return result;
}

// 使用示例
const url = 'https://example.com/search?q=hello&page=1&sort=date';
const params = parseQueryString(url);
console.log(params);
// { q: 'hello', page: '1', sort: 'date' }

4. 处理文件名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 下载包含中文的文件
function downloadFile(filename, content) {
  const blob = new Blob([content], { type: 'text/plain' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  // 文件名需要编码
  a.download = encodeURIComponent(filename);
  a.click();

  URL.revokeObjectURL(url);
}

downloadFile('测试文件.txt', 'Hello World');

5. 处理URL哈希

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 设置URL哈希(用于单页应用路由)
function setHash(hash) {
  window.location.hash = encodeURIComponent(hash);
}

// 获取URL哈希
function getHash() {
  return decodeURIComponent(window.location.hash.slice(1));
}

// 使用示例
setHash('/page/你好');
console.log(getHash());  // '/page/你好'

特殊情况处理

1. 空格的编码

空格有两种编码方式:

  • %20:标准URL编码
  • +:application/x-www-form-urlencoded格式(表单提交使用)
1
2
3
4
5
6
// encodeURI和encodeURIComponent使用%20
console.log(encodeURIComponent('hello world'));  // "hello%20world"

// 表单提交时使用+
// application/x-www-form-urlencoded格式
console.log(encodeURIComponent('hello world').replace(/%20/g, '+'));  // "hello+world"

2. 加号(+)的处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// +号在application/x-www-form-urlencoded中表示空格
// 需要正确处理

function encodeFormParam(value) {
  return encodeURIComponent(value).replace(/%20/g, '+');
}

function decodeFormParam(value) {
  return decodeURIComponent(value.replace(/\+/g, '%20'));
}

console.log(encodeFormParam('hello world'));  // "hello+world"
console.log(decodeFormParam('hello+world'));  // "hello world"

3. 中文编码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 中文使用UTF-8编码后,每字节转为%XX
const chinese = '你好';
console.log(encodeURIComponent(chinese));
// "%E4%BD%A0%E5%A5%BD"

// 手动实现
function customEncodeURIComponent(str) {
  const utf8Bytes = encodeURIComponent(str).replace(/%/g, '');
  const pairs = utf8Bytes.match(/.{1,2}/g);
  return pairs.map(pair => `%${pair}`).join('');
}

4. 二次编码问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 避免二次编码
function safeEncode(str) {
  try {
    // 先解码再编码
    const decoded = decodeURIComponent(str);
    return encodeURIComponent(decoded);
  } catch {
    // 如果解码失败,说明已经是编码状态
    return str;
  }
}

// 使用示例
const encoded = encodeURIComponent('hello world');
console.log(encoded);  // "hello%20world"
console.log(safeEncode(encoded));  // "hello%20world" (不变)

安全注意事项

1. 防止URL注入攻击

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ❌ 不安全:直接拼接URL
function search(query) {
  const url = `/search?q=${query}`;
  window.location = url;
}

// ✅ 安全:使用URL编码
function search(query) {
  const url = `/search?q=${encodeURIComponent(query)}`;
  window.location = url;
}

2. 验证URL参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 解析并验证URL参数
function safeParseParam(value, type = 'string') {
  const decoded = decodeURIComponent(value);

  switch (type) {
    case 'number':
      const num = Number(decoded);
      if (isNaN(num)) throw new Error('Invalid number');
      return num;

    case 'boolean':
      return decoded === 'true';

    default:
      // 限制长度
      if (decoded.length > 1000) {
        throw new Error('Parameter too long');
      }
      return decoded;
  }
}

3. 处理恶意字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 过滤危险字符
function sanitizeURL(url) {
  // 移除控制字符
  url = url.replace(/[\x00-\x1F\x7F]/g, '');

  // 防止JavaScript伪协议
  if (url.toLowerCase().startsWith('javascript:')) {
    throw new Error('Invalid URL protocol');
  }

  return url;
}

实用工具函数

1. 构建完整URL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function buildURL(baseUrl, params) {
  const url = new URL(baseUrl);

  Object.entries(params).forEach(([key, value]) => {
    url.searchParams.set(key, value);
  });

  return url.toString();
}

// 使用示例
const url = buildURL('https://api.example.com/search', {
  q: '你好世界',
  page: 1,
  sort: 'date'
});
console.log(url);
// "https://api.example.com/search?q=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C&page=1&sort=date"

2. URL参数增删改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class URLParams {
  constructor(url) {
    this.url = new URL(url);
  }

  get(name) {
    return this.url.searchParams.get(name);
  }

  set(name, value) {
    this.url.searchParams.set(name, value);
    return this;
  }

  delete(name) {
    this.url.searchParams.delete(name);
    return this;
  }

  has(name) {
    return this.url.searchParams.has(name);
  }

  toString() {
    return this.url.toString();
  }
}

// 使用示例
const params = new URLParams('https://example.com?page=1&sort=date');
params.set('page', 2);
params.delete('sort');
console.log(params.toString());
// "https://example.com/?page=2"

3. 批量编码对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function encodeObject(obj, prefix = '') {
  const pairs = [];

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const value = obj[key];
      const encodedKey = prefix ? `${prefix}[${key}]` : key;

      if (typeof value === 'object' && value !== null) {
        pairs.push(...encodeObject(value, encodedKey));
      } else {
        pairs.push([
          encodeURIComponent(encodedKey),
          encodeURIComponent(value)
        ]);
      }
    }
  }

  return pairs.map(pair => pair.join('='));
}

// 使用示例
const data = {
  user: {
    name: '张三',
    age: 25
  },
  filters: ['date', 'popular']
};

console.log(encodeObject(data));
// ["user[name]=%E5%BC%A0%E4%B8%89", "user[age]=25", "filters[0]=date", "filters[1]=popular"]

常见问题

Q1: 什么时候使用encodeURI vs encodeURIComponent?

  • encodeURI:编码完整URL,保留URL结构字符(:/?#等)
  • encodeURIComponent:编码URL组件(如参数值),编码所有特殊字符
1
2
3
4
5
6
7
8
const url = 'https://example.com/path';
const query = 'hello world';

// ✅ 正确
encodeURI(url + '?q=' + encodeURIComponent(query));

// ❌ 错误:过度编码
encodeURIComponent(url + '?q=' + query);

Q2: 如何处理已编码的URL?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 检查是否已编码
function isURIEncoded(str) {
  try {
    return decodeURIComponent(str) !== str;
  } catch {
    return false;
  }
}

// 安全编码(避免二次编码)
function safeEncode(str) {
  return isURIEncoded(str) ? str : encodeURIComponent(str);
}

Q3: URL编码有长度限制吗?

不同浏览器和服务器对URL长度有不同限制:

  • IE: 2083字符
  • Chrome: 32173-79177字符
  • Firefox: >65536字符
  • 服务器: 通常2048字符

如果参数过长,考虑使用POST请求。

总结

URL编码是Web开发的基础技能,正确理解和使用它能够:

  • 避免特殊字符导致的错误
  • 防止安全漏洞
  • 提高应用的健壮性

记住这些要点:

  • 使用encodeURIComponent编码参数值
  • 使用encodeURI编码完整URL
  • 始终验证和清理用户输入
  • 注意不同编码格式的区别
  • 使用现代API(URL、URLSearchParams)