Giải thích Unicode: Hướng dẫn cơ bản về mã hóa ký tự

9 phút đọc

Unicode là tiêu chuẩn phổ quát để biểu diễn văn bản trong máy tính. Nó gán một số duy nhất (code point) cho mọi ký tự trong mọi hệ thống chữ viết, từ chữ cái Latin đến chữ Hán đến emoji. Hiểu Unicode là điều cần thiết cho bất kỳ ai làm việc với văn bản đa ngôn ngữ và đa nền tảng.

Unicode là gì?

Trước Unicode, các khu vực khác nhau sử dụng các hệ thống mã hóa khác nhau (ASCII cho tiếng Anh, Shift_JIS cho tiếng Nhật, GB2312 cho tiếng Trung). Điều này gây ra các vấn đề tương thích liên tục khi chia sẻ văn bản giữa các hệ thống. Unicode đã giải quyết vấn đề này bằng cách tạo ra một bảng mã duy nhất bao phủ tất cả các hệ thống chữ viết - hiện tại hơn 154.000 ký tự trên 168 hệ thống chữ viết. Để hiểu sâu hơn về chủ đề này, sách tham khảo mã hóa Unicode cung cấp kiến thức chuyên sâu có giá trị.

Điều quan trọng là phân biệt giữa “bộ ký tự” và “mã hóa.” Unicode định nghĩa bộ ký tự - ánh xạ các ký tự sang code point. UTF-8, UTF-16 và UTF-32 là các mã hóa định nghĩa cách các code point được biểu diễn dưới dạng chuỗi byte. Các tiêu chuẩn cũ như Shift_JIS gộp bộ ký tự và mã hóa lại với nhau, nhưng Unicode cố ý tách biệt chúng. Sự tách biệt này cho phép nhà phát triển chọn mã hóa phù hợp nhất cho trường hợp sử dụng của họ trong khi vẫn làm việc với cùng một bộ ký tự phổ quát.

So sánh UTF-8, UTF-16 và UTF-32

Mã hóaByte mỗi ký tựPhù hợp nhất choGhi chú
UTF-81–4 byteWeb, lưu trữPhổ biến nhất; tương thích ngược với ASCII
UTF-162 hoặc 4 byteWindows, Java, JavaScriptĐược sử dụng nội bộ bởi nhiều ngôn ngữ lập trình
UTF-324 byte (cố định)Xử lý nội bộĐơn giản nhưng tốn bộ nhớ

Thiết kế độ dài thay đổi của UTF-8 dựa trên một sơ đồ mẫu bit thông minh. Các bit đầu của byte đầu tiên cho biết ký tự sử dụng bao nhiêu byte: 0xxxxxxx cho 1 byte (tương thích ASCII), 110xxxxx cho 2 byte, 1110xxxx cho 3 byte, và 11110xxx cho 4 byte. Các byte tiếp theo luôn theo mẫu 10xxxxxx. Thiết kế này có nghĩa là bạn có thể xác định ranh giới ký tự từ bất kỳ vị trí nào trong luồng byte - một lợi thế đáng kể cho xử lý luồng và truy cập ngẫu nhiên. Ngược lại, UTF-16 biểu diễn các ký tự BMP (U+0000–U+FFFF) dưới dạng giá trị 16-bit đơn, nhưng các ký tự ngoài BMP (U+10000 trở lên) yêu cầu cặp thay thế (surrogate pair): một surrogate cao (U+D800–U+DBFF) theo sau bởi một surrogate thấp (U+DC00–U+DFFF). Đây là lý do tại sao emoji và một số ký tự CJK cần xử lý đặc biệt trong các ngôn ngữ sử dụng UTF-16 nội bộ, như JavaScript và Java.

Code point và ký tự

Code point Unicode được viết dưới dạng U+XXXX (ví dụ: U+0041 cho "A"). Mặt phẳng đa ngôn ngữ cơ bản (BMP) bao phủ các code point từ U+0000 đến U+FFFF và bao gồm hầu hết các ký tự thường dùng. Các mặt phẳng bổ sung (U+10000 trở lên) chứa emoji, chữ viết lịch sử và các ký tự hiếm.

Tại sao số ký tự khác với số byte

Trong UTF-8, một ký tự ASCII (A–Z) sử dụng 1 byte, một ký tự có dấu châu Âu sử dụng 2 byte, một ký tự CJK sử dụng 3 byte (xem hướng dẫn của chúng tôi về ký tự toàn chiều rộng so với nửa chiều rộng), và một emoji sử dụng 4 byte. Điều này có nghĩa là một chuỗi 10 ký tự có thể từ 10 đến 40 byte tùy thuộc vào các ký tự được sử dụng.

