news 2026/10/10 0:29:22

微信聊天记录导出与年度报告:SQLite解密到数据可视化全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信聊天记录导出与年度报告:SQLite解密到数据可视化全流程

简介:面向需要在本地永久保存微信聊天记录,并进一步生成HTML、Word、CSV等通用文档及年度聊天报告的用户,这份资源提供了从微信备份解析到数据可视化的一整套实现思路与脚本支撑。包体共238个文件,压缩包约25MB,以94个Python脚本为核心,配合17个HTML模板、61张PNG图表和36个SVG图形,可完成聊天记录解析、文档导出、关键词统计与图表展示;另含JSON配置、Markdown说明及少量可执行文件,便于直接运行与二次修改。目前已有648人学习下载。内容涵盖微信备份与提取方法、第三方解析工具的使用技巧、各类格式转换的具体实现,以及用Pandas、matplotlib生成年度报告的思路,同时对隐私合规与安全风险给出了提醒。按模板、脚本、图表分类存放,适合想自己动手完成聊天记录长期管理和社交数据年报的个人开发者与微信应用爱好者。

1. 微信聊天记录还在手机里吃灰?本地导出与年度报告实操指南

做微信相关开发或者单纯想做个人数据归档的人,迟早会撞上同一个痛点:聊天记录只能躺在微信应用里按时间慢慢翻,官方既不开放导出,也不给任何统计视角。而实际上,微信的聊天数据就存成手机里的一个 SQLite 数据库文件,只要通过备份路径把它取出来、解开加密层,你就能拿到全部原始数据,导成 HTML、Word、CSV 永久保存,还能写脚本生成一份年度聊天报告。这篇笔记会把整条链路拆开:备份与解密、字段语义、三格式导出、高频翻车点,最后落到一个半自动增量归档方案。适合想掌握微信应用数据流向、或想拿真实数据练手的开发者,按步骤走基本能复现。

2. 先拿到原始数据库:Android 与 iOS 两条备份路径,以及 EnMicroMsg.db 的解密钥匙

2.1 Android 路径:不 root 也能备份,但需要一个“伪取证”流程

微信在 Android 端的核心数据库是/data/data/com.tencent.mm/MicroMsg/{32位哈希目录}/EnMicroMsg.db。正常文件管理看不到,因为没有 root 权限读不了/data/data,但有两条绕过路径:一是 Android 10 以下系统用adb backup抽取应用数据;二是用多数国产系统自带的应用数据备份功能,把微信数据整体备份出来再解包。两者拿到的都是带加密的 SQLite 文件,后续解密方式一致。

以adb backup为例,备份前先把手机用 USB 连上电脑,开启开发者模式里的 USB 调试,然后在电脑端执行:

# Android 8.0 以下可以指定系统版本参数,部分新机型需使用 apk 纯数据备份 adb backup -f wechat_backup.ab -noapk com.tencent.mm

执行到这一步,手机屏幕上会弹出“允许备份?”,必须点确认并输入密码(不想加密可以留空,但建议输一个,防止备份文件中途被读取)。备份完成后得到一个.ab文件,它不是直接可解压的压缩包,头部有 Android Backup 格式的魔数。用abe工具解包:

java -jar abe.jar unpack wechat_backup.ab wechat_backup.tar tar -tf wechat_backup.tar | grep -i EnMicroMsg.db

解包后能找到apps/com.tencent.mm/db/EnMicroMsg.db和apps/com.tencent.mm/shared_prefs/下的系统配置文件。后者的价值比数据库本身还高——里面有解密需要的参数。iOS 端的思路类似,只是微信数据分散在多个db文件里,一般需要做表级合并,这里不展开讲,先把 Android 链路跑通再说。

2.2 解密密钥的推导逻辑:IMEI+uin 做 MD5 取前 7 位

微信对EnMicroMsg.db的加密逻辑在 7.0 版本之前非常固定:数据库密码是IMEI + uin拼接后做 MD5,取前 7 位。其中IMEI是设备标识,uin是微信账号在本地存储的用户标识,可以从 shared_prefs 里读。老版本很多教程教你用*#06#查 IMEI,但 Android 10 之后系统对 IMEI 读取限制很严,而且部分设备上报的 IMEI 是虚拟化的,直接用会解密失败。我一般从系统配置里读,而不是靠设备查询:

# 在备份解包后的目录执行,取出配置里的 uin 和 IMEI grep -o '"uin":[0-9]*' apps/com.tencent.mm/shared_prefs/system_config_prefs.xml | head -1 grep -o '"IMEI":[^,}]*' apps/com.tencent.mm/shared_prefs/__system_config_prefs.xml | head -1

拿到这两个值后,用下面的 Python 片段算密码:

import hashlib def compute_db_pass(imei: str, uin: str) -> str: """ 微信旧版数据库密钥推导:IMEI + uin 拼接后取 MD5 前 7 位。 注意:微信 7.0 之后部分版本改成了只用 uin 计算,遇到解密失败先试这条路。 """ concat_str = f"{imei}{uin}".encode("utf-8") md5_hex = hashlib.md5(concat_str).hexdigest() return md5_hex[:7] # 示例参数,实际从 shared_prefs 里替换 imei = "860000000000000" uin = "123456789" db_pass = compute_db_pass(imei, uin) print(f"DB_PASS = {db_pass}")

计算逻辑很简单,但这里有两个前提。第一,IMEI与uin的拼接顺序不能反,是先 IMEI 后 uin;第二,MD5 算完只取十六进制结果的前 7 个字符,不是 8 位也不是 16 位。密钥出来后,用 sqlcipher 对加密库做一次解密转储,得到一个标准的明文 SQLite 文件:

sqlcipher EnMicroMsg.db # 在 sqlcipher 命令行中执行以下语句 PRAGMA key = '你的DB_PASS'; PRAGMA cipher_migrate; ATTACH DATABASE 'plaintext.db' AS plaintext KEY ''; SELECT sqlcipher_export('plaintext'); DETACH DATABASE plaintext;

cipher_migrate是新老版本加密参数切换时的关键指令。微信 7.0 之后改了 SQLCipher 的默认页面大小和 KDF 迭代次数,不执行这条迁移,sqlite 会报file is encrypted or is not a database。执行上面的语句后,plaintext.db就是一个无加密的 SQLite 库,之后再用任何工具打开都行。

2.3 表结构速览:先搞懂 message 表再动手

解密拿到明文库只是第一步,更关键的是读懂表结构。微信数据库里表很多,但核心只需要看这几张:message是聊天记录本体,rcontact是联系人表,chatroom是群聊信息表。用 DB Browser for SQLite 打开plaintext.db,查看message表的 schema,重点字段长这样:

字段名类型含义
msgIdINTEGER本地自增主键,不是服务端消息 ID
msgSvrIdINTEGER服务端消息 ID,微信分配的全局唯一标识
typeINTEGER消息类型,见下方说明
isSendINTEGER1 表示自己发送,0 表示对方发送
createTimeINTEGER毫秒级时间戳
talkerTEXT聊天对象标识,单聊是对方 username,群聊是群 ID,以@结尾
contentTEXT消息内容,不同 type 对应的存储格式差异极大

其中type字段是最容易误读的。基础消息类型:1 是文本,3 是图片,34 是语音,43 是视频,49 是文件/链接/小程序等混合类型,10000 是系统通知。特别注意 49 类型的content字段,里面存的通常是 XML 结构的内容,像<msg><appmsg ...><title>标题</title><des>描述</des></appmsg></msg>,不能直接当作文本导出。而talker字段也不是可读的微信号或昵称,需要去rcontact表里用username字段关联查询才能拿到备注名或昵称。

到这一步,数据已经躺在了明文库里,接下来就是按需导出。导出前先跑几条查询,确认数据完整性:

-- 用这段 SQL 做“体检”,确认库能用、数据不是空的 SELECT COUNT(*) FROM message; SELECT MIN(createTime), MAX(createTime) FROM message; SELECT type, COUNT(*) FROM message GROUP BY type;

如果最大值时间是最近几天、消息总量上万,说明备份和解密都成功。若最大时间停在几个月前,说明备份的不是最新数据,回头重新走一遍备份流程。

3. 三格式导出:CSV 打底、HTML 阅读、Word 归档的完整流程

3.1 环境准备与连接明文库

导出前确认 Python 环境里装好 sqlite3、jinja2、python-docx 这几个基础库。sqlite3 是内置的,后两个需要手动装:

pip install jinja2 python-docx

连接明文库时有一点要注意:用只读模式打开,防止误操作把源库改了。虽然明文库是从备份解密出来的,弄坏了可以重新解密,但反复折腾也耽误时间。我一般这样连接:

