文档

数据通道

读操作优先走本地数据库(毫秒级、全量、群消息带发送者),不满足条件自动退回控件树。

两条独立的数据通道

数据库路线 控件树路线(UIA)
速度 毫秒级(本地 sqlite) 秒级(遍历控件 + 滚动)
范围 全量历史 只有「可见区域」注册的控件
群消息发送者 直接给出 wxid + 昵称 拿不到
通讯录 3160 个(本机实测) 只读到渲染出来的十几行
会话列表 100 条 十几条
前置条件 微信已登录 + 能读其进程内存取密钥 微信版本必须暴露控件树

写操作(发消息/发文件/点赞)只能走控件树 —— 微信的设计如此。 读操作优先走数据库。

出厂 WxParam.DATA_SOURCE = "auto":能用库就走库,不能就自动退回 UIA。

上手

from wx4auto import WeChat, WxParam

wx = WeChat()

wx.DBStatus()                    # 体检:账号 / 数据目录 / 密钥 / 各库

# 会话:瞬时、全量、带未读和最后一条预览
for s in wx.GetSession():
    print(s.name, s.unread, s.content)

# 消息:全量历史 + 群消息的发送者 wxid
for m in wx.GetAllMessage(who="某群", limit=500):
    print(m.time_str, m.sender or m.sender_username, m.content, m.type)

# 群成员(470 人的群也是瞬时)
for mb in wx.GetGroupMembers("某群"):
    print(mb.name, mb.username, mb.is_owner)

# 只想要纯 UIA(完全不动微信进程内存)
WxParam.DATA_SOURCE = "uia"

返回顺序是最新的在前(时间倒序),取“最后一条”要用 [0]。

数据层能干的更多事

wx.DB.sessions(100)              # 会话
wx.DB.messages("某群", 500)       # 消息(DBMessage:type/attr/content/sender/time/seq)
wx.DB.new_messages("某群", since_seq=水位)   # 增量(监听用,比 UIA 快且不漏)
wx.DB.groups()                   # 所有群(含成员数)
wx.DB.group_members("某群")       # 群成员
wx.DB.contacts("张")              # 搜联系人(全量)
wx.DB.chats()                    # 所有含消息的会话
wx.DB.labels()                   # 标签
wx.DB.export_history("D:/chat.json", fmt="json", users=["某群"])
wx.ResolveTarget("张老师")         # 显示名 → wxid

消息类型比 UIA 路线更精确

数据库里 local_type 只有粗分类(49 = 「文件/链接/卡片」大杂烩), 本库会再看正文 XML 的 <type> 细分:

归类的 m.type 来源
text / image / voice / video / emotion / location local_type
file(6) / link(5) / quote(57) / card(42) / miniapp(33,36) / transfer(2000) / redpacket(2001) / merge(19) local_type=49 + 正文 <type>
recall / tickle / system local_type=10000 + 正文关键字

群聊文本的正文带 发送者wxid:\n 前缀,m.content 会自动摘掉,原文在 m.raw_content。

同名联系人:别选错人

这是最常见的坑。通讯录里重名很常见 —— 实测有 3 个联系人都叫「空白」。

选错之后的现象非常隐蔽:

  • 读消息 → 那个人的分表根本不存在 → 静默返回 0 条
  • 挂监听 → 存在性校验通过 → 挂到了错的人身上,永远收不到消息

看起来就是「新加的好友读不到消息、也监听不了」,而聊天记录其实一直在库里。

现在按**「谁真的在聊」**消歧,判据按可靠性排序:

  1. 出现在会话表里(真的聊过),且在会话表里越靠前越近
  2. Msg_<md5> 分表里消息条数更多
  3. 都没有 → 取第一个,但明确 WARNING 告警(不静默选错)

消歧过程会打 INFO 日志,一眼看得出选了谁、凭什么:

