Bạn có từng nghĩ Unicode chỉ là bảng mã 16-bit đơn giản? Nếu vậy, bạn không hề đơn độc, nhưng sự thật thú vị hơn nhiều!
Nhiều người vẫn lầm tưởng rằng Unicode chỉ sử dụng 16 bit để mã hóa, dẫn đến giới hạn 65.536 ký tự. Tuy nhiên, quan niệm này không hoàn toàn chính xác.
Mặc dù phiên bản đầu tiên của Unicode (từ 1991 đến 1995) thực sự sử dụng 16 bit, Unicode 2.0 (ra mắt tháng 6/1996) đã mở rộng không gian mã hóa. Unicode hiện tại mã hóa ký tự trong khoảng từ U+0000 đến U+10FFFF, tương ứng với không gian mã 21 bit. Tùy thuộc vào phương pháp mã hóa (UTF-8, UTF-16, UTF-32), mỗi ký tự được biểu diễn bằng chuỗi từ 1-4 byte (UTF-8), 1-2 đơn vị mã 16 bit (UTF-16), hoặc 1 đơn vị mã 32 bit duy nhất (UTF-32).
Trước khi Unicode xuất hiện, thế giới tồn tại hàng trăm hệ mã hóa khác nhau, nhưng không hệ nào bao quát đầy đủ mọi ký tự. ASCII, bảng mã dựa trên chữ Latin cho tiếng Anh, là một ví dụ điển hình.
ASCII sử dụng 7 bit để biểu diễn (giá trị thập phân từ 0 đến 127). Các ký tự từ 32 đến 127 là các ký tự in được, ví dụ ‘ ‘ (dấu cách) là 32, ‘A’ là 65. Các mã dưới 32 là ký tự điều khiển (ví dụ ESC, Backspace).
Với 7 hoặc 8 bit, bạn chỉ biểu diễn được tối đa 256 ký tự. Điều này đủ cho tiếng Anh, nhưng không đáp ứng được nhu cầu của các ngôn ngữ khác. Các hệ mã hóa khác thường xung đột, sử dụng cùng một số cho các ký tự khác nhau, hoặc ngược lại. Mỗi máy tính hỗ trợ nhiều chuẩn mã hóa, dẫn đến nguy cơ sai lệch khi trao đổi dữ liệu.
Unicode ra đời để giải quyết vấn đề này, cung cấp một biểu diễn số duy nhất cho mỗi ký tự, bất kể nền tảng, chương trình hay ngôn ngữ nào. Chuẩn Unicode (Unicode Standard) cung cấp phương thức nhất quán để mã hóa toàn bộ ngôn ngữ viết trên thế giới. Để đơn giản hóa và tăng hiệu quả, Unicode gán một số duy nhất cho mỗi ký tự. Unicode hỗ trợ UTF-8, UTF-16 và UTF-32 để mã hóa cùng một bộ ký tự.
Bảng Mã Unicode
Vậy, Unicode thực sự là gì? Unicode là một bảng mã, ánh xạ một số duy nhất (code point) đến một ký tự. Ký tự này có thể là chữ cái (tiếng Anh “a”, “b”, tiếng Việt “á”, “ớ”), chữ tượng hình (tiếng Nhật) hoặc ký tự đặc biệt (“$”, “%”, dấu chấm câu).
Mỗi số được gọi là một điểm mã (code point), một khái niệm lý thuyết. Việc điểm mã được biểu diễn trong bộ nhớ hoặc ổ đĩa là một vấn đề khác. Điểm mã được biểu diễn dưới dạng U+0639, với “U+” tượng trưng cho “Unicode” và phần hệ số là hệ hexa. Ví dụ, U+0041 là số hexa 0041 (tương đương số thập phân 65), biểu diễn ký tự “A”.
Mỗi ký tự được gán một tên duy nhất để phân biệt, ví dụ U+0041 là “LATIN CAPITAL LETTER A”, U+0A1B là “GURMUKHI LETTER CHA”.
Ví dụ, xâu ký tự “Hello” trong Unicode tương ứng với 5 điểm mã:
U+0048 U+0065 U+006C U+006C U+006F
Số lượng ký tự mà Unicode có thể định nghĩa là rất lớn, vượt xa 65.536. Phiên bản Unicode mới nhất (9.0.0) biểu diễn 128.172 ký tự.
Một số thuật ngữ thường dùng trong Unicode bao gồm: không gian mã (code space), plane, code unit, block…
Không gian mã là không gian chứa tất cả điểm mã Unicode. Unicode chia thành 17 plane, mỗi plane chứa 65.536 ký tự (tương đương 16 bit), do đó tổng kích thước không gian mã là 17 × 65.536 = 1.114.112. Phiên bản Unicode 9.0 chỉ sử dụng chưa tới 10% không gian mã. Unicode giới hạn ở 17 plane, không mở rộng thêm, vì không gian mã hiện tại đã đủ cho mục tiêu của Unicode.
Để hiểu rõ hơn về plane, hãy xem lại định dạng điểm mã Unicode U+0639. Unicode mã hóa ký tự trong dải từ U+0000 đến U+10FFFF. Mỗi plane sử dụng 65.536 ký tự, tương đương 0000 – FFFF trong hệ hexa. 17 plane được đánh số từ 0 đến 16 (00-10 trong hệ hexa), chiếm 2 vị trí đầu tiên trong định dạng 6 số (hhhhhh).
Định dạng điểm mã Unicode có dạng U+000639, với “U+” là Unicode, 2 số đầu mô tả plane, 4 số cuối là điểm mã trong plane đó. Plane đầu tiên là Basic Multilingual Plane (plane 0), chứa gần như hầu hết hệ thống chữ viết và ký hiệu thường dùng trên thế giới (U+0000 đến U+FFFF).
Đơn vị mã (code unit) liên quan đến phương thức mã hóa. UTF-8 có code unit là 1 byte, UTF-16 là 2 byte, UTF-32 là 4 byte.
Blocks là một dải các điểm mã có đặc điểm chung về ngôn ngữ hoặc chức năng. Block đầu tiên (U+0000 đến U+001F) biểu diễn các ký tự điều khiển (32 ký tự). Block tiếp theo (Basic Latin, U+0020 đến U+007F) biểu diễn các ký tự trong bảng mã ASCII.
Tiếng Việt sử dụng ký tự trong block Basic Latin (không dấu, như “a”, “A”) và các block Latin-1 Supplement, Latin Extended-B và Latin Extended Additional (có dấu, như “á”, “ấ”, “Ớ”). Bạn có thể tìm thông tin về các Unicode block tại đây.
Các Phương Thức Mã Hóa Unicode (Encoding)
Phương thức dịch điểm mã sang nhị phân gọi là mã hóa ký tự (character encoding). Unicode có 3 phương thức mã hóa: UTF-8, UTF-16 và UTF-32.
-
UTF-32: Đơn giản nhất, mỗi điểm mã được biểu diễn trực tiếp bằng một đơn vị mã 32 bit.
-
UTF-16: Điểm mã trong plane 0 (U+0000 đến U+FFFF) được biểu diễn bằng một đơn vị mã 16 bit. Các điểm mã từ plane 1 trở đi cần một cặp đơn vị mã 16 bit.
-
UTF-8: Sử dụng đơn vị mã 8 bit và có thể biểu diễn mọi ký tự trong dải U+0000 đến U+10FFFF. UTF-8 tương thích với ASCII.
UTF-8 là phương thức mã hóa phổ biến nhất hiện nay. Hiểu cách UTF-8 mã hóa giúp chúng ta có cái nhìn toàn diện hơn về mã hóa ký tự. Ưu điểm của UTF-8:
- Biểu diễn được mọi điểm mã Unicode.
- Tương thích với ASCII.
- Tiết kiệm không gian hơn UTF-16 hoặc UTF-32. Ký tự mã hóa theo UTF-8 có thể nằm trong khoảng từ 1-4 byte. Văn bản tiếng Anh thông thường chỉ cần 1-2 byte.
- Không yêu cầu sử dụng BOM (byte order mark).
Để hiểu rõ hơn, hãy xem bảng sau:
- Với ký tự chỉ có 1 byte (hầu hết là ASCII), bit đầu tiên luôn là 0 để tương thích.
- Với ký tự multi-byte, byte đầu tiên bắt đầu với 2-4 số 1 để biểu diễn số byte ký tự sẽ dùng, theo sau là một số 0.
- Phần bit còn lại trong byte đầu tiên và những byte tiếp theo được dùng để điền những bit biểu diễn điểm mã, ngoại trừ việc mỗi byte tiếp theo sẽ bắt đầu với 10.
Ví dụ U+00A3 (ký hiệu bảng Anh £), giá trị thập phân của A3 là 163, chuyển sang nhị phân là 10100011 (8 bit). Vì là single-byte phải bắt đầu bằng 0, nên cần 2 byte để biểu diễn U+00A3.
Vì là ký tự multi-byte, byte đầu tiên bắt đầu bằng hai số 1 để chỉ ra rằng có hai byte cần được sử dụng, theo sau là một số 0. Lúc này byte đầu tiên sẽ có dạng là 110xxxxx.
Những byte sau byte đầu tiên luôn được bắt đầu với 10. Vì vậy, 3 bit đầu tiên của byte thứ nhất là 110, 2 bit đầu tiên của byte thứ hai là 10, chúng ta đã dùng 5 bit. Còn lại 11 bit để lấp đầy bằng điểm mã. Vì giá trị nhị phân của U+00A3 là 10100011 (8 bit), ta thêm 3 số 0 vào đầu cho đủ 11 bit, thành 00010100011, rồi lấp vào số bit còn thừa đã tính ở trên, cắt từ trái sang phải. Như vậy byte đầu tiên sẽ là 11000010, byte tiếp theo là 10100011.
Ứng Dụng
Bây giờ bạn đã hiểu về Unicode và UTF-8. Hãy thử trả lời các câu hỏi sau (lưu ý lưu lại với encoding là UTF-8):
$str_jp = 'た';
$str_vn_1 = 'á';
$str_vn_2 = 'ớ';
// 1. strlen($str_jp)
// 2. mb_strlen($str_jp, 'UTF-8');
// 3. $str_jp[0];
// 4. $str_jp[0] . $str_jp[1] . $str_jp[2];
// 5. strlen($str_vn_1);
// 6. strlen($str_vn_2);
Đáp án:
- 3
- 1
- �
- た
- 2
- 3
strlen đo chiều dài dựa trên số byte, còn mb_strlen đo dựa trên encoding. Tiếng Nhật mã hóa theo UTF-8 cần 3 byte, nên strlen trả về 3, mb_strlen trả về 1.
$str[index] trả về byte tại vị trí index, nên kết quả lỗi, vì trình duyệt không hiểu (11100011 không trùng với ký tự nào).
Câu 4 nối 3 byte lại với nhau. Unicode dựa vào byte đầu tiên (11100011) để biết rằng cần 3 byte để biểu diễn ký tự, sau đó tìm thêm 2 byte tiếp theo.
Ở câu thứ 5 và 6, tiếng Việt sử dụng ký tự thuộc nhiều block khác nhau, có ở cả Basic Latin (1 byte), Latin-1 Supplement, Latin Extended-B và Latin Extended Additional (1-3 byte), nên kết quả strlen thay đổi tùy theo ký tự.
Mong rằng bài viết này giúp bạn hiểu rõ hơn về Unicode và xử lý ký tự.
Tài liệu tham khảo:
