← 返回首页

从一款"读不出来"的客户端里,把聊天记录整批导出

本地库加密、没有调试端口、无障碍树是空的、合成的按键被无视——把所有"优雅读取"的路都验完之后,只剩下模拟真人操作这一条。这篇记录这条路怎么走通,以及途中踩的坑。

2026-09 · 自动化 / OCR / 逆向思路 · 全文已脱敏

先说清楚边界。这是一款受终端管控的桌面客户端,我只导出自己有权查看的会话,全程不绕过任何安全管控:不解密客户端本地的加密库、不去除水印、不碰他人数据、不批量传播内容。方法本身只对"自己的数据、自己的电脑"有意义。

文中所有具体对象都做了模糊化处理:不出现软件名、厂商名、单位名、群名、坐标与任何内部地址。

一、先说结论:为什么只剩"模拟真人操作"

这不是偷懒,是把能试的路全试过之后剩下的唯一一条。下面每一条都是实测结论,不是推测:

思路结论实测结果
直接读客户端本地库✗库是国密 SM4 加密;而且属于红线,不做
调试端口直读页面 DOM✗进程没有任何监听端口
无障碍树直读(UIA / MSAA)✗整棵树只有 3 个节点:主窗口 + 两个管控软件的水印层窗口,聊天区零暴露(ControlView / RawView / FindAll 三种走法结果一致)
后台消息注入(PostMessage)✗前台状态下:合成的"滚轮 + Shift 点击"不动选区、合成的 Ctrl+C 剪贴板序号不变;同一时刻真实 SendInput 复制正常(220 字)。双方完整性级别相同,排除 UIPI 拦截
截图识别版面✗PrintWindow 抓不到内容——单窗口 + 浏览器内核离屏渲染,一张画布画到底
去水印 / 解密本地库✗明确不做的红线

推论:只剩 UI 自动化一条路。而真实输入(SendInput)只作用于前台窗口,所以采集时那个客户端必须待在最前面——"完全后台、不占电脑"在这台机器上做不到。能优化的只有两件事:少打扰(空闲让路)和少浪费时间(增量而不是全量)。

一个意外收获:这条路走通之后,反而解锁了"能看见屏幕"的能力(屏幕 BitBlt 可以抓到真实画面),后来核对界面、排查问题全靠它。

二、核心动作:Shift + 滚轮,逐屏扩选

思路很朴素:在会话里模拟"按住 Shift 点一下、滚一屏、再点一下",把消息一段段选中复制出来。

每轮流程

  1. 先回滚 2 格滚轮(制造与上一轮的重叠);
  2. 在锚点(会话区最底部)点一下,定"最新边界";
  3. 按住 Shift,重复 20 次:[向上滚 3 档 → 在上方一个扫描点点一下];
  4. 松开 Shift → Ctrl+C → 剪贴板原文落盘;
  5. 连续两轮"最老消息没变化"=到顶,停。

三个设计要点

  • 锚点在底、扫描点在上,两点保持同一横坐标——同 X 是刻意的:避免点到聊天里的图片。
  • 为什么一次能吃掉一大段:Shift + 点击的语义是"从锚点扩选到目标",所以点一次吃一大段,不必逐条点。
  • 为什么每轮要回滚:实测相邻两轮的接缝会恰好漏 1 条,靠重叠消掉(基准群 136/136 零差,两遍独立扫描逐字节一致)。

为什么放弃"录制鼠标拖拽":最初录过一段 24 秒的拖拽轨迹,但轨迹和"某个具体会话的消息高度、行数"强绑定——换一个群就失效(只录到开头两条,或者干脆录进图片)。Shift 方案不需要录制,换群即用。

三、全自动切群:搜索框 + 双通道 OCR 核对

自动批处理要能自己找到目标会话,并且确认没找错:

  1. 切群用搜索框,不用点列表:列表会滚动、行高不固定;搜索框位置固定,输入关键字后点下拉第一行最稳。
  2. 中文输入用 SendInput + KEYEVENTF_UNICODE(对输入法免疫)。注意:x64 下 INPUT 结构体必须是 40 字节,写成 32 会静默失败——SendInput 返回 0,什么都不发生。
  3. 双通道 OCR 核对,防止切错群:
    • 通道 A(点击之前):OCR 识别下拉第一行,按 recall 口径比对,容忍"人数后缀""下划线被认成横""偏旁被拆开"这类识别垃圾;同时要拦住"进入全局搜索"那一行——没有命中时它排第一,点下去界面会被带到搜索页,之后所有切群全乱。
    • 通道 B(点击之后):OCR 识别会话标题,按双向 min(准确率与召回率取小)比对。
    • 任一通道 ≥ 0.85 即通过。放宽不等于放水:多个同类群名只差地区前缀,两个通道都独立看前缀,所以切到别的地区时两边都只有 0.82、都过不了。

双通道就是为生僻字设计的:OCR 把某些地名读错是常态。单通道会频繁误杀(实测被误杀过 3 次),双通道之后 35/35 一次通过。

四、增量:每周只取新增

全量重跑没意义——新消息永远在最新处,增量只需要"从最新往上扫,一接上上次的进度就停"。

机制

  1. 从该会话已归档的 JSON 里取最后一条时间 = 水位线;
  2. 切到该会话 → 从最新往上扫 → 某一轮的最老消息 ≤ 水位线就停(说明已经接上了);
  3. 本次新增 = 新批次 − 归档已有(同一套去重键);
  4. 产出到带时间戳的目录:每个会话一份 md / csv,加一份带状态列的汇总,原始批次另存。

