Đếm ký tự Emoji: Tại sao một Emoji có thể được tính là nhiều ký tự
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 Unicode | Năm phát hành | Emoji thêm mới | Tổng tích lũy (ước tính) |
|---|---|---|---|
| 6.0 | 2010 | 722 | 722 |
| 7.0 | 2014 | 250 | ~1,000 |
| 8.0 | 2015 | 41 | ~1,050 |
| 9.0 | 2016 | 72 | ~1,100 |
| 11.0 | 2018 | 157 | ~1,600 |
| 13.0 | 2020 | 117 | ~3,300 |
| 15.0 | 2022 | 31 | ~3,600 |
| 16.0 | 2024 | 8 | ~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
| Emoji | Hiển thị | Code Point | Byte UTF-8 | Byte UTF-16 | Byte UTF-32 |
|---|---|---|---|---|---|
| 😀 (U+1F600) | 1 ký tự | 1 | 4 | 4 | 4 |
| 👍🏻 (U+1F44D U+1F3FB) | 1 ký tự | 2 | 8 | 8 | 8 |
| 👨👩👧👦 | 1 ký tự | 7 | 25 | 22 | 28 |
| 🇺🇸 (U+1F1FA U+1F1F8) | 1 ký tự | 2 | 8 | 8 | 8 |
| 1️⃣ (U+0031 U+FE0F U+20E3) | 1 ký tự | 3 | 7 | 6 | 12 |
| 🏳️🌈 | 1 ký tự | 4 | 14 | 12 | 16 |
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:
- Variation Selector: U+FE0F (hiển thị emoji) và U+FE0E (hiển thị văn bản). Ví dụ, ❤ (U+2764) hiển thị dưới dạng ❤️ (emoji màu) với U+FE0F, hoặc ❤︎ (ký hiệu văn bản) với U+FE0E. Các ký tự vô hình này thêm vào số byte mà không hiển thị cho người dùng.
- Skin Tone Modifier: U+1F3FB đến U+1F3FF cung cấp 5 cấp độ dựa trên thang Fitzpatrick (phân loại tông da da liễu). Mỗi modifier thêm 1 code point (4 byte).
- Regional Indicator Symbol: Emoji cờ sử dụng 26 Regional Indicator Symbol (U+1F1E6–U+1F1FF) tương ứng với A–Z, kết hợp theo cặp. 🇺🇸 là U+1F1FA (U) + U+1F1F8 (S). Chúng ánh xạ đến mã quốc gia ISO 3166-1, nên các tổ hợp không xác định (ví dụ: U+1F1FF + U+1F1FF) tạo ra hiển thị không xác định.
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
- X (Twitter): Emoji được đếm nội bộ là 2 ký tự mỗi cái, bất kể độ phức tạp. Ngay cả emoji gia đình ZWJ 👨👩👧👦 cũng chỉ đếm là 2. Trong giới hạn 280 ký tự, sử dụng nhiều emoji có thể khiến bài đăng bị cắt ngắn nếu bạn không tính đến điều này.
- Instagram: Trong giới hạn 2.200 ký tự caption, emoji được đếm là 1 ký tự mỗi cái. Tuy nhiên, emoji trong hashtag không thể tìm kiếm được, nên hiệu quả hơn khi giữ hashtag không có emoji.
- LINE: Tin nhắn văn bản đếm emoji là 1 ký tự. Tuy nhiên, sticker emoji độc quyền của LINE và emoji Unicode được xử lý khác nhau nội bộ, điều này quan trọng cho nhà phát triển làm việc với LINE Messaging API.
- Slack: Nội dung tin nhắn đếm emoji là 1 ký tự, nhưng emoji tùy chỉnh sử dụng shortcode (ví dụ:
:thumbsup:), và toàn bộ chuỗi shortcode bao gồm dấu hai chấm tính vào giới hạn ký tự. - SMS: Chỉ cần một emoji cũng chuyển mã hóa từ GSM-7 sang UCS-2, giảm giới hạn mỗi tin nhắn từ 160 xuống 70 ký tự. Đối với SMS marketing, điều này có thể gần gấp đôi chi phí gửi mỗi tin nhắn.
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 .length | 2 | 4 | 11 | Đơn vị mã UTF-16 |
JavaScript [...str].length | 1 | 2 | 7 | Code point Unicode |
Python 3 len() | 1 | 2 | 7 | Code point Unicode |
Rust .len() | 4 | 8 | 25 | Byte UTF-8 |
Rust .chars().count() | 1 | 2 | 7 | Code point Unicode |
Swift .count | 1 | 1 | 1 | Cụm grapheme |
Go len() | 4 | 8 | 25 | Byte UTF-8 |
Java .length() | 2 | 4 | 11 | Đơ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
- Sử dụng
String.lengthcho giới hạn ký tự trong JavaScript:"👨👩👧👦".lengthtrả về 11, nhưng số ký tự hiển thị là 1. Xác thực biểu mẫu sử dụngString.lengthsẽ hạn chế không công bằng đầu vào chứa nhiều emoji. Sử dụngIntl.Segmenterđể đếm cụm grapheme chính xác. - Cắt ngắn chuỗi giữa chuỗi ZWJ: Cắt emoji gia đình 👨👩👧👦 tại vị trí byte hoặc đơn vị mã tùy ý sẽ phá vỡ chuỗi ZWJ, khiến các emoji người riêng lẻ hiển thị tách biệt - hoặc tệ hơn, tạo ra ký tự không hợp lệ. Luôn cắt ngắn tại ranh giới cụm grapheme.
- Sử dụng MySQL
utf8thay vìutf8mb4: Bộ ký tựutf8của MySQL chỉ hỗ trợ tối đa 3 byte mỗi ký tự, không thể lưu trữ emoji (code point 4 byte ngoài BMP). Bạn phải sử dụngutf8mb4. Ngoài ra, cộtVARCHAR(255)lưu trữ văn bản có emoji gia đình (tối đa 25 byte mỗi cái trong UTF-8) sẽ đạt giới hạn kích thước sớm hơn nhiều so với số ký tự hiển thị gợi ý. PostgreSQL hỗ trợ toàn bộ phạm vi UTF-8 theo mặc định, nên vấn đề này không phát sinh. - Khớp emoji bằng regex cơ bản:
/./trong JavaScript chỉ khớp một nửa của surrogate pair. Sử dụng/./u(cờ Unicode) để khớp toàn bộ code point, và/./v(cờ Unicode Sets, ES2024) để khớp toàn bộ chuỗi ZWJ như đơn vị duy nhất. - Lạm dụng emoji trong dòng tiêu đề email: Một số ứng dụng email không hiển thị emoji chính xác, hiển thị văn bản lỗi hoặc khoảng trống. Đối với email doanh nghiệp, an toàn hơn khi giảm thiểu sử dụng emoji do hiển thị không thể đoán trước trên các môi trường của người nhận.
Hướng dẫn cho nhà phát triển: Đếm Emoji chính xác
- Phân đoạn cụm grapheme với
Intl.Segmenter: Được giới thiệu trong ES2022,Intl.Segmenterchia 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)].lengthcho 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+. - Phát hiện và loại bỏ emoji bằng regex: Thuộc tính Unicode escape
/\p{Emoji_Presentation}/uphá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. - 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️⃣).
- 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
utf8mb4và đặt độ dàiVARCHARkhoả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ểuTEXTthay 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.