extract-wechat-chat-moments.py 3.1 KB
#!/usr/bin/env python3
"""
微信聊天 + 朋友圈提取 —— 给 lifelog/photolog 人脉档案页用(里程碑 2/4)。

- 聊天:EnMicroMsg.db(SQLCipher,key e03ce81 + compat=1 + page1024)的 message 表,
  type=1 文本、talker 非 @chatroom → messages.json [{talker,isSend,createTime,content}]。
  注意 sqlcipher .mode json 输出开头有 PRAGMA 的 "ok",要 raw[raw.index('['):] 跳过,
  否则 app 端 JSONArray 解析失败、消息导不进去(实测踩过)。
- 朋友圈:SnsMicroMsg.db **是明文 SQLite**(不加密!)的 SnsInfo 表,content 是 protobuf,
  正文用「抽含中文最多的可读段」近似提取 → moments.json [{userName,createTime,text}]。

产物 push 到设备 filesDir/wx_enroll/{messages.json,moments.json},app 内「从微信导入联系人」
的 WeChatEnrollWorker 会 clear+重灌进 wechat_messages / moments 表,按 wxid 关联 Person。

用法:python3 extract-wechat-chat-moments.py /tmp/EnMicroMsg.db /tmp/SnsMicroMsg.db e03ce81 ./out
"""
import sys, os, re, json, subprocess


def sqlcipher_json(db, key, sql):
    script = f'PRAGMA key="{key}";\nPRAGMA cipher_compatibility=1;\nPRAGMA cipher_page_size=1024;\n.mode json\n{sql}\n'
    out = subprocess.run(['sqlcipher', db], input=script, capture_output=True, text=True).stdout
    return json.loads(out[out.index('['):])  # 跳过 PRAGMA 的 "ok"


def extract_messages(en_db, key):
    return sqlcipher_json(en_db, key, """
        SELECT talker, isSend, createTime, content FROM message
        WHERE type=1 AND talker NOT LIKE '%@chatroom' AND content IS NOT NULL AND content != ''
        ORDER BY createTime ASC;""")


def moment_text(blob):
    if not blob:
        return ''
    s = blob.decode('utf-8', 'ignore')
    segs = re.findall(r'[一-鿿A-Za-z0-9,。!?、,.!?#@::…~()()【】\-—\s]{2,}', s)

    def score(x):
        x = x.strip()
        if x.startswith(('http', 'wxid', 'mmsns', 'mp.weixin')):
            return -1
        if re.fullmatch(r'[A-Za-z0-9]+', x):
            return -1
        return len(re.findall(r'[一-鿿]', x))
    best = max(segs, key=score, default='')
    return best.strip() if score(best) > 0 else ''


def extract_moments(sns_db):
    import sqlite3  # SnsMicroMsg 是明文
    c = sqlite3.connect(sns_db)
    out = []
    for u, t, content in c.execute("SELECT userName, createTime, content FROM SnsInfo WHERE content IS NOT NULL"):
        txt = moment_text(content)
        if txt:
            ct = int(t)
            ct = ct * 1000 if ct < 1e12 else ct  # 秒→毫秒
            out.append({'userName': u, 'createTime': ct, 'text': txt})
    return out


def main():
    en_db, sns_db, key, out_dir = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
    os.makedirs(out_dir, exist_ok=True)
    msgs = extract_messages(en_db, key)
    json.dump(msgs, open(os.path.join(out_dir, 'messages.json'), 'w'), ensure_ascii=False)
    moments = extract_moments(sns_db)
    json.dump(moments, open(os.path.join(out_dir, 'moments.json'), 'w'), ensure_ascii=False)
    print(f'聊天 {len(msgs)} 条,朋友圈 {len(moments)} 条')


if __name__ == '__main__':
    main()