Ký tự toàn chiều rộng và nửa chiều rộng | Ảnh hưởng đến việc đếm ký tự

11 phút đọc

Khi làm việc với văn bản bao gồm các ký tự Đông Á, việc hiểu sự khác biệt giữa ký tự "toàn chiều rộng" và "nửa chiều rộng" là rất cần thiết. Sự phân biệt này ảnh hưởng đến kết quả đếm ký tự, giới hạn nhập liệu biểu mẫu, kích thước lưu trữ cơ sở dữ liệu và thậm chí cả mã hóa URL. Dù bạn là nhà phát triển, người viết hay người dùng thông thường, khái niệm này là không thể tránh khỏi. Để tìm hiểu sâu về mã hóa ký tự, sách về Unicode và mã hóa ký tự trên Amazon trình bày chủ đề này một cách chi tiết. Bài viết này bao quát một cách có hệ thống mọi thứ từ định nghĩa cơ bản và đặc tả kỹ thuật Unicode đến so sánh kích thước byte giữa các bảng mã và các trường hợp đặc biệt trong thực tế.

Thực tế kỹ thuật đằng sau "Toàn chiều rộng" và "Nửa chiều rộng" - Thuộc tính East Asian Width của Unicode

Mặc dù các thuật ngữ "toàn chiều rộng" (全角) và "nửa chiều rộng" (半角) bắt nguồn từ ngành máy tính Nhật Bản, Unicode chính thức định nghĩa chiều rộng ký tự trong UAX #11 (Unicode Standard Annex #11: East Asian Width). Mỗi code point được gán một trong sáu thuộc tính chiều rộng:

  • F (Fullwidth): Dạng toàn chiều rộng của ký tự. Các biến thể toàn chiều rộng của ASCII (A, 1, v.v., U+FF01–U+FF60)
  • H (Halfwidth): Dạng nửa chiều rộng. Katakana nửa chiều rộng (ア, イ, v.v., U+FF61–U+FF9F)
  • W (Wide): Ký tự có chiều rộng lớn trong ngữ cảnh Đông Á. Chữ Hán CJK thống nhất, Hiragana, Katakana, v.v.
  • Na (Narrow): Ký tự có chiều rộng hẹp trong ngữ cảnh Đông Á. Chữ cái Latin cơ bản (A–Z), v.v.
  • A (Ambiguous): Ký tự có chiều rộng thay đổi tùy ngữ cảnh. Một số chữ cái Hy Lạp, ký tự Cyrillic, v.v.
  • N (Neutral): Ký tự không được sử dụng trong ngữ cảnh Đông Á

Những gì mọi người thường gọi là "toàn chiều rộng" bao gồm cả loại F và W, trong khi "nửa chiều rộng" bao gồm cả loại H và Na. Loại A (Ambiguous) cần được chú ý đặc biệt - tùy thuộc vào cài đặt terminal hoặc trình soạn thảo, các ký tự này có thể hiển thị với chiều rộng đơn hoặc chiều rộng kép. Ví dụ, "α" (chữ cái Hy Lạp nhỏ alpha) có thể hiển thị dạng toàn chiều rộng trong Command Prompt của Windows nhưng dạng nửa chiều rộng trong Terminal của macOS.

Ký tự toàn chiều rộng

Ký tự toàn chiều rộng chiếm gấp đôi chiều rộng hiển thị so với ký tự nửa chiều rộng trong môi trường phông chữ có chiều rộng cố định. Trong thuộc tính East Asian Width của Unicode, chúng được phân loại là W (Wide) hoặc F (Fullwidth). Hầu hết các ký tự tiếng Nhật gốc đều là toàn chiều rộng:

Ký tự nửa chiều rộng

Ký tự nửa chiều rộng chiếm khoảng một nửa chiều rộng hiển thị so với ký tự toàn chiều rộng. Trong Unicode, chúng được phân loại là Na (Narrow) hoặc H (Halfwidth). Các ký tự ASCII tiêu chuẩn thuộc loại này:

Katakana nửa chiều rộng không được khuyến khích vì nó bắt nguồn từ tiêu chuẩn JIS X 0201. Được thiết lập năm 1969, tiêu chuẩn này định nghĩa dakuten (゙) và handakuten (゚) là các ký tự riêng biệt để đưa katakana vào không gian mã 7-bit/8-bit hạn chế. Kết quả là, "ガ" trở thành "ガ" - được tính là 2 ký tự. Ngay cả chuẩn hóa Unicode NFC cũng không kết hợp dakuten katakana nửa chiều rộng, khiến sự chênh lệch số ký tự rất dễ xảy ra. Trừ khi có lý do cụ thể, nên luôn sử dụng katakana toàn chiều rộng.

