Đếm ký tự Emoji: Tại sao một Emoji có thể được tính là nhiều ký tự

8 phút đọc

Một emoji trông như một ký tự thực tế có thể chiếm 2, 4, hoặc thậm chí 7 ký tự trở lên tùy thuộc vào cách đếm. Sự khác biệt này gây nhầm lẫn trên mạng xã hội, trong cơ sở dữ liệu và trong lập trình. Để hiểu sâu về mã hóa ký tự, hãy tham khảo sách tham khảo mã hóa Unicode. Bài viết này giải thích lý do kỹ thuật và ý nghĩa thực tế.

Lịch sử Emoji và sự phát triển theo phiên bản Unicode

Bộ emoji đầu tiên trên thế giới được tạo ra năm 1999 bởi Shigetaka Kurita tại NTT DoCoMo cho nền tảng di động i-mode — 176 biểu tượng được hiển thị dưới dạng pixel art 12×12. Ban đầu, mỗi nhà mạng di động Nhật Bản (DoCoMo, au, SoftBank) triển khai emoji độc lập, gây ra lỗi hiển thị thường xuyên khi tin nhắn được gửi giữa các nhà mạng. Để giải quyết vấn đề tương thích này, Google và Apple đề xuất chuẩn hóa emoji cho Unicode Consortium, và 722 emoji được chính thức đưa vào Unicode 6.0 năm 2010.

Kể từ đó, emoji đã phát triển với mỗi phiên bản Unicode lớn.

Phiên bản UnicodeNăm phát hànhEmoji thêm mớiTổng tích lũy (ước tính)
6.02010722722
7.02014250~1,000
8.0201541~1,050
9.0201672~1,100
11.02018157~1,600
13.02020117~3,300
15.0202231~3,600
16.020248~3,790

Số lượng bổ sung mới đã giảm trong những năm gần đây. Unicode Consortium hiện đánh giá nghiêm ngặt liệu emoji được đề xuất có thể được biểu diễn bằng cách kết hợp emoji hiện có qua chuỗi ZWJ hay không, và từ chối phân bổ code point mới khi việc kết hợp khả thi. Quá trình từ đề xuất đến phát hành chính thức mất khoảng hai năm, yêu cầu dữ liệu về tần suất sử dụng dự kiến và bằng chứng phân biệt với emoji hiện có.

Kích thước byte theo mã hóa: Dữ liệu đo lường

EmojiHiển thịCode PointByte UTF-8Byte UTF-16Byte UTF-32
😀 (U+1F600)1 ký tự1444
👍🏻 (U+1F44D U+1F3FB)1 ký tự2888
👨‍👩‍👧‍👦1 ký tự7252228
🇺🇸 (U+1F1FA U+1F1F8)1 ký tự2888
1️⃣ (U+0031 U+FE0F U+20E3)1 ký tự37612
🏳️‍🌈1 ký tự4141216

Trong UTF-8, các code point ngoài Basic Multilingual Plane (BMP) chiếm 4 byte mỗi cái. UTF-16 biểu diễn chúng dưới dạng surrogate pair (hai đơn vị 16-bit), cũng tổng cộng 4 byte. UTF-32 có chiều rộng cố định 4 byte mỗi code point, giúp tính toán đơn giản nhưng hiệu quả bộ nhớ kém nhất trong ba loại. Emoji gia đình 👨‍👩‍👧‍👦 chiếm 25 byte trong UTF-8 là yếu tố quan trọng khi thiết kế kích thước cột cơ sở dữ liệu.

Cách ZWJ, Variation Selector và Surrogate Pair hoạt động

Unicode sử dụng ZWJ (Zero Width Joiner, U+200D) để kết hợp nhiều code point thành một emoji hiển thị duy nhất. Emoji gia đình 👨‍👩‍👧‍👦 được tạo thành từ "man (U+1F468) + ZWJ + woman (U+1F469) + ZWJ + girl (U+1F467) + ZWJ + boy (U+1F466)" — tổng cộng 7 code point.

Thiết kế này được áp dụng vì việc đăng ký riêng lẻ mọi tổ hợp tông da, giới tính và nghề nghiệp sẽ cần hàng chục nghìn code point. Kết hợp ZWJ cho phép đa dạng thông qua việc kết hợp các khối xây dựng cơ bản, ngăn chặn cạn kiệt code point.

Ngoài ZWJ, một số code point vô hình kiểm soát hiển thị emoji:

Biểu diễn chuỗi nội bộ của JavaScript sử dụng UTF-16, nên emoji ngoài BMP (U+10000 trở lên) được biểu diễn dưới dạng surrogate pair — hai đơn vị mã 16-bit. Đây là lý do cơ bản "😀".length trả về 2 thay vì 1. Tách một surrogate pair (high surrogate U+D800–U+DBFF, low surrogate U+DC00–U+DFFF) tạo ra chuỗi không hợp lệ, khiến việc cắt ngắn chuỗi đặc biệt nguy hiểm.

Sự khác biệt đếm và hiển thị Emoji theo nền tảng

Cùng một emoji cũng có thể trông khác biệt đáng kể giữa các nền tảng Apple, Google, Samsung và Microsoft. Ví dụ, 🔫 (súng lục) đã được Apple đổi thành thiết kế súng nước đồ chơi, và các nhà cung cấp khác cũng làm theo - nhưng vào các thời điểm khác nhau. Khi giao diện emoji quan trọng trong marketing hoặc thiết kế UI, hãy xem trước nội dung trên các nền tảng lớn trước khi xuất bản.

Sự khác biệt số ký tự giữa các ngôn ngữ lập trình

Cùng một emoji tạo ra giá trị length khác nhau tùy thuộc vào ngôn ngữ lập trình, vì mỗi ngôn ngữ sử dụng biểu diễn chuỗi nội bộ khác nhau.

Ngôn ngữ / Phương thức"😀""👍🏻""👨‍👩‍👧‍👦"Đơn vị đếm
JavaScript .length2411Đơn vị mã UTF-16
JavaScript [...str].length127Code point Unicode
Python 3 len()127Code point Unicode
Rust .len()4825Byte UTF-8
Rust .chars().count()127Code point Unicode
Swift .count111Cụm grapheme
Go len()4825Byte UTF-8
Java .length()2411Đơn vị mã UTF-16

Chỉ Swift đếm theo cụm grapheme, trả về 1 cho bất kỳ emoji nào bất kể độ phức tạp nội bộ. JavaScript và Java sử dụng UTF-16 nội bộ, nên emoji ngoài BMP được đếm là 2 (surrogate pair). Rust và Go trả về số byte, khiến chúng không phù hợp để đếm ký tự mà không có xử lý bổ sung. Nhà phát triển phải hiểu chính xác length của ngôn ngữ họ trả về gì.

Lỗi phổ biến và cách tránh

Hướng dẫn cho nhà phát triển: Đếm Emoji chính xác

  1. Phân đoạn cụm grapheme với Intl.Segmenter: Được giới thiệu trong ES2022, Intl.Segmenter chia chuỗi theo cụm grapheme - đơn vị mà người dùng nhận thức là ký tự đơn. [...new Intl.Segmenter().segment(str)].length cho số ký tự hiển thị chính xác cho bất kỳ emoji nào. Có sẵn trong Node.js 16+, Chrome 87+ và Safari 15.4+.
  2. Phát hiện và loại bỏ emoji bằng regex: Thuộc tính Unicode escape /\p{Emoji_Presentation}/u phát hiện emoji trong chuỗi. Lưu ý rằng \p{Emoji} cũng khớp với chữ số (0-9) và #, nên hãy sử dụng \p{Emoji_Presentation} hoặc \p{Extended_Pictographic} khi chỉ nhắm đến emoji hình ảnh.
  3. Bộ kiểm thử emoji toàn diện: Khi kiểm thử, hãy bao gồm tối thiểu 5 danh mục sau: (1) emoji cơ bản (😀), (2) có modifier tông da (👍🏻), (3) chuỗi ZWJ (👨‍👩‍👧‍👦), (4) cờ (🇺🇸), và (5) chuỗi keycap (1️⃣).
  4. Phương pháp tốt nhất cho thiết kế cơ sở dữ liệu: Xác định kích thước cột dựa trên số byte, không phải số ký tự hiển thị. Trong MySQL, sử dụng utf8mb4 và đặt độ dài VARCHAR khoảng "số ký tự tối đa dự kiến × 4." Đối với ứng dụng chat sử dụng nhiều emoji, hãy cân nhắc sử dụng kiểu TEXT thay thế.

Kết luận

Đếm emoji phức tạp hơn vẻ ngoài. Các nền tảng và ngôn ngữ lập trình khác nhau đếm emoji khác nhau. Để nắm vững xử lý chuỗi giữa các ngôn ngữ, hãy khám phá hướng dẫn lập trình xử lý văn bản. Sử dụng Bộ đếm ký tự để có số ký tự chính xác tính đến độ phức tạp của emoji.