照與表情組合實(shí)戰(zhàn))
1. 先把“隱藏代碼”這件事說(shuō)透表情在聊天框里到底是什么微信聊天里那些“隱藏代碼”說(shuō)白了就是一段段看不見(jiàn)或半可見(jiàn)的 Unicode 字符序列。你按下發(fā)送鍵的那一刻發(fā)出去的并不是一張圖片而是一串碼位Code Point對(duì)方手機(jī)收到這串?dāng)?shù)字后用自己系統(tǒng)里的字體把它“畫(huà)”成一個(gè)表情。所以同一個(gè)表情安卓和 iOS 畫(huà)出來(lái)胖瘦不一樣老設(shè)備干脆畫(huà)成一個(gè)方塊——這不是微信的鍋是字體渲染的差異。這篇東西想解決三個(gè)問(wèn)題搞清楚這些碼位怎么讀、SoftBank 那套老編碼和現(xiàn)在的 Unified統(tǒng)一碼怎么對(duì)上、以及怎么在自己的輸入習(xí)慣里把它變成可以天天用的東西。SoftBank 對(duì)照表和Unified 碼位是全文的兩條主線(xiàn)前者解決“老資料看不懂”后者解決“新表情怎么自己造”。適合三類(lèi)人看經(jīng)常在群里被問(wèn)“這個(gè)表情你哪來(lái)的”的重度聊天用戶(hù)、需要做文案或社群運(yùn)營(yíng)的內(nèi)容崗、以及想在自己程序里處理表情字符的開(kāi)發(fā)者。前面的編碼原理部分稍硬但我會(huì)盡量用生活化的類(lèi)比講后半部分的實(shí)操配置可以直接抄。我見(jiàn)過(guò)太多人把“隱藏代碼”理解成某種秘鑰以為輸入一串字符就能解鎖系統(tǒng)里藏著的表情包。實(shí)際情況要樸素得多所謂的“隱藏”指的是這些字符在鍵盤(pán)面板上找不到入口但在文本層面是合法存在的你可以復(fù)制、可以粘貼、可以存成快捷短語(yǔ)。一旦理解了這一點(diǎn)后面所有的玩法都是水到渠成的事。1.1 一條表情消息的“身份證”碼位、碼點(diǎn)與字節(jié)任何一個(gè)表情在 Unicode 標(biāo)準(zhǔn)里都有一個(gè)唯一的編號(hào)寫(xiě)作 UXXXX。比如那個(gè)最常見(jiàn)的笑臉編號(hào)是 U1F600一個(gè)心形是 U2764。這個(gè)編號(hào)叫碼位它只是一個(gè)抽象的數(shù)字跟“長(zhǎng)什么樣”沒(méi)有必然關(guān)系——具體畫(huà)成什么樣由字體文件決定。這就像身份證號(hào)和人臉的關(guān)系號(hào)碼唯一但同一個(gè)人在不同相機(jī)里拍出來(lái)氣質(zhì)可能完全不同。真正傳輸?shù)臅r(shí)候這個(gè)數(shù)字要變成字節(jié)。UTF-8 編碼下U1F600 會(huì)被編成 4 個(gè)字節(jié)F0 9F 98 80。UTF-16 下則是兩個(gè) 16 位單元也就是所謂的代理對(duì)Surrogate PairD83D DE00。這里就是第一個(gè)大坑——很多老的接口、老的數(shù)據(jù)庫(kù)字段、老的文本處理函數(shù)是按“一個(gè) 16 位單元等于一個(gè)字符”來(lái)設(shè)計(jì)的遇到代理對(duì)就會(huì)把一個(gè)表情切成兩半于是你看到的就是兩個(gè)問(wèn)號(hào)或者一個(gè)亂碼方塊。我做社群文案的時(shí)候踩過(guò)一次把一段帶表情的文案粘進(jìn)某個(gè)統(tǒng)計(jì)表格導(dǎo)出的 CSV 里表情全變成了問(wèn)號(hào)。排查了半天才發(fā)現(xiàn)那個(gè)工具按 UTF-16 單元截?cái)嘧址冒岩粋€(gè)代理對(duì)從中間劈開(kāi)了。后來(lái)我的做法是凡是涉及表情的文本處理一律先確認(rèn)編碼是 UTF-8并且用能正確處理碼位而不是字節(jié)或 16 位單元的函數(shù)來(lái)切分。理解這一層之后你就明白為什么“復(fù)制粘貼”是表情傳播的主要方式復(fù)制的是碼位序列不是圖像。這也解釋了另一個(gè)現(xiàn)象——你把微信里的表情復(fù)制到記事本可能變成黑白或者方塊因?yàn)橛浭卤居玫淖煮w不一定覆蓋這個(gè)碼位。1.2 為什么同一個(gè)表情在不同手機(jī)上長(zhǎng)得不一樣字體覆蓋率是第一層原因。系統(tǒng)字體比如各家的系統(tǒng) emoji 字體會(huì)定期更新新加入的表情在老系統(tǒng)上就沒(méi)有對(duì)應(yīng)字形只能退化顯示成空白方塊或者“豆腐塊”。這就是為什么你發(fā)一個(gè)剛出的新表情用舊手機(jī)的朋友會(huì)回你一句“你發(fā)的啥我這邊是空白”。第二層原因是呈現(xiàn)方式的選擇。Unicode 里有一批字符是“雙重身份”的它既可以當(dāng)普通的文本符號(hào)比如一個(gè)黑白的心形、一個(gè)普通的星號(hào)也可以當(dāng)彩色表情。到底畫(huà)成哪種取決于后面跟不跟一個(gè)叫變體選擇符的隱形字符。這個(gè)機(jī)制是后面第三章的重點(diǎn)也是很多“我明明復(fù)制對(duì)了怎么變黑白了”的根源。第三層原因是平臺(tái)對(duì)組合序列的支持程度。有些表情是由好幾個(gè)碼位拼起來(lái)的比如“一家人”“女醫(yī)生”“彩虹旗”。支持完整的系統(tǒng)能拼出正確圖形支持一半的系統(tǒng)可能只顯示第一個(gè)組成部分于是“一家三口”變成了“一個(gè)人”。這三層原因決定了后面所有排查工作的順序先看碼位對(duì)不對(duì)再看有沒(méi)有丟變體選擇符最后才懷疑字體版本。1.3 誰(shuí)適合看這篇三類(lèi)人的不同用法第一類(lèi)是純聊天用戶(hù)。你不需要記任何碼位只需要跳到第四章把常用的組合存成輸入法的自定義短語(yǔ)兩三個(gè)字母就能打出一長(zhǎng)串。第四章給了三套方案按你的動(dòng)手意愿選一套就行。第二類(lèi)是內(nèi)容運(yùn)營(yíng)和文案崗。你需要的是“穩(wěn)定不翻車(chē)”也就是發(fā)出去之后在大面積設(shè)備上都能正常顯示。這類(lèi)讀者重點(diǎn)看第五章那里有一張故障對(duì)照表覆蓋了顯示成方塊、被裁切、變成問(wèn)號(hào)、字?jǐn)?shù)超限這幾種情況照著查就行。第三類(lèi)是開(kāi)發(fā)者。你需要的是可編程的處理方式包括怎么識(shí)別一段文本里的表情、怎么安全地截?cái)?、怎么?gòu)造組合序列。第四章第三節(jié)有一段可以直接跑的腳本覆蓋了拆分碼位、檢測(cè)組合序列、批量替換這幾個(gè)常用動(dòng)作。順便說(shuō)一句網(wǎng)上流傳的很多“表情代碼大全”其實(shí)是把碼位表直接貼出來(lái)不解釋結(jié)構(gòu)也不說(shuō)明版本。這類(lèi)資料的問(wèn)題在于你照著抄了一旦顯示不對(duì)就無(wú)從下手。這篇的寫(xiě)法是先講結(jié)構(gòu)再給表目的是讓你具備自己查、自己驗(yàn)的能力。2. SoftBank、Unified 與那套被收編的老編碼要講清楚對(duì)照表必須先講一段歷史。在 Unicode 正式收錄表情之前早期的移動(dòng)端平臺(tái)各自在自己系統(tǒng)的私有區(qū)域里定義了一套圖形字符。私有使用區(qū)Private Use Area是 Unicode 里預(yù)留的一段范圍大致在 UE000 到 UF8FF 之間標(biāo)準(zhǔn)不規(guī)定這些碼位代表什么留給各家自己用。SoftBank 當(dāng)年的表情就放在這個(gè)區(qū)域里所以你在老資料里看到的 E04A、E04B 這類(lèi)四位編號(hào)本質(zhì)上就是“私有約定”不是標(biāo)準(zhǔn)字符。2.1 私有使用區(qū)的歷史位置與它帶來(lái)的麻煩私有使用區(qū)的好處是自由壞處是不可移植。同一段字節(jié)在 A 家的設(shè)備上是太陽(yáng)在 B 家的設(shè)備上可能是別的圖形甚至什么都不顯示。當(dāng)年的跨平臺(tái)聊天經(jīng)常出現(xiàn)“我發(fā)的是笑臉你收到的是問(wèn)號(hào)”根子就在這兒。后來(lái)統(tǒng)一碼聯(lián)盟做了一件很重要的事把這些民間表情逐一收編進(jìn)標(biāo)準(zhǔn)碼位。收編的過(guò)程中產(chǎn)生了一張映射表也就是常說(shuō)的 emoji 對(duì)照表把各家私有碼位對(duì)應(yīng)到統(tǒng)一的碼位。SoftBank 那一列就是從這張表里來(lái)的。這里有個(gè)細(xì)節(jié)值得注意映射不是一對(duì)一的完美替換。有些私有碼位在標(biāo)準(zhǔn)里有多個(gè)候選有些標(biāo)準(zhǔn)表情在私有區(qū)里壓根沒(méi)有對(duì)應(yīng)物還有些映射在不同版本的映射文件里注釋不一致。所以你手里的對(duì)照表如果和別人的對(duì)不上先別懷疑自己抄錯(cuò)了很可能是版本差異。判斷依據(jù)應(yīng)該是碼位本身加上名稱(chēng)而不是憑圖形去猜。2.2 Unified 是怎么把這些表情收編的統(tǒng)一碼的做法是分塊收錄。表情主要集中在幾個(gè)區(qū)塊里U1F300 到 U1F5FF 是雜項(xiàng)符號(hào)和象形文字U1F600 到 U1F64F 是表情臉U1F680 到 U1F6FF 是交通和地圖符號(hào)U2600 到 U27BF 里則散落著一批從傳統(tǒng)符號(hào)“升級(jí)”成表情的字符比如太陽(yáng)、云、雨傘、心形、星號(hào)。所謂“升級(jí)”就是我前面說(shuō)的雙重身份。U2600 這個(gè)碼位在標(biāo)準(zhǔn)里的正式名稱(chēng)是“帶射線(xiàn)的黑色太陽(yáng)”它本來(lái)就是個(gè)文本符號(hào)后來(lái)為了兼容表情需求標(biāo)準(zhǔn)規(guī)定如果它后面跟著變體選擇符 UFE0F就應(yīng)當(dāng)以彩色表情形式呈現(xiàn)如果跟著 UFE0E或者什么都不跟就以文本形式呈現(xiàn)。這一條是理解“隱藏代碼”的關(guān)鍵。SoftBank 那批老碼位絕大多數(shù)被映射到了這些標(biāo)準(zhǔn)碼位上。所以你手上如果有一份 SoftBank 編號(hào)想在新設(shè)備上用正確做法是查表?yè)Q成 Unified 碼位而不是原樣粘貼。原樣粘貼的私有區(qū)字符新設(shè)備上大概率什么都顯示不出來(lái)——因?yàn)槟嵌畏秶F(xiàn)在基本是空的。2.3 對(duì)照表怎么讀三列結(jié)構(gòu)最實(shí)用我建議你自己維護(hù)的對(duì)照表至少包含三列SoftBank 碼位、Unified 碼位、名稱(chēng)。名稱(chēng)這一列別省它是你排查時(shí)的錨點(diǎn)。圖形列可以加但優(yōu)先級(jí)最低因?yàn)閳D形會(huì)隨字體版本變化。下面這張表給的是結(jié)構(gòu)演示列了幾個(gè)高頻的天氣類(lèi)示例。注意不同版本的映射文件對(duì)同一碼位的注釋可能存在出入實(shí)際使用時(shí)請(qǐng)以你的字庫(kù)和映射文件為準(zhǔn)并用實(shí)測(cè)顯示來(lái)驗(yàn)證。SoftBank 碼位Unified 碼位名稱(chēng)備注E04AU2600 UFE0F太陽(yáng)需跟變體選擇符才顯示彩色E04BU2601 UFE0F云同上E04CU2614帶雨的傘標(biāo)準(zhǔn)碼位本身即表情E000 段分散映射大量圖形需逐條查表不可批量假設(shè)無(wú)對(duì)應(yīng)U1F600 起新表情臉?biāo)接袇^(qū)時(shí)代不存在讀這張表的時(shí)候要抓住一個(gè)邏輯左邊是歷史右邊是現(xiàn)在。你在網(wǎng)上看到一段“表情代碼”先判斷它是哪一列的東西。如果是 E 開(kāi)頭的四位十六進(jìn)制那基本是私有區(qū)老碼位需要轉(zhuǎn)換如果是 U1F 開(kāi)頭那已經(jīng)是標(biāo)準(zhǔn)碼位直接用就行但要留意變體選擇符和組合序列。另外提醒一個(gè)實(shí)操經(jīng)驗(yàn)不要試圖把整張對(duì)照表背下來(lái)。真正有用的是記住幾個(gè)高頻的剩下的靠查。我自己的做法是維護(hù)一個(gè)純文本文件左邊寫(xiě)“關(guān)鍵詞”右邊寫(xiě)“可以直接粘貼的字符”需要的時(shí)候搜關(guān)鍵詞。這比記碼位高效得多也更不容易出錯(cuò)。3. 五類(lèi)高頻“隱藏代碼”逐個(gè)拆解這一章是全文最硬的部分但也是最值錢(qián)的。把這五類(lèi)搞清楚你看到任何一串“神秘代碼”都能自己判斷它是什么、為什么這么寫(xiě)、會(huì)不會(huì)翻車(chē)。3.1 變體選擇符一個(gè)看不見(jiàn)的字符決定彩色還是黑白變體選擇符有兩個(gè)常用的UFE0F 表示“請(qǐng)用表情形式呈現(xiàn)”UFE0E 表示“請(qǐng)用文本形式呈現(xiàn)”。它們本身不占顯示寬度也不產(chǎn)生任何圖形純粹是給渲染器的指令。這就是最典型的“隱藏代碼”——你眼睛看不見(jiàn)它但它決定了你看到的是彩色小人還是黑白符號(hào)。舉幾個(gè)常見(jiàn)的例子。心形 U2764不帶變體選擇符時(shí)很多系統(tǒng)畫(huà)成細(xì)黑線(xiàn)的心加上 UFE0F 之后變成飽滿(mǎn)的紅色愛(ài)心。太陽(yáng) U2600、云 U2601、雨傘 U2614 都是同一套邏輯。還有那個(gè)非常常用的對(duì)勾 U2714、叉號(hào) U274C也都是靠這個(gè)開(kāi)關(guān)切換形態(tài)。這里的坑在于復(fù)制粘貼的過(guò)程中變體選擇符極易丟失。因?yàn)樗豢梢?jiàn)很多編輯器、很多輸入框在粘貼時(shí)會(huì)做“清理不可見(jiàn)字符”的處理順手就把它刪了。你會(huì)發(fā)現(xiàn)自己復(fù)制的時(shí)候明明是彩色的粘到別處就變成黑白線(xiàn)條了。這不是錯(cuò)覺(jué)。排查方法很簡(jiǎn)單把那段文本粘到一個(gè)能看到碼位的工具里很多編輯器有“顯示不可見(jiàn)字符”的選項(xiàng)數(shù)一下心形后面有沒(méi)有 FE0F。如果沒(méi)有手動(dòng)補(bǔ)一個(gè)或者換一個(gè)不會(huì)被清理的路徑。再講一個(gè)更隱蔽的情況有些平臺(tái)會(huì)在存儲(chǔ)時(shí)把變體選擇符規(guī)范化掉。比如某次我做一批文案在本地編輯器里全部正常導(dǎo)進(jìn)某個(gè)后臺(tái)之后全部變黑白。后來(lái)發(fā)現(xiàn)是那個(gè)后臺(tái)的輸入框?qū)ξ谋咀隽?NFC 規(guī)范化之外的額外處理。解決辦法是改用圖片或者干脆選一個(gè)本身就是表情碼位的字符比如 U1F49B 黃色心形繞開(kāi)這個(gè)開(kāi)關(guān)。3.2 零寬連接符家庭、職業(yè)、情侶表情的拼裝邏輯零寬連接符ZWJ的碼位是 U200D。它的作用是把前后兩個(gè)表情“焊”在一起告訴渲染器這兩個(gè)別分開(kāi)畫(huà)請(qǐng)合成一個(gè)新圖形。最典型的應(yīng)用是家庭組合。一個(gè)人 ZWJ 另一個(gè)人渲染器一看這個(gè)結(jié)構(gòu)就畫(huà)成“兩個(gè)人手拉手”。再加一個(gè) ZWJ 和一個(gè)小孩就變成“一家三口”。同樣的邏輯用在職業(yè)上一個(gè)人 ZWJ 一個(gè)工具符號(hào)就變成“某個(gè)職業(yè)的人”。用在情侶上兩個(gè)人 ZWJ 心形就變成“帶心的情侶”。這個(gè)機(jī)制的妙處在于組合爆炸基礎(chǔ)元素就那么幾十個(gè)通過(guò) ZWJ 拼接能造出成千上萬(wàn)種組合。這也是為什么每年新增的表情看起來(lái)很多實(shí)際上很多是新組合而不是新圖形。但它也是兼容性最差的一類(lèi)。老系統(tǒng)不認(rèn)識(shí)這個(gè)序列就會(huì)退化成“把每個(gè)組成部分單獨(dú)畫(huà)出來(lái)”于是“一家三口”變成“三個(gè)人并排”中間還多出一個(gè)空白ZWJ 本身不顯示但可能占位。所以在正式場(chǎng)合發(fā)這類(lèi)組合表情一定要先在目標(biāo)設(shè)備上預(yù)覽。我自己的經(jīng)驗(yàn)是群里發(fā)著玩隨便用正式文案一律不用 ZWJ 組合。因?yàn)槭鼙娫O(shè)備太雜你沒(méi)法保證每個(gè)人都看到你想要的效果。順帶提一個(gè)常見(jiàn)的誤解。有人以為 ZWJ 是“刪除”或者“連接兩個(gè)詞”的其實(shí)它在排版里確實(shí)有“不斷行”的語(yǔ)義在表情里則是“合成”的語(yǔ)義。同一個(gè)字符在不同上下文里承擔(dān)不同職責(zé)這在 Unicode 里很常見(jiàn)不用糾結(jié)看上下文就行。3.3 膚色修飾符五個(gè)檔位的疊加順序有講究膚色修飾符一共五個(gè)碼位從 U1F3FB 到 U1F3FF分別對(duì)應(yīng)從淺到深的五個(gè)檔位。用法是緊跟在具備膚色屬性的人體表情之后。關(guān)鍵的規(guī)則是順序。修飾符必須跟在基礎(chǔ)表情之后不能放前面。比如“豎起大拇指”加“中等膚色”正確的寫(xiě)法是大拇指在前、修飾符在后。放反了渲染器就認(rèn)不出來(lái)會(huì)顯示成兩個(gè)獨(dú)立的東西一個(gè)大拇指加一個(gè)色塊。第二個(gè)規(guī)則是只能跟一個(gè)。跟兩個(gè)修飾符是非法的渲染器的行為不確定有的取第一個(gè)有的直接忽略全部。第三個(gè)規(guī)則是不是所有表情都支持。只有標(biāo)準(zhǔn)里標(biāo)注了“emoji modifier base”的那批人體部位或人物表情才支持比如手勢(shì)、人物、部分職業(yè)。如果你給一個(gè)蘋(píng)果加膚色修飾符它是不會(huì)變色的。這條規(guī)則在實(shí)際使用中的價(jià)值在于它解釋了為什么“同一個(gè)手勢(shì)別人發(fā)的深色我發(fā)的淺色”。不是版本問(wèn)題是修飾符不同。還有個(gè)小細(xì)節(jié)值得留意膚色修飾符在組合序列里的位置。比如“女醫(yī)生”加膚色標(biāo)準(zhǔn)寫(xiě)法是“人物 ZWJ 職業(yè)符號(hào) 膚色修飾符”修飾符放在整個(gè)序列的最后。放在中間某些位置部分渲染器會(huì)認(rèn)部分不會(huì)屬于“能跑但不穩(wěn)”的寫(xiě)法不建議用。3.4 國(guó)旗與鍵帽區(qū)域指示符與合成序列國(guó)旗的實(shí)現(xiàn)方式很有意思。它沒(méi)有為每個(gè)國(guó)家單獨(dú)分配一個(gè)碼位那樣太浪費(fèi)而是用了區(qū)域指示符號(hào)U1F1E6 到 U1F1FF 這 26 個(gè)碼位對(duì)應(yīng)字母 A 到 Z。兩個(gè)區(qū)域指示符連在一起就代表一個(gè)兩位的國(guó)家或地區(qū)代碼渲染器查表把它畫(huà)成對(duì)應(yīng)的旗幟。比如某個(gè)兩字母代碼寫(xiě)成兩個(gè)對(duì)應(yīng)的區(qū)域指示符就出旗子。這個(gè)設(shè)計(jì)很優(yōu)雅但也有個(gè)副作用在不支持的國(guó)家或地區(qū)它會(huì)退化成兩個(gè)方框字母。你會(huì)看到兩個(gè)帶字母的方塊并排看起來(lái)像亂碼。這不是錯(cuò)誤是降級(jí)顯示。鍵帽序列是另一類(lèi)合成。它的結(jié)構(gòu)是一個(gè)基礎(chǔ)字符數(shù)字 0-9、井號(hào)、星號(hào) 變體選擇符 UFE0F 組合用鍵帽 U20E3。三個(gè)部分缺一不可。少了 U20E3就只是一個(gè)數(shù)字少了 FE0F某些系統(tǒng)上會(huì)顯示成文本樣式順序錯(cuò)了整個(gè)序列就散了。這一類(lèi)在聊天里的用途是編號(hào)。比如做群公告的步驟列表用鍵帽數(shù)字比用普通數(shù)字更醒目。但要注意鍵帽序列在某些輸入法里會(huì)被當(dāng)成普通字符處理粘貼后可能被拆開(kāi)用之前先預(yù)覽一下。3.5 標(biāo)簽序列那類(lèi)幾乎沒(méi)人提的隱形標(biāo)簽還有一類(lèi)更冷門(mén)的東西標(biāo)簽字符。碼位范圍大致在 UE0020 到 UE007F配合 U1F3F4黑旗使用可以拼出一些特殊的旗幟比如某些地區(qū)的旗幟就是“黑旗 一串標(biāo)簽字符 終止符”組成的。這類(lèi)序列的特點(diǎn)是支持度極低。很多系統(tǒng)壓根不認(rèn)直接把整串顯示成黑旗加一串空白或亂碼。所以網(wǎng)上流傳的一些“隱藏旗幟代碼”你在自己手機(jī)上可能是正常的發(fā)給別人就變成一坨。我的建議很直接除非你有明確的驗(yàn)證手段否則不要用標(biāo)簽序列。它屬于“技術(shù)上存在實(shí)用價(jià)值有限”的那一類(lèi)。真要表達(dá)用文字說(shuō)清楚更穩(wěn)妥。4. 在聊天框里真正用起來(lái)三套可復(fù)制的實(shí)操方案原理講完了接下來(lái)是能直接抄的部分。我給三套方案按動(dòng)手成本從低到高排你按自己的情況選。4.1 方案一自定義短語(yǔ)兩三個(gè)字母打出一長(zhǎng)串這是性?xún)r(jià)比最高的一套。絕大多數(shù)輸入法都支持“自定義短語(yǔ)”或“文本替換”功能你可以設(shè)一條規(guī)則輸入某個(gè)短碼自動(dòng)展開(kāi)成一長(zhǎng)串內(nèi)容。具體操作路徑各家輸入法略有不同但邏輯一致進(jìn)設(shè)置找“詞庫(kù)”或“快捷輸入”或“自定義短語(yǔ)”新建一條左邊填觸發(fā)碼右邊填展開(kāi)內(nèi)容。觸發(fā)碼建議用不會(huì)和正常輸入沖突的組合比如兩個(gè)不常用字母加一個(gè)數(shù)字。這里的關(guān)鍵技巧是觸發(fā)碼要有語(yǔ)義。比如你想存一個(gè)“點(diǎn)贊三連”的組合觸發(fā)碼可以用“dz3”存一個(gè)“分隔線(xiàn)”用“fgx”。有語(yǔ)義的短碼記起來(lái)毫不費(fèi)力純隨機(jī)碼過(guò)兩周就忘了。第二個(gè)技巧是分組管理。我自己的短語(yǔ)庫(kù)分了四組常用表情組合、分隔線(xiàn)與邊框、常用文案模板、特殊符號(hào)。每組用不同的前綴比如表情組統(tǒng)一用“e”開(kāi)頭符號(hào)組統(tǒng)一用“s”開(kāi)頭。這樣即使短語(yǔ)條數(shù)上百也不會(huì)互相干擾。第三個(gè)技巧是存“結(jié)構(gòu)”而不是“成品”。什么意思如果你存了一整句話(huà)帶表情用的時(shí)候只能整句用如果你存的是“表情 空格 占位”用的時(shí)候還能改。我更傾向于存短小的片段讓組合在輸入時(shí)完成靈活度高得多。這套方案唯一的缺點(diǎn)是換設(shè)備要重新配。有些輸入法支持賬號(hào)同步能省點(diǎn)事不支持的話(huà)建議把你的短語(yǔ)庫(kù)導(dǎo)出成一個(gè)純文本文件存著換機(jī)時(shí)批量導(dǎo)回去。4.2 方案二建一個(gè)“速查表”收藏夾隨用隨取如果你不想配輸入法或者用的設(shè)備經(jīng)常換那就用最原始也最可靠的辦法建一個(gè)純文本速查表。具體做法在任意筆記工具里新建一條按用途分區(qū)每區(qū)下面一行一個(gè)條目左邊寫(xiě)關(guān)鍵詞右邊寫(xiě)可以直接復(fù)制的內(nèi)容。用的時(shí)候搜索關(guān)鍵詞復(fù)制粘貼。這個(gè)方案看起來(lái)笨但有兩個(gè)無(wú)可替代的優(yōu)勢(shì)。第一是跨設(shè)備零成本只要筆記能同步哪臺(tái)設(shè)備都能用。第二是可驗(yàn)證你可以在同一條筆記里同時(shí)存“預(yù)期效果”的說(shuō)明時(shí)間久了自己也不會(huì)忘。我的速查表里有一欄是專(zhuān)門(mén)記“坑”的。比如某條組合在某個(gè)平臺(tái)上會(huì)顯示成方塊我就在旁邊標(biāo)注一下。這個(gè)習(xí)慣幫我省了很多重復(fù)排查的時(shí)間。4.3 方案三用腳本批量生成與清洗如果你要處理成百上千條或者需要把一批文案里的老碼位統(tǒng)一換成新碼位那就得上腳本了。下面這段 Python 覆蓋了三個(gè)最常用的動(dòng)作拆分碼位、檢測(cè)組合序列、批量替換。import re import unicodedata # 1. 把一段文本拆成碼位方便肉眼檢查有沒(méi)有丟變體選擇符 def dump_codepoints(text): for ch in text: cp ord(ch) try: name unicodedata.name(ch) except ValueError: name 未命名 print(fU{cp:04X}\t{name}) # 2. 檢測(cè)是否包含組合序列ZWJ 或變體選擇符或膚色修飾符 def has_sequence(text): patterns { ZWJ: \u200d, VS16: \ufe0f, 膚色修飾符: [\U0001F3FB-\U0001F3FF], 鍵帽: \u20e3, 標(biāo)簽字符: [\U000E0020-\U000E007F], } return {k: bool(re.search(v, text)) for k, v in patterns.items()} # 3. 把私有使用區(qū)的老碼位替換成標(biāo)準(zhǔn)碼位 # 映射關(guān)系請(qǐng)以你自己的映射文件為準(zhǔn)這里只放兩條演示 SB_TO_UNIFIED { \ue04a: \u2600\ufe0f, # 太陽(yáng) \ue04b: \u2601\ufe0f, # 云 } def convert_softbank(text): for old, new in SB_TO_UNIFIED.items(): text text.replace(old, new) return text if __name__ __main__: sample 測(cè)試\ue04a文本\u2764\ufe0f組合 print(--- 碼位拆解 ---) dump_codepoints(sample) print(--- 序列檢測(cè) ---) print(has_sequence(sample)) print(--- 替換后 ---) print(convert_softbank(sample))這段腳本的價(jià)值不在于它本身多復(fù)雜而在于它把“看不見(jiàn)的東西”變成了“看得見(jiàn)的東西”。你只要跑一次 dump_codepoints就能立刻知道一段文本里到底藏了什么、有沒(méi)有缺東西。這個(gè)動(dòng)作我在排查顯示問(wèn)題時(shí)做了不下幾十次。補(bǔ)充一句實(shí)操提醒處理文本時(shí)不要用按字節(jié)切片的函數(shù)。要按碼位或者按字形簇grapheme cluster切。Python 里按碼位遍歷是安全的但如果要按“用戶(hù)感知的一個(gè)字符”切就得用專(zhuān)門(mén)的庫(kù)因?yàn)橐粋€(gè)表情可能由好幾個(gè)碼位組成直接從中間切開(kāi)就廢了。5. 踩坑記錄顯示異常、被裁切、變成問(wèn)號(hào)的排查順序這一章是我這些年攢下來(lái)的故障清單按發(fā)生頻率排序。遇到問(wèn)題從上往下查基本能覆蓋九成情況。5.1 常見(jiàn)故障對(duì)照表現(xiàn)象最可能的原因排查動(dòng)作解決方式顯示成空白方塊字體不覆蓋該碼位查碼位是否屬于較新版本換舊表情或改用文字變成黑白線(xiàn)條丟了變體選擇符 FE0F用腳本 dump 碼位手動(dòng)補(bǔ) FE0F變成兩個(gè)問(wèn)號(hào)按 16 位單元截?cái)啻韺?duì)被劈開(kāi)檢查處理鏈路的編碼全程用 UTF-8按碼位切組合表情散開(kāi)系統(tǒng)不支持 ZWJ 序列換一臺(tái)設(shè)備預(yù)覽改用單個(gè)表情粘貼后內(nèi)容變少編輯器清理了不可見(jiàn)字符換粘貼路徑用純文本編輯器中轉(zhuǎn)發(fā)送后超出字?jǐn)?shù)限制組合序列占多個(gè)碼位數(shù)一下實(shí)際碼位數(shù)精簡(jiǎn)或拆成兩條旗幟變成兩個(gè)字母塊區(qū)域指示符降級(jí)顯示確認(rèn)目標(biāo)平臺(tái)是否支持改用文字表述這張表最有用的是第三行。很多人以為“問(wèn)號(hào)”是表情不存在其實(shí)往往是編碼被截?cái)嗔?。區(qū)分方法很簡(jiǎn)單如果是一個(gè)方塊多半是字體沒(méi)有如果是兩個(gè)問(wèn)號(hào)或者一個(gè)問(wèn)號(hào)加半個(gè)亂碼多半是字節(jié)層面被切了。5.2 排查順序從碼位到字體再到平臺(tái)我習(xí)慣按這個(gè)順序走能少走很多彎路。第一步確認(rèn)碼位本身是對(duì)的。用前面那段腳本 dump 一下看看是不是你想要的碼位有沒(méi)有多出或少掉不可見(jiàn)字符。這一步能解決大部分問(wèn)題因?yàn)楹芏喙收显从趶?fù)制過(guò)程中丟了東西。第二步確認(rèn)呈現(xiàn)方式。如果是那種“雙重身份”的字符檢查變體選擇符在不在。這一步專(zhuān)門(mén)解決“為什么是黑白的”。第三步確認(rèn)組合結(jié)構(gòu)。如果是多碼位組合檢查順序?qū)Σ粚?duì)、有沒(méi)有非法疊加。這一步解決“為什么散開(kāi)了”。第四步確認(rèn)字體版本。把同一段文本發(fā)到另一臺(tái)設(shè)備上看如果那邊正常說(shuō)明是當(dāng)前設(shè)備的字體問(wèn)題不是內(nèi)容問(wèn)題。第五步確認(rèn)平臺(tái)處理。如果所有設(shè)備都不對(duì)那就得懷疑中間經(jīng)過(guò)了某個(gè)會(huì)改文本的環(huán)節(jié)比如輸入框、后臺(tái)、導(dǎo)出工具。這時(shí)候換一條路徑比如先存成文件再發(fā)能快速定位。這個(gè)順序的邏輯是從內(nèi)容到環(huán)境因?yàn)閮?nèi)容問(wèn)題占的比例最大而且改起來(lái)最便宜。5.3 幾個(gè)容易被忽略的細(xì)節(jié)第一個(gè)細(xì)節(jié)是字?jǐn)?shù)統(tǒng)計(jì)。很多平臺(tái)按碼位計(jì)數(shù)一個(gè)組合表情可能占五六個(gè)碼位。你以為發(fā)了一個(gè)表情實(shí)際上占了六個(gè)字的位置。做長(zhǎng)文案的時(shí)候要留余量不然會(huì)被截?cái)?。我遇到過(guò)一條文案被硬生生截掉半個(gè)組合表情最后顯示成一堆碎片非常難看。第二個(gè)細(xì)節(jié)是昵稱(chēng)和簽名的限制。有些位置對(duì)字符類(lèi)型有限制某些組合序列會(huì)被過(guò)濾掉或者被規(guī)范化成別的形式。設(shè)置完之后一定要退出來(lái)看一眼實(shí)際效果別只信編輯框里的預(yù)覽。第三個(gè)細(xì)節(jié)是復(fù)制到不同應(yīng)用時(shí)的差異。同一段文本從聊天框復(fù)制到備忘錄、從備忘錄復(fù)制到表格每經(jīng)過(guò)一個(gè)環(huán)節(jié)都可能被處理一次。我的經(jīng)驗(yàn)是要跨應(yīng)用搬運(yùn)帶表情的內(nèi)容先用純文本編輯器做中轉(zhuǎn)。純文本編輯器最“老實(shí)”不會(huì)自作主張改你的字符。第四個(gè)細(xì)節(jié)是輸入法的聯(lián)想干擾。你打某個(gè)短碼輸入法可能彈出聯(lián)想的候選詞把你要的短語(yǔ)頂?shù)?。解決方法是把觸發(fā)碼設(shè)得更“不像詞”比如加個(gè)數(shù)字或者用雙寫(xiě)字母。6. 組合玩法與邊界能做的不等于該做的講完原理和排查最后聊聊怎么用得好看、以及什么場(chǎng)合該收手。6.1 分隔線(xiàn)與邊框的構(gòu)造分隔線(xiàn)是最實(shí)用的組合玩法。做法很簡(jiǎn)單選一個(gè)重復(fù)起來(lái)好看的符號(hào)復(fù)制十到二十個(gè)中間不加空格。常用的有橫線(xiàn)類(lèi)、波浪類(lèi)、方塊類(lèi)。要點(diǎn)是選等寬或接近等寬的符號(hào)不然排出來(lái)會(huì)歪。邊框稍微麻煩一點(diǎn)因?yàn)橐紤]對(duì)齊。我的做法是用三行第一行是左上角、若干橫線(xiàn)、右上角第二行是豎線(xiàn)、空格、豎線(xiàn)第三行是左下角、橫線(xiàn)、右下角。但不同設(shè)備上字符寬度不一致很容易歪所以要留出調(diào)整余地寧可少用幾個(gè)字符。這里有個(gè)反直覺(jué)的經(jīng)驗(yàn)不要追求絕對(duì)對(duì)齊。在聊天場(chǎng)景里稍微不齊沒(méi)人會(huì)在意但如果你為了對(duì)齊用了大量全角半角混排在某些設(shè)備上會(huì)崩得很慘。我的原則是“看起來(lái)大致整齊就行”把精力放在內(nèi)容上。6.2 組合表情的“語(yǔ)法”順序、數(shù)量與嵌套組合表情不是隨便拼的。有三條經(jīng)驗(yàn)規(guī)則。第一順序有意義。ZWJ 連接的前后是有方向的某些組合反過(guò)來(lái)就是不成立的。膚色修飾符必須在最后。變體選擇符必須緊跟基礎(chǔ)字符。第二數(shù)量有上限。一個(gè)組合序列里塞太多 ZWJ渲染器可能放棄合成直接攤開(kāi)顯示。我試過(guò)把四個(gè)元素連在一起在部分設(shè)備上就散架了。一般兩到三個(gè)元素是安全區(qū)。第三不要嵌套。把兩個(gè)已經(jīng)合成好的組合再用 ZWJ 連起來(lái)屬于未定義行為各家表現(xiàn)不一致。這種寫(xiě)法看著聰明實(shí)際不可靠。6.3 什么場(chǎng)合該收手最后說(shuō)點(diǎn)“不技術(shù)”但更重要的事。第一正式溝通少用組合表情。給客戶(hù)、給上級(jí)、給不熟的人發(fā)消息組合表情的兼容風(fēng)險(xiǎn)不值當(dāng)。一個(gè)簡(jiǎn)單的表情足夠表達(dá)語(yǔ)氣復(fù)雜組合留給熟人場(chǎng)景。第二不要用表情替代標(biāo)點(diǎn)。有些人喜歡用表情當(dāng)逗號(hào)句號(hào)一句話(huà)里塞十幾個(gè)。在群聊里沒(méi)問(wèn)題但在需要存檔、檢索、轉(zhuǎn)發(fā)的場(chǎng)合會(huì)大大降低可讀性。第三注意場(chǎng)合的文化差異。同一類(lèi)圖形在不同語(yǔ)境里的含義可能完全不同跨群、跨年齡層發(fā)送之前想一想別把調(diào)侃發(fā)給不該調(diào)侃的人。我自己在實(shí)際操作中的體會(huì)是表情的價(jià)值在于“降低表達(dá)成本”而不是“炫技”。真正好用的不是那些復(fù)雜到?jīng)]人見(jiàn)過(guò)的組合而是那幾個(gè)你隨手就能打出來(lái)、對(duì)方一眼就能看懂的東西。把常用的二十個(gè)存成短語(yǔ)比背下來(lái)兩百個(gè)碼位有用得多。這套東西學(xué)到這里就夠了剩下的靠日常用。