JIS X 0201 và JIS X 0208 - Nguồn gốc lịch sử của toàn chiều rộng và nửa chiều rộng

Sự phân biệt toàn chiều rộng/nửa chiều rộng gắn liền với sự phát triển của các tiêu chuẩn mã hóa ký tự Nhật Bản. JIS X 0201, được thiết lập năm 1969, bao gồm mã 7-bit tương thích ASCII cùng 63 ký tự katakana nửa chiều rộng trong phạm vi 8-bit. Đây là thế giới của 1 ký tự = 1 byte.

JIS X 0208, được thiết lập năm 1978, định nghĩa một bộ ký tự lớn bao gồm 6.349 chữ Kanji. Vì 1 byte chỉ có thể biểu diễn 256 giá trị, nên cần không gian mã 2 byte. Sự khác biệt kích thước vật lý giữa "ký tự 1 byte" và "ký tự 2 byte" được thể hiện trực quan dưới dạng sự khác biệt chiều rộng hiển thị "nửa chiều rộng" và "toàn chiều rộng" trong môi trường phông chữ có chiều rộng cố định.

Nói cách khác, "toàn chiều rộng = 2 byte" là đúng về mặt thực tế trong các bảng mã Shift_JIS và EUC-JP, nhưng điều này không còn đúng trong thế giới UTF-8 ngày nay. Sự tồn tại dai dẳng của phương trình này là do nhiều hệ thống được xây dựng trong ngành CNTT Nhật Bản trong giai đoạn 1990–2000 đã giả định bảng mã Shift_JIS.

So sánh kích thước byte giữa các bảng mã

Cùng một ký tự có thể có kích thước byte rất khác nhau tùy thuộc vào bảng mã. Bảng sau so sánh kích thước byte cho các ký tự đại diện:

Ký tựUTF-8UTF-16Shift_JISEUC-JP
A (chữ cái nửa chiều rộng)1 byte2 byte1 byte1 byte
あ (hiragana)3 byte2 byte2 byte2 byte
漢 (kanji)3 byte2 byte2 byte2 byte
A (chữ cái toàn chiều rộng)3 byte2 byte2 byte2 byte
ア (katakana nửa chiều rộng)3 byte2 byte1 byte2 byte
€ (ký hiệu euro)3 byte2 byteN/AN/A
𠮷 (CJK Extension B)4 byte4 byte (cặp thay thế)N/AN/A

Một điểm chính cần nhớ: trong UTF-8, katakana nửa chiều rộng "ア" tiêu tốn 3 byte. Trong khi nó chỉ là 1 byte trong Shift_JIS, nó trở thành 3 byte giống như hiragana toàn chiều rộng trong UTF-8. Trực giác rằng "nửa chiều rộng nghĩa là kích thước dữ liệu nhỏ hơn" không nhất thiết đúng trong môi trường UTF-8.

Ảnh hưởng đến việc đếm ký tự - Sự khác biệt giữa các nền tảng

Hầu hết các công cụ đếm ký tự đều tính cả ký tự toàn chiều rộng và nửa chiều rộng là "1 ký tự" mỗi cái. Tuy nhiên, phương pháp đếm khác nhau tùy nền tảng, và cùng một văn bản có thể cho kết quả khác nhau.

Phương pháp đếm"Hello 世界" Result
Đếm ký tự Unicode (tiêu chuẩn)7 ký tự
Đếm byte (Shift_JIS)9 byte (5+4)
Đếm byte (UTF-8)11 byte (5+6)
Đếm byte (UTF-16)14 byte (tất cả ký tự × 2)

Hiểu cách các nền tảng lớn xử lý việc đếm toàn chiều rộng/nửa chiều rộng cũng rất hữu ích trên thực tế:

Nền tảngPhương pháp đếmXử lý toàn chiều rộng
X (trước đây là Twitter)Đếm có trọng số1 ký tự tiếng Nhật = 2 đơn vị (140 ký tự trong tổng 280)
LINEĐếm ký tự UnicodeToàn chiều rộng/nửa chiều rộng đều tính là 1
SMSPhụ thuộc bảng mãTiếng Nhật: tối đa 70 ký tự mỗi tin nhắn (UCS-2)
MySQL VARCHAR(n)Đếm ký tự (UTF-8mb4)Toàn chiều rộng/nửa chiều rộng đều tính là 1 (giới hạn byte áp dụng)
Oracle VARCHAR2(n BYTE)Đếm byte1 ký tự toàn chiều rộng = 3 byte trong UTF-8