import sqlite3 DB_PATH = "plaintext.db" conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True) conn.row_factory = sqlite3.Row # 让查询结果按字段名访问

连接成功后,先做一个联系人映射字典,把talker转成可读名字。这个映射是导出 HTML 和 Word 的共用前置:

def build_contact_map(conn): """从 rcontact 表构建 username -> 昵称/备注 的映射。""" contact_map = {} rows = conn.execute( "SELECT username, conRemark, nickname FROM rcontact" ).fetchall() for row in rows: # 优先级:备注名 > 昵称 > username,特殊字符剔除 display_name = row["conRemark"] or row["nickname"] or row["username"] display_name = display_name.strip().replace("\n", " ") contact_map[row["username"]] = display_name return contact_map

conRemark和nickname都可能为空,做三层回退是为了保证导出后不会出现一堆乱码 ID。后续导出函数都会依赖这个映射表。

3.2 CSV:切片、字段清洗与编码(utf-8-sig)

CSV 是最工程化的导出格式,既能直接丢进 Excel,也能给后续的数据分析做原料。导出最小单元我建议按“单个聊天对象一个文件”来切,避免一个超大 CSV 挤在一起。

import csv import datetime def timestamp_to_str(ms: int) -> str: """微信 createTime 是毫秒时间戳,转成可读的本地时间字符串。""" if ms > 10**12: # 防御性判断:个别备份把秒和毫秒搞混 ms = ms // 1000 return datetime.datetime.fromtimestamp(ms).strftime("%Y-%m-%d %H:%M:%S") def export_talker_to_csv(conn, talker_id: str, filepath: str, contact_map: dict) -> None: """导出单个聊天对象的消息记录为 CSV。""" rows = conn.execute( """ SELECT createTime, isSend, type, content FROM message WHERE talker = ? ORDER BY createTime ASC """, (talker_id,), ).fetchall() with open(filepath, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["时间", "方向", "内容"]) for row in rows: direction = "发送" if row["isSend"] == 1 else "接收" content = row["content"] # 只处理纯文本,其他类型先打标记 if row["type"] == 1: writer.writerow([timestamp_to_str(row["createTime"]), direction, content]) else: # 导出时把类型写进括号,方便后续筛选 writer.writerow([timestamp_to_str(row["createTime"]), direction, f"[{row['type']}] {content}"])

编码用utf-8-sig而不是utf-8,这是最大的坑之一。Excel 直接打开“标准 utf-8”的 CSV 会中文乱码,因为微软家的软件默认按本地代码页(GBK)解析;utf-8-sig在文件头加了 BOM,Excel 能自动识别。如果你不打算用 Excel,只想给 pandas 用,去掉 BOM 更干净,这里按目标场景选择。

3.3 HTML:多聊天对象分文件渲染

HTML 是给人看的格式,目标是打开就能按时间顺序读,视觉上接近微信对话界面。我习惯把每个聊天对象导出为一个独立 HTML 文件,再用一个 index.html 汇总入口。模板用 jinja2 渲染:

from jinja2 import Template html_template = Template(""" <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>{{ title }}</title> <style> body { max-width: 800px; margin: 20px auto; font-family: sans-serif; } .msg { margin: 8px 0; padding: 6px 10px; border-radius: 6px; } .send { background: #d9f7be; text-align: right; } .receive { background: #f0f0f0; text-align: left; } .time { color: #888; font-size: 12px; margin-bottom: 2px; } </style> </head> <body> <h1>{{ title }}</h1> <p>总消息数:{{ total_count }} 条</p> {% for item in messages %} <div class="time">{{ item.time }}</div> <div class="msg {{ 'send' if item.is_send else 'receive' }}"> {{ item.content | escape }} </div> {% endfor %} </body> </html> """)

渲染时把查询出的message列表组织成模板需要的结构。有个关键点是 HTML 转义,聊天内容里各种尖括号、引号都有,直接用| escape走掉,否则模板渲染出来的页面可能出现标签错乱。

导出所有聊天对象时需要遍历去重后的talker列表:

