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

微信里收藏了很多表情包,换到飞书以后,最麻烦的不是「还能不能继续用」,而是「怎么批量搬过去」。

微信客户端没有给「收藏的单个表情」提供一个正式的批量导出入口。飞书这边也不是随便丢一个几百张图片的包进去就完事,它一次只能导入 50 张图。

所以这件事最后变成了一个很朴素的方案:

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

整体步骤

实际操作只需要两段。

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

打开微信网页版:

https://wx.qq.com/

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

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

(() => {
  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
    ...

飞书一次只能导入 50 张图,所以每个 group-* 文件夹最多放 50 个表情。导入时按文件夹分批选就行。

为什么要绕到微信网页版

这个方案不是去读微信的「收藏表情库」。

更准确地说,它利用的是另一个事实:如果你把收藏表情发到聊天里,微信网页版为了展示这条消息,就必须把对应图片加载出来。

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

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

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

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

这个边界很重要。

它不是一个微信备份工具,也不是绕过微信账号数据的接口。它只能导出「你已经发到当前聊天,并且微信网页版能加载出来」的自定义表情。

为什么不直接贴 400 行代码

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

因为脚本里不只是几行 DOM 查询,还包含这些逻辑:

  • 定位当前聊天滚动容器。
  • 向上滚动,让历史消息懒加载出来。
  • 收集 custom_emoji 图片地址。
  • 下载图片二进制。
  • 按文件头识别 gifpngjpgwebp
  • 按内容去重。
  • 手写 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:

  • GIF47 49 46
  • PNG89 50 4E 47
  • JPGFF D8
  • WEBPRIFF ... WEBP

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

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

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

第三,按内容去重。

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

第四,默认按 50 个一组。

这不是技术洁癖,是为了飞书导入限制。zip 里提前分好组,比导入时手动数 50 张舒服很多。

使用时要注意什么

这个方案有几个前提。

  • 你得能登录微信网页版。
  • 你得把想导出的表情发到当前聊天。
  • 你得等网页版实际加载出这些表情。
  • 你最好在一个专用聊天里操作,别在日常群聊里混着导。

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

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

最后

这件事有点像一个小型搬家工具。

它没有去解决「微信为什么不提供批量导出」这个产品问题,也没有试图做成全自动迁移。它只是把原本很重复、很机械的几步串起来:

  • 把表情发出来。
  • 让网页版加载出来。
  • 从 DOM 里找到图片。
  • 在浏览器本地打包。
  • 按飞书限制分组。

对我来说,这就够用了。

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