把微信收藏表情导出来,再分批导入飞书

微信里收藏了很多表情包,换到飞书以后,真正麻烦的是「怎么批量搬过去」。

这次迁移时,我没有在微信客户端找到收藏表情的批量导出入口;飞书的导入界面一次最多接受 50 张图片。下面按当时的界面和限制记录做法,后续版本需要重新确认。

最后走的是一条很朴素的路线:

先让微信网页版看到这些表情,再从网页里把图片资源取出来,最后按 50 个一组打成 zip。微信负责把表情发出来,浏览器负责拿资源,脚本负责收集和分组。

操作步骤

实际操作分两段。

第一段,从微信导出到本地。

打开微信网页版:

https://wx.qq.com/

扫码登录后,打开「文件传输助手」,或者一个专门用来转发表情的小群。

然后在手机微信里,把要导出的收藏表情逐个发到这个聊天里。等网页版能看到这些表情以后,打开浏览器 DevTools 的 Console。

执行前先打开脚本源码检查。即使没有外部依赖,加载到已登录页面里的脚本仍能访问页面内容;不愿授予这类访问能力,就不要运行。博客地址只是分发入口,不是安全保证。

Chrome 第一次在 DevTools Console 里粘贴代码时,可能会要求先输入 allow pasting。这是浏览器为了防止用户误粘恶意代码做的提醒。不要随手跳过这条提醒:只执行能确认来源、并且愿意信任的脚本。

确认后执行:

(() => {
  const s = document.createElement('script');
  s.src = 'https://shengsheng.fun/files/wechat-emoji-to-feishu/scripts/wechat-emoji-exporter.js';
  document.head.appendChild(s);
})();

脚本会在页面右下角显示进度,完成后浏览器会下载一个类似这样的文件:

wechat-emojis-2026-05-29-12-30-00.zip

第二段,从本地导入飞书。

解压以后,目录大概是这样:

wechat-emojis/
  group-01/
    wechat-emoji-0001.gif
    ...
    wechat-emoji-0050.png
  group-02/
    wechat-emoji-0051.jpg
    ...

为适配当时的导入上限,每个 group-* 文件夹最多放 50 个表情。导入时按文件夹分批选择。

为什么要绕到微信网页版

这个方案利用的是微信网页版已经加载到聊天里的表情图片,并不读取微信的「收藏表情库」。收藏表情发到聊天里以后,微信网页版为了展示这条消息,就必须把对应图片加载出来。

在页面里,这类自定义表情大致会变成这样的元素:

<img class="custom_emoji msg-img" src="...webwxgetmsgimg...">

脚本做的第一件事,就是只找当前聊天里的这些 img.custom_emoji.msg-img。头像、公众号图片、聊天列表图标都不在导出范围内。

找到以后,再用当前网页已有的登录态请求这些图片资源。这里没有读取 Cookie,也没有把图片上传到别的地方;所有处理都发生在当前浏览器里。

导出范围就是当前聊天中、微信网页版能加载出来的自定义表情。尚未发出的收藏表情不在页面里,脚本也拿不到。

为什么不直接贴 400 行代码

最早直接在控制台里跑了一段完整脚本。它能用,但不适合分享。

完整脚本除了几行 DOM 查询,还包含这些逻辑:

  • 定位当前聊天滚动容器。
  • 向上滚动,让历史消息懒加载出来。
  • 收集 custom_emoji 图片地址。
  • 下载图片二进制。
  • 按文件头识别 gif、png、jpg、webp。
  • 按内容去重。
  • 手写 zip 结构。
  • 按 50 个一组生成目录。
  • 触发浏览器下载。

直接把完整代码贴到文档里,读者复制起来很痛苦,也不方便后续修 bug。

所以完整脚本放成博客静态资源:

https://shengsheng.fun/files/wechat-emoji-to-feishu/scripts/wechat-emoji-exporter.js

控制台里只需要插入一个 script 标签。

这里没有用 GitHub Raw 当脚本 CDN。GitHub Raw 更适合看源码或下载文件,不适合作为网页里的 <script src> 长期加载;浏览器可能会因为 MIME 类型和 nosniff 策略拒绝执行。放在自己博客的静态资源里,路径、缓存和响应类型都更容易掌控。

脚本里几个取舍

第一,文件格式不看 Content-Type。

实测微信接口返回的 header 不一定准确。有些 GIF 或 PNG 可能也会被标成 image/jpeg。如果只看 Content-Type,动图很容易被保存成错误后缀。

所以脚本会读文件头,也就是常说的 magic number:

  • GIF:47 49 46
  • PNG:89 50 4E 47
  • JPG:FF D8
  • WEBP:RIFF ... WEBP

第二,zip 里不做二次压缩。

表情文件通常已经是 GIF、PNG、JPG 或 WebP,本身就是压缩格式。再做 deflate 收益不大,但会让纯浏览器脚本复杂很多。

所以脚本手写的是一个最小 ZIP:每个文件作为 stored entry 放进去,再生成 central directory。这样浏览器里不需要额外加载 JSZip 之类的第三方库。

第三,按内容去重。

同一个表情如果发了两次,URL 可能不同,但图片内容一样。脚本会用文件大小和 CRC32 做一个轻量去重,避免最后 zip 里重复出现同一张图。

第四,默认按 50 个一组,在 zip 里提前分好目录,避免导入时手动计数。

使用时要注意什么

这个方案有几个前提。

  • 能登录微信网页版。
  • 想导出的表情已经发到当前聊天。
  • 网页版已经实际加载出这些表情。
  • 最好在专用聊天里操作,不要混在日常群聊里导出。

如果表情特别多,可以分几批发。脚本会尝试向上滚动当前聊天加载历史消息,但网页版本身如果没加载到,脚本也拿不到。

这个小工具只解决搬运问题

把收藏表情发到聊天里、登录网页版、分批导入飞书,仍然要手动完成。脚本负责滚动加载聊天记录、从 DOM 收集已加载的图片、下载去重,并在浏览器里打包分组;它没有直接访问收藏库,也没有操作飞书导入界面。

如果以后微信网页版结构变了,最容易坏的地方大概是表情元素选择器和图片接口 URL。脚本已经把这些逻辑集中在一起,后续需要调整时,也比重新手动整理几百个表情轻松。