关键前提:客户端会按会话记住滚动位置。上次全量扫描是停在顶部结束的,一周后切回去仍在顶部 → 增量读不到最新。两条解法:

  • 推荐:完全退出客户端再重新打开(实测:重开后每个会话点开都直接停在最新处)。这一次性把所有会话归位。
  • 兜底:脚本自动"往最新方向爬"——滚轮和点击交替(裸滚轮会被客户端合并:连发 1000 档只前进 2 天;交替则 30 个循环能走 2 个月),每爬一段用一次真实复制当探针,直到"视窗里最新的一条"不再变化。实测某个大群从顶部爬完 18 个月,用了 9 段、76 秒。

实测成绩:35 个会话 / 8 分 16 秒 / 34 个第一轮就接上水位线 / 0 次切错群 / 0 次 OCR 失败。

额外收获:增量会自愈历史缺口。这次 4 个会话共新增 44 条,其中一部分是从"上次归档最新时间之前"的区间补回来的——说明当初全量在顶部附近留了小缺口。增量每轮会越过水位线一两周,所以每周跑一次,这类缺口会慢慢补上。

五、注意事项(全是踩出来的)

窗口与焦点

  • 窗口识别要三重判定:进程名 + 窗口类名特征 + 尺寸下限。管控软件的水印层窗口比主窗口还大,按"最大窗口"选一定会选错。
  • 不要在启动时 SW_RESTORE:对已经最大化的窗口调用会把它还原,之前算好的绝对坐标全部作废;只在 IsIconic 时才 restore。
  • 全局热键用 RegisterHotKey,WM_HOTKEY 只有注册线程收得到 → 主线程所有等待都要换成"能被热键打断"的版本,否则按键要等最长那段 sleep 结束才生效(曾最坏 8 秒)。
  • 用户切到别的窗口会把前台抢走 → 所有模式在"前台不是目标应用"时必须中止并提示。

输入

  • 连续快速在同一位置点击会被判成双击 → 客户端弹出图片查看器盖住会话 → 之后每轮都是 0 条。对策:点击间隔 ≥ 0.1 秒,且两次点击之间夹一个滚轮事件(滚轮会打断双击判定)。
  • 中文不要走命令行参数(PowerShell 编码会搞坏,实测群名变乱码)→ 用文件传参或交互式输入。
  • 发送前要把"我们自己注入的输入"标记出来(标记写进鼠标、滚轮、键盘三个底层函数),否则"空闲让路"逻辑会把自己的动作当成用户在动电脑,每轮白等一个空闲周期(实测从 4.8 秒/轮 退化到 25 秒/轮)。

解析与去重

  • 复制出来的表头格式随消息年代变化:当年消息是"姓名 M/D HH:MM:SS",跨年旧消息是"姓名 YYYY/MM/DD HH:MM:SS"。正则不吃年份会整轮判 0 条并提前结束扫描(曾在某年 01-04 处中断,修好后单批 1642 → 1703 条)。
  • 去重键必须含正文。原键是(年月日时分秒, 发送人),不含正文——于是"同一人同一秒先发文字、紧接着发图片或文件"这种最常见的用法被合并掉了。全量扫 2010 个轮次文件、69,949 条原始消息,查出 174 处,实际误删 98 条。改成"同秒同人且正文相同或互为前缀"之后才对。教训:任何"按时间 + 人"去重的策略,先问一句"同秒能发两条吗"。

环境细节

  • 控制台编码:聊天里的 emoji 会让 GBK 控制台 print 直接崩、中断扫描 → 启动时把 stdout / stderr 重设为 UTF-8。
  • 剪贴板被管控软件塞了一堆私有格式 → 要取 CF_UNICODETEXT(保留行结构),取别的会拿到一堆噪声。
  • OCR 用系统自带的 Windows.Media.Ocr。裁剪框要放得够宽:标题左边界会随群名长度、面板开合变化,裁窄了会让匹配掉到 0.73、连续误杀。

方法层

  • 工具要位置无关:批次与导出目录的查找必须递归,否则一搬目录,批处理就以为所有会话都没采过、重采一遍。
  • 每次采集都留原始分轮数据。解析或去重逻辑一改就能重导——那 98 条被误删的消息就是这么找回来的,不用重新去客户端采一遍。
  • 验证手段:新会话没有基准时,从最底起跑两遍(换参数),把两次导出取并集后双向核对——双方范围相互覆盖的那段,就能查出"对方有而自己没有"的消息。两遍都进正式结果,不白跑。
  • 用命令行参数把长跑切成小段(限条数、只跑某个群之类),便于观察和止损。

六、已知边界

  • 图片和文件在文本里只留占位,不含本体(要做的话得逐条点开截屏,耗时翻好几倍,而且截图会带上水印)。
  • 撤回的消息、以及采集中从未进过视口的历史,都拿不到。
  • 采集中不能把前台切走。
  • 全量扫描后会话停在顶部,下次增量需要"重开客户端"或"脚本爬升"。

七、一句话总结

客户端把所有"优雅读取"的路都堵死了(加密库 / 单窗口离屏渲染 / 无障碍树为空 / 无调试端口 / 无视合成消息),所以只能模拟真人操作;

而模拟真人操作的关键,是把每一个"看起来能用、实测不行"的直觉都验一遍,剩下的那条路再往死里打磨:

坐标点对、接缝重叠、双通道核对、水位线停止、空闲让路、原始数据留底。

写于 2026-09 · 文中数据均为实测值,涉及的对象与坐标已模糊化处理。