数据通道
读操作优先走本地数据库(毫秒级、全量、群消息带发送者),不满足条件自动退回控件树。
两条独立的数据通道
| 数据库路线 | 控件树路线(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 条
- 挂监听 → 存在性校验通过 → 挂到了错的人身上,永远收不到消息
看起来就是「新加的好友读不到消息、也监听不了」,而聊天记录其实一直在库里。
现在按**「谁真的在聊」**消歧,判据按可靠性排序:
- 出现在会话表里(真的聊过),且在会话表里越靠前越近
Msg_<md5>分表里消息条数更多- 都没有 → 取第一个,但明确 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) # 翻页(只要更早的)
⚠ 两个实测结论:
- 引擎里的 rel 路径是相对
db_storage且用反斜杠的(sns\sns.db)。 写成sns/sns.db会报“无可用密钥” —— 密钥是按这个键名存的。 - 本地库不一定有点赞/评论:微信只把“你看过的那条”的交互缓存到本地
(实测 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时不会主动扫描内存。