Emoji và Unicode

Emoji là một trong những yếu tố phức tạp nhất trong Unicode. Như hướng dẫn đếm Unicode emoji của chúng tôi giải thích, chúng phức tạp hơn vẻ bề ngoài:

Ý nghĩa thực tế cho nhà phát triển

  1. Độ dài chuỗi thay đổi theo phương thức: .length của JavaScript đếm đơn vị mã UTF-16, không phải ký tự. Một emoji có thể báo cáo độ dài là 2.
  2. Lưu trữ cơ sở dữ liệu: Sử dụng UTF-8 (utf8mb4 trong MySQL) để hỗ trợ tất cả ký tự Unicode bao gồm emoji
  3. Mã hóa URL: Các ký tự không phải ASCII trong URL phải được mã hóa phần trăm
  4. Sắp xếp: Đối chiếu Unicode phụ thuộc vào ngôn ngữ và phức tạp

Nguồn gốc của Unicode

Khái niệm Unicode bắt đầu vào khoảng năm 1987, khi Joe Becker tại Xerox cùng Lee Collins và Mark Davis tại Apple hình dung việc mã hóa mọi ký tự trên thế giới trong 16 bit (65.536 code point). Ban đầu họ tin rằng 65.536 sẽ là đủ, nhưng khối lượng khổng lồ của các chữ Hán CJK (Trung, Nhật, Hàn) - hàng chục nghìn ký tự - nhanh chóng chứng minh điều ngược lại. Tiêu chuẩn cuối cùng được mở rộng lên 21 bit, chứa hơn 1,1 triệu code point. Sự đánh giá thấp ban đầu này là lý do UTF-16 cần cơ chế cặp thay thế phức tạp cho các ký tự ngoài Mặt phẳng đa ngôn ngữ cơ bản.

Sự phát triển kho ký tự của Unicode diễn ra đều đặn. Unicode 1.0 (1991) chứa khoảng 7.000 ký tự. Unicode 3.0 (1999) mở rộng lên khoảng 49.000 với việc bổ sung CJK Unified Ideographs Extension A. Unicode 6.0 (2010) chính thức tích hợp emoji, đạt khoảng 110.000 ký tự. Unicode 15.1 (2023) chứa khoảng 149.000 ký tự. CJK Unified Ideographs chiếm khoảng 60% tổng số ký tự được gán, minh họa mức độ ảnh hưởng lớn của các hệ thống chữ viết Đông Á đối với không gian Unicode.

CJK Unified Ideographs liên quan đến một quyết định thiết kế gây tranh cãi được gọi là “Han Unification” (Thống nhất chữ Hán). Các ký tự trông hơi khác nhau giữa tiếng Trung, tiếng Nhật và tiếng Hàn được gán cùng một code point. Ví dụ, ký tự “直” có sự khác biệt nhỏ về nét giữa tiếng Nhật và tiếng Trung giản thể, nhưng cả hai đều ánh xạ đến U+76F4. Sự thống nhất này tiết kiệm code point nhưng có thể gây ra vấn đề hiển thị - một văn bản tiếng Nhật có thể hiển thị bằng phông chữ tiếng Trung, tạo ra các glyph không chính xác một cách tinh vi. Cuộc tranh luận về Han Unification vẫn tiếp tục giữa các nhà phát triển và nhà thiết kế chữ CJK.

Tại sao UTF-8 trở thành tiêu chuẩn web

UTF-8 được thiết kế vào năm 1992 bởi Ken Thompson và Rob Pike - cùng Rob Pike sau này đồng sáng tạo ngôn ngữ lập trình Go. Truyền thuyết kể rằng họ đã phác thảo sơ đồ mã hóa trên một chiếc khăn ăn trong nhà hàng. Lợi thế chính của UTF-8 là tương thích ngược hoàn toàn với ASCII: bất kỳ văn bản ASCII hợp lệ nào cũng tự động là UTF-8 hợp lệ. Con đường di chuyển không tốn chi phí này đã thúc đẩy việc áp dụng nhanh chóng, và theo W3Techs, khoảng 98% trang web hiện nay sử dụng UTF-8.

Điều gì xảy ra khi mã hóa sai

Sự không khớp mã hóa gây ra nhiều vấn đề thực tế:

Tác động đến việc đếm ký tự

