Skip to content

Commit

Permalink
feat: add options to choose if enable with_img and combine_img
Browse files Browse the repository at this point in the history
Signed-off-by: Shuyang Wu <[email protected]>
  • Loading branch information
Yiyiyimu committed May 3, 2021
1 parent 7c4e9ce commit a37921a
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 92 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
*.db-shm
*.db-wal
*.zip
*.exe
*.spec
build/
dist/
__pycache__/
qq/
QQ*/
com.tencent.mobileqq/
com.tencent.mobileqq/
chatimg/
27 changes: 18 additions & 9 deletions GUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand All @@ -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()
119 changes: 59 additions & 60 deletions QQ_History.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<img src="{}" width="{}" height="{}" />'.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")
Expand All @@ -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()
Expand All @@ -101,19 +60,23 @@ 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')
except:
# 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
Expand All @@ -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],
'<img src="{}" alt="{}" />'.format(get_base64_from_pic(os.path.join('emoticon', filename)), index))
msg[pos:pos + 2], '<img src="{}" alt="{}" />'.format(emoticon_path, index))
else:
msg = msg.replace(msg[pos:pos + 2],
'[emoji:{}]'.format(str(num)))
Expand Down Expand Up @@ -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 '<img src="{}" width="{}" height="{}" />'.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:
Expand All @@ -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)
43 changes: 21 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,70 @@
# 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` 提取明文的密钥,不用再手动输入或解密
- 支持群聊记录导出
- 支持 私聊/群聊 的 备注/昵称 自动填入
- 支持 slowtable 的直接整合
- 支持新版 QQ 表情
- 20210120 支持图片

## v2.2 更新
- 支持导出图片至聊天记录
- 支持合并图片至单一文件方便传输

## TODO
- [x] support troop message output
- [x] use com.tencent.mobileqq/f/kc as key
- [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)
Expand Down
Binary file modified img/GUI.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed img/example_img.png
Binary file not shown.
Binary file added img/images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes

0 comments on commit a37921a

Please sign in to comment.