Bộ đếm ký tự hiển thị số ký tự toàn chiều rộng và nửa chiều rộng riêng biệt, vì vậy bạn có thể làm việc với cả hai phương pháp đếm.

Vấn đề phổ biến từ nhầm lẫn toàn chiều rộng/nửa chiều rộng

Ký tự toàn chiều rộng trong lập trình - Bẫy ẩn

Sự xâm nhập của khoảng trắng toàn chiều rộng (U+3000) trong lập trình đặc biệt nghiêm trọng. Vì khoảng trắng toàn chiều rộng và nửa chiều rộng (U+0020) trông gần như giống hệt nhau, các nhà phát triển thường không thể xác định nguyên nhân ngay cả khi đọc thông báo lỗi.

Ngôn ngữThông báo lỗi
PythonSyntaxError: invalid character '\u3000'
Javaillegal character: '\u3000'
JavaScriptSyntaxError: Invalid or unexpected token
C/C++error: stray '\343' in program (byte đầu UTF-8)
RubySyntaxError: invalid multibyte char (UTF-8)

Ngoài khoảng trắng toàn chiều rộng, việc vô tình sử dụng dấu hai chấm toàn chiều rộng ":" (U+FF1A) thay vì dấu hai chấm nửa chiều rộng ":" (U+003A), hoặc lẫn dấu chấm phẩy toàn chiều rộng ";" (U+FF1B), cũng là những lỗi phổ biến. Trong các định dạng dữ liệu có cấu trúc như JSON và YAML, dấu hai chấm toàn chiều rộng gây ra lỗi cú pháp.

Trong tìm kiếm thương mại điện tử, các hệ thống coi "Tシャツ" (T toàn chiều rộng) và "Tシャツ" (T nửa chiều rộng) là các truy vấn khác nhau có thể trả về kết quả rất khác nhau. Các nghiên cứu cho thấy khoảng 10–15% truy vấn tìm kiếm thương mại điện tử chứa các biến thể toàn chiều rộng/nửa chiều rộng.

CSV/TSV và cạm bẫy ký tự toàn chiều rộng

Trong các tệp CSV (Comma-Separated Values) được sử dụng rộng rãi để trao đổi dữ liệu, việc trộn lẫn dấu phẩy toàn chiều rộng "," (U+FF0C) với dấu phẩy nửa chiều rộng "," (U+002C) gây ra các vấn đề nghiêm trọng. Hầu hết các trình phân tích CSV chỉ nhận dạng dấu phẩy nửa chiều rộng là dấu phân cách, vì vậy các trường chứa dấu phẩy toàn chiều rộng không được tách, gây lệch cột.

Tương tự, trong các tệp TSV (Tab-Separated Values), khoảng trắng toàn chiều rộng được sử dụng thay cho ký tự tab ngăn cản việc tách cột chính xác. Khi mở tệp CSV trong Excel mà thấy văn bản bị lỗi hoặc cột bị lệch, nên nghi ngờ sự nhiễm ký tự toàn chiều rộng.

Mã hóa URL và ký tự toàn chiều rộng

Khi ký tự toàn chiều rộng xuất hiện trong URL, mã hóa phần trăm (RFC 3986) chuyển đổi mỗi byte thành định dạng %XX. Một ký tự tiếng Nhật có 3 byte trong UTF-8 sẽ mở rộng thành 9 ký tự như %E3%81%82.

Ví dụ, "東京都" (3 ký tự) trở thành %E6%9D%B1%E4%BA%AC%E9%83%BD (27 ký tự) trong URL. Xét đến giới hạn độ dài URL (thường là 2.048 ký tự), URL chứa nhiều ký tự toàn chiều rộng có thể nhanh chóng đạt đến giới hạn. Khi sử dụng tiếng Nhật trong tên tệp hoặc tên thư mục, sự mở rộng này phải được tính đến trong thiết kế.

