2026/10/10 0:30:24

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

微信聊天记录导出与年度报告: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 解密密钥的推导逻辑IMEIuin 做 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(fDB_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本地自增主键不是服务端消息 IDmsgSvrIdINTEGER服务端消息 ID微信分配的全局唯一标识typeINTEGER消息类型见下方说明isSendINTEGER1 表示自己发送0 表示对方发送createTimeINTEGER毫秒级时间戳talkerTEXT聊天对象标识单聊是对方 username群聊是群 ID以结尾contentTEXT消息内容不同 type 对应的存储格式差异极大其中type字段是最容易误读的。基础消息类型1 是文本3 是图片34 是语音43 是视频49 是文件/链接/小程序等混合类型10000 是系统通知。特别注意 49 类型的content字段里面存的通常是 XML 结构的内容像msgappmsg ...title标题/titledes描述/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(ffile:{DB_PATH}?modero, uriTrue) 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_mapconRemark和nickname都可能为空做三层回退是为了保证导出后不会出现一堆乱码 ID。后续导出函数都会依赖这个映射表。3.2 CSV切片、字段清洗与编码utf-8-sigCSV 是最工程化的导出格式既能直接丢进 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, encodingutf-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在文件头加了 BOMExcel 能自动识别。如果你不打算用 Excel只想给 pandas 用去掉 BOM 更干净这里按目标场景选择。3.3 HTML多聊天对象分文件渲染HTML 是给人看的格式目标是打开就能按时间顺序读视觉上接近微信对话界面。我习惯把每个聊天对象导出为一个独立 HTML 文件再用一个 index.html 汇总入口。模板用 jinja2 渲染from jinja2 import Template html_template Template( !DOCTYPE html html langzh-CN head meta charsetutf-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 classtime{{ item.time }}/div div classmsg {{ 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( titlef与 {display_name} 的聊天记录, total_countlen(messages), messagesmessages, ) with open(filepath, w, encodingutf-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} 的聊天记录, level1) 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, level2) 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用 level1 做主标题、level2 做日期分节导出的目录结构会比较清爽。若聊天对象很多、记录量很大不建议一次跑全部按联系人分批导出更稳。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 条。原因新版微信开启 WALWrite-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(rtitle(.*?)/title, raw, re.S) if title_match: return title_match.group(1).strip() url_match re.search(rurl(.*?)/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 算关键词生成可分享 HTML5.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 词输出年度报告最直观的部分是“你今年说得最多的词”。这部分只对文本消息type1做词频统计不需要引入 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, formatpng, dpi150, bbox_inchestight) 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, markero, linewidth1.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, encodingutf-8) as f: return json.load(f).get(last_msgSvrId, 0) def save_cursor(last_id: int) - None: 把当前最大 msgSvrId 写回游标文件。 with open(CURSOR_FILE, w, encodingutf-8) as f: json.dump({last_msgSvrId: last_id}, f, ensure_asciiFalse) 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 重新导出的库文件才是真正不可再生的资产。从那以后我每次手动折腾聊天数据前都强制先跑一遍只读校验确认最新消息和游标对得上才继续动手这个习惯救过我至少两次。希望帮到你。本文还有配套的精品资源点击获取