diff --git a/.gitignore b/.gitignore
index ee741a3..4d54918 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,10 +4,12 @@
*.db-shm
*.db-wal
*.zip
+*.exe
*.spec
build/
dist/
__pycache__/
qq/
QQ*/
-com.tencent.mobileqq/
\ No newline at end of file
+com.tencent.mobileqq/
+chatimg/
diff --git a/GUI.py b/GUI.py
index af70caf..598113a 100644
--- a/GUI.py
+++ b/GUI.py
@@ -9,15 +9,18 @@
def Enter():
- db_path, qq_self, qq, img_path = e1.get(), e2.get(), e3.get(), e6.get()
+ db_path, qq_self, qq = e1.get(), e2.get(), e3.get()
group = 1 if e4.get() == '私聊' else 2
emoji = 1 if e5.get() == '新' else 2
+ with_img = True if e6.get() == '是' else False
+ combine_img = True if e7.get() == '否' else False
if (db_path == "" or qq_self == "" or qq == ""):
info.set("信息不完整!")
return ()
info.set("开始导出")
try:
- QQ_History.main(db_path, qq_self, qq, group, emoji, img_path)
+ QQ_History.main(db_path, qq_self, qq, group,
+ emoji, with_img, combine_img)
info.set("完成")
except Exception as e:
info.set(repr(e))
@@ -76,15 +79,21 @@ def url():
e5.current(0)
e5.grid(row=4, column=1, columnspan=3, sticky="ew", pady=3)
-ttk.Label(root, text="chatimg:").grid(row=5, column=0, sticky="e")
-e6 = ttk.Entry(root, textvariable=img_path_get)
-e6.grid(row=5, column=1, columnspan=2, sticky="ew", pady=3)
-ttk.Button(root, text="选择", command=SelectImgPath,
- width=5).grid(row=5, column=3)
+ttk.Label(root, text="导出图片:").grid(row=5, column=0, sticky="e")
+e6 = ttk.Combobox(root)
+e6['values'] = ('是', '否')
+e6.current(0)
+e6.grid(row=5, column=1, columnspan=3, sticky="ew", pady=3)
+
+ttk.Label(root, text="合并图片:").grid(row=6, column=0, sticky="e")
+e7 = ttk.Combobox(root)
+e7['values'] = ('否', '是')
+e7.current(0)
+e7.grid(row=6, column=1, columnspan=3, sticky="ew", pady=3)
root.grid_columnconfigure(2, weight=1)
info.set("开始")
-ttk.Button(root, textvariable=info, command=Enter).grid(row=6, column=1)
+ttk.Button(root, textvariable=info, command=Enter).grid(row=7, column=1)
tmp = open("tmp.png", "wb+")
tmp.write(base64.b64decode(github_mark))
@@ -93,6 +102,6 @@ def url():
os.remove("tmp.png")
button_img = tk.Button(root, image=github, text='b', command=url, bd=0)
-button_img.grid(row=6, rowspan=7, column=0, sticky="ws")
+button_img.grid(row=7, rowspan=7, column=0, sticky="ws")
root.mainloop()
diff --git a/QQ_History.py b/QQ_History.py
index 43c0338..ded499c 100644
--- a/QQ_History.py
+++ b/QQ_History.py
@@ -30,50 +30,8 @@ def crc64(s):
return v
-def get_base64_from_pic(path):
- with open(path, "rb") as image_file:
- return (b'data:image/png;base64,' + base64.b64encode(image_file.read())).decode("utf-8")
-
-
-def decode_pic(data, img_path):
- try:
- doc = PicRec()
- doc.ParseFromString(data)
- url = 'chatimg:' + doc.md5
- filename = hex(crc64(url))
- filename = 'Cache_' + filename.replace('0x', '')
- rel_path = os.path.join(img_path, filename[-3:], filename)
- if os.path.exists(rel_path):
- w = 'auto' if doc.uint32_thumb_width == 0 else str(
- doc.uint32_thumb_width)
- h = 'auto' if doc.uint32_thumb_height == 0 else str(
- doc.uint32_thumb_height)
- return ''.format(get_base64_from_pic(rel_path), w, h)
- except:
- pass
- return '[图片]'
-
-
-def decode_mix_msg(data):
- try:
- doc = Elem()
- doc.ParseFromString(data)
- img_src = ''
- if doc.picMsg:
- img_src = decode_pic(doc.picMsg)
- return img_src + doc.textMsg.decode('utf-8')
- except:
- pass
- return '[混合消息]'
-
-
-def decode_share_url(msg):
- # TODO
- return '[分享卡片]'
-
-
class QQoutput():
- def __init__(self, path, qq_self, qq, mode, emoji, img_path):
+ def __init__(self, path, qq_self, qq, mode, emoji, with_img, combine_img):
self.db_path = path
self.key = self.get_key() # 解密用的密钥
db = os.path.join(path, "databases", qq_self + ".db")
@@ -85,7 +43,8 @@ def __init__(self, path, qq_self, qq, mode, emoji, img_path):
self.qq = qq
self.mode = mode
self.emoji = emoji
- self.img_path = img_path
+ self.with_img = with_img
+ self.combine_img = combine_img
self.num_to_name = {}
self.emoji_map = self.map_new_emoji()
@@ -101,6 +60,7 @@ def decrypt(self, data, msg_type=-1000):
for i in range(0, len(data)):
msg += chr(ord(data[i]) ^ ord(self.key[i % len(self.key)]))
return msg
+
if msg_type == -1000:
try:
return msg.decode('utf-8')
@@ -108,12 +68,15 @@ def decrypt(self, data, msg_type=-1000):
# print(msg)
pass
return '[decode error]'
+
+ if not self.with_img:
+ return None
elif msg_type == -2000:
- return decode_pic(msg, self.img_path)
+ return self.decode_pic(msg)
elif msg_type == -1035:
- return decode_mix_msg(msg)
+ return self.decode_mix_msg(msg)
elif msg_type == -5008:
- return decode_share_url(msg)
+ return self.decode_share_url(msg)
# for debug
# return '[unknown msg_type {}]'.format(msg_type)
return None
@@ -125,13 +88,18 @@ def add_emoji(self, msg):
num = ord(msg[pos + 1])
if str(num) in self.emoji_map:
index = self.emoji_map[str(num)]
+
if self.emoji == 1:
filename = "new/s" + index + ".png"
else:
filename = "old/" + index + ".gif"
+
+ emoticon_path = os.path.join('emoticon', filename)
+ if self.combine_img:
+ emoticon_path = self.get_base64_from_pic(emoticon_path)
+
msg = msg.replace(
- msg[pos:pos + 2],
- ''.format(get_base64_from_pic(os.path.join('emoticon', filename)), index))
+ msg[pos:pos + 2], ''.format(emoticon_path, index))
else:
msg = msg.replace(msg[pos:pos + 2],
'[emoji:{}]'.format(str(num)))
@@ -266,10 +234,50 @@ def map_new_emoji(self):
new_emoji_map[e["AQLid"]] = str(int(e["EMCode"]) - 100)
return new_emoji_map
+ def get_base64_from_pic(self, path):
+ with open(path, "rb") as image_file:
+ return (b'data:image/png;base64,' + base64.b64encode(image_file.read())).decode("utf-8")
-def main(db_path, qq_self, qq, mode, emoji, img_path):
+ def decode_pic(self, data):
+ try:
+ doc = PicRec()
+ doc.ParseFromString(data)
+ url = 'chatimg:' + doc.md5
+ filename = hex(crc64(url))
+ filename = 'Cache_' + filename.replace('0x', '')
+ rel_path = os.path.join("./chatimg/", filename[-3:], filename)
+ if os.path.exists(rel_path):
+ w = 'auto' if doc.uint32_thumb_width == 0 else str(
+ doc.uint32_thumb_width)
+ h = 'auto' if doc.uint32_thumb_height == 0 else str(
+ doc.uint32_thumb_height)
+ if self.combine_img:
+ rel_path = self.get_base64_from_pic(rel_path)
+ return ''.format(rel_path, w, h)
+ except:
+ pass
+ return '[图片]'
+
+ def decode_mix_msg(self, data):
+ try:
+ doc = Elem()
+ doc.ParseFromString(data)
+ img_src = ''
+ if doc.picMsg:
+ img_src = self.decode_pic(doc.picMsg)
+ return img_src + doc.textMsg.decode('utf-8')
+ except:
+ pass
+ return '[混合消息]'
+
+ def decode_share_url(self, msg):
+ # TODO
+ return '[分享卡片]'
+
+
+def main(db_path, qq_self, qq, mode, emoji, with_img, combine_img):
try:
- q = QQoutput(db_path, qq_self, qq, mode, emoji, img_path)
+ q = QQoutput(db_path, qq_self, qq, mode, emoji, with_img, combine_img)
q.output()
except Exception as e:
with open('log.txt', 'w') as f:
@@ -281,12 +289,3 @@ def main(db_path, qq_self, qq, mode, emoji, img_path):
raise ValueError("信息填入错误")
else:
raise BaseException("Error! See log.txt")
-
-
-db_path = "C:/Users/30857/Desktop/qq备份/apps/com.tencent.mobileqq/"
-qq_self = "308571034"
-qq = "939840382"
-mode = 2
-emoji = 1
-img_path = "C:/Users/30857/Desktop/qq备份/chatpic/chatimg"
-main(db_path, qq_self, qq, mode, emoji, img_path)
diff --git a/README.md b/README.md
index 1dc68e3..a3f5b41 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,46 @@
# QQ聊天记录导出
-可执行文件[Github下载链接](https://github.com/Yiyiyimu/QQ_History_Backup/releases/download/v2.1/QQ_History_Backup-v2.1.zip),[百度网盘下载链接](https://pan.baidu.com/s/1zp3Cg724B-Z65eJjGuKHVQ)(86y6) ,可直接运行。
+可执行文件[Github下载链接](https://github.com/Yiyiyimu/QQ_History_Backup/releases/download/v2.2/QQ_History_Backup-v2.2.zip),[百度网盘下载链接](https://pan.baidu.com/s/1Qit4IRfZdCzJ88n6sfONCg)(ovxt) ,可直接运行。
## 简介
作为国内最常用的聊天工具之一,QQ 为了用户留存度,默认聊天记录备份无法脱离 QQ 被独立打开。
-目前[版本](#致谢)往往需要自行编译,本方法在之前版本的基础上简化了操作,制作了GUI方便使用;并且不再需要提供密钥,自动填入备注/昵称,添加了QQ表情的一并导出。
+目前[版本](#致谢)往往需要自行编译,本方法在之前版本的基础上简化了操作,制作了GUI方便使用;并且不再需要提供密钥,自动填入备注/昵称,添加了QQ表情和图片的一并导出。
## 获取聊天记录文件夹方法
-如果root了,直接在以下地址就可以找到。建议压缩文件夹后复制导出。
+如果手机root,聊天记录可在以下地址找到。因为小文件较多建议压缩文件夹后复制导出。
```
data\data\com.tencent.mobileqq
```
-如果没有root,可以通过手机自带的备份工具备份整个QQ软件,具体方法可以参见
+如果没有root,可以通过手机自带的备份工具备份整个QQ,具体方法可以参见
> 怎样导出手机中的QQ聊天记录? - 益新软件的回答 - 知乎
> https://www.zhihu.com/question/28574047/answer/964813560
+如果同时需要在聊天记录中显示图片,拷贝手机中 `Android/data/com.tencent.mobileqq/Tencent/MobileQQ/chatpic/chatimg` 至 `GUI.exe` 同一文件夹中
+
## GUI使用方法
![GUI_image](./img/GUI.png)
- com.tencent.mobileqq:选择备份后的相应文件夹,一般为`apps/com.tencent.mobileqq`
- 表情版本:默认为新版QQ表情。如果你的聊天记录来自很早以前(比如我),可以切换为旧版的表情
+- 单一文件:默认为否
+ - 不启用单一文件好处在于:1. 使导出的 HTML 文件具有可读性;2. 减小 HTML 文件体积方便打开
+ - 启用单一文件好处:拷贝时不需要和 `emoticon` 以及 `chatimg` 文件夹一起拷贝,更加方便
## 输出截图
-为了方便离线查看,emoji 已经集成到了输出的 `HTML` 文件中
-
-![screenshot](./img/screenshot.png)
-
-有bug的话提issue,记得附上log.txt里的内容
-
-## 显示图片
+![screenshot](./img/layout.png)
+![screenshot](./img/images.png)
-- 需要额外的步骤
-- 手机连电脑 adb pull /sdcard/Andoird/data/com.tencent.mobileqq ./
-- 或者 找工具把这个路径放到和运行程序同目录
+如果没有启用单一文件,拷贝生成的聊天记录时需要一起拷贝 `emoticon` 以及 `chatimg` 文件夹.
-![screenshot](./img/example_img.png)
-
-- 注:图片必须在手机上看过一次才有,因为QQ是看了才下载原图
+有bug的话提issue,记得附上log.txt里的内容。
## v2 更新
- 直接从 `files/kc` 提取明文的密钥,不用再手动输入或解密
@@ -52,7 +48,10 @@ data\data\com.tencent.mobileqq
- 支持 私聊/群聊 的 备注/昵称 自动填入
- 支持 slowtable 的直接整合
- 支持新版 QQ 表情
-- 20210120 支持图片
+
+## v2.2 更新
+- 支持导出图片至聊天记录
+- 支持合并图片至单一文件方便传输
## TODO
- [x] support troop message output
@@ -60,12 +59,12 @@ data\data\com.tencent.mobileqq
- [x] decode friend/troop name, to use in result
- [x] auto-combine db and slow-table
- [x] update to new qq emoji
+- [x] use pic in mobile folder, to better present result
+- [ ] export voicelines
- [ ] add desensitization data to create e2e test
- [ ] add Makefile, to run build/test
-- [x] use pic in mobile folder, to better present result
-- [ ] 支持图片缩略图的加载
-- [ ] 支持分享卡片消息
-- [ ] 提高图文消息显示兼容性
+- [ ] support thumbnail images
+- [ ] support sharing cards
## 致谢
1. [roadwide/qqmessageoutput](https://github.com/roadwide/qqmessageoutput)
diff --git a/img/GUI.png b/img/GUI.png
index 163126e..1f5a621 100644
Binary files a/img/GUI.png and b/img/GUI.png differ
diff --git a/img/example_img.png b/img/example_img.png
deleted file mode 100644
index f6721a8..0000000
Binary files a/img/example_img.png and /dev/null differ
diff --git a/img/images.png b/img/images.png
new file mode 100644
index 0000000..f7ab0d1
Binary files /dev/null and b/img/images.png differ
diff --git a/img/screenshot.png b/img/layout.png
similarity index 100%
rename from img/screenshot.png
rename to img/layout.png