Chuyển đến nội dung

Thế nào được tính là một ký tự?

Bốn hệ thống khác nhau đếm cùng một đoạn văn bản theo bốn cách khác nhau. Với văn bản tiếng Anh thông thường thì chúng khớp nhau, vì thế vấn đề vẫn vô hình cho đến ngày chúng không còn khớp.

Xem xét lần cuối

Một emoji, bốn câu trả lời

Lấy một emoji giơ ngón cái có chọn màu da, hoặc emoji gia đình. Đếm nó theo bốn cách và bạn nhận được bốn con số, không con số nào sai.

Người đọc thấy một ký tự. Một hình, nhấn phím xóa lùi một lần là mất.

Biểu thức chính quy có thể thấy bảy. Nó được ghép từ nhiều điểm mã (code point) nối với nhau bằng các ký tự nối vô hình — một emoji gốc, một ký tự biến đổi, một ký tự nối, một emoji khác, và cứ thế.

JavaScript báo mười một. Nó lưu văn bản theo đơn vị 16 bit, và mọi thứ nằm ngoài dải cơ bản chiếm hai đơn vị, nên mỗi điểm mã đó có thể bị tính gấp đôi.

Cột cơ sở dữ liệu thấy hai mươi lăm byte. Khi được mã hóa UTF-8 để lưu trữ, mỗi điểm mã chiếm tối đa bốn byte.

Với văn bản tiếng Anh thông thường, cả bốn con số đều bằng nhau. Chính vì vậy vấn đề vô hình cho đến khi có người nhập một cái tên có dấu, một câu tiếng Nhật, hay chỉ một emoji — và một trường đã chạy ổn suốt ba năm bắt đầu cắt cụt dữ liệu.

Giới hạn của bạn tính theo con số nào

Câu hỏi thực tế không bao giờ là “văn bản này dài bao nhiêu”. Mà là “cái gì đang đặt ra giới hạn”, và có bốn câu trả lời phổ biến.

Cột cơ sở dữ liệu khai báo VARCHAR đếm theo byte trong hầu hết các hệ quản trị. Đó là lý do một trường chứa thoải mái 255 ký tự tiếng Anh lại từ chối ít ký tự tiếng Nhật hơn nhiều — mỗi ký tự ba byte — và vì sao một cái tên có dấu đôi khi không vừa trong khi bản không dấu lại vừa.

Tin nhắn SMS dài 160 ký tự theo bảng chữ cái bảy bit riêng của nó, và giảm xuống 70 ký tự mỗi tin ngay khi có một ký tự nằm ngoài bảng đó. Chỉ một dấu ngoặc kép cong dán từ trình soạn thảo văn bản, hoặc một emoji, cũng có thể biến tin nhắn một phần thành ba phần và khiến bạn trả gấp ba tiền.

Trình kiểm tra biểu mẫu bằng JavaScript hầu như luôn đếm theo đơn vị UTF-16, vì thế một trường ghi 280 ký tự có thể từ chối một chuỗi trông ngắn hơn nhiều nếu nó chứa đầy emoji.

Con người đếm những gì họ nhìn thấy — định nghĩa duy nhất có ý nghĩa với một tiêu đề, thẻ title hay bất cứ thứ gì có giới hạn về hiển thị.

Những chỗ các cách đếm khác nhau

Biết đầu vào nào gây rắc rối hữu ích hơn biết lý thuyết, vì những đầu vào đó có thể đoán trước được.

Emoji, đặc biệt là emoji ghép — màu da, gia đình, cờ, nghề nghiệp. Một lá cờ là hai chữ cái chỉ vùng (regional indicator); một nghề nghiệp thường là một người, một ký tự nối và một đồ vật.

Ký tự có dấu và ký tự kết hợp. Chữ é có thể được viết bằng một điểm mã, hoặc bằng chữ e theo sau là dấu sắc kết hợp. Chúng trông giống hệt nhau, nhưng sắp xếp khác nhau, so sánh ra không bằng nhau, và đếm ra khác nhau — mà cả hai dạng đều xuất hiện trong dữ liệu thực tế, thường ngay trong cùng một cột.

Chữ viết không phải Latin. Ký tự tiếng Trung, tiếng Nhật và tiếng Hàn chiếm ba byte mỗi ký tự trong UTF-8. Tiếng Hindi, tiếng Thái, tiếng Tamil và tiếng Ả Rập tạo thành các cụm mà người đọc nhìn như một đơn vị nhưng dài nhiều điểm mã.

Ký hiệu toán học và âm nhạc, nằm ngoài dải cơ bản nên bị tính gấp đôi trong UTF-16.