名字 '空白' 有 3 个同名候选,按「谁在聊」选中 wxid_zuz53h61hzf322
  (wxid_zuz53h61hzf322(会话#3/消息13), wxid_2vlxs7gekvhl12(会话无/消息0), …)

★ 想百分百避免歧义就直接传 wxid:wx.SendMsg("...", who="wxid_xxx")。 验收脚本:tools/verify_same_name.py。

读不到消息时的排查顺序

被问过很多次,先给结论:密钥是按“库文件”给的,不是按好友给的。 微信把所有单聊/群聊都塞进同一个 message_0.db 的 Msg_<md5(wxid)> 分表里 —— 新加一个好友不会产生新文件,也就没有“这个人的密钥失效”这回事。

按顺序查,一层层排除:

层 怎么查 结论怎么看
① 库能不能解密 eng._message_dbs() 逐个 eng._open(rel) 能打开 = 密钥没问题
② 这个人的分表在不在 Msg_<md5(username)> 表存在 = 库里确实有这块数据
③ 表里有没有行 SELECT COUNT(*) FROM "Msg_…" 0 行 = 真的没聊过
④ 名字有没有撞车 通讯录里同名几个? ★ 见上一节,最常见的坑

tools/audit_message_keys.py 会把 ①②③ 全量跑一遍。 本机实测:4/5 消息库可解密(剩下的 message_fts.db 是全文索引且属瞬时 checkpoint)。

媒体下载

for m in wx.DB.messages("某群", 200):
    if m.type == "image":
        r = wx.Media.image(m)              # 只读本机缓存,不驱动界面
        print(r.data["path"])
    if m.type == "voice":
        wx.Media.voice(m)                  # 从 media_*.db 取 SILK 数据

⚠️ 图片解密需要密钥。上游的密钥锚点对的是微信 4.1.10+; 4.1.8.107 上 cfgDword 派生拿不到,此时会立刻返回清晰原因(不会卡住): 在微信里点开任意一张图一次让密钥驻留内存,再把 WxParam.IMAGE_KEY_SCAN_TIMEOUT = 60 就能扫到。 消息 / 朋友圈等非媒体读取不受影响。

朋友圈:读 sns.db 拿全量历史

UIA 只能看到朋友圈窗口当前渲染出来的那几条;数据库里是全量 (实测某账号 195 条,读取 0.04 秒):

wx.DB.moments_status()          # 体检:sns 库可用吗、多少条
for m in wx.DB.moments(20):     # 全量历史,按时间倒序
    print(m.time_str, m.nickname, m.text[:20], len(m.images))
wx.DB.moments(5, who="张三")     # 只看某个人
wx.DB.moments(5, before=ts)      # 翻页(只要更早的)

⚠ 两个实测结论:

  1. 引擎里的 rel 路径是相对 db_storage 且用反斜杠的(sns\sns.db)。 写成 sns/sns.db 会报“无可用密钥” —— 密钥是按这个键名存的。
  2. 本地库不一定有点赞/评论:微信只把“你看过的那条”的交互缓存到本地 (实测 195 条朋友圈、SnsMessage* 表 0 行)。读不到就是空,不编造。

撤回回捞

微信撤回是原地改写原文行(sort_seq 不变),所以按水位增量拉是看不到撤回的。 必须“先有镜像”:

wx.Recall.backfill("某群", limit=500)   # 先把现有原文镜像一份
wx.Recall.scan("某群")                  # 扫一次,比对出被撤回的
for ev in wx.Recall.recalled("某群"):
    print(ev)

关于 dbkit 的来源与风险

wx4auto/dbkit/ 里的引擎移植自开源项目 wechatauto-replica(v1.2.6.1,Apache-2.0)。 按许可要求保留了版权与许可证全文,改动清单见 wx4auto/dbkit/NOTICE.md。

它是怎么拿到数据的:读微信配置文件定位数据目录 → 扫描 Weixin.exe 进程内存取出配置对象里的 32 字节密钥 → 按页解密 SQLCipher 数据库到临时目录(带缓存)→ 用标准 sqlite3 只读查询。

风险(请自行判断):

  • OpenProcess + ReadProcessMemory 只读扫描客户端进程内存, 不注入、不改内存、不注入 DLL、不碰网络协议;这是社区公开做法。
  • 但它确实是在扫描微信客户端进程的内存,极端情况下可能被安全软件告警。
  • 不想用就把 WxParam.DATA_SOURCE = "uia";不调用 wx.DB 时不会主动扫描内存。