Emoji 組合的意思從哪來:投稿、去重、排行與檢舉機制

🥺👉👈 是什麼意思?💀💀💀 為什麼是「笑死」?我在顏文字花園加了一個新區塊:Emoji 組合 ↗。每一組 emoji 都寫了意思和使用時機,點一下就能複製。登入之後,你也可以投稿自己的組合。排行看的是「有多少不同的人複製過」,不是按讚數。
這是顏文字花園第一個要用到伺服器和資料庫的功能。伺服器的程式跑在 Cloudflare Worker 上(由 Cloudflare 代管、執行的小程式),資料存在 Cloudflare 的資料庫 D1。這也是我第一次在工具站開放大家投稿。這篇記錄幾個做法:意思從哪來、怎麼認出兩組 emoji 其實是同一組、排行怎麼防灌票,還有投稿不先審核的時候,靠什麼擋掉垃圾內容。
先講清楚:這不是 Emoji Kitchen
中文搜尋「emoji 組合」,很多人要找的其實是 Google 的 Emoji Kitchen。它會把兩個 emoji 合成一張新貼圖,做出來的是圖片,不能當文字貼上。
這裡的 Emoji 組合是純文字。兩個以上的 emoji 排在一起,合起來表達一個意思。複製之後,貼到哪裡都能用。

圖:Emoji 組合首頁。每張卡片都是可以直接複製的文字,不是圖片;「熱門」看的是有幾個不同的人複製過。
意思從哪來
新功能剛上線,一定會碰到一個現實問題:還沒有人投稿,排行也沒有東西可以排。所以上線那天,站上先放了 168 組我自己整理的組合,分成 8 個主題:害羞告白、笑死、難過崩潰、可愛、美感、無言尷尬、讚美打氣、節日季節,每個主題 21 組。
每一組都有兩段文字:
- 標題:卡片上那一行,最多 24 個字。例如 🥺👉👈「害羞戳手指」、👀🍿「搬椅子看戲」。
- 說明:這組是什麼意思、什麼時候用。例如 🙂👍:「微笑比讚,其實是冷淡的『好喔』。有陰陽怪氣的味道,小心使用。」

圖:主題頁的卡片多了一段說明,告訴你這組 emoji 的意思和使用時機。
這些說明查字典查不到。同一個 emoji,不同年紀、不同平台的人讀起來可能完全不一樣。自由時報 2026 年 6 月有一篇報導,談 Threads 上的「EMOJI 聊天法」,裡面提到:😂 在年輕人眼中已經過時,🙂 則帶有冷淡或陰陽怪氣的意思(自由時報報導 ↗)。
我的做法是先整理這類在地的觀察,再跟 AI 一起起草標題和說明,一條一條校對過才上線。中文和英文各寫一份,簡體中文則是從繁體自動轉換。
大家投稿的規則是這樣:標題一定要填,2 到 24 個字;想的話可以再附一段 60 字以內的說明。兩段都不能放連結。
遇到值得收錄的投稿,我會幫它補上主題,以及中英文的標題和說明。補齊之後,它才會出現在主題頁,主題頁是會給 Google 收錄的。為什麼要多這一步?因為主題頁能被搜尋到,靠的就是這些說明文字。一頁如果只有一排排 emoji、幾乎沒有文字,對搜尋引擎來說就是內容太少的「薄頁」。
同一組 emoji,怎麼判定重複
「自動找出重複」聽起來只是比對兩串文字一不一樣,但 emoji 比想像中複雜:
- 👨👩👧 看起來是一個 emoji,其實是三個人物 emoji,中間用兩個看不見的「連接符」黏在一起(英文叫 ZWJ),一共 5 個字元。
- 👍🏽 是 👍 後面再接一個代表膚色的字元。
- ❤ 和 ❤️ 只差一個看不見的字元,它負責告訴手機或電腦「這裡要畫成彩色 emoji」,叫做變體選擇符。
不管你輸入什麼,系統都會先把它整理成同一種寫法,再拿去比對:
- 拿掉空白和看不見的空格。連接符要留著,它是很多 emoji 的一部分。
- 拿站內 Emoji 頁那份清單當「白名單」(清單上有的才收),從左到右一個一個切出 emoji。每次都挑清單裡對得上的最長那一段,所以 👨👩👧 會整個切成一個,不會變成三個人。清單只收到 Emoji 15.1 版為止的 emoji。比這更新的版本,舊手機還顯示不出來,會變成方框,所以一律不收。
- 數量要 2 到 12 個,總長不能超過 50 個字元。只有一個 emoji 的話,直接去 Emoji 頁找就好。
- 換成標準寫法:該有的變體選擇符補上,膚色拿掉。
- 最後把標準寫法裡的變體選擇符也拿掉,剩下的就是這一組的「身分證字號」。資料庫裡,同一個字號只能有一筆。
結果是這樣:👍 和 👍🏽 算同一組。順序不同就算兩組,🥺👉👈 和 👈👉🥺 不一樣。連接符會保留,👨👩👧 不會被拆成 👨👩👧。
你在投稿頁輸入的時候,系統就會馬上檢查。如果已經有人投過一樣的組合,頁面會直接顯示那一則,你可以改成收藏它,或去那一則留言,不會多出一則一模一樣的。