Kỹ thuật quản lý chuyên nghiệp

  1. Bật "hiển thị ký tự ẩn" trong trình soạn thảo văn bản. Trong VS Code, đặt editor.renderWhitespace: "all" để phân biệt trực quan khoảng trắng toàn chiều rộng. Ngoài ra, bật editor.unicodeHighlight.ambiguousCharacters: true sẽ đánh dấu các ký tự thuộc loại Ambiguous.
  2. Sử dụng regex để phát hiện chữ và số toàn chiều rộng. Mẫu [A-Za-z0-9] tìm chữ và số toàn chiều rộng để chuyển đổi hàng loạt.
  3. Triển khai chuẩn hóa phía máy chủ cho dữ liệu nhập biểu mẫu. Tự động chuyển đổi đầu vào toàn chiều rộng sang nửa chiều rộng để ngăn ngừa lỗi.
  4. Sử dụng phím tắt IME để chuyển đổi nhanh. Trên Windows, F10 chuyển đổi sang chữ và số nửa chiều rộng. Trên macOS, sử dụng tính năng chuyển đổi của phương thức nhập.
  5. Thiết lập hook pre-commit của Git để phát hiện khoảng trắng toàn chiều rộng. Chạy grep -rn $'\xe3\x80\x80' sẽ bắt khoảng trắng toàn chiều rộng trong toàn bộ kho lưu trữ trước khi chúng được commit.

Mẫu triển khai tự động chuyển đổi biểu mẫu web

Trong các dịch vụ web Nhật Bản, việc tự động chuyển đổi toàn chiều rộng sang nửa chiều rộng được triển khai rộng rãi cho các trường số điện thoại, mã bưu chính và địa chỉ email. Đây là một mẫu triển khai phổ biến.

Logic JavaScript cơ bản để chuyển đổi chữ và số toàn chiều rộng sang nửa chiều rộng tận dụng độ lệch code point Unicode. Chữ và số toàn chiều rộng (U+FF01–U+FF5E) khác với các ký tự ASCII nửa chiều rộng tương ứng (U+0021–U+007E) đúng 0xFEE0.

function toHalfWidth(str) {
  return str.replace(/[\uFF01-\uFF5E]/g, ch =>
    String.fromCharCode(ch.charCodeAt(0) - 0xFEE0)
  ).replace(/\u3000/g, ' ');
}

Hàm này chuyển đổi chữ và số cùng ký hiệu toàn chiều rộng sang nửa chiều rộng, đồng thời chuyển đổi khoảng trắng toàn chiều rộng sang khoảng trắng nửa chiều rộng. Tuy nhiên, việc chuyển đổi katakana toàn chiều rộng sang katakana nửa chiều rộng liên quan đến xử lý dakuten/handakuten phức tạp, vì vậy nên sử dụng thư viện chuyên dụng.

Đối với phần tử HTML input, thay vì thuộc tính CSS ime-mode đã lỗi thời, thuộc tính inputmode có thể kiểm soát chế độ nhập. Đặt inputmode="numeric" sẽ hiển thị bàn phím số trên thiết bị di động, giảm nguy cơ nhập toàn chiều rộng.

Phát hiện toàn chiều rộng/nửa chiều rộng dựa trên Regex trong thực tế

Thoát thuộc tính Unicode trong biểu thức chính quy rất hiệu quả để phát hiện ký tự toàn chiều rộng và nửa chiều rộng:

// Detect full-width characters (Wide + Fullwidth)
const fullwidthPattern = /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF01-\uFF60]/;

// Detect half-width katakana
const halfwidthKatakana = /[\uFF61-\uFF9F]/;

// Detect full-width alphanumerics only (useful for conversion targeting)
const fullwidthAlphaNum = /[\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A]/;

Để chuẩn hóa toàn chiều rộng/nửa chiều rộng trước khi lưu trữ cơ sở dữ liệu, NFKC (Normalization Form Compatibility Composition) rất hiệu quả. Trong JavaScript, "A".normalize("NFKC") chuyển đổi "A" toàn chiều rộng thành "A" nửa chiều rộng. Tuy nhiên, NFKC cũng mở rộng các ký tự như "㍻" thành "平成", vì vậy phạm vi áp dụng phải được cân nhắc kỹ lưỡng.

Ký tự vùng xám

Một số ký tự không thể phân loại đơn giản là toàn chiều rộng hay nửa chiều rộng. Đây là các ký tự được phân loại là A (Ambiguous) trong thuộc tính East Asian Width của Unicode.

Một ví dụ đáng chú ý là dấu ngã sóng (〜, U+301C) so với dấu ngã toàn chiều rộng (~, U+FF5E). Chúng trông gần như giống hệt nhau nhưng là các ký tự Unicode khác nhau. Triển khai Shift_JIS của Windows đã ánh xạ dấu ngã sóng (U+301C) thành dấu ngã toàn chiều rộng (U+FF5E), gây ra lỗi văn bản khi trao đổi tệp giữa các hệ điều hành. Vấn đề này, được gọi là "vấn đề dấu ngã sóng", bắt nguồn từ các cách diễn giải khác nhau về glyph dấu ngã sóng trong bảng mã ký tự JIS X 0208.

