ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从爬虫到SQLite:构建一份结构化Emoji元数据字典的完整实战

从爬虫到SQLite:构建一份结构化Emoji元数据字典的完整实战 这阵子我在做评论文本分析最头疼的反而不是怎么用 Python 爬虫把数据取回来而是怎么把评论里的 Emoji 符号变成可分析的元数据字典。主流的情感分析词表基本都只处理文字一个 和 放在句子末尾模型直接跑偏更不要说不同平台同一个表情还渲染得完全两回事。所以我干脆花了两天时间把 Unicode 官方和几个开源仓库的 Emoji 数据全部抓下来合并清洗成一份结构化的元数据字典最后同步到 SQLite 里日常查询直接一条 SQL 解决。这篇文章就是这次实战的完整记录。我会先讲清楚为什么要造这样一本字典、数据源怎么选然后分别拆解两个核心文件的解析逻辑、多源去重合并的策略、字典落地存储的姿势最后把这次踩过的四个坑原原本本写出来。适合的人群是有一定 Python 基础、想练手 requests 和 json 处理的朋友以及做 NLP、运营标签、前端组件、测试覆盖检测时被 emoji 坑过的开发者。整个项目用到的版本是 Python 3.10依赖只要 requests 和标准库没有花哨的框架。1. 为什么要造这样一本 Emoji 元数据字典1.1 真实场景被 Emoji 整得够呛后再动手之前做一个舆情聚合的爬虫项目评论区里有大量表情。一开始我以为 emoji 就是“一个字符”直接用字符集判断就能处理结果踩了一连串跟头。第一个坑是同一句话在不同终端展示效果不同。比如“笑哭了”这个表情苹果手机上是一个捂脸的小黄脸微信上是另一种微博可能又是第三种。用户在 A 平台表达的情绪到了 B 平台被渲染成完全不同的视觉符号如果我只比对码位根本覆盖不了这种“同一含义、不同码位”的差异。第二个坑是 Emoji 的组合序列。一家三口 ‍‍ 并不是三个独立表情拼在一起那么简单它是由 U1F468、U200D、U1F469、U200D、U1F467 组合出来的。爬虫抓到原始字节后如果不知道 ZWJ零宽连接符的存在就会把这个序列拆成“ ”语义信息全丢。第三个坑是文本分析场景。经典情感词典压根不收录 这种符号但它又是热度很高的情绪表达。想要把这类符号转成语义标签就得有一份“码位 → 中文含义/关键词/情感倾向”的映射表而通用的第三方库又经常字段不全、更新滞后。所以我想要的不只是一个“Emoji 字符列表”而是一份包含码位、字符本身、官方名称、别名、关键词、分组、子分组、平台图片文件名、版本号、皮肤色调标记的完整元数据字典。有了它下游不管是做搜索、做渲染、做统计还是做模型训练都能站在同一份可靠数据上。1.2 字典到底要装哪些字段先说清楚“元数据”在这里指什么。它不是一个简单的“这个字符叫笑脸”而是要回答五个问题它是什么包括 Unicode 码位、Emoji 字符文本、全限定状态。它叫什么包括官方英文名、短名称、别名列表、常用关键词。它属于哪包括 Emoji 分类里的 group 和 subgroup方便筛选和归类。它在哪用包括各平台渲染时的图片文件名、代码点序列、字符在字体中的 sheet 坐标。它怎么变化包括从哪个 Unicode 版本加入、是否带皮肤色调、是否属于某个 ZWJ 家族。我实际落地时用了下面这些核心字段这样设计是经过取舍的不是拍脑袋字段说明为什么需要unified码位串多个码位用-连接全部大写全局唯一主键emoji真实字符文本如 展示、落库、渲染name官方英文名基础命名short_names社区通用短别名如:grinning:聊天软件/编辑器转换keywords搜索关键词数组文本检索和语义标签group / subgroup官方分组信息筛选、浏览、统计statusfully-qualified / unqualified 等区分规范推荐序列和变体version_added最早加入的 Unicode 版本判断兼容性platform_images各平台图片文件名多平台渲染统一skin_tone / family_id皮肤色调和家庭分组标识处理组合序列1.3 项目预期的成品形态最终产出一份 emoji_dict.json 和一份 emoji.db其中 JSON 适合直接内嵌到前端或工具里SQLite 适合后端服务和批量查询。整个过程包含三个阶梯抓取从 Unicode 官方网站拉取 emoji-test.txt从开源仓库拉取 emoji.json 这样的补充文件。清洗将多来源数据按码位对齐处理归一化、去重、别名合并、冲突兜底。落库写 JSON 快照、建 SQLite 表、加索引、提供查询函数。我个人强烈建议不要手写几百个 URL 去抓网页那既慢又不稳定。真正的重点是把数据源的结构吃透解析函数写得足够通用之后再扩容就很简单。2. 数据源选型官方权威、社区丰富、平台映射三手抓2.1 候选数据源逐个拆解市面上的 Emoji 数据大概分成三类官方权威数据、社区维护数据、平台厂商导出数据。我把当时考察的几个主要数据源列成了一张表方便你理解我的选型逻辑。数据源提供内容优点缺点Unicode 官方 emoji-test.txt码位、状态、英文名、分组成员最权威、覆盖最全、持续更新没有关键词和图片文件名纯文本iamcal/emoji-data名称、短名称、关键词、图片文件名、sheet 坐标字段丰富结构规整适合补全更新滞后于官方个别码位缺失muan/emoji.json字符、短代码映射结构简单适合聊天场景字段太少只有一层映射关系emoji-api.com 等在线接口JSON 一步到位用起来最省事有请求限制稳定性不可控先说 Unicode 官方 emoji-test.txt。它是整个项目的定海神针。它本身就是一个纯文本文件每行描述一个 Emoji 的码位、状态和名称并且用注释行标明 group 和 subgroup。比如1F600 ; fully-qualified # grinning face这一行表示 U1F600 是一个推荐表情官方名称叫 grinning face。再说 iamcal/emoji-data。它提供了非常多的辅助字段short_names、keywords、多平台 image 文件名甚至还包括non_qualified这类非推荐码位。这些正好补足官方数据缺失的部分。缺点是它不保证跟着每一版 Unicode 同步更新所以只能作为补充源不能当主源。最后是那些在线 Emoji API。它们的好处是省事一条 URL 能拿到完整 JSON字段也做得很人性化。但爬虫项目最忌讳把主数据挂在别人家的接口上。所以我只把它们作为“抽查校验源”用来检查自己合并出来的记录有没有明显字段错位从来没当成主数据来用。2.2 最终确定的组合策略我的最终组合是两主一验主源一Unicode 官方 emoji-test.txt负责提供权威码位、名称、分组、状态。主源二iamcal/emoji-data 仓库的 emoji.json 和 categories.json负责提供短名称、关键词、图片文件名。校验源emoji-api 在线接口只做抽查。这两份主源的格式都比较稳定经过十几年发展也没有大的格式变化。我的经验是优先选“稳定格式”的数据源而不是“最新内容”的数据源。格式稳定意味着你的解析代码写一次可以跑很久维护成本很低内容更新则用脚本定期重新拉取即可。2.3 版本策略不锁定版本但要记录版本Unicode 官方 URL 支持固定版本号https://www.unicode.org/Public/emoji/latest/emoji-test.txt https://www.unicode.org/Public/emoji/15.1/emoji-test.txt我第一次拉取时用的是latest因为这样能保证数据总是最新的。但注意一个问题latest是个动态指针今天指向 15.1明天可能指向 16.0。你的解析逻辑如果没变化那当然没问题但如果解析逻辑和版本绑定比如新增了某些状态值就必须把版本号记录在数据里。我的做法很直接抓取后读取文件开头把文件头里的版本注释解析出来存到 meta 表。这样每次重新抓取都能清楚地知道当前字典是基于哪个版本。增量更新也不复杂因为官方提供的是完整文件不是 patch直接全量覆盖即可。3. 爬虫落地把 emoji-test.txt 和 emoji.json 啃干净3.1 请求、限速、重试影响成败的三个小细节很多新手写爬虫时把注意全放在解析上结果在请求层被拦住。这里虽然只是拉两个文本/JSON 文件我依然按生产环境的思路写了统一的请求封装包含三个要点。第一User-Agent 必须伪装。虽然 unicode.org 不怎么反爬但 iamcal 的 GitHub 仓库对请求头还是有一定要求的。不要用python-requests/2.x这种默认 UA 直接打用 Chrome 的 UA 更稳。HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/plain, application/json;q0.9, */*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }第二限速很关键。这里每个资源只有几百 KB一次性拉完可能只要几秒但如果你要做一个每周自动更新任务最好在两次请求之间加 0.5 到 1 秒的间隔避免触发 CDN 的速率限制。第三重试必须有指数退避。网络层抖动、源头 5xx、被动限流都可能出现。我写了这样一个通用函数import time import requests def fetch_with_retry(session, url, attempts3): delay 1 for i in range(attempts): try: resp session.get(url, timeout20) if resp.status_code 200: return resp if resp.status_code in (403, 429): print(f[retry] status{resp.status_code}, wait{delay}s, url{url}) time.sleep(delay * (2 ** i)) else: resp.raise_for_status() except requests.RequestException as exc: if i attempts - 1: raise print(f[retry] error{exc}, wait{delay * (2 ** i)}s) time.sleep(delay * (2 ** i)) raise RuntimeError(fretry exhausted: {url})很多人觉得就拉一两个文件没必要这么复杂但我见过太多项目因为“临时用一下”直接裸请求结果源头限流后整个流程崩掉最后还是要回来补这些基础代码。与其后面返工不如一开始就封装好。3.2 用“状态机”解析 emoji-test.txtemoji-test.txt 的解析在我看来是整个项目最核心的部分。它的结构有规律但不完全规整直接 split 容易出错需要有一个“当前分组、当前子分组”的上下文状态。先看一个真实的片段长什么样# group: Smileys Emotion # subgroup: face-smiling 1F600 ; fully-qualified # grinning face 1F636-200D-1F32B ; fully-qualified # ️ face in clouds 263A-FE0F ; fully-qualified # ☺️ smiling face每一行有三种情况# group:开头更新当前大组名。# subgroup:开头更新当前子组名。普通数据行格式是码位 ; 状态 # 字符 名称。关键点在于数据行的名称部分并不是简单的空格分割。# grinning face里第一个 token 是 emoji 字符本身从第二个 token 开始才是名称而名称里可能有很多空格。所以解析名称时不能把整段 comment 全部当名称。我的解析代码是这样的import re import requests from requests import Session def parse_emoji_test(content: str) - list[dict]: current_group current_subgroup records [] for line in content.splitlines(): line line.strip() if line.startswith(# group:): current_group line.split(:, 1)[1].strip() continue if line.startswith(# subgroup:): current_subgroup line.split(:, 1)[1].strip() continue if line.startswith(#) or not line: continue # 数据行格式: 1F600 ; fully-qualified # grinning face code_part, remain line.split(;, 1) remain remain.strip() status remain.split(#)[0].strip() comment remain.split(#, 1)[1].strip() # comment 里第一个 token 是 emoji 字符后面才是名称 tokens comment.split() emoji_char tokens[0] name .join(tokens[1:]) if len(tokens) 1 else codes [c.strip().upper() for c in code_part.split( ) if c.strip()] unified -.join(codes) records.append({ unified: unified, emoji: emoji_char, name: name, status: status, group: current_group, subgroup: current_subgroup, }) return records这里有个细节我专门强调一下code_part.split( )而不是直接取整段因为一行可能包含多个码位比如1F636-200D-1F32B是三个码位用连字符连接的序列。我中间还做了.upper()保证主键格式统一避免有人把码位写成了小写。解析完之后最好先打印前 20 条确认格式不要直接跳到合并和落库。这一步我吃过亏后面会在踩坑部分细讲。3.3 解析 JSONP 包装的 emoji.jsoniamcal/emoji-data 仓库里的 emoji.json 是标准的 JSON 数组每个元素类似{ name: grinning face, unified: 1F600, non_qualified: null, image: 1f600.png, sheet_x: 0, sheet_y: 0, short_names: [grinning], text: null, keywords: [face, grin, grinning face] }但这里有一个小坑有些历史版本或镜像仓库会把 JSON 包成 JSONP 格式也就是返回内容首尾多了括号和回调函数名比如callback([...])。直接json.loads会报错所以必须先剥离。我的做法是先判断文本开头如果以(开始就强制去掉首尾多余的括号再做一次json.loadsimport json def parse_emoji_json(content: str) - list[dict]: content content.strip() if content.startswith(() and content.endswith()): content content[1:-1] if content.startswith(callback(): content content[len(callback():-1] return json.loads(content)解析后拿到的是一个列表我需要把它按unified字段转换成字典方便后面与官方数据合并。unified在 iamcal 里是大写、不带空格、用短横线连接的格式正好和 emoji-test.txt 解析出来的统一格式一致。还有个小技巧github 的 raw 文件域名是raw.githubusercontent.com但如果你的服务器在部分地区访问有问题建议用https://cdn.jsdelivr.net/gh/iamcal/emoji-datamain/emoji.json这类 CDN 代理。我本地测试时直接访问 raw 通常没问题但既然写经验帖就提一句免得你部署到云主机上遇到拉不下来的情况。3.4 内置增量更新按版本自动追新因为整个数据是“完整快照”式的所以增量更新其实很温柔下载最新文件 → 解析 → 覆盖旧字典 → 记录新版本号。但要注意一个事情不要等到线上出问题才去手动更新建议把它做进同一个脚本里每次运行前自动检查版本。我在爬虫脚本里加了一个简单的版本检查def get_current_version(config_path: str) - str: with open(config_path, r, encodingutf-8) as f: cfg json.load(f) return cfg.get(emoji_version, 0) def update_or_rebuild(config_path: str): raw fetch_with_retry(session, EMOJI_TEST_URL) # 从文件头里提取版本号 m re.search(r# Version:\s*([0-9.]), raw.text) version m.group(1) if version get_current_version(config_path): print(already up-to-date) return # 否则全量重抓并更新 meta build_dictionary(version)这样每次跑脚本如果版本没变化就跳过只有版本更新才执行完整构建节省时间也避免无谓地覆盖数据。4. 多源合并与数据归一化没有这一步字典就是半成品4.1 字符归一化先处理 NFC/NFD否则主键天崩地裂大凡做 Unicode 数据处理都绕不过 NFC 和 NFD。简单说同一个字符可以用两种方式存储一种是预组合形式NFC比如一个完整的“é”作为一个码位一种是分解形式NFD由字母 e 和重音符号两个码位组成。Emoji 序列里同样存在这种问题特别是那些带变体选择符FE0F的字符。比如 U00A9©既可以是纯文本符号也可以是 Emoji 形式的 ©️区别就在于后面有没有跟FE0F。如果你抓来的数据里一个来源写成00A9另一个来源写成00A9-FE0F直接按字符串比对会把它们当成两个完全不同的实体但实际上它们在很多场景下应该归并到同一条元数据里。我的解决方案是主键永远只用标准化码位串。所谓标准化就是把码位转成大写把没有意义的空字符和前缀去掉多个码位用连字符连接。这套逻辑本身不复杂但必须在合并前统一执行。def normalize_codes(codes: str) - str: RAW_PREFIX U codes codes.strip().upper() codes codes.replace(RAW_PREFIX, ) parts [p for p in re.split(r[\s-], codes) if p] normalized_parts [] for p in parts: if len(p) 4: p p.zfill(4) normalized_parts.append(p) return -.join(normalized_parts)注意里面用zfill(4)补零1F600本身就是 5 位不用补但FE0F是 4 位要统一成 4 位。这样00A9-FE0F和00A9 FE0F就都能对齐到同一个标准串。4.2 主键设计用码位串而非字符本身用 emoji 字符本身当主键是最容易犯的错误。原因有两个第一同一个 emoji 在不同平台上的码位写法可能是不同的。有的平台会把FE0F省略有的必须带。虽然渲染结果一致但字符文本不同直接作为字典 key 会导致同一个含义出现多条记录。第二有些 emoji 在数据库和 JSON 里看起来一模一样但底层字节不同。比如男女一起的符号 由两个码位组成但还有带 ZWJ 的变体形式肉眼难以分辨却不能用同一个 key。所以我在整个项目里只认unified字段也就是标准化后的码位串。举个例子1F468-200D-1F469-200D-1F467是“爸爸、妈妈、女儿”的家庭序列而1F468-200D-1F468-200D-1F466是“两个爸爸、儿子”的家庭序列它们字符文本看着都类似但码位串是完全不一样的必须分开记录。4.3 冲突优先级与字段兜底当官方数据和 iamcal 数据合并时一定会出现字段不一致。比如同一个grinning face官方名称和社区短名称可能不完全一致某个来源的关键词数组里还可能混入脏数据。我不可能每次都手动调所以要定一个简单明确的优先级规则以 Unicode 官方数据为准group、subgroup、name、status、unified、emoji。以 iamcal 数据为补充short_names、keywords、image、sheet_x、sheet_y。任一侧缺失的字段用另一侧补齐。两个来源都有但内容不同的字段官方优先。具体合并逻辑是这样的def merge_records(official_records: dict, extended_records: dict) - dict: merged {code: dict(record) for code, record in official_records.items()} for code, ext in extended_records.items(): if code not in merged: continue rec merged[code] rec.setdefault(short_names, ext.get(short_names, [])) rec.setdefault(keywords, ext.get(keywords, [])) rec.setdefault(image, ext.get(image, )) if sheet_x in ext and sheet_y in ext: rec.setdefault(sheet_x, ext[sheet_x]) rec.setdefault(sheet_y, ext[sheet_y]) return merged用setdefault而不是直接赋值就是保证官方已有字段不会被覆盖。对于关键词这种数组类型的数据我还会做去重和截断防止某些来源一口气塞了几十个重复词。4.4 系列 Emoji把 ZWJ 序列和皮肤色调家族并成组Emoji 的复杂度很大程度来自两类序列ZWJ 组合序列和带皮肤色调的序列。ZWJ 组合序列用 U200D 连接多个基础 Emoji比如‍‍1F468-200D-1F469-200D-1F467‍‍‍1F468-200D-1F469-200D-1F467-200D-1F466这类序列在展示层是一个整体但在字典里它的元数据本身也是独立的条目。处理时我额外加了family_id用来把同一类语义的序列链到一起。做法很简单去掉200D之后取序列中所有非 ZWJ 码位拼接成一个指纹作为 family 的 ID。皮肤色调序列更常见比如 由基础符号1F44D加肤色修饰符1F3FB组成。我在合并时用正则判断是否存在1F3FB到1F3FF这个区间如果有就标记 skin_tone。这样以后想筛选“所有带深色皮肤的 Thumbs Up”直接按字段过滤就行。这两类逻辑不是必须的但如果你想把“全网最全”这四个字做实组合序列的处理是躲不过去的。很多人只会处理基础 emoji一遇到 ZWJ 序列就把它拆成多段那字典的维度就损失了大半。5. 字典落地JSON 结构设计与 SQLite 查询优化5.1 最终 JSON 长什么样清洗合并之后我导出成一份按 unified 排序的 JSON 数组单个条目长这样{ unified: 1F600, emoji: , name: grinning face, status: fully-qualified, group: Smileys Emotion, subgroup: face-smiling, short_names: [grinning], keywords: [face, grin, grinning face], image: 1f600.png, sheet_x: 0, sheet_y: 0, skin_tone: null, family_id: grinning-face, version_added: 1.0 }这里我刻意没有把中文翻译名放进正式字段因为中文翻译维护量大而且容易因为翻译口径不同产生歧义。我更推荐的做法是把中文含义放到 keywords 里用搜索而不是硬编码的方式去匹配。如果你确实需要中文名可以后续单独维护一张映射表。写 JSON 时务必用ensure_asciiFalse不然 emoji 字符会被转义成一堆\ud83d完全没法读。同时设置indent2让文件可读性更好但这会增大体积。如果只是给程序用可以输出压缩格式如果偶尔要人工翻查就用缩进格式。import json def save_json(data, path): with open(path, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)5.2 为什么查询层用 SQLiteJSON 足够轻量适合直接分发给前端和离线工具但一旦字典条目超过 3000 条你要频繁做按关键词搜 Emoji“按分组筛图标这类操作Python 里反复 json.loads 再逐条遍历就很繁琐。所以我额外把数据同步到了 SQLite。SQLite 的优势非常明显单文件、零部署、支持 SQL 和索引查询性能比内存对象遍历更稳定尤其是在跨语言调用时只要路径里有 db 文件什么语言都能读。建表语句我设计成这样CREATE TABLE IF NOT EXISTS emoji ( id INTEGER PRIMARY KEY AUTOINCREMENT, unified TEXT NOT NULL UNIQUE, emoji TEXT NOT NULL, name TEXT NOT NULL, status TEXT, group_name TEXT, subgroup TEXT, short_names TEXT, keywords TEXT, image TEXT, sheet_x INTEGER, sheet_y INTEGER, skin_tone TEXT, family_id TEXT, version_added TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_emoji_group ON emoji(group_name, subgroup); CREATE INDEX IF NOT EXISTS idx_emoji_family ON emoji(family_id); CREATE INDEX IF NOT EXISTS idx_emoji_name ON emoji(name);keywords 这种数组字段在 SQLite 里没有数组类型我直接存成 JSON 字符串查询时用 LIKE 匹配。虽然不算最优雅但数据量只有几千条性能完全没问题。5.3 为“可扩展”预留的松弛字段做字典类项目最忌讳数据结构设计成“死”的。Unicode 每年都在加新表情可能某一天会出现你现在的字段根本描述不了的新特性。所以我在表里预留了几个进气口skin_tone目前只存null或肤色码位名未来如果出现头发颜色修饰符可以直接复用。family_id目前用于 ZWJ 系列分组未来也可以用于“同一语义基础 Emoji 的变体聚合”。keywords从短字符串变为 JSON 数组随时可以加中文关键词、行业标签、情感倾向。version_added直接存字符串比如1.0、15.1方便版本比较。我还会在数据库里建一张meta表记录数据源 URL、抓取时间、Emoji 版本号避免将来搞不清楚这份字典是哪天抓的、基于哪个版本。CREATE TABLE IF NOT EXISTS meta ( key TEXT PRIMARY KEY, value TEXT );写入时这样用conn.execute(INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?), (emoji_version, version)) conn.execute(INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?), (updated_at, datetime.now().isoformat())) conn.commit()6. 爬虫实战中绕不开的四个坑编码、限流、存储与终端显示6.1 坑一忘带 User-Agent拿回来的全是乱码这是我第一次跑通脚本时踩的坑。直接用requests.get(url)去拿 emoji-test.txt打印前几行发现中文全部变成了乱码。原因不是文件本身有问题而是响应头里的 Content-Type 没有明确给出 charsetrequests 默认按 ISO-8859-1 解码了。解决办法有两个resp session.get(url, headersHEADERS) resp.encoding utf-8 text resp.text更稳妥的做法是直接用resp.content.decode(utf-8, errorsreplace)完全不信任响应头里的编码声明。这里用errorsreplace只是兜底正常 UTF-8 文本不会触发替换万一源头出现坏字节也不会让整个脚本崩溃。6.2 坑二源头也会 403限速和重试策略必须配齐i amcal 的 github 仓库有一次在我连续跑了几十次脚本后直接返回了 403。Unicode 官方偶尔也会在短时间内太多请求时返回 429。这提醒我哪怕再“友好”的公开数据源也不是让你无限高频访问的。我最后的处理方案很简单每次访问间隔至少 0.5 秒整个构建流程最多抓十几个文件重试次数设 3 次指数退避从 1 秒开始。这个策略实测下来很稳没有再被源头拒绝过。另外放了一个session.headers层面的统一 UA避免每次创建 session 时重复设置。6.3 坑三Emoji 存进 SQLite 后“查不出来”有次我在 SQLite 里执行SELECT * FROM emoji WHERE name LIKE %grin%结果没有问题。但当我尝试按 emoji 字段精确匹配时发现明明存进去了却搜不到。排查一圈后发现问题不在 SQL而在终端。sqlite3 命令行工具在 Windows 和多数 Linux 终端里对 emoji 的渲染支持很差查询结果看起来是乱码或者空但并不代表数据有问题。用 Python 读取后repr(emoji)就能看到真实的\U0001f600。所以判断“存没存进去”时不要看终端显示要看长度和字节row conn.execute(SELECT emoji, unified, name FROM emoji WHERE unified1F600).fetchone() assert len(row[0]) 1 or row[0] 另外如果数据库表字段带COLLATE NOCASE或LIKE查询时emoji 本身虽然不受影响但建议对unified字段做精确匹配不要对emoji字段做 LIKE因为某些 emoji 序列里存在多个码位LIKE 很容易匹配到意料之外的序列。6.4 坑四别用 print 验证抓取结果在 Windows 控制台直接 print emoji 很可能会报UnicodeEncodeError: gbk codec cant encode character。Linux 终端虽然能显示但在某些 SSH 会话里也会变成方框。为了验证抓取结果不要依赖肉眼更不要 print 一堆 emoji 出来看。我的验证方式是写一个简单的断言函数def validate_records(records): codes {r[unified] for r in records} assert len(codes) len(records), unified duplicated assert all(r.get(emoji) for r in records), empty emoji found assert all(r.get(name) for r in records), empty name found如果非要可视化把抓取到的结果导出成 HTML在浏览器里看比终端可靠得多。或者用print(repr(record[emoji]))打出来的是转义形式至少在日志里是可读的。最后说点实在的。这本字典跑通之后我现在做任何跟文本相关的小项目都会先灌一份进去。写情感分析脚本时先把评论里的 Emoji 替换成语义标签写运营后台时直接把 JSON 字典喂给前端作图标筛选组件写测试用例时拿version_added字段来判断某个老设备的渲染兼容性。一个花一下午自己建的字典比每次临时去网上找各种“Emoji 大全 JSON”要省心得多。如果你也想做 NLP 或消息系统我真心建议先别偷懒用别人现成的小 JSON自己抓一次、清洗一次、落库一次你对 Emoji 的底层结构会建立起完全不一样的体感。新版 Unicode 发布后定时脚本每周跑一次检查版本就够了这也是“全网最全”唯一可持续的维护方式。
返回列表