字符编码是所有数字文本的基础。无论你是在调试乱码邮件、修复数据库排序规则,还是搞清楚为什么 JSON 文件在某些字符上出错,理解 ASCII、Unicode 和 UTF-8 都是必不可少的。本综合指南解释了计算机中文本的表示方式、从 ASCII 到 Unicode 的演进过程,以及为什么 UTF-8 编码成为了现代网络的通用标准。
1. ASCII — 7 位的基石
ASCII(美国信息交换标准代码)于 1963 年发布,使用 7 位表示 128 个字符(0–127)。包括 33 个控制字符(0–31 和 127)和 95 个可打印字符(32–126)。
ASCII 专为英文文本和电传打字机设计。每个字符占一个字节,高位(第 7 位)未使用或用于奇偶校验。
95 个可打印字符包括大写字母(A–Z)、小写字母(a–z)、数字(0–9)和 33 个标点/符号字符。控制字符包括 NUL(0)、TAB(9)、LF(10)、CR(13)和 ESC(27)。
| 十进制 | 十六进制 | 字符 | 说明 |
|---|---|---|---|
0 | 00 | NUL | Null character |
9 | 09 | TAB | Horizontal tab |
10 | 0A | LF | Line feed (newline) |
13 | 0D | CR | Carriage return |
27 | 1B | ESC | Escape |
32 | 20 | (space) | Space |
48 | 30 | 0 | Digit zero |
57 | 39 | 9 | Digit nine |
65 | 41 | A | Uppercase A |
90 | 5A | Z | Uppercase Z |
97 | 61 | a | Lowercase a |
122 | 7A | z | Lowercase z |
127 | 7F | DEL | Delete |
2. 扩展 ASCII — 代码页混战
由于 ASCII 只使用 7 位,一个字节中第 8 位(值 128–255)是空闲的。不同系统用这个范围表示不同字符,产生了代码页——不兼容的 ASCII 扩展。
ISO 8859-1(Latin-1)添加了西欧字符,如 é、ü、ñ 和 å。它是 HTTP/1.1 和早期 HTML 的默认编码。Windows-1252 是微软对 ISO 8859-1 的超集,在 128–159 范围内添加了弯引号(“ ”)、长破折号(—)和欧元符号(€)等字符。
根本问题是:字节 0xE9 在 Latin-1 中表示 é,在 Windows-1251(西里尔文)中表示 щ,在 Windows-1252 中表示 é。如果不知道使用的是哪个代码页,就无法正确解码文本。这就是为什么"乱码"在早期互联网上如此猖獗。
其他著名的代码页包括 ISO 8859-5(西里尔文)、ISO 8859-15(Latin-9,用欧元符号替代了 Latin-1)、Shift_JIS 和 EUC-JP(日文)、Big5(繁体中文)以及 GB2312/GBK(简体中文)。
# Same byte, different interpretations:
Byte: 0xE9
Latin-1 (ISO 8859-1): é (LATIN SMALL LETTER E WITH ACUTE)
Windows-1251 (Cyrillic): щ (CYRILLIC SMALL LETTER SHCHA)
Windows-874 (Thai): ้ (THAI CHARACTER MAI THO)
# This is why encoding metadata is critical!3. Unicode — 统一字符集
Unicode 是一个通用字符集,为每种文字中的每个字符分配一个唯一的码位。码位写成 U+ 后跟 4–6 个十六进制数字(如 U+0041 = A,U+4E16 = 世,U+1F600 = 😀)。
Unicode 目前定义了超过 154,000 个字符,涵盖 168 种文字,从拉丁文和西里尔文到埃及象形文字和表情符号。最大码位为 U+10FFFF,理论上限为 1,114,112 个码位。
Unicode 被组织为 17 个<strong>平面</strong>,每个平面包含 65,536 个码位:
| 平面 | 名称 | 码位范围 | 内容 |
|---|---|---|---|
0 | Basic Multilingual Plane (BMP) | U+0000 - U+FFFF | Latin, Cyrillic, Greek, CJK, Arabic, Hebrew, most symbols |
1 | Supplementary Multilingual Plane (SMP) | U+10000 - U+1FFFF | Emoji, historic scripts, musical notation, math symbols |
2 | Supplementary Ideographic Plane (SIP) | U+20000 - U+2FFFF | Rare CJK ideographs |
3-13 | Unassigned | U+30000 - U+DFFFF | Reserved for future use |
14 | Supplementary Special-purpose Plane (SSP) | U+E0000 - U+EFFFF | Tag characters, variation selectors |
15-16 | Private Use Areas | U+F0000 - U+10FFFF | Custom characters (not standardized) |
关键区别:Unicode 是一个字符集(将数字映射到字符),不是编码。编码决定了这些数字如何存储为字节。这就是 UTF-8、UTF-16 和 UTF-32 的作用。
# Unicode code points are abstract numbers:
U+0041 → A (Latin capital letter A)
U+00E9 → é (Latin small letter e with acute)
U+4E16 → 世 (CJK Unified Ideograph - "world")
U+1F600 → 😀 (Grinning face emoji)
# These are just numbers — the encoding determines
# how they become bytes in memory or files.4. UTF-8 编码 — 可变长度的精妙设计
UTF-8(Unicode 转换格式 — 8 位)是一种可变长度编码,每个字符使用 1 到 4 个字节。它由 Ken Thompson 和 Rob Pike 于 1992 年设计,现在是网络上占主导地位的编码(超过 98% 的网站使用)。
UTF-8 的设计非常优雅:它与 ASCII 向后兼容(任何有效的 ASCII 文件也是有效的 UTF-8),它是自同步的(你可以从任何位置找到字符边界),而且非 NUL 字符永远不会产生零字节(对 C 字符串安全)。
编码算法使用前缀系统来指示字节数:
| 字节数 | 码位范围 | 字节 1 | 字节 2 | 字节 3 | 字节 4 |
|---|---|---|---|---|---|
1 | U+0000 - U+007F | 0xxxxxxx | - | - | - |
2 | U+0080 - U+07FF | 110xxxxx | 10xxxxxx | - | - |
3 | U+0800 - U+FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | - |
4 | U+10000 - U+10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
让我们跟踪编码字符 é(U+00E9,带尖音符的拉丁小写字母 e)的过程:
# Encoding é (U+00E9) to UTF-8:
# Step 1: 0x00E9 = 0000 0000 1110 1001 in binary
# Step 2: U+00E9 falls in range U+0080–U+07FF → 2 bytes
# Step 3: Template: 110xxxxx 10xxxxxx
# Step 4: Fill in bits from right:
# 00E9 = 000 1110 1001
# Byte 1: 110_00011 = 0xC3
# Byte 2: 10_101001 = 0xA9
# Result: é = C3 A9 (2 bytes in UTF-8)
echo -n "é" | xxd
# Output: 00000000: c3a9一个更复杂的例子——中文字符 世(U+4E16,意为"世界"):
# Encoding 世 (U+4E16) to UTF-8:
# Step 1: 0x4E16 = 0100 1110 0001 0110 in binary
# Step 2: U+4E16 falls in range U+0800–U+FFFF → 3 bytes
# Step 3: Template: 1110xxxx 10xxxxxx 10xxxxxx
# Step 4: Fill in bits:
# 4E16 = 0100 1110 0001 0110
# Byte 1: 1110_0100 = 0xE4
# Byte 2: 10_111000 = 0xB8
# Byte 3: 10_010110 = 0x96
# Result: 世 = E4 B8 96 (3 bytes in UTF-8)
echo -n "世" | xxd
# Output: 00000000: e4b8 96对于表情符号如 😀(U+1F600,微笑脸):
# Encoding 😀 (U+1F600) to UTF-8:
# Step 1: 0x1F600 = 0001 1111 0110 0000 0000 in binary
# Step 2: U+1F600 falls in range U+10000–U+10FFFF → 4 bytes
# Step 3: Template: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
# Step 4: Fill in bits:
# 1F600 = 0 0001 1111 0110 0000 0000
# Byte 1: 11110_000 = 0xF0
# Byte 2: 10_011111 = 0x9F
# Byte 3: 10_011000 = 0x98
# Byte 4: 10_000000 = 0x80
# Result: 😀 = F0 9F 98 80 (4 bytes in UTF-8)
echo -n "😀" | xxd
# Output: 00000000: f09f 98805. UTF-16 和 UTF-32 — UTF-8 的替代方案
UTF-16 每个字符使用 2 或 4 个字节。BMP 中的字符(U+0000–U+FFFF)使用 2 个字节。BMP 外的字符使用代理对——两个 16 位代码单元。
代理对的工作方式:从码位减去 0x10000,将得到的 20 位分成高代理(0xD800–0xDBFF)和低代理(0xDC00–0xDFFF)。
例如,😀(U+1F600):0x1F600 - 0x10000 = 0xF600。高 10 位:0x3D → 0xD800 + 0x3D = 0xD83D。低 10 位:0x200 → 0xDC00 + 0x200 = 0xDE00。所以 😀 在 UTF-16 中是 D83D DE00。
UTF-16 的使用场景:JavaScript 字符串(String.charCodeAt() 返回 UTF-16 代码单元)、Java 的 char 类型、Windows API(wchar_t)以及 .NET 的 System.String。
// JavaScript uses UTF-16 internally
const emoji = "😀";
// .length counts UTF-16 code units, NOT characters
console.log(emoji.length); // 2 (surrogate pair)
console.log(emoji.charCodeAt(0)); // 55357 (0xD83D - high surrogate)
console.log(emoji.charCodeAt(1)); // 56832 (0xDE00 - low surrogate)
// Use codePointAt() for actual Unicode code points
console.log(emoji.codePointAt(0)); // 128512 (0x1F600)
// Use spread or Array.from for correct character counting
console.log([...emoji].length); // 1 (correct!)
console.log(Array.from(emoji).length); // 1
// String.fromCodePoint handles supplementary characters
console.log(String.fromCodePoint(0x1F600)); // 😀UTF-32 每个字符固定使用 4 个字节。它是最简单的编码(码位 = 存储值),但对拉丁文本浪费空间(ASCII 的 4 倍大小)。它在一些程序内部用于简化字符索引,但很少用于存储或传输。
| 编码 | 大小 | 主要用途 |
|---|---|---|
| UTF-8 | 1-4 bytes/char | Web, files, APIs, JSON, databases |
| UTF-16 | 2 or 4 bytes/char | JavaScript, Java, Windows, .NET |
| UTF-32 | 4 bytes/char (fixed) | Internal processing, random access |
6. BOM(字节顺序标记)
字节顺序标记(BOM)是一个特殊的 Unicode 字符 U+FEFF,放在文件开头以指示其编码和字节顺序。
对于 UTF-16 和 UTF-32,BOM 是必不可少的——它告诉读取器文件使用的是大端序还是小端序字节顺序。
| 编码 | BOM 字节 | 说明 |
|---|---|---|
| UTF-8 | EF BB BF | Optional (discouraged) |
| UTF-16 BE | FE FF | Big-endian byte order |
| UTF-16 LE | FF FE | Little-endian byte order |
| UTF-32 BE | 00 00 FE FF | Big-endian byte order |
| UTF-32 LE | FF FE 00 00 | Little-endian byte order |
UTF-8 BOM 争议:UTF-8 没有字节顺序问题(字节始终按相同顺序),所以 BOM 在技术上是不必要的。Microsoft 工具(记事本、Excel)默认添加 UTF-8 BOM(EF BB BF),这可能导致问题:
- PHP 脚本可能在 headers 之前输出 BOM,导致"headers already sent"错误
- 如果 shebang 行前面有 BOM 字节,Shell 脚本可能失败
- JSON 解析器可能拒绝以 BOM 开头的文件(RFC 8259 禁止这样做)
- 连接文件可能将 BOM 嵌入输出的中间
建议:除非特定工具要求(如 Excel CSV 导入),否则不要对 UTF-8 文件使用 BOM。大多数现代编辑器和系统无需 BOM 即可处理 UTF-8。
# Check for BOM in a file:
xxd file.txt | head -1
# UTF-8 BOM: 00000000: efbb bf...
# Remove UTF-8 BOM with sed (Linux/macOS):
sed -i '1s/^\xEF\xBB\xBF//' file.txt
# Remove BOM with Python:
with open('file.txt', 'rb') as f:
content = f.read()
if content.startswith(b'\xef\xbb\xbf'):
content = content[3:]
with open('file.txt', 'wb') as f:
f.write(content)7. HTML 中的编码
正确的编码声明确保浏览器正确渲染页面。浏览器通过三种方式确定编码:
1. HTTP Content-Type 头(最高优先级):
Content-Type: text/html; charset=utf-82. HTML meta 标签(必须在前 1024 字节内):
<!-- HTML5 (recommended) -->
<meta charset="UTF-8">
<!-- HTML4 / XHTML equivalent -->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">3. HTML 字符引用用于单个字符:
<!-- Named entity -->
<p>Copyright © 2025</p>
<!-- Decimal numeric entity -->
<p>© = é = 世</p>
<!-- Hex numeric entity -->
<p>© = é = 世</p>
<!-- Emoji via hex entity -->
<p>😀 = 😀</p>优先顺序:HTTP 头 > BOM > meta 标签 > 编码嗅探。始终同时设置 HTTP 头和 meta 标签以获得最大可靠性。
对于 HTML5,<meta charset="UTF-8"> 声明应始终是 <head> 内的第一个元素,以确保在解析任何文本内容之前就被处理。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"> <!-- Must be first! -->
<title>My Page</title>
</head>
<body>
<p>café 世界 😀</p> <!-- All render correctly with UTF-8 -->
</body>
</html>8. 编程语言中的编码
不同编程语言处理字符串编码的方式不同。以下是主要语言的工作方式:
Python 3 区分 str(文本,Unicode)和 bytes(原始字节)。所有字符串默认为 Unicode:
# Python 3: str is Unicode, bytes is raw bytes
text = "café 世界 😀" # str (Unicode)
encoded = text.encode('utf-8') # bytes
decoded = encoded.decode('utf-8') # back to str
print(type(text)) # <class 'str'>
print(type(encoded)) # <class 'bytes'>
print(encoded) # b'caf\xc3\xa9 \xe4\xb8\x96\xe7\x95\x8c \xf0\x9f\x98\x80'
print(len(text)) # 8 (characters)
print(len(encoded)) # 18 (bytes)
# Handling encoding errors
bad_bytes = b'\xc3\x28' # Invalid UTF-8
# text = bad_bytes.decode('utf-8') # UnicodeDecodeError!
text = bad_bytes.decode('utf-8', errors='replace') # '\ufffd('
text = bad_bytes.decode('utf-8', errors='ignore') # '('
# Reading files with explicit encoding
with open('file.txt', 'r', encoding='utf-8') as f:
content = f.read()JavaScript / TypeScript 内部使用 UTF-16。TextEncoder 和 TextDecoder API 处理编码转换:
// JavaScript: strings are UTF-16, use TextEncoder for UTF-8
const text = "café 世界 😀";
// TextEncoder converts string → UTF-8 bytes
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
console.log(bytes); // Uint8Array(18) [99, 97, 102, ...]
console.log(bytes.length); // 18 bytes
// TextDecoder converts UTF-8 bytes → string
const decoder = new TextDecoder('utf-8');
const decoded = decoder.decode(bytes);
console.log(decoded); // "café 世界 😀"
// Handling errors
const badBytes = new Uint8Array([0xC3, 0x28]);
const strictDecoder = new TextDecoder('utf-8', { fatal: true });
try {
strictDecoder.decode(badBytes); // throws TypeError
} catch (e) {
console.log('Invalid UTF-8 sequence');
}
// Correct character counting with Intl.Segmenter
const segmenter = new Intl.Segmenter();
const graphemes = [...segmenter.segment(text)];
console.log(graphemes.length); // 8 (correct grapheme clusters)Go 使用 rune 类型(int32 的别名)表示 Unicode 码位。字符串是 UTF-8 字节序列:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
text := "café 世界 😀"
// len() returns byte count
fmt.Println(len(text)) // 18 bytes
// utf8.RuneCountInString() returns character count
fmt.Println(utf8.RuneCountInString(text)) // 8 runes
// Range over string iterates by rune (not byte)
for i, r := range text {
fmt.Printf("byte %d: U+%04X %c (%d bytes)\n",
i, r, r, utf8.RuneLen(r))
}
// byte 0: U+0063 c (1 bytes)
// byte 1: U+0061 a (1 bytes)
// byte 2: U+0066 f (1 bytes)
// byte 3: U+00E9 é (2 bytes)
// byte 5: U+0020 (1 bytes)
// byte 6: U+4E16 世 (3 bytes)
// byte 9: U+754C 界 (3 bytes)
// byte 12: U+0020 (1 bytes)
// byte 13: U+1F600 😀 (4 bytes)
}9. 常见编码错误
乱码(日语中的"文字化け")是用错误编码解码字节导致的乱码文本。以下是常见模式和修复方法:
常见乱码模式及其原因:
é而不是é— UTF-8 字节被当作 Latin-1 解读蟩而不是世— UTF-8 字节被当作 Latin-1 解读(3 字节字符)���(替换字符)— UTF-8 解码中的无效字节序列???(问号)— 字符在目标编码中无法表示
# Why does "é" become "é"?
# UTF-8 encodes é as two bytes: C3 A9
# If those bytes are read as Latin-1:
# C3 → Ã
# A9 → ©
# Result: "é" instead of "é"
# Why does "世" become "世"?
# UTF-8 encodes 世 as three bytes: E4 B8 96
# If those bytes are read as Latin-1:
# E4 → ä B8 → ¸ 96 → (control char, often shown as –)
# Result: "世" instead of "世"调试工具:
xxd或hexdump— 检查文件中的原始字节file -i filename(Linux/macOS)— 检测文件编码iconv -f FROM -t TO filename— 在编码之间转换chardet/chardetect(Python 库)— 自动检测编码
使用 iconv 的常见修复方法:
# Convert from Latin-1 to UTF-8:
iconv -f ISO-8859-1 -t UTF-8 input.txt > output.txt
# Convert from Windows-1252 to UTF-8:
iconv -f WINDOWS-1252 -t UTF-8 input.txt > output.txt
# List all available encodings:
iconv -l
# Detect encoding with chardet (Python):
pip install chardet
chardetect file.txt
# file.txt: utf-8 with confidence 0.99在 Python 中,你可以修复双重编码的文本:
# Fix double-encoded UTF-8 in Python:
# If UTF-8 bytes were incorrectly decoded as Latin-1 then re-encoded
broken = "café" # é was double-encoded
fixed = broken.encode('latin-1').decode('utf-8')
print(fixed) # "café"
# Using chardet to auto-detect encoding:
import chardet
with open('mystery.txt', 'rb') as f:
raw = f.read()
result = chardet.detect(raw)
print(result)
# {'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}
text = raw.decode(result['encoding'])10. 表情符号编码
表情符号是 Unicode 字符,主要位于补充多语言平面(U+1F000–U+1FFFF)和其他补充平面。在 UTF-8 中,大多数表情符号需要 4 个字节。
# Simple emoji: single code point
😀 = U+1F600
UTF-8: F0 9F 98 80 (4 bytes)
UTF-16: D83D DE00 (4 bytes, surrogate pair)ZWJ(零宽连接符)序列使用 U+200D 将多个表情符号组合成单个字形。例如,家庭表情符号由单个人物表情符号通过 ZWJ 连接而成:
# ZWJ (Zero Width Joiner) sequences:
# 👨👩👧👦 = Man + ZWJ + Woman + ZWJ + Girl + ZWJ + Boy
# U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
# In JavaScript:
const family = "👨👩👧👦";
console.log(family.length); // 11 (UTF-16 code units!)
console.log([...family].length); // 7 (code points, still wrong!)
// Correct: use Intl.Segmenter
const seg = new Intl.Segmenter();
console.log([...seg.segment(family)].length); // 1 (one grapheme cluster)
# In Python:
import unicodedata
family = "👨\u200D👩\u200D👧\u200D👦"
print(len(family)) # 7 code points
# For grapheme clusters, use the 'grapheme' library变体选择器修改字符的呈现方式。U+FE0F(VS16)请求表情呈现,而 U+FE0E(VS15)请求文本呈现:
# Variation selectors change presentation:
# ❤ (U+2764) + U+FE0F → ❤️ (emoji presentation, red heart)
# ❤ (U+2764) + U+FE0E → ❤︎ (text presentation, outline heart)
# ☺ (U+263A) + U+FE0F → ☺️ (emoji style)
# ☺ (U+263A) + U+FE0E → ☺︎ (text style)
# In HTML:
# <span>❤️</span> → red emoji heart
# <span>❤︎</span> → text-style heart肤色修饰符(U+1F3FB–U+1F3FF)跟在基础表情符号后面以改变肤色。基础 + 修饰符显示为单个字符:
# Skin tone modifiers (Fitzpatrick scale):
# 👋 (U+1F44B) + 🏻 (U+1F3FB) → 👋🏻 (light skin)
# 👋 (U+1F44B) + 🏽 (U+1F3FD) → 👋🏽 (medium skin)
# 👋 (U+1F44B) + 🏿 (U+1F3FF) → 👋🏿 (dark skin)
# Each modifier adds 4 bytes in UTF-8:
# 👋 alone: F0 9F 91 8B (4 bytes)
# 👋🏽 : F0 9F 91 8B F0 9F 8F BD (8 bytes)国旗序列使用成对的区域指示符号(U+1F1E6–U+1F1FF)。每个字母对应一个国家代码:
# Flag emoji use Regional Indicator Symbols:
# 🇺🇸 = U+1F1FA (RI U) + U+1F1F8 (RI S) = US flag
# 🇯🇵 = U+1F1EF (RI J) + U+1F1F5 (RI P) = JP flag
# 🇩🇪 = U+1F1E9 (RI D) + U+1F1EA (RI E) = DE flag
# Each regional indicator is 4 bytes in UTF-8
# So each flag emoji is 8 bytes in UTF-8由于这些组合机制,一个可见的"字符"(字素簇)可以包含许多 Unicode 码位和许多字节。带有肤色的家庭表情符号在 UTF-8 中可以超过 25 个字节。
11. 最佳实践
遵循以下规则以避免项目中的编码问题:
- 始终使用 UTF-8。在编辑器、数据库、HTTP 头和 HTML meta 标签中都设置它。新项目没有理由使用其他编码。
- 显式声明编码。永远不要依赖编码检测或默认值。在 HTML 中使用
<meta charset="UTF-8">,在 HTTP 头中使用Content-Type: text/html; charset=utf-8。 - 数据库排序规则很重要。在 MySQL/MariaDB 中使用
utf8mb4(而不是utf8)。MySQL 的utf8仅支持 3 字节字符(不支持表情符号)。对于排序规则,使用utf8mb4_unicode_ci或utf8mb4_0900_ai_ci。 - 将文件保存为不带 BOM 的 UTF-8。配置编辑器以 UTF-8 格式保存。除非特别要求,否则避免使用 BOM。
- 在边界处理编码。尽早将字节解码为字符串(在输入时),尽晚将字符串编码为字节(在输出时)。
- 使用非 ASCII 数据测试。在测试数据中使用像
"café 世界 😀"这样的字符串,以便尽早发现编码错误。 - 使用参数化查询。永远不要用字符串拼接构造 SQL。参数化查询可以正确处理编码并防止 SQL 注入。
- 规范化 Unicode。使用 NFC 规范化进行存储和比较。字符 é 可以表示为单个码位(U+00E9)或 e + 组合尖音符(U+0065 U+0301)。NFC 确保使用一致的形式。
使用正确编码创建数据库的示例:
-- MySQL / MariaDB: Always use utf8mb4
CREATE DATABASE myapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
bio TEXT
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- PostgreSQL: UTF-8 is the default and recommended encoding
CREATE DATABASE myapp
ENCODING 'UTF8'
LC_COLLATE 'en_US.UTF-8'
LC_CTYPE 'en_US.UTF-8';
-- Check current encoding:
-- MySQL: SHOW VARIABLES LIKE 'character_set%';
-- PostgreSQL: SHOW server_encoding;# .editorconfig — enforce UTF-8 across your project
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true12. 常见问题
ASCII 和 UTF-8 有什么区别?
ASCII 是一个 7 位字符集,包含 128 个字符(英文字母、数字、符号和控制字符)。UTF-8 是一种可变长度编码,可以表示所有 154,000 多个 Unicode 字符。UTF-8 向后兼容 ASCII——前 128 个 UTF-8 字符与 ASCII 完全相同,各使用一个字节。ASCII 之外的字符在 UTF-8 中使用 2–4 个字节。
为什么应该使用 UTF-8 而不是 UTF-16 或 UTF-32?
UTF-8 对于以拉丁文为主的文本(包括大多数代码、标记和网页内容)是最节省空间的。它向后兼容 ASCII,因此现有的 ASCII 工具无需更改即可使用。它也是网络(98% 以上的网站)、JSON(RFC 8259 要求)和大多数现代 API 的标准。UTF-16 在 JavaScript 和 Java 内部使用,UTF-32 用于方便的码位索引,但两者都不推荐用于交换或存储。
如何修复文本中的乱码?
首先,通过检查字节模式确定原始编码。常见模式:é 而不是 é 意味着 UTF-8 被错误地当作 Latin-1 读取。修复方法:在 Python 中使用 text.encode("latin-1").decode("utf-8")。对于文件,使用 iconv -f WRONG_ENCODING -t utf-8 file.txt。Python 的 chardet 库可以自动检测原始编码。
Unicode 和 UTF-8 有什么区别?
Unicode 是一个字符集——从数字(码位)到字符的映射。例如,U+0041 = A,U+4E16 = 世。UTF-8 是一种编码——将这些码位转换为字节以进行存储和传输的规则。可以把 Unicode 看作字典,UTF-8 看作书写风格。UTF-16 和 UTF-32 等其他编码也可以表示 Unicode 字符,只是使用不同的字节模式。
为什么 MySQL 的 utf8 不支持表情符号,应该用什么替代?
MySQL 的 utf8 字符集实际上是一个非标准的最大 3 字节 UTF-8 子集。它无法存储在 UTF-8 中需要 4 个字节的字符,包括所有表情符号(如 😀 U+1F600)和许多 CJK 字符。请改用 utf8mb4,它支持完整的 UTF-8 范围(1–4 字节)。创建数据库或表时,请指定:CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。
理解字符编码是构建可靠软件的基础。从 ASCII 到 Unicode 再到 UTF-8 的演进解决了全球文本表示问题。始终使用 UTF-8,显式声明编码,并使用多样化的字符进行测试以避免编码错误。