def export_all_to_html(conn, output_dir: str, contact_map: dict) -> None: """为每个聊天对象生成 HTML 文件,并写一个 index 入口。""" talkers = conn.execute( "SELECT DISTINCT talker FROM message" ).fetchall() html_files = [] for row in talkers: talker_id = row["talker"] display_name = contact_map.get(talker_id, talker_id) # 文件名用安全字符,避免特殊符号导致路径问题 safe_name = display_name.replace("/", "_").replace("\\", "_") filepath = f"{output_dir}/{safe_name}.html" messages = prepare_messages(conn, talker_id) html_content = html_template.render( title=f"与 {display_name} 的聊天记录", total_count=len(messages), messages=messages, ) with open(filepath, "w", encoding="utf-8") as f: f.write(html_content) html_files.append((display_name, len(messages), filepath)) # 生成 index.html,这里省略索引表格的渲染部分

prepare_messages里只需处理文本消息,把图片语音这类在 HTML 里输出为占位提示,比如[图片] [语音]。完整导出的 HTML 文件可以长期用浏览器打开,不用依赖任何工具,这是它比 Word 更适合日常检索的原因。

3.4 Word:按天分节,适合长期存档

Word 导出解决的问题不一样:很多人需要把聊天记录作为材料上交或者打印归档,HTML 不好打印、CSV 不像正式档案,Word 按天分节排版是最接近存档需求的。

from docx import Document from docx.shared import Pt from docx.enum.text import WD_ALIGN_PARAGRAPH def export_to_word(conn, talker_id: str, filepath: str, contact_map: dict) -> None: """按天分节导出聊天记录为 Word 文档。""" doc = Document() display_name = contact_map.get(talker_id, talker_id) doc.add_heading(f"与 {display_name} 的聊天记录", level=1) rows = conn.execute( """ SELECT createTime, isSend, content, type FROM message WHERE talker = ? ORDER BY createTime ASC """, (talker_id,), ).fetchall() current_date = None for row in rows: dt = datetime.datetime.fromtimestamp(row["createTime"] / 1000) day_str = dt.strftime("%Y-%m-%d") if day_str != current_date: doc.add_heading(day_str, level=2) current_date = day_str time_str = dt.strftime("%H:%M:%S") direction = "我" if row["isSend"] == 1 else display_name content = row["content"] if row["type"] == 1 else f"[消息类型 {row['type']}]" p = doc.add_paragraph() run = p.add_run(f"{time_str} {direction}:{content}") run.font.size = Pt(11) doc.save(filepath)

Word 导出的核心是createTime转日期后做分段判断,每天一个二级标题。这样打印出来的文档层次清晰,也方便按日期检索。注意add_heading用 level=1 做主标题、level=2 做日期分节,导出的目录结构会比较清爽。若聊天对象很多、记录量很大,不建议一次跑全部,按联系人分批导出更稳。

4. 避坑自查:解密失败、乱码、库文件为 0 字节等五个高频翻车点

4.1 数据源与库文件问题排查

翻车点一:解密报错file is not a database。现象:执行PRAGMA key后直接报file is encrypted or is not a database,库打不开。原因:DB_PASS 计算错误,IMEI 从系统拿到的虚拟化值或 uin 读错了;也可能是微信新版本换了加密参数。解决:从shared_prefs配置里读参数,不要信操作系统查询;同时在解密时加PRAGMA cipher_migrate;,个别版本还必须调整cipher_page_size,我一般会先试默认参数,失败后手动设成 4096 再试一次。

翻车点二:备份出来的 db 文件只有 5KB。现象:解包备份后看到EnMicroMsg.db很小,导出的记录数量严重偏少,甚至 0 条。原因:新版微信开启 WAL(Write-Ahead Logging)模式,数据主体在-wal文件里,只拷主库必然缺数据。解决:备份时把同目录下的EnMicroMsg.db-wal和EnMicroMsg.db-shm一并取出,在 sqlcipher 打开前先执行PRAGMA wal_checkpoint;把 WAL 内容合并回主库,再导出明文。检查备份是否完整,可以看-wal文件是否存在以及大小是否接近主库。

翻车点三:解密后导出的 CSV 时间显示为 1970-01-01。现象:所有时间戳都变成 1970 年附近,聊天记录完全错位。原因:createTime是毫秒级时间戳,被当成秒级直接格式化。解决:格式化前判断数值量级,大于10**12的先除以 1000。这类问题本质是数据单位踩坑,建议在timestamp_to_str里做防御性判断,一劳永逸。

4.2 导出结果与语义问题排查