Nên làm gì

Đếm đúng thứ mà giới hạn của bạn đếm. Một công cụ đếm hiển thị cả bốn con số trả lời thẳng câu hỏi, nhanh hơn là ngồi suy luận. Trong code, hãy dùng hàm tương ứng: Intl.Segmenter trong JavaScript đếm những gì người đọc thấy, len(s.encode("utf-8")) trong Python đếm byte, mb_strlen thay vì strlen trong PHP.

Sửa cột chứ đừng sửa biểu mẫu. Nếu trường cơ sở dữ liệu là nơi đặt giới hạn, giải pháp lâu dài nằm ở phía trên: khai báo utf8mb4 trong MySQL hoặc text trong PostgreSQL. Kiểu “utf8” cũ của MySQL nổi tiếng là chỉ dùng tối đa ba byte mỗi ký tự và hoàn toàn không lưu được emoji — một cái bẫy lâu đời đã âm thầm cắt cụt rất nhiều dữ liệu thật. Đếm cẩn thận ở biểu mẫu chỉ là cách chữa cháy cho một cột lẽ ra phải được khai báo khác.

Chuẩn hóa ngay khi nhận dữ liệu. Chuyển văn bản về một dạng chuẩn duy nhất ngay lúc nhập nghĩa là hai cách viết chữ é trở thành một, và việc so sánh, sắp xếp lẫn đếm đều bắt đầu khớp nhau. Ngôn ngữ lập trình nào cũng có hàm cho việc này, và làm một lần ở điểm đầu vào dễ hơn nhiều so với xử lý cả hai dạng ở khắp nơi.

Trường hợp liên quan: base64

Đáng nhắc đến vì nó gây ra kiểu bất ngờ tương tự. Mã hóa dữ liệu thành base64 lấy mỗi lần ba byte và viết thành bốn ký tự, nên kết quả lớn hơn khoảng một phần ba — đó là số học, không phải kém hiệu quả. Một file đính kèm 6 MB thành khoảng 8 MB văn bản, vì thế một file dưới giới hạn dung lượng khá xa vẫn có thể bị từ chối khi đã được mã hóa để truyền đi.

Cùng phép tính đó giải thích vì sao email có file đính kèm bị trả về dù trông như nằm trong giới hạn. Nếu bạn cần đối chiếu với một mức trần, hãy tính theo dạng đã mã hóa.

Câu hỏi thường gặp

Vì sao trường 255 ký tự của tôi lại từ chối văn bản ngắn hơn?

Gần như chắc chắn vì nó đếm theo byte chứ không theo ký tự. Chữ cái có dấu chiếm hai byte trong UTF-8, còn ký tự CJK chiếm ba, nên 255 byte có thể chỉ là 85 ký tự tiếng Nhật. Khai báo cột là utf8mb4 hoặc text sẽ khắc phục triệt để.

Một emoji thật sự khiến tôi tốn ba tin nhắn SMS?

Có thể. Tin nhắn chỉ chứa ký tự trong bảng chữ cái GSM được 160 ký tự mỗi phần; chỉ một ký tự nằm ngoài bảng đó sẽ chuyển cả tin nhắn sang kiểu mã hóa 70 ký tự. Vì vậy một tin nhắn 150 ký tự có một emoji sẽ thành ba phần thay vì một.

Nên dùng cách đếm nào cho thẻ title?

Không cách nào chính xác cả — công cụ tìm kiếm cắt theo độ rộng pixel chứ không theo số ký tự, nên một tiêu đề toàn chữ hoa rộng sẽ bị cắt sớm hơn tiêu đề chữ thường hẹp. Khoảng 60 ký tự là quy tắc thường dùng, và đó là hướng dẫn chứ không phải giới hạn.

Vì sao hai chuỗi trông giống hệt nhau lại không khớp?

Thường là vì một ký tự có dấu được viết theo hai cách khác nhau — một bên là một điểm mã duy nhất, bên kia là chữ cái gốc cộng dấu kết hợp. Chuẩn hóa cả hai về cùng một dạng trước khi so sánh sẽ khiến chúng khớp nhau, và nên làm việc đó ngay tại điểm dữ liệu đi vào hệ thống của bạn.

Đếm từ có ổn định hơn đếm ký tự không?

Với tiếng Anh thì phần lớn là có. Nhưng không phải lúc nào cũng vậy: tiếng Trung và tiếng Nhật không tách từ bằng dấu cách, tiếng Thái cũng không, nên đếm từ trong các hệ chữ đó cần phân đoạn từ, và mỗi công cụ cho ra một kết quả khác nhau.