圖:輸入 🙂 👍🏽,空格和膚色都拿掉了,系統馬上認出這就是已經存在的 🙂👍。
就算兩個人幾乎同時送出同一組,資料庫也會擋下晚到的那一筆,改成顯示已經存在的那一則。隱藏或下架的組合,字號還是佔著。所以被拿掉的組合,不能過一陣子再重投一次。
技術細節:怎麼認出重複
- 輸入先做 NFC 正規化,再去掉空白、零寬空格、word joiner 與 BOM。ZWJ 不去掉。
- 上面說的「字元」精確來說是碼位(code point)。標準寫法是 fully-qualified、不含膚色修飾字元的序列;去重鍵再拿掉 VS15/VS16,存在 D1 的 UNIQUE 欄位上。兩人同時送出時,也是這個 UNIQUE 限制擋下後到的那一筆。
- 切 emoji 沒有用瀏覽器的
Intl.Segmenter,而是對白名單做最長比對。一方面 Firefox 125 以後才支援它;另一方面,不在白名單裡的字元反正都要拒絕,白名單本來就是最後的判斷依據。 - 這套規則寫成一支純函式,瀏覽器的即時檢查和 Worker 的最終檢查共用同一份,不會出現「畫面說可以、伺服器說不行」。
排行:數的是「不同的人」
排行看的是複製次數。可是「有人複製了」這件事,是你的瀏覽器回報給伺服器的,寫個小程式就能一直送假的回報來灌票。所以我數的不是次數,而是「每天有幾個不同的人複製」。
- 在你的瀏覽器裡:同一個分頁裡,同一組只回報一次,而且會先存起來,過幾秒再一起送出。就算回報失敗,也不影響你複製。
- 在伺服器上:伺服器每天產生一把新的隨機金鑰,再拿這把金鑰和你的 IP 位址(你上網時的門牌號碼)算出一串代碼,當作「今天的訪客代碼」。同一個代碼、同一組、同一天只算一次。
- 兩天後刪掉那天的金鑰:金鑰一刪,任何人(包括我)都算不回某個代碼來自哪個位址。算複製數也不用 cookie,不會在你的裝置上留下能認出你的東西。

圖:同一個人按再多次也只算一次;拿來認人的金鑰,兩天後就刪掉。
一開始的設計,還把瀏覽器的 User-Agent(瀏覽器自己報上的型號和版本)也算進代碼裡。後來做安全檢查時拿掉了,因為它是自己報的,想改就能改,換一個就能多算一票。代價是共用同一個對外 IP 的人,例如同一間公司或同一個家裡的 Wi-Fi,會被當成同一個人,數字會偏低。不過排行只需要知道誰前誰後,這樣就夠用了。
另一道關卡是「能不能進排行」。投稿要在兩個不同的日子都有人複製過,才會進首頁的熱門排行。同一天灌十萬次,也只算一天。就算花錢買一大批代理伺服器(proxy,可以一直換 IP 位址),一天之內換再多位址,也跨不過「兩個不同日子」這道門檻。我先放的那 168 組,以及我整理過的組合,不受這個限制。
技術細節:訪客代碼
- 瀏覽器端用
sendBeacon批次回報。 - 訪客代碼是用當天的金鑰(32 個隨機位元組)對 IP 前段做 HMAC-SHA256,取前 8 個位元組。IPv4 取整個位址;IPv6 取前 64 位元,因為同一個家庭或同一支手機,會在這一整段裡換來換去。
- 「一天」以 UTC 計算。
不先審核,靠什麼擋
第一版的做法是「投稿立刻顯示,標示未審核;審核過才進排行、才給 Google 收錄」。上線隔天我改了主意:這個區塊以後的內容,主要會來自大家投稿,分類和編輯挑選沒有那麼重要,排行應該交給複製數決定。剛上線時的順序是我先排好的,之後再由複製數慢慢改變。現在投稿不用等審核,送出後立刻公開,可以複製、留言,也會出現在「最新」。