Cùng một chuỗi có thể có số byte rất khác nhau tùy thuộc vào mã hóa. Từ tiếng Anh “hello” là 5 byte trong UTF-8, nhưng “こんにちは” trong tiếng Nhật là 15 byte trong UTF-8 mặc dù chỉ có 5 ký tự. Sự khác biệt này quan trọng cho việc định kích thước cột cơ sở dữ liệu, giới hạn payload API và tính toán độ dài URL. Bộ đếm ký tự hiển thị cả số ký tự và số byte để giúp bạn lập kế hoạch cho những khác biệt này.

Một vấn đề tinh vi khác là chuẩn hóa Unicode. Cùng một ký tự trực quan có thể được biểu diễn bằng các chuỗi code point khác nhau. Ví dụ, “é” có thể là một code point duy nhất U+00E9 (dạng NFC) hoặc hai code point - “e” (U+0065) + dấu sắc kết hợp (U+0301) ở dạng NFD. Hệ thống file macOS (APFS/HFS+) có xu hướng sử dụng chuẩn hóa NFD, trong khi Windows và Linux thường sử dụng NFC. Điều này có nghĩa là một file được tạo trên macOS có thể không tìm thấy theo tên trên các hệ điều hành khác nếu ứng dụng thực hiện so sánh chuỗi ở mức byte. Khi so sánh chuỗi theo chương trình, chuẩn hóa về NFC trước là thực hành được khuyến nghị.

Emoji: Khi một ký tự không phải là một code point

Emoji là ký tự Unicode, nhưng cấu trúc bên trong của chúng phức tạp hơn vẻ bề ngoài. Emoji gia đình 👨‍👩‍👧‍👦 thực tế là 7 code point được nối bằng Zero Width Joiner (ZWJ, U+200D). Biến thể tông da sử dụng một emoji cơ sở cộng với Skin Tone Modifier - hai code point cho cái trông như một ký tự.

Điều này có ý nghĩa thực tế cho nhà phát triển: thuộc tính .length của JavaScript đếm đơn vị mã UTF-16, không phải ký tự nhìn thấy được. Một emoji gia đình đơn lẻ có thể báo cáo độ dài là 11. Để có được số ký tự “trực quan,” hãy sử dụng API Intl.Segmenter hoặc thư viện phân đoạn cụm grapheme.

Thực hành mã hóa ký tự chuyên nghiệp

Những thực hành này giúp ngăn ngừa các vấn đề liên quan đến mã hóa trước khi chúng xảy ra:

  1. Đặt mã hóa trong .editorconfig: Thêm charset = utf-8 đảm bảo mọi thành viên trong nhóm tạo file với cùng mã hóa
  2. Sử dụng .gitattributes cho ký tự xuống dòng: Đặt * text=auto xử lý sự khác biệt ký tự xuống dòng đa nền tảng một cách tự động
  3. Kiểm tra thanh trạng thái của trình soạn thảo: VS Code hiển thị mã hóa file hiện tại ở góc dưới bên phải. Nhấp vào đó để chuyển đổi giữa các mã hóa
  4. Mặc định sử dụng utf8mb4 trong cơ sở dữ liệu: Chọn utf8mb4 ngay từ đầu để hỗ trợ emoji và tất cả ký tự Unicode, tránh việc di chuyển tốn kém sau này

Tương lai của Unicode

Unicode có thể chứa tới 1.114.112 code point (U+0000 đến U+10FFFF), nhưng chỉ khoảng 154.000 hiện được gán - khoảng 13% tổng không gian. Dung lượng còn lại được dành cho các hệ thống chữ viết lịch sử vẫn đang được phân loại, emoji mới (hàng chục đến hàng trăm được thêm hàng năm), và các mục đích sử dụng trong tương lai chưa được hình dung. Unicode Consortium phát hành phiên bản mới mỗi năm, và tiêu chuẩn tiếp tục phát triển khi giao tiếp bằng chữ viết của nhân loại mở rộng.

Quan niệm sai lầm phổ biến và trường hợp đặc biệt

Ngay cả những nhà phát triển có kinh nghiệm đôi khi cũng có những giả định sai về Unicode. Dưới đây là những cạm bẫy phổ biến nhất:

Kết luận

Unicode là nền tảng của xử lý văn bản hiện đại. Hiểu sự khác biệt giữa ký tự, code point và byte - và biết tại sao UTF-8 trở thành tiêu chuẩn web - giúp bạn xây dựng ứng dụng đa ngôn ngữ mạnh mẽ và tránh các cạm bẫy mã hóa. Để tìm hiểu các mẫu triển khai thực tế, hướng dẫn lập trình quốc tế hóa là nguồn tài liệu tuyệt vời. Sử dụng Bộ đếm ký tự để xem văn bản của bạn đo lường như thế nào theo cả ký tự và byte.