翻车点四:文本消息导出后混杂大量 XML 串。现象:CSV 里出现一坨一坨的<msg>...</msg>结构,真正内容淹没在标签中。原因:49 类型消息的内容就是 XML 或复合结构,直接当作文本导出是不对的。解决:导出时按type分流,文本消息走原样导出;链接、文件、小程序类消息从中提取title或url字段:

import re def parse_link_content(raw: str) -> str: """从微信混合类型的XML中提取可读信息,提取不到就返回原文。""" title_match = re.search(r"<title>(.*?)</title>", raw, re.S) if title_match: return title_match.group(1).strip() url_match = re.search(r"<url>(.*?)</url>", raw, re.S) if url_match: return url_match.group(1).strip() return raw[:100]

翻车点五:多个聊天对象的记录串台。现象:导出的talker全部映射成了同一个昵称,或者群聊记录跑到单聊文件里。原因:rcontact表存在群号与微信号同值冲突,直接用username做关联会有错配风险。解决:先用chatroom表拿到群 ID 名单,对群聊对象单独走群映射逻辑,再对单聊对象走rcontact;无法匹配时把原始 ID 原样输出,不要硬猜。导出后用“非空校验”和“方向占比”两个指标快速检查,避免错配文件覆盖掉正确文件。

5. 年度聊天报告:设计四个核心指标,用 jieba 算关键词,生成可分享 HTML

5.1 指标选择:为什么是“总量、高峰时段、连续活跃天数、对话热度”这四个

年度报告不能只是把数据罗列一遍,要回答几个你能感知的问题:今年聊了多少?什么时候最活跃?和谁聊得最多?中间有没有长期断联?这四个维度对应四个可以独立查询的指标,不涉及复杂建模,却最能反映一年的聊天全貌。

def compute_yearly_stats(conn, year: int) -> dict: """计算年度报告的四个核心指标。""" start_ts = f"{year}-01-01 00:00:00" end_ts = f"{year}-12-31 23:59:59" total = conn.execute( """ SELECT COUNT(*) FROM message WHERE createTime BETWEEN ? AND ? """, (to_ms(start_ts), to_ms(end_ts)), ).fetchone()[0] # 按小时统计活跃分布,createTime转本地时区后取hour hourly = conn.execute( """ SELECT CAST(strftime('%H', createTime / 1000, 'unixepoch', 'localtime') AS INT) AS h, COUNT(*) AS cnt FROM message WHERE createTime BETWEEN ? AND ? GROUP BY h ORDER BY cnt DESC """, (to_ms(start_ts), to_ms(end_ts)), ).fetchall() # 按天统计连续活跃天数 daily_active = conn.execute( """ SELECT COUNT(DISTINCT date(createTime / 1000, 'unixepoch', 'localtime')) FROM message WHERE createTime BETWEEN ? AND ? """, (to_ms(start_ts), to_ms(end_ts)), ).fetchone()[0] # 按聊天对象统计消息总数与字数量,排前10 top_chats = conn.execute( """ SELECT talker, COUNT(*) AS cnt, SUM(CASE WHEN type = 1 THEN LENGTH(content) ELSE 0 END) AS total_chars FROM message WHERE createTime BETWEEN ? AND ? GROUP BY talker ORDER BY cnt DESC LIMIT 10 """, (to_ms(start_ts), to_ms(end_ts)), ).fetchall() return { "total": total, "hourly_peak": hourly[0][0] if hourly else None, "daily_active_days": daily_active, "top_chats": top_chats, }

to_ms是把日期字符串转成毫秒时间戳的辅助函数,可以用datetime.strptime实现。这里有几个设计意图:总消息量直接反映活跃度,不解释;小时峰值能看出你是“深夜聊天型”还是“工作摸鱼型”;连续活跃天数反映关系稳定性;对话热度必须结合字数和条数一起看,光有条数会被小程序刷屏记录误导。

5.2 词频与关键词:jieba 切词、停用词过滤与 TOP 词输出

年度报告最直观的部分是“你今年说得最多的词”。这部分只对文本消息(type=1)做词频统计,不需要引入 NLP 大模型。分词用 jieba 的默认模式就能跑,关键是停用词表要自己维护一个基础版。