圖:投稿送出就公開;之後是進排行、進主題頁、被隱藏還是過期下架,看大家的複製、檢舉、我的整理和時間。
少了人工審核,就要換一整組防線:
- 登入和擋機器人:投稿、留言、檢舉都要先用 Google 登入。投稿和留言還要通過 Cloudflare 的擋機器人驗證(Turnstile)。平常看不到,只有系統起疑時才會請你點一下。
- 數量上限:每個帳號每天最多投 5 則。每個 IP 位址每天也只有 5 則額度,因為開新的 Google 帳號幾乎不用成本,換 IP 位址就麻煩得多。
- 文字規則:標題、說明、留言和暱稱一律不能放連結,也不能夾帶某些看不見的控制字元,例如能讓文字方向顛倒的那種。
- 關鍵字清單:專門擋廣告、色情、辱罵和歧視字眼。比對前會先把全形英文字母換成半形,長得跟英文字母一樣的俄文、希臘字母也換成對應的英文字母,不然把 a 換成俄文的 а 就能繞過去。標題、說明和暱稱命中就直接拒絕,留言命中則先藏起來,等我看過。
- 檢舉自動隱藏:只要有三個註冊滿一天的帳號檢舉,這則就會自動隱藏,等我處理。只算註冊滿一天的帳號,是因為臨時開三個新帳號太容易了。
- 不給 Google 收錄:每一則投稿自己的頁面都標了
noindex,意思是請搜尋引擎不要收錄。只有補齊說明的組合,才會進主題頁。 - 會過期:投稿 180 天內沒有人複製、沒有人檢舉,也沒有被我整理,就會自動下架。
- 緊急開關:改一個設定就能馬上關掉投稿,不用改程式、重新上線。
一個差點上線的 bug
有一個 bug 值得記下來。自動隱藏的判斷是一段 SQL(跟資料庫要資料的語法)。這段 SQL 裡面還包著另一段查詢(相關子查詢),我在裡面只寫了 id,沒寫是哪張表的 id。D1 底層是 SQLite,它會先從最裡層找這個欄位,而使用者資料表(users)剛好也有 id。結果條件變成「檢舉人就是被檢舉的對象」,永遠不會成立,自動隱藏也就永遠不會觸發。
這個 bug 是跑「冒煙測試」時抓到的:我在自己的電腦上,把網頁跟伺服器溝通的每一支 API 都快速試過一遍。從那之後,這類子查詢我一律幫外層的表取別名,欄位也寫完整。
隱私:只留做事需要的最少資料
- 登入:Google 登入只用到 client ID,這是網站在 Google 那邊的公開編號。你登入後,Google 會交給網站一張登入憑證(ID token),伺服器再用 Google 公開的金鑰,確認它真的是 Google 發的。所以整套設計不需要 client secret 這種得藏好的密碼。資料庫只存 Google 給的帳號識別碼,和站上顯示的暱稱(第一次登入時隨機產生,可以自己改),不存 email、姓名和大頭照。
- 用到才載入:要等你按下登入或送出,才會載入 Google 的登入程式和 Turnstile。
- 登入狀態:用一個 30 天的 cookie 記住你登入了。這個 cookie 設成 HttpOnly,網頁上的程式讀不到它。資料庫只存它的雜湊值,這是一種單向的指紋,算得出來、倒推不回去。
- 刪除帳號:會刪掉你的登入狀態、收藏、留言和你送出的檢舉。投稿會改成匿名,你填的標題和說明也會一起拿掉;還沒進排行的投稿則整則隱藏。被停權的帳號例外:刪除後會留下帳號識別碼的雜湊值,避免刪掉帳號再登入,就能解除停權。
這些規則都寫在顏文字花園的隱私權頁 ↗。頁面上寫的和程式實際做的對得上,是我做這類功能最在意的事。
小結
- 意思要有來源:一開始的 168 組,是整理過台灣網友的用法才寫的;大家投稿的說明,要經過整理才會進可以被搜尋到的頁面。
- 比對重複前,先統一寫法:用白名單切出 emoji,拿掉膚色和變體選擇符,最後由資料庫擋下重複。
- 排行只數不同的人:拿來認人的金鑰每天換,兩天後刪掉,連我也查不回來自哪個 IP。
- 不先審核,防線就要成套:登入、擋機器人、數量上限、文字規則、檢舉、過期,少一道都會被繞過。
想試試看的話,Emoji 組合 ↗不用登入就能瀏覽和複製。顏文字花園的完整介紹,可以看Emoji・顏文字工具介紹;那顆會眨眼的 Logo 怎麼做,寫在這篇。