Tương tự, ký hiệu yên (¥, U+00A5) và dấu gạch chéo ngược (\, U+005C) hiển thị giống hệt nhau trong một số môi trường tiếng Nhật. Điều này bắt nguồn từ JIS X 0201 gán ký hiệu yên vào vị trí 0x5C (dấu gạch chéo ngược trong ASCII). Phông chữ tiếng Nhật của Windows vẫn hiển thị dấu gạch chéo ngược dưới dạng ký hiệu yên, đó là lý do tại sao C:¥UsersC:\Users cùng tồn tại trong ký hiệu đường dẫn tệp.

Phương pháp tốt nhất cho cơ sở dữ liệu về chuẩn hóa toàn chiều rộng/nửa chiều rộng

Chuẩn hóa văn bản toàn chiều rộng/nửa chiều rộng trước khi lưu trữ cơ sở dữ liệu trực tiếp cải thiện độ chính xác tìm kiếm và chất lượng dữ liệu.

  1. Chuẩn hóa tại thời điểm nhập: Áp dụng chuẩn hóa NFKC trong tầng ứng dụng trước khi INSERT. Điều này tự động chuyển đổi chữ và số toàn chiều rộng sang nửa chiều rộng. Sách về phương pháp tốt nhất cho thiết kế cơ sở dữ liệu trình bày chi tiết các chiến lược chuẩn hóa.
  2. Chuẩn hóa tại thời điểm tìm kiếm: Áp dụng cùng phép chuẩn hóa cho truy vấn tìm kiếm để hấp thụ các biến thể ký hiệu giữa dữ liệu lưu trữ và điều kiện tìm kiếm. Trong MySQL, sử dụng COLLATE utf8mb4_unicode_ci cho phép đối chiếu không phân biệt chữ hoa/thường và chiều rộng.
  3. Thiết kế cột: Làm rõ liệu độ dài VARCHAR dựa trên ký tự (MySQL) hay dựa trên byte (Oracle), và đặt giới hạn byte tính đến 1 ký tự toàn chiều rộng = 3 byte trong UTF-8.
  4. Thiết kế chỉ mục: Khi cần tìm kiếm không phân biệt chiều rộng, tạo một cột riêng lưu trữ giá trị đã chuẩn hóa và đánh chỉ mục cột đó để tra cứu hiệu quả.

Quy tắc sử dụng toàn chiều rộng và nửa chiều rộng

Biết khi nào sử dụng ký tự toàn chiều rộng so với nửa chiều rộng là điều cần thiết để tạo ra văn bản tiếng Nhật hoàn chỉnh. Mặc dù quy ước khác nhau tùy phương tiện và hướng dẫn phong cách, các quy tắc sau được chấp nhận rộng rãi.

  1. Sử dụng nửa chiều rộng cho ký tự chữ và số trong văn bản ngang (ví dụ: 2024年, 100円)
  2. Sử dụng ngoặc toàn chiều rộng cho trích dẫn tiếng Nhật (ví dụ: 「こんにちは」)
  3. Luôn sử dụng nửa chiều rộng cho URL và địa chỉ email
  4. Tuân theo định dạng được chỉ định (toàn chiều rộng hoặc nửa chiều rộng) khi điền biểu mẫu

Trong nội dung web, thực hành tiêu chuẩn là sử dụng nửa chiều rộng cho tất cả ký tự chữ và số cùng khoảng trắng nửa chiều rộng, trong khi giữ dấu câu tiếng Nhật (。và 、) ở dạng toàn chiều rộng. Tránh hoàn toàn khoảng trắng toàn chiều rộng - chúng là nguồn phổ biến gây ra các vấn đề định dạng ẩn trong HTML và mã.

Kết luận

Sự phân biệt toàn chiều rộng/nửa chiều rộng không chỉ đơn thuần là vấn đề thẩm mỹ - nó ảnh hưởng trực tiếp đến việc đếm ký tự, tính toán byte, thiết kế cơ sở dữ liệu, thiết kế URL và tính đúng đắn trong lập trình. Nền tảng của nó là di sản lịch sử của JIS X 0201/0208 và đặc tả kỹ thuật của thuộc tính East Asian Width trong Unicode. Bằng cách hiểu chính xác sự khác biệt kích thước byte giữa các bảng mã và áp dụng các kỹ thuật thực tế như chuẩn hóa NFKC và phát hiện dựa trên regex, bạn có thể ngăn ngừa các vấn đề toàn chiều rộng/nửa chiều rộng trước khi chúng xảy ra. Sử dụng Bộ đếm ký tự để kiểm tra phân tích toàn chiều rộng và nửa chiều rộng nhằm quản lý ký tự chính xác.