import jieba from collections import Counter STOP_WORDS = { "嗯", "啊", "哈", "的", "了", "吗", "呢", "这", "那", "就", "我", "你", "他", "她", "我们", "你们", "他们", "在", "是", "有", "和", "就", "都", "也", } def top_keywords(conn, year: int, top_n: int = 20) -> list: """提取年度高频关键词,返回 [(word, count)]。""" start_ts = f"{year}-01-01 00:00:00" end_ts = f"{year}-12-31 23:59:59" rows = conn.execute( """ SELECT content FROM message WHERE type = 1 AND createTime BETWEEN ? AND ? """, (to_ms(start_ts), to_ms(end_ts)), ).fetchall() counter = Counter() for row in rows: content = row["content"] if not content or len(content) > 500: continue # 太长的内容多半是转发的截断文本,不参与分词 words = jieba.cut(content) counter.update( w.strip() for w in words if w.strip() and len(w) >= 2 and w.strip() not in STOP_WORDS ) return counter.most_common(top_n)

jieba.cut返回生成器,遍历时同时做清洗能省内存。这里过滤条件有三个:长度至少 2 个字符(排除单字语气词)、不是停用词、没有空白。如果聊天内容里大量出现“哈哈哈哈”,这种叠词会在分词后被拆成“哈哈”或“哈哈哈”,建议把这类词也加进停用词表,不然 TOP 20 全是笑声。

5.3 报告渲染:把图表结果嵌入 HTML 模板

年度报告如果只有数字没图表,传到群里没人愿意看。用 matplotlib 画三张小图:月度消息量趋势、小时分布热力条形图、TOP10 对话对象柱状图。生成的图片先落盘,再和统计指标一起交给 HTML 模板渲染。

import matplotlib matplotlib.use("Agg") # 无图形界面环境下必须设置后端 import matplotlib.pyplot as plt import base64 import io def chart_to_base64(fig): """把 matplotlib 图像转成 base64 字符串,嵌入 HTML。""" buf = io.BytesIO() fig.savefig(buf, format="png", dpi=150, bbox_inches="tight") plt.close(fig) return base64.b64encode(buf.getvalue()).decode("utf-8") def plot_monthly_trend(conn, year: int) -> str: """按月度统计消息条数,返回 base64 PNG。""" rows = conn.execute( """ SELECT CAST(strftime('%m', createTime / 1000, 'unixepoch', 'localtime') AS INT) AS m, COUNT(*) AS cnt FROM message WHERE createTime BETWEEN ? AND ? GROUP BY m ORDER BY m """, (to_ms(f"{year}-01-01 00:00:00"), to_ms(f"{year}-12-31 23:59:59")), ).fetchall() months = [r["m"] for r in rows] counts = [r["cnt"] for r in rows] fig, ax = plt.subplots(figsize=(8, 3)) ax.plot(months, counts, marker="o", linewidth=1.5) ax.set_xlabel("月份") ax.set_ylabel("消息条数") ax.set_xticks(range(1, 13)) return chart_to_base64(fig)

matplotlib.use("Agg")必须放在导入 pyplot 之前,否则在部分环境会直接报cannot connect to display。图表转 base64 后和统计指标一起填进报告模板,最终产出一个自包含的单文件 HTML——没有外部图片依赖,发给谁都能打开。模板里用表格展示 TOP10 排行,图片穿插在图表区,整体控制在 1MB 以内。

6. 增量备份与永久归档:让导出这件事成为半自动化的“后悔药”

6.1 用 msgSvrId 做游标,实现差异导出

全量导出第一次跑完就好,之后每个月再做全量无疑是浪费时间和磁盘。增量导出的关键是用msgSvrId做游标:它是服务端分配的唯一 ID,只增不减,比createTime更适合做同步标志位。

import json import os CURSOR_FILE = "export_cursor.json" def load_cursor() -> int: """读取上次导出的游标位置,文件不存在就从头导出。""" if not os.path.exists(CURSOR_FILE): return 0 with open(CURSOR_FILE, "r", encoding="utf-8") as f: return json.load(f).get("last_msgSvrId", 0) def save_cursor(last_id: int) -> None: """把当前最大 msgSvrId 写回游标文件。""" with open(CURSOR_FILE, "w", encoding="utf-8") as f: json.dump({"last_msgSvrId": last_id}, f, ensure_ascii=False) def incremental_export(conn, contact_map: dict) -> None: """只导出游标之后的新消息,并更新游标。""" last_id = load_cursor() rows = conn.execute( """ SELECT * FROM message WHERE msgSvrId > ? ORDER BY msgSvrId ASC """, (last_id,), ).fetchall() if not rows: print("无新增消息") return # 对 rows 执行和全量导出相同的清洗与渲染逻辑,此处省略 new_max_id = max(r["msgSvrId"] for r in rows) save_cursor(new_max_id)

增量导出后,HTML 和 Word 需要重新渲染,CSV 可以用追加模式写入,但注意追加前检查是否有与游标不一致的边界数据。每次跑完增量,顺手对一下MAX(msgSvrId)和游标是否一致,不一致就是有脏数据。

6.2 归档策略:校验和与冷备份

导出的 HTML、CSV、Word 文件如果只存在电脑里,一年后硬盘挂了就全没了。我习惯把每个聊天对象的 CSV 按年压缩,生成一个带校验和的压缩包,再丢到两个不同的备份位置。校验和脚本很简单:

# 归档并计算 SHA256,归档后第一时间做一次完整性校验 tar -czf chat_export_2024.tar.gz chat_export/ sha256sum chat_export_2024.tar.gz > chat_export_2024.sha256 sha256sum -c chat_export_2024.sha256

明文 SQLite 库也可以一并压缩归档,但建议和 CSV 分开存放——万一 CSV 有格式问题是可以用 SQL 重新导出的,库文件才是真正不可再生的资产。从那以后,我每次手动折腾聊天数据前,都强制先跑一遍只读校验,确认最新消息和游标对得上才继续动手,这个习惯救过我至少两次。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 0:29:01

NeRF文物三维重建:低视角高保真建模实战指南

简介&#xff1a;本资源是一套面向计算机视觉与文化遗产数字化方向学习者、研究者的NeRF三维文物重建实战项目&#xff0c;聚焦解决高保真文物数字建模难题&#xff0c;适用于具备Python编程基础及一定深度学习经验的中高级开发者。压缩包共59个文件&#xff0c;含40张文物多视…

作者头像 李华
网站建设 2026/10/10 0:26:47

存储性能测试实战:从IO特性到压测选型

前阵子帮某团队优化一套日志采集系统的存储&#xff0c;测试结果单看峰值很不错&#xff0c;可一上业务就延迟飙升&#xff0c;查了半天&#xff0c;问题出在最基础的IO特征没对齐。这类事在存储运维里太常见了&#xff0c;很多人压测只跑一个fio默认参数&#xff0c;拿到的数字…

作者头像 李华
网站建设 2026/10/10 0:26:18

电脑DIY内存条选购与超频指南:从原理到装机避坑

1. 为什么内存条是DIY装机里最容易被低估的一环很多人第一次自己攒机&#xff0c;注意力几乎全被CPU和显卡吸走了&#xff0c;觉得这两样选好了机器就稳了。结果装完开机&#xff0c;游戏帧数忽高忽低&#xff0c;剪辑软件预览卡顿&#xff0c;浏览器开二十个标签页就开始转圈&…

作者头像 李华
网站建设 2026/10/10 0:21:10

瓶装白酒疵品检测数据集实战:从数据拆解到YOLO基线训练

简介&#xff1a;这份瓶装白酒疵品检测数据集面向计算机视觉与工业质检方向的学习者和研究者&#xff0c;用于训练和评估瓶身划痕、标签破损、密封不良等疵品的自动识别模型&#xff0c;适合作为机器学习入门到进阶的实战素材。压缩包共约2000个文件&#xff0c;以4516张jpg图像…

作者头像 李华
网站建设 2026/10/10 0:20:07

C/C++字符串与数值转换全解析:从atoi到strtod的选型与避坑指南

1. 字符串与数值互转的全景认知1.1 为什么这组函数值得单独拎出来讲C/C 里做字符串和数值之间的转换&#xff0c;几乎每个项目都会碰到。从配置文件解析、命令行参数处理&#xff0c;到日志分析、协议报文拆解&#xff0c;这组函数无处不在。但恰恰因为它们太常见&#xff0c;很…

作者头像 李华
网站建设 2026/10/10 0:19:34

LBS全链路实战:Logstash同步、ES地理检索与小程序轻量集成

1. 这不是“加个定位按钮”就能搞定的事&#xff1a;LBS服务的真实技术断层很多人看到“基于位置服务”这六个字&#xff0c;第一反应是打开微信小程序地图组件、调用wx.getLocation&#xff0c;再把经纬度传给后端——完事。我去年在某高校实验室带一个模拟项目X时&#xff0c…

作者头像 李华