- 您可以关注下方微信公众号,回复:联系方式,获取QQ交流群群号,并在群内寻求帮助。 -
-
-
-
-
-
-
-
-
- 出错了
", f"{self.contact.remark}
") - f.write(html_head) - self.rangeSignal.emit(len(messages)) - for index, message in enumerate(messages): - type_ = message[2] - sub_type = message[3] - timestamp = message[5] - if (type_ == 3 and self.message_types.get(3)) or (type_ == 34 and self.message_types.get(34)) or ( - type_ == 47 and self.message_types.get(47)): - pass - else: - self.progressSignal.emit(1) - - if type_ == 1 and self.message_types.get(type_): - self.text(f, message) - elif type_ == 3 and self.message_types.get(type_): - self.image(f, message) - elif type_ == 34 and self.message_types.get(type_): - self.audio(f, message) - elif type_ == 43 and self.message_types.get(type_): - self.video(f, message) - elif type_ == 47 and self.message_types.get(type_): - self.emoji(f, message) - elif type_ == 10000 and self.message_types.get(type_): - self.system_msg(f, message) - elif type_ == 49 and sub_type == 57 and self.message_types.get(1): - self.refermsg(f, message) - elif type_ == 49 and sub_type == 6 and self.message_types.get(4906): - self.file(f, message) - elif type_ == 49 and sub_type == 3 and self.message_types.get(4903): - self.music_share(f, message) - elif type_ == 49 and sub_type == 5 and self.message_types.get(4905): - self.share_card(f, message) - elif type_ == 49 and sub_type == 2000 and self.message_types.get(492000): - self.transfer(f, message) - elif type_ == 50 and self.message_types.get(50): - self.call(f, message) - if index % 2000 == 0: - print(f"【导出 HTML {self.contact.remark}】{index}/{len(messages)}") - f.write(html_end) - f.close() - print(f"【完成导出 HTML {self.contact.remark}】{len(messages)}") - self.count_finish_num(1) - - def count_finish_num(self, num): - """ - 记录子线程完成个数 - @param num: - @return: - """ - self.num += 1 - print("子线程完成", self.num, "/", self.total_num) - if self.num == self.total_num: - # 所有子线程都完成之后就发送完成信号 - self.okSignal.emit(1) - - -class OutputMedia(QThread): - """ - 导出语音消息 - """ - - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact): - super().__init__() - self.contact = contact - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - messages = msg_db.get_messages_by_type(self.contact.wxid, 34) - for message in messages: - is_send = message[4] - msgSvrId = message[9] - try: - audio_path = media_msg_db.get_audio( - msgSvrId, output_path=origin_path + "/voice" - ) - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(34) - - -class OutputEmoji(QThread): - """ - 导出表情包 - """ - - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact): - super().__init__() - self.contact = contact - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - messages = msg_db.get_messages_by_type(self.contact.wxid, 47) - for message in messages: - str_content = message[7] - try: - pass - # emoji_path = get_emoji(str_content, thumb=True, output_path=origin_path + '/emoji') - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(47) - - -class OutputImage(QThread): - """ - 导出图片 - """ - - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact): - super().__init__() - self.contact = contact - self.child_thread_num = 2 - self.child_threads = [0] * (self.child_thread_num + 1) - self.num = 0 - - def count1(self, num): - self.num += 1 - print("图片导出完成一个") - if self.num == self.child_thread_num: - self.okSingal.emit(47) - print("图片导出完成") - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - messages = msg_db.get_messages_by_type(self.contact.wxid, 3) - base_path = os.path.join(OUTPUT_DIR, '聊天记录', self.contact.remark, 'image') - for message in messages: - str_content = message[7] - BytesExtra = message[10] - timestamp = message[5] - try: - image_path = hard_link_db.get_image( - str_content, BytesExtra, thumb=False - ) - if not os.path.exists(os.path.join(Me().wx_dir, image_path)): - image_thumb_path = hard_link_db.get_image( - str_content, BytesExtra, thumb=True - ) - if not os.path.exists(os.path.join(Me().wx_dir, image_thumb_path)): - continue - image_path = image_thumb_path - image_path = get_image( - image_path, base_path=base_path - ) - try: - os.utime(origin_path + image_path[1:], (timestamp, timestamp)) - except: - pass - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(47) - - -class OutputImageChild(QThread): - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact, messages): - super().__init__() - self.contact = contact - self.messages = messages - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - for message in self.messages: - str_content = message[7] - BytesExtra = message[10] - timestamp = message[5] - try: - image_path = hard_link_db.get_image( - str_content, BytesExtra, thumb=False - ) - if not os.path.exists(os.path.join(Me().wx_dir, image_path)): - image_thumb_path = hard_link_db.get_image( - str_content, BytesExtra, thumb=True - ) - if not os.path.exists(os.path.join(Me().wx_dir, image_thumb_path)): - continue - image_path = image_thumb_path - image_path = get_image( - image_path, base_path=f"/data/聊天记录/{self.contact.remark}/image" - ) - try: - os.utime(origin_path + image_path[1:], (timestamp, timestamp)) - except: - pass - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(47) - print("图片子线程完成") diff --git a/app/util/exporter/exporter_json.py b/app/util/exporter/exporter_json.py deleted file mode 100644 index 0de2510..0000000 --- a/app/util/exporter/exporter_json.py +++ /dev/null @@ -1,193 +0,0 @@ -import json -import random -import os - -from app.DataBase import msg_db -from app.person import Me -from .exporter import ExporterBase - - -def merge_content(conversions_list) -> list: - """ - 合并一组对话中连续发送的句子 - @param conversions_list: - @return: - """ - merged_data = [] - current_role = None - current_content = "" - str_time = '' - for item in conversions_list: - if 'str_time' in item: - str_time = item['str_time'] - else: - str_time = '' - if current_role is None: - current_role = item["role"] - current_content = item["content"] - elif current_role == item["role"]: - current_content += "\n" + item["content"] - else: - # merged_data.append({"role": current_role, "content": current_content, 'str_time': str_time}) - merged_data.append({"role": current_role, "content": current_content}) - current_role = item["role"] - current_content = item["content"] - str_time = item.get('str_time') - - # 处理最后一组 - if current_role is not None: - # merged_data.append({"role": current_role, "content": current_content,'str_time': str_time}) - merged_data.append({"role": current_role, "content": current_content}) - return merged_data - - -def system_prompt(): - system = { - "role": "system", - # "content": f"你是{Me().name},一个聪明、热情、善良的男大学生,后面的对话来自{self.contact.remark}(!!!注意:对方的身份十分重要,你务必记住对方的身份,因为跟不同的人对话要用不同的态度、语气),你要认真地回答他" - "content": f"你是{Me().name},一个聪明、热情、善良的人,后面的对话来自你的朋友,你要认真地回答他" - } - return system - - -def message_to_conversion(group): - conversions = [system_prompt()] - while len(group) and group[-1][4] == 0: - group.pop() - for message in group: - is_send = message[4] - if len(conversions) == 1 and is_send: - continue - if is_send: - json_msg = { - "role": "assistant", - "content": message[7] - } - else: - json_msg = { - "role": "user", - "content": message[7] - } - json_msg['str_time'] = message[8] - conversions.append(json_msg) - if len(conversions) == 1: - return [] - return merge_content(conversions) - - -class JsonExporter(ExporterBase): - def split_by_time(self, length=300): - messages = msg_db.get_messages_by_type(self.contact.wxid, type_=1, time_range=self.time_range) - start_time = 0 - res = [] - i = 0 - while i < len(messages): - message = messages[i] - timestamp = message[5] - is_send = message[4] - group = [ - system_prompt() - ] - while i < len(messages) and timestamp - start_time < length: - if is_send: - json_msg = { - "role": "assistant", - "content": message[7] - } - else: - json_msg = { - "role": "user", - "content": message[7] - } - group.append(json_msg) - i += 1 - if i >= len(messages): - break - message = messages[i] - timestamp = message[5] - is_send = message[4] - while is_send: - json_msg = { - "role": "assistant", - "content": message[7] - } - group.append(json_msg) - i += 1 - if i >= len(messages): - break - message = messages[i] - timestamp = message[5] - is_send = message[4] - start_time = timestamp - res.append( - { - "conversations": group - } - ) - res_ = [] - for item in res: - conversations = item['conversations'] - res_.append({ - 'conversations': merge_content(conversations) - }) - return res_ - - def split_by_intervals(self, max_diff_seconds=300): - messages = msg_db.get_messages_by_type(self.contact.wxid, type_=1, time_range=self.time_range) - res = [] - i = 0 - current_group = [] - while i < len(messages): - message = messages[i] - timestamp = message[5] - is_send = message[4] - while is_send and i + 1 < len(messages): - i += 1 - message = messages[i] - is_send = message[4] - current_group = [messages[i]] - i += 1 - while i < len(messages) and messages[i][5] - current_group[-1][5] <= max_diff_seconds: - current_group.append(messages[i]) - i += 1 - while i < len(messages) and messages[i][4]: - current_group.append(messages[i]) - i += 1 - res.append(current_group) - res_ = [] - for group in res: - conversations = message_to_conversion(group) - if conversations: - res_.append({ - 'conversations': conversations - }) - return res_ - - def to_json(self): - print(f"【开始导出 json {self.contact.remark}】") - origin_path = self.origin_path - os.makedirs(origin_path, exist_ok=True) - filename = os.path.join(origin_path, f"{self.contact.remark}") - - # res = self.split_by_time() - res = self.split_by_intervals(60) - # 打乱列表顺序 - random.shuffle(res) - - # 计算切分比例 - split_ratio = 0.2 # 20% for the second list - - # 计算切分点 - split_point = int(len(res) * split_ratio) - - # 分割列表 - train_data = res[split_point:] - dev_data = res[:split_point] - with open(f'{filename}_train.json', "w", encoding="utf-8") as f: - json.dump(train_data, f, ensure_ascii=False, indent=4) - with open(f'{filename}_dev.json', "w", encoding="utf-8") as f: - json.dump(dev_data, f, ensure_ascii=False, indent=4) - self.okSignal.emit(1) - - def run(self): - self.to_json() diff --git a/app/util/exporter/exporter_txt.py b/app/util/exporter/exporter_txt.py deleted file mode 100644 index 9e37105..0000000 --- a/app/util/exporter/exporter_txt.py +++ /dev/null @@ -1,146 +0,0 @@ -import os - -from app.DataBase import msg_db -from app.util.exporter.exporter import ExporterBase -from app.config import OUTPUT_DIR -from app.util.compress_content import parser_reply, share_card - - -class TxtExporter(ExporterBase): - def text(self, doc, message): - str_content = message[7] - str_time = message[8] - is_send = message[4] - display_name = self.get_display_name(is_send, message) - name = display_name - doc.write( - f'''{str_time} {name}\n{str_content}\n\n''' - ) - - def image(self, doc, message): - str_time = message[8] - is_send = message[4] - display_name = self.get_display_name(is_send, message) - doc.write( - f'''{str_time} {display_name}\n[图片]\n\n''' - ) - - def audio(self, doc, message): - str_time = message[8] - is_send = message[4] - display_name = self.get_display_name(is_send, message) - doc.write( - f'''{str_time} {display_name}\n[语音]\n\n''' - ) - def emoji(self, doc, message): - str_time = message[8] - is_send = message[4] - display_name = self.get_display_name(is_send, message) - doc.write( - f'''{str_time} {display_name}\n[表情包]\n\n''' - ) - - def file(self, doc, message): - str_time = message[8] - is_send = message[4] - display_name = self.get_display_name(is_send, message) - doc.write( - f'''{str_time} {display_name}\n[文件]\n\n''' - ) - - def refermsg(self, doc, message): - """ - 处理回复消息 - @param doc: - @param message: - @return: - """ - str_time = message[8] - is_send = message[4] - content = parser_reply(message[11]) - refer_msg = content.get('refer') - display_name = self.get_display_name(is_send, message) - if refer_msg: - doc.write( - f'''{str_time} {display_name}\n{content.get('title')}\n引用:{refer_msg.get('displayname')}:{refer_msg.get('content')}\n\n''' - ) - else: - doc.write( - f'''{str_time} {display_name}\n{content.get('title')}\n引用:未知\n\n''' - ) - - def system_msg(self, doc, message): - str_content = message[7] - str_time = message[8] - str_content = str_content.replace('重新编辑]]>', "") - doc.write( - f'''{str_time} {str_content}\n\n''' - ) - - def video(self, doc, message): - str_time = message[8] - is_send = message[4] - display_name = self.get_display_name(is_send, message) - doc.write( - f'''{str_time} {display_name}\n[视频]\n\n''' - ) - def music_share(self, doc, message): - is_send = message[4] - str_time = message[8] - display_name = self.get_display_name(is_send, message) - doc.write( - f'''{str_time} {display_name}\n[音乐分享]\n\n''' - ) - - def share_card(self, doc, message): - is_send = message[4] - bytesExtra = message[10] - compress_content_ = message[11] - str_time = message[8] - card_data = share_card(bytesExtra, compress_content_) - display_name = self.get_display_name(is_send, message) - doc.write( - f'''{str_time} {display_name} - [链接]:title:{card_data.get('title')} - description:{card_data.get('description')} - url:{card_data.get('url')} - name:{card_data.get('app_name')} - \n\n''' - ) - - def export(self): - # 实现导出为txt的逻辑 - print(f"【开始导出 TXT {self.contact.remark}】") - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - os.makedirs(origin_path, exist_ok=True) - filename = os.path.join(origin_path, self.contact.remark+'.txt') - messages = msg_db.get_messages(self.contact.wxid, time_range=self.time_range) - total_steps = len(messages) - with open(filename, mode='w', newline='', encoding='utf-8') as f: - for index, message in enumerate(messages): - type_ = message[2] - sub_type = message[3] - self.progressSignal.emit(int((index + 1) / total_steps * 100)) - if type_ == 1 and self.message_types.get(type_): - self.text(f, message) - elif type_ == 3 and self.message_types.get(type_): - self.image(f, message) - elif type_ == 34 and self.message_types.get(type_): - self.audio(f, message) - elif type_ == 43 and self.message_types.get(type_): - self.video(f, message) - elif type_ == 47 and self.message_types.get(type_): - self.emoji(f, message) - elif type_ == 10000 and self.message_types.get(type_): - self.system_msg(f, message) - elif type_ == 49 and sub_type == 57 and self.message_types.get(1): - self.refermsg(f, message) - elif type_ == 49 and sub_type == 6 and self.message_types.get(4906): - self.file(f, message) - elif type_ == 49 and sub_type == 3 and self.message_types.get(4903): - self.music_share(f, message) - elif type_ == 49 and sub_type == 5 and self.message_types.get(4905): - self.share_card(f, message) - print(f"【完成导出 TXT {self.contact.remark}】") - self.okSignal.emit(1) \ No newline at end of file diff --git a/app/util/exporter/output.py b/app/util/exporter/output.py deleted file mode 100644 index 963df8d..0000000 --- a/app/util/exporter/output.py +++ /dev/null @@ -1,466 +0,0 @@ -import csv -import os -import time -import traceback -from typing import List - -import docx -from PyQt5.QtCore import pyqtSignal, QThread -from PyQt5.QtWidgets import QFileDialog -from docx.oxml.ns import qn -from docxcompose.composer import Composer - -from app.util.exporter.exporter_ai_txt import AiTxtExporter -from app.util.exporter.exporter_csv import CSVExporter -from app.util.exporter.exporter_docx import DocxExporter -from app.util.exporter.exporter_html import HtmlExporter -from app.util.exporter.exporter_json import JsonExporter -from app.util.exporter.exporter_txt import TxtExporter -from app.DataBase.hard_link import decodeExtraBuf -from app.config import OUTPUT_DIR -from app.DataBase.package_msg import PackageMsg -from app.DataBase import media_msg_db, hard_link_db, micro_msg_db, msg_db -from app.log import logger -from app.person import Me -from app.util.image import get_image - -os.makedirs(os.path.join(OUTPUT_DIR, '聊天记录'), exist_ok=True) - - -class Output(QThread): - """ - 发送信息线程 - """ - startSignal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - rangeSignal = pyqtSignal(int) - okSignal = pyqtSignal(int) - batchOkSignal = pyqtSignal(int) - nowContact = pyqtSignal(str) - i = 1 - CSV = 0 - DOCX = 1 - HTML = 2 - CSV_ALL = 3 - CONTACT_CSV = 4 - TXT = 5 - JSON = 6 - AI_TXT = 7 - Batch = 10086 - - def __init__(self, contact, type_=DOCX, message_types={}, sub_type=[], time_range=None, parent=None): - super().__init__(parent) - self.children = [] - self.last_timestamp = 0 - self.sub_type = sub_type - self.time_range = time_range - self.message_types = message_types - self.sec = 2 # 默认1000秒 - self.contact = contact - self.msg_id = 0 - self.output_type: int | List[int] = type_ - self.total_num = 1 - self.num = 0 - - def progress(self, value): - self.progressSignal.emit(value) - - def output_image(self): - """ - 导出全部图片 - @return: - """ - return - - def output_emoji(self): - """ - 导出全部表情包 - @return: - """ - return - - def to_csv_all(self): - """ - 导出全部聊天记录到CSV - @return: - """ - - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录') - os.makedirs(origin_path, exist_ok=True) - filename = QFileDialog.getSaveFileName(None, "save file", os.path.join(os.getcwd(), 'messages.csv'), - "csv files (*.csv);;all files(*.*)") - if not filename[0]: - return - self.startSignal.emit(1) - filename = filename[0] - # columns = ["用户名", "消息内容", "发送时间", "发送状态", "消息类型", "isSend", "msgId"] - columns = ['localId', 'TalkerId', 'Type', 'SubType', - 'IsSender', 'CreateTime', 'Status', 'StrContent', - 'StrTime', 'Remark', 'NickName', 'Sender'] - - packagemsg = PackageMsg() - messages = packagemsg.get_package_message_all() - # 写入CSV文件 - with open(filename, mode='w', newline='', encoding='utf-8-sig') as file: - writer = csv.writer(file) - writer.writerow(columns) - # 写入数据 - writer.writerows(messages) - self.okSignal.emit(1) - - def contact_to_csv(self): - """ - 导出联系人到CSV - @return: - """ - filename = QFileDialog.getSaveFileName(None, "save file", os.path.join(os.getcwd(), 'contacts.csv'), - "csv files (*.csv);;all files(*.*)") - if not filename[0]: - return - self.startSignal.emit(1) - filename = filename[0] - # columns = ["用户名", "消息内容", "发送时间", "发送状态", "消息类型", "isSend", "msgId"] - columns = ['UserName', 'Alias', 'Type', 'Remark', 'NickName', 'PYInitial', 'RemarkPYInitial', 'smallHeadImgUrl', - 'bigHeadImgUrl', 'label', 'gender', 'telephone', 'signature', 'country/region', 'province', 'city'] - contacts = micro_msg_db.get_contact() - # 写入CSV文件 - with open(filename, mode='w', newline='', encoding='utf-8-sig') as file: - writer = csv.writer(file) - writer.writerow(columns) - # 写入数据 - # writer.writerows(contacts) - for contact in contacts: - detail = decodeExtraBuf(contact[9]) - gender_code = detail.get('gender') - if gender_code == 0: - gender = '未知' - elif gender_code == 1: - gender = '男' - else: - gender = '女' - writer.writerow([*contact[:9], contact[10], gender, detail.get('telephone'), detail.get('signature'), - *detail.get('region')]) - - self.okSignal.emit(1) - - def batch_export(self): - print('开始批量导出') - print(self.sub_type, self.message_types) - print(len(self.contact)) - print([contact.remark for contact in self.contact]) - self.batch_num_total = len(self.contact) * len(self.sub_type) - self.batch_num = 0 - self.rangeSignal.emit(self.batch_num_total) - for contact in self.contact: - # print('联系人', contact.remark) - for type_ in self.sub_type: - # print('导出类型', type_) - if type_ == self.DOCX: - self.to_docx(contact, self.message_types, True) - elif type_ == self.TXT: - # print('批量导出txt') - self.to_txt(contact, self.message_types, True) - elif type_ == self.AI_TXT: - # print('批量导出txt') - self.to_ai_txt(contact, self.message_types, True) - elif type_ == self.CSV: - self.to_csv(contact, self.message_types, True) - elif type_ == self.HTML: - self.to_html(contact, self.message_types, True) - elif type_ == self.JSON: - self.to_json(contact,self.message_types,True) - - def batch_finish_one(self, num): - self.nowContact.emit(self.contact[self.batch_num // len(self.sub_type)].remark) - self.batch_num += 1 - if self.batch_num == self.batch_num_total: - self.okSignal.emit(1) - - def merge_docx(self, n): - conRemark = self.contact.remark - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', conRemark) - filename = f"{origin_path}/{conRemark}_{n}.docx" - if n == 10086: - # self.document.append(self.document) - file = os.path.join(origin_path, f'{conRemark}.docx') - try: - self.document.save(file) - except PermissionError: - file = file[:-5] + f'{time.time()}' + '.docx' - self.document.save(file) - self.okSignal.emit(1) - return - doc = docx.Document(filename) - self.document.append(doc) - os.remove(filename) - if n % 50 == 0: - # self.document.append(self.document) - file = os.path.join(origin_path, f'{conRemark}-{n // 50}.docx') - try: - self.document.save(file) - except PermissionError: - file = file[:-5] + f'{time.time()}' + '.docx' - self.document.save(file) - doc = docx.Document() - doc.styles["Normal"].font.name = "Cambria" - doc.styles["Normal"]._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") - self.document = Composer(doc) - - def to_docx(self, contact, message_types, is_batch=False): - doc = docx.Document() - doc.styles["Normal"].font.name = "Cambria" - doc.styles["Normal"]._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") - self.document = Composer(doc) - Child = DocxExporter(contact, type_=self.DOCX, message_types=message_types, time_range=self.time_range) - self.children.append(Child) - Child.progressSignal.connect(self.progress) - if not is_batch: - Child.rangeSignal.connect(self.rangeSignal) - Child.okSignal.connect(self.merge_docx if not is_batch else self.batch_finish_one) - Child.start() - - def to_json(self, contact, message_types, is_batch=False): - Child = JsonExporter(contact, type_=self.JSON, message_types=message_types, time_range=self.time_range) - self.children.append(Child) - Child.progressSignal.connect(self.progress) - if not is_batch: - Child.rangeSignal.connect(self.rangeSignal) - Child.okSignal.connect(self.okSignal if not is_batch else self.batch_finish_one) - Child.start() - - def to_txt(self, contact, message_types, is_batch=False): - Child = TxtExporter(contact, type_=self.TXT, message_types=message_types, time_range=self.time_range) - self.children.append(Child) - Child.progressSignal.connect(self.progress) - if not is_batch: - Child.rangeSignal.connect(self.rangeSignal) - Child.okSignal.connect(self.okSignal if not is_batch else self.batch_finish_one) - Child.start() - - def to_ai_txt(self, contact, message_types, is_batch=False): - Child = AiTxtExporter(contact, type_=self.TXT, message_types=message_types, time_range=self.time_range) - self.children.append(Child) - Child.progressSignal.connect(self.progress) - if not is_batch: - Child.rangeSignal.connect(self.rangeSignal) - Child.okSignal.connect(self.okSignal if not is_batch else self.batch_finish_one) - Child.start() - - def to_html(self, contact, message_types, is_batch=False): - Child = HtmlExporter(contact, type_=self.output_type, message_types=message_types, time_range=self.time_range) - self.children.append(Child) - Child.progressSignal.connect(self.progress) - if not is_batch: - Child.rangeSignal.connect(self.rangeSignal) - Child.okSignal.connect(self.count_finish_num) - Child.start() - self.total_num = 1 - if message_types.get(34): - # 语音消息单独的线程 - self.total_num += 1 - output_media = OutputMedia(contact, time_range=self.time_range) - self.children.append(output_media) - output_media.okSingal.connect(self.count_finish_num) - output_media.progressSignal.connect(self.progressSignal) - output_media.start() - if message_types.get(47): - # emoji消息单独的线程 - self.total_num += 1 - output_emoji = OutputEmoji(contact, time_range=self.time_range) - self.children.append(output_emoji) - output_emoji.okSingal.connect(self.count_finish_num) - output_emoji.progressSignal.connect(self.progressSignal) - output_emoji.start() - if message_types.get(3): - # 图片消息单独的线程 - self.total_num += 1 - output_image = OutputImage(contact, time_range=self.time_range) - self.children.append(output_image) - output_image.okSingal.connect(self.count_finish_num) - output_image.progressSignal.connect(self.progressSignal) - output_image.start() - - def to_csv(self, contact, message_types, is_batch=False): - Child = CSVExporter(contact, type_=self.CSV, message_types=message_types, time_range=self.time_range) - self.children.append(Child) - Child.progressSignal.connect(self.progress) - if not is_batch: - Child.rangeSignal.connect(self.rangeSignal) - Child.okSignal.connect(self.okSignal if not is_batch else self.batch_finish_one) - Child.start() - - def run(self): - if self.output_type == self.DOCX: - self.to_docx(self.contact, self.message_types) - elif self.output_type == self.CSV_ALL: - self.to_csv_all() - elif self.output_type == self.CONTACT_CSV: - self.contact_to_csv() - elif self.output_type == self.TXT: - self.to_txt(self.contact, self.message_types) - elif self.output_type == self.AI_TXT: - self.to_ai_txt(self.contact, self.message_types) - elif self.output_type == self.CSV: - self.to_csv(self.contact, self.message_types) - elif self.output_type == self.HTML: - self.to_html(self.contact, self.message_types) - elif self.output_type == self.JSON: - self.to_json(self.contact, self.message_types) - elif self.output_type == self.Batch: - self.batch_export() - - def count_finish_num(self, num): - """ - 记录子线程完成个数 - @param num: - @return: - """ - self.num += 1 - if self.num == self.total_num: - # 所有子线程都完成之后就发送完成信号 - if self.output_type == self.Batch: - self.batch_finish_one(1) - else: - self.okSignal.emit(1) - self.num = 0 - - def cancel(self): - self.requestInterruption() - - -class OutputMedia(QThread): - """ - 导出语音消息 - """ - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact, time_range=None): - super().__init__() - self.contact = contact - self.time_range = time_range - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - messages = msg_db.get_messages_by_type(self.contact.wxid, 34, time_range=self.time_range) - for message in messages: - is_send = message[4] - msgSvrId = message[9] - try: - audio_path = media_msg_db.get_audio(msgSvrId, output_path=origin_path + "/voice") - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(34) - - -class OutputEmoji(QThread): - """ - 导出表情包 - """ - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact, time_range=None): - super().__init__() - self.contact = contact - self.time_range = time_range - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - messages = msg_db.get_messages_by_type(self.contact.wxid, 47, time_range=self.time_range) - for message in messages: - str_content = message[7] - try: - pass - # emoji_path = get_emoji(str_content, thumb=True, output_path=origin_path + '/emoji') - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(47) - - -class OutputImage(QThread): - """ - 导出图片 - """ - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact, time_range): - super().__init__() - self.contact = contact - self.child_thread_num = 2 - self.time_range = time_range - self.child_threads = [0] * (self.child_thread_num + 1) - self.num = 0 - - def count1(self, num): - self.num += 1 - print('图片导出完成一个') - if self.num == self.child_thread_num: - self.okSingal.emit(47) - print('图片导出完成') - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - messages = msg_db.get_messages_by_type(self.contact.wxid, 3, time_range=self.time_range) - base_path = os.path.join(OUTPUT_DIR, '聊天记录', self.contact.remark, 'image') - for message in messages: - str_content = message[7] - BytesExtra = message[10] - timestamp = message[5] - try: - image_path = hard_link_db.get_image(str_content, BytesExtra, up_dir=Me().wx_dir, thumb=False) - image_path = get_image(image_path, base_path=base_path) - try: - os.utime(origin_path + image_path[1:], (timestamp, timestamp)) - except: - pass - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(47) - - -class OutputImageChild(QThread): - okSingal = pyqtSignal(int) - progressSignal = pyqtSignal(int) - - def __init__(self, contact, messages, time_range): - super().__init__() - self.contact = contact - self.messages = messages - self.time_range = time_range - - def run(self): - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - for message in self.messages: - str_content = message[7] - BytesExtra = message[10] - timestamp = message[5] - try: - image_path = hard_link_db.get_image(str_content, BytesExtra, thumb=False) - if not os.path.exists(os.path.join(Me().wx_dir, image_path)): - image_thumb_path = hard_link_db.get_image(str_content, BytesExtra, thumb=True) - if not os.path.exists(os.path.join(Me().wx_dir, image_thumb_path)): - continue - image_path = image_thumb_path - image_path = get_image(image_path, base_path=f'/data/聊天记录/{self.contact.remark}/image') - try: - os.utime(origin_path + image_path[1:], (timestamp, timestamp)) - except: - pass - except: - logger.error(traceback.format_exc()) - finally: - self.progressSignal.emit(1) - self.okSingal.emit(47) - print('图片子线程完成') - - -if __name__ == "__main__": - pass diff --git a/app/util/file.py b/app/util/file.py deleted file mode 100644 index 8c8562f..0000000 --- a/app/util/file.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import traceback -import shutil - -import requests - -from app.log import log, logger -from app.util.protocbuf.msg_pb2 import MessageBytesExtra -from ..person import Me - -root_path = './data/files/' -if not os.path.exists('./data'): - os.mkdir('./data') -if not os.path.exists(root_path): - os.mkdir(root_path) - - -class File: - def __init__(self): - self.open_flag = False - - -def get_file(bytes_extra, file_name, output_path=root_path) -> str: - try: - msg_bytes = MessageBytesExtra() - msg_bytes.ParseFromString(bytes_extra) - file_path = '' - real_path = '' - if len(msg_bytes.message2) > 0: - for filed in msg_bytes.message2: - if filed.field1 == 4: - file_original_path = filed.field2 - file_path = os.path.join(output_path, file_name) - if os.path.exists(file_path): - # print('文件' + file_path + '已存在') - return file_path - if os.path.isabs(file_original_path): # 绝对路径可能迁移过文件目录,也可能存在其他位置 - if os.path.exists(file_original_path): - real_path = file_original_path - else: # 如果没找到再判断一次是否是迁移了目录 - if file_original_path.find(r"FileStorage") != -1: - real_path = Me().wx_dir + file_original_path[ - file_original_path.find("FileStorage") - 1:] - else: - if file_original_path.find(Me().wxid) != -1: - real_path = Me().wx_dir + file_original_path.replace(Me().wxid, '') - else: - real_path = Me().wx_dir + file_original_path - if real_path != "": - if os.path.exists(real_path): - print('开始获取文件' + real_path) - shutil.copy2(real_path, file_path) - else: - print('文件' + file_original_path + '已丢失') - file_path = '' - return file_path - except: - logger.error(traceback.format_exc()) - return "" diff --git a/app/util/image.py b/app/util/image.py deleted file mode 100644 index e8915cf..0000000 --- a/app/util/image.py +++ /dev/null @@ -1,135 +0,0 @@ -import os -import traceback - -from app.log import logger -from app.person import Me - -# 图片字节头信息, -# [0][1]为jpg头信息, -# [2][3]为png头信息, -# [4][5]为gif头信息 -pic_head = [0xff, 0xd8, 0x89, 0x50, 0x47, 0x49] -# 解密码 -decode_code = 0 - - -def get_code(dat_read) -> tuple[int, int]: - """ - 自动判断文件类型,并获取dat文件解密码 - :param file_path: dat文件路径 - :return: 如果文件为jpg/png/gif格式,则返回解密码,否则返回-1 - """ - try: - if not dat_read: - return -1, -1 - head_index = 0 - while head_index < len(pic_head): - # 使用第一个头信息字节来计算加密码 - # 第二个字节来验证解密码是否正确 - code = dat_read[0] ^ pic_head[head_index] - idf_code = dat_read[1] ^ code - head_index = head_index + 1 - if idf_code == pic_head[head_index]: - return head_index, code - head_index = head_index + 1 - print("not jpg, png, gif") - return -1, -1 - except: - logger.error(f'image解析发生了错误:\n\n{traceback.format_exc()}') - return -1, -1 - - -def decode_dat(file_path, out_path) -> str: - """ - 解密文件,并生成图片 - :param file_path: dat文件路径 - :return: 无 - """ - if not os.path.exists(file_path): - return None - with open(file_path, 'rb') as file_in: - data = file_in.read() - - file_type, decode_code = get_code(data[:2]) - if decode_code == -1: - return '' - - filename = os.path.basename(file_path) - if file_type == 1: - pic_name = os.path.basename(file_path)[:-4] + ".jpg" - elif file_type == 3: - pic_name = filename[:-4] + ".png" - elif file_type == 5: - pic_name = filename[:-4] + ".gif" - else: - pic_name = filename[:-4] + ".jpg" - file_outpath = os.path.join(out_path, pic_name) - if os.path.exists(file_outpath): - return file_outpath - - # 对数据进行异或加密/解密 - with open(file_outpath, 'wb') as file_out: - file_out.write(bytes([byte ^ decode_code for byte in data])) - print(file_path, '->', file_outpath) - return file_outpath - - -def decode_dat_path(file_path, out_path) -> str: - """ - 解密文件,并生成图片 - :param file_path: dat文件路径 - :return: 无 - """ - if not os.path.exists(file_path): - return '' - with open(file_path, 'rb') as file_in: - data = file_in.read(2) - file_type, decode_code = get_code(data) - if decode_code == -1: - return '' - filename = os.path.basename(file_path) - if file_type == 1: - pic_name = os.path.basename(file_path)[:-4] + ".jpg" - elif file_type == 3: - pic_name = filename[:-4] + ".png" - elif file_type == 5: - pic_name = filename[:-4] + ".gif" - else: - pic_name = filename[:-4] + ".jpg" - file_outpath = os.path.join(out_path, pic_name) - return file_outpath - - -def get_image(path, base_path) -> str: - if path: - base_path = os.path.join(os.getcwd(),base_path) - output_path = decode_dat(os.path.join(Me().wx_dir, path), base_path) - relative_path = './image/' + os.path.basename( - output_path) if output_path else 'https://www.bing.com/images/search?view=detailV2&ccid=Zww6woP3&id=CCC91337C740656E800E51247E928ACD3052FECF&thid=OIP.Zww6woP3Em49TdSG_lnggAHaEK&mediaurl=https%3a%2f%2fmeekcitizen.files.wordpress.com%2f2018%2f09%2f404.jpg%3fw%3d656&exph=360&expw=640&q=404&simid=608040792714530493&FORM=IRPRST&ck=151E7337A86F1B9C5C5DB08B15B90809&selectedIndex=21&itb=0' - return relative_path - else: - return ':/icons/icons/404.png' - - -def get_image_abs_path(path, base_path) -> str: - if path: - base_path = os.path.join(os.getcwd(),base_path) - output_path = decode_dat(os.path.join(Me().wx_dir, path), base_path) - return output_path - else: - return ':/icons/icons/404.png' - - -def get_image_path(path, base_path) -> str: - if path: - base_path = os.getcwd() + base_path - output_path = decode_dat_path(os.path.join(Me().wx_dir, path), base_path) - relative_path = './image/' + os.path.basename( - output_path) if output_path else 'https://www.bing.com/images/search?view=detailV2&ccid=Zww6woP3&id=CCC91337C740656E800E51247E928ACD3052FECF&thid=OIP.Zww6woP3Em49TdSG_lnggAHaEK&mediaurl=https%3a%2f%2fmeekcitizen.files.wordpress.com%2f2018%2f09%2f404.jpg%3fw%3d656&exph=360&expw=640&q=404&simid=608040792714530493&FORM=IRPRST&ck=151E7337A86F1B9C5C5DB08B15B90809&selectedIndex=21&itb=0' - return relative_path - else: - return ':/icons/icons/404.png' - - -if __name__ == "__main__": - pass diff --git a/app/util/music.py b/app/util/music.py deleted file mode 100644 index 6ad9296..0000000 --- a/app/util/music.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -import traceback -import shutil - -from app.log import log, logger -from app.util.protocbuf.msg_pb2 import MessageBytesExtra -import requests -from urllib.parse import urlparse, parse_qs -import re - -root_path = './data/music/' -if not os.path.exists('./data'): - os.mkdir('./data') -if not os.path.exists(root_path): - os.mkdir(root_path) - - -class File: - def __init__(self): - self.open_flag = False - - -def get_music_path(url, file_title, output_path=root_path) -> str: - try: - parsed_url = urlparse(url) - if '.' in parsed_url.path: - # 获取扩展名 - file_extension = parsed_url.path.split('.')[-1] - - pattern = r'[\\/:*?"<>|\r\n]+' - file_title = re.sub(pattern, "_", file_title) - file_name = file_title + '.' + file_extension - music_path = os.path.join(output_path, file_name) - if os.path.exists(music_path): - # print('文件' + music_path + '已存在') - return music_path - header = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.40 Safari/537.36 Edg/87.0.664.24' - } - requests.packages.urllib3.disable_warnings() - response = requests.get(url,headers=header,verify=False) - if response.status_code == 200: - with open(music_path, 'wb') as f: - f.write(response.content) - else: - music_path = '' - print("音乐" + file_name + "获取失败:请求地址:" + url) - else: - music_path = '' - print('音乐文件已失效,url:' + url) - return music_path - except Exception as e: - print(f"Get Music Path Error: {e}") - logger.error(traceback.format_exc()) - return "" diff --git a/app/util/path.py b/app/util/path.py deleted file mode 100644 index 54b4da7..0000000 --- a/app/util/path.py +++ /dev/null @@ -1,81 +0,0 @@ -import os -import winreg - -from app.person import Me -from app.util import image - -os.makedirs('./data/image', exist_ok=True) - - -def get_abs_path(path, base_path="/data/image"): - # return os.path.join(os.getcwd(), 'app/data/icons/404.png') - if path: - base_path = os.getcwd() + base_path - output_path = image.decode_dat(os.path.join(Me().wx_dir, path), base_path) - return output_path if output_path else ':/icons/icons/404.png' - else: - return ':/icons/icons/404.png' - - -def get_relative_path(path, base_path, type_='image'): - if path: - base_path = os.getcwd() + base_path - output_path = image.decode_dat(os.path.join(Me().wx_dir, path), base_path) - relative_path = './image/' + os.path.basename( - output_path) if output_path else 'https://www.bing.com/images/search?view=detailV2&ccid=Zww6woP3&id=CCC91337C740656E800E51247E928ACD3052FECF&thid=OIP.Zww6woP3Em49TdSG_lnggAHaEK&mediaurl=https%3a%2f%2fmeekcitizen.files.wordpress.com%2f2018%2f09%2f404.jpg%3fw%3d656&exph=360&expw=640&q=404&simid=608040792714530493&FORM=IRPRST&ck=151E7337A86F1B9C5C5DB08B15B90809&selectedIndex=21&itb=0' - return relative_path - else: - return ':/icons/icons/404.png' - - -def mkdir(path): - if not os.path.exists(path): - os.mkdir(path) - - -def wx_path(): - try: - is_w_dir = False - - try: - key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ) - value, _ = winreg.QueryValueEx(key, "FileSavePath") - winreg.CloseKey(key) - w_dir = value - is_w_dir = True - except Exception as e: - w_dir = "MyDocument:" - - if not is_w_dir: - try: - user_profile = os.environ.get("USERPROFILE") - path_3ebffe94 = os.path.join(user_profile, "AppData", "Roaming", "Tencent", "WeChat", "All Users", - "config", - "3ebffe94.ini") - with open(path_3ebffe94, "r", encoding="utf-8") as f: - w_dir = f.read() - is_w_dir = True - except Exception as e: - w_dir = "MyDocument:" - - if w_dir == "MyDocument:": - try: - # 打开注册表路径 - key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders") - documents_path = winreg.QueryValueEx(key, "Personal")[0] # 读取文档实际目录路径 - winreg.CloseKey(key) # 关闭注册表 - documents_paths = os.path.split(documents_path) - if "%" in documents_paths[0]: - w_dir = os.environ.get(documents_paths[0].replace("%", "")) - w_dir = os.path.join(w_dir, os.path.join(*documents_paths[1:])) - # print(1, w_dir) - else: - w_dir = documents_path - except Exception as e: - profile = os.environ.get("USERPROFILE") - w_dir = os.path.join(profile, "Documents") - msg_dir = os.path.join(w_dir, "WeChat Files") - return msg_dir - except FileNotFoundError: - return '.' diff --git a/app/util/protocbuf/__init__.py b/app/util/protocbuf/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/util/search.py b/app/util/search.py deleted file mode 100644 index 59e8a00..0000000 --- a/app/util/search.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import List - -from fuzzywuzzy import process - - -def search_by_content(key, choices: List[List]): - result = [] - for i, choice in enumerate(choices): - res = process.extractOne(key, choice) - result.append((res, i)) - result.sort(key=lambda x: x[0][1], reverse=True) - k = result[0][1] - item = result[0][0][0] - return choices[k].index(item) diff --git a/app/web_ui/__init__.py b/app/web_ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/web_ui/templates/charts.html b/app/web_ui/templates/charts.html deleted file mode 100644 index dc2a2cf..0000000 --- a/app/web_ui/templates/charts.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - -我们第一次聊天在
-{{first_time}}
-距今已有
-
-
-
-
-
-
-
-
- 我们第一次聊天在
-{{first_time}}
-距今已有
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 未眠人
-{{sub_title}}
-| - - | -
-
-
- - {{nickname}} - |
-
- 我们第一次聊天发生在
-- {{first_time}}
-可掌控的才真正属于你
- -你一共给{{contact_num}}个联系人
发送了{{send_msg_num}}条消息
收到了{{receive_msg_num}}条消息
总计{{total_text_num}}字
男{{man_contact_num}}人 女{{woman_contact_num}}人
- - - - - -{{contact.remark}}
-
- Copyrights © 2022-2024 SiYuan 版权所有. Inc.
- -
- 出错了
", f"{self.contact.remark}
") + # avatar_urls, avatar_paths = self.get_avatar_urls() + avatar_urls = [] + avatar_paths = [] + html_head = html_head.replace("{{avatarPaths}}", json.dumps(avatar_paths)) + html_head = html_head.replace("{{avatarUrls}}", json.dumps(avatar_urls)).replace('{{wxid}}', + f'"{self.contact.wxid}"') + f.write(html_head) + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + + # QMe().save_avatar(self.origin_path + '/avatar/' + Me().wxid + '.png') + # self.contact.save_avatar(self.origin_path + '/avatar/' + self.contact.wxid + '.png') + date_id_map = {} + timelineData = {} + PageTimeline = {} + server_id_Page = {} + server_id_Idx = {} + + AllIndex = [] + ImageIndex = [] + FileIndex = [] + LinkIndex = [] + MusicIndex = [] + TransferIndex = [] + MiniProgramIndex = [] + VideoNumberIndex = [] + dateDataMap = {} + i = 0 + itemsPerPage = 100 + num = 1 + html_json = [] + image_tasks = [] + video_tasks = [] + file_tasks = [] + audio_tasks = [] + image_dir = os.path.join(self.origin_path, 'image') + video_dir = os.path.join(self.origin_path, 'video') + audio_dir = os.path.join(self.origin_path, 'voice') + file_dir = os.path.join(self.origin_path, 'file') + total_steps = len(messages) + select_msg_cnt = 0 # 要导出的消息数量 + msg_index = 0 + + def parser_merged(merged_message): + for msg in merged_message.messages: + type_ = msg.type + if type_ == MessageType.Image: + msg.set_file_name() + image_tasks.append( + ( + os.path.join(Me().wx_dir, msg.path), + os.path.join(image_dir, msg.str_time[:7]), + msg.file_name + ) + ) + image_tasks.append( + ( + os.path.join(Me().wx_dir, msg.thumb_path), + os.path.join(image_dir, msg.str_time[:7]), + msg.file_name + '_t' + ) + ) + msg.path = f"./image/{msg.str_time[:7]}/{msg.file_name}" + msg.thumb_path = f"./image/{msg.str_time[:7]}/{msg.file_name + '_t'}" + elif type_ == MessageType.File: + origin_file_path = os.path.join(Me().wx_dir, msg.path) + file_tasks.append( + ( + origin_file_path, + os.path.join(file_dir, msg.str_time[:7]), + '' + ) + ) + msg.path = f'./file/{msg.str_time[:7]}/{os.path.basename(origin_file_path)}' + elif type_ == MessageType.Video: + msg.set_file_name() + video_tasks.append( + ( + os.path.join(Me().wx_dir, msg.path), + os.path.join(video_dir, msg.str_time[:7]), + msg.file_name + ) + ) + ext = os.path.basename(msg.path).split('.')[-1] + msg.path = f'./video/{msg.str_time[:7]}/{msg.file_name}.{ext}' + elif type_ == MessageType.MergedMessages: + parser_merged(msg) + + for index, message in enumerate(messages): + if not self._is_running: + break + if index and index % 1000 == 0: + self.update_progress_callback(index / total_steps) + type_ = message.type + if not self.is_selected(message): + continue + server_id = message.server_id + if type_ == MessageType.Image: + ImageIndex.append(msg_index) + message.set_file_name() + image_tasks.append( + ( + os.path.join(Me().wx_dir, message.path), + os.path.join(image_dir, message.str_time[:7]), + message.file_name + ) + ) + image_tasks.append( + ( + os.path.join(Me().wx_dir, message.thumb_path), + os.path.join(image_dir, message.str_time[:7]), + message.file_name + '_t' + ) + ) + message.path = f"./image/{message.str_time[:7]}/{message.file_name}" + message.thumb_path = f"./image/{message.str_time[:7]}/{message.file_name + '_t'}" + elif type_ == MessageType.File: + FileIndex.append(msg_index) + origin_file_path = os.path.join(Me().wx_dir, message.path) + file_tasks.append( + ( + origin_file_path, + os.path.join(file_dir, message.str_time[:7]), + '' + ) + ) + if os.path.isfile(origin_file_path): + message.path = f'./file/{message.str_time[:7]}/{os.path.basename(origin_file_path)}' + elif type_ == MessageType.Video: + ImageIndex.append(msg_index) + message.set_file_name() + video_tasks.append( + ( + os.path.join(Me().wx_dir, message.path), + os.path.join(video_dir, message.str_time[:7]), + message.file_name + ) + ) + ext = os.path.basename(message.path).split('.')[-1] + message.path = f'./video/{message.str_time[:7]}/{message.file_name}.{ext}' + elif type_ == MessageType.Audio: + message.set_file_name() + audio_tasks.append( + ( + self.database.get_media_buffer(message.server_id, self.contact.is_public()), + os.path.join(audio_dir, message.str_time[:7]), + message.file_name + ) + ) + message.path = f'./voice/{message.str_time[:7]}/{message.file_name + ".mp3"}' + elif type_ == MessageType.LinkMessage or type_ == MessageType.LinkMessage2 or type_ == MessageType.LinkMessage4 or type_ == MessageType.LinkMessage5 or type_ == MessageType.LinkMessage6: + LinkIndex.append(msg_index) + elif type_ == MessageType.Music: + MusicIndex.append(msg_index) + elif type_ == MessageType.Transfer: + TransferIndex.append(msg_index) + elif type_ == MessageType.Applet or type_ == MessageType.Applet2: + MiniProgramIndex.append(msg_index) + elif type_ == MessageType.WeChatVideo: + VideoNumberIndex.append(msg_index) + elif type_ == MessageType.MergedMessages: + parser_merged(message) + msg_index += 1 + is_select = True + html_json.append(message.to_json()) + if is_select: + select_msg_cnt += 1 + # 把时间戳转换为格式化时间 + str_time = message.str_time + # 2024-01-01 + year = str_time[:4] + month = int(str_time[5:7]) + curpage = math.ceil(select_msg_cnt / itemsPerPage) + if str_time[:10] not in date_id_map: + date_id_map[str_time[:10]] = str(server_id) + if str_time[:10] not in dateDataMap: + dateDataMap[str_time[:10]] = [curpage, str(server_id)] + + if year not in timelineData: + timelineData[year] = {} + if month not in timelineData[year]: + timelineData[year][month] = [] + timelineData[year][month].append(curpage) + timelineData[year][month].append(str(server_id)) + + if curpage not in PageTimeline: + PageTimeline[curpage] = {} + PageTimeline[curpage]['year'] = year + PageTimeline[curpage]['month'] = month + + server_id_Page[str(server_id)] = curpage + server_id_Idx[str(server_id)] = select_msg_cnt - 1 + + # print(image_tasks) + # print(file_tasks) + # print(video_tasks) + # print(audio_tasks) + logger.info('解析图片') + # 使用多进程,导出所有图片 + batch_decode_image_multiprocessing(Me().xor_key, image_tasks) + print('开始复制文件') + logger.info(f'开始复制{len(video_tasks + file_tasks)}') + # 使用多线程,复制文件、视频到导出文件夹 + copy_files(video_tasks + file_tasks) + print('开始导出语音') + logger.info('开始导出语音') + decode_audios(audio_tasks) + + AllIndex = list(range(len(html_json))) + + replace_map = { + "{{timelineData}}": timelineData, + "{{PageTimeline}}": PageTimeline, + "{{server_id_Page}}": server_id_Page, + "{{server_id_Idx}}": server_id_Idx, + "{{dateDataMap}}": dateDataMap, + "{{AllIndex}}": AllIndex, + "{{ImageIndex}}": ImageIndex, + "{{FileIndex}}": FileIndex, + "{{LinkIndex}}": LinkIndex, + "{{MusicIndex}}": MusicIndex, + "{{TransferIndex}}": TransferIndex, + "{{MiniProgramIndex}}": MiniProgramIndex, + "{{VideoNumberIndex}}": VideoNumberIndex + } + + def dict_to_js(dic: dict): + for key, value in dic.items(): + if isinstance(value, str): + if value.startswith('http'): + dic[key] = value + else: + dic[key] = html.escape(value) + elif isinstance(value, dict): + dic[key] = dict_to_js(value) + return dic + + print('开始字符串转义') + logger.info('开始字符串转义') + # 字符串转义,防止JS出现语法错误 + html_data = [] + for item in copy.deepcopy(html_json): + html_data.append(dict_to_js(item)) + + f.write(json.dumps(html_data, ensure_ascii=False, indent=4)) + for key, value in replace_map.items(): + html_end = html_end.replace(key, json.dumps(value)) + + f.write(html_end) + f.close() + + with open(filename + '.json', 'w', encoding='utf-8') as f: + json.dump(html_json, f, ensure_ascii=False, indent=4) + + self.update_progress_callback(1) + print(f"【完成导出 HTML {self.contact.remark}】{len(messages)}") + self.finish_callback(self.exporter_id) diff --git a/exporter/exporter_json.py b/exporter/exporter_json.py new file mode 100644 index 0000000..999f41b --- /dev/null +++ b/exporter/exporter_json.py @@ -0,0 +1,305 @@ +import json +import random +import os + +from wxManager import Me, MessageType +from exporter.exporter import ExporterBase, remove_privacy_info, get_new_filename + + +class JsonStrategy: + SPLIT_BY_TIME = 0 # 距离第一条消息的时间范围 + SPLIT_BY_INTERVALS = 1 # 相邻消息的时间间隔 + SLIDING_WINDOW = 2 # 滑动窗口法分割 + + +class AssistantUser: + SELF = 0 # 自己是ai助手 + CONTACT = 1 # 好友是ai助手 + + +class JsonConfig: + prompt: str = '' + shuffle: bool = True # 是否随机打乱数据 + train_ratio: int = 80 # 训练集占比(百分比) + model: str = 'Alpaca' # 可选:GLM4,ChatGLM3 + model_keys = { + 'GLM4': 'messages', + 'ChatGLM3': 'conversations' + } + strategy: int = JsonStrategy.SPLIT_BY_INTERVALS # json导出策略 + intervals: int = 120 # 相邻两条消息的最大间隔时间 + span: int = 300 # 第一条消息跟最后一条消息的间隔时间 + window_size: int = 10 # 窗口大小 + step: int = 3 # 步长 + assistant = AssistantUser.SELF + + def get_model_keys(self): + return self.model_keys.get(self.model, 'messages') + + +def modify(output, history): + return output + + +def merge_content(conversions_list) -> list: + """ + 合并一组对话中连续发送的句子 + @param conversions_list: + @return: + """ + merged_data = [] + current_role = None + current_content = "" + str_time = '' + for item in conversions_list: + if 'str_time' in item: + str_time = item['str_time'] + else: + str_time = '' + if current_role is None: + current_role = item["role"] + current_content = item["content"] + elif current_role == item["role"]: + current_content += "," + item["content"] + else: + # merged_data.append({"role": current_role, "content": current_content, 'str_time': str_time}) + if len(current_content) < 3 and current_role == 'assistant': + current_content = modify(current_content, merged_data) + merged_data.append({"role": current_role, "content": current_content}) + current_role = item["role"] + current_content = item["content"] + str_time = item.get('str_time') + + # 处理最后一组 + if current_role is not None: + # merged_data.append({"role": current_role, "content": current_content,'str_time': str_time}) + merged_data.append({"role": current_role, "content": current_content}) + return merged_data + + +def is_first_msg(conversions): + if not conversions: + return True + else: + return len(conversions) == 1 and conversions[0]['role'] == 'system' + + +def conversion_to_history(conversations): + res = [] + has_system_prompt = conversations[0].get('role') == 'system' + s_index, e_index = (1, len(conversations) - 3) if has_system_prompt else (0, len(conversations) - 2) + for i in range(s_index, e_index, 2): + res.append( + [ + conversations[i].get('content'), conversations[i + 1].get('content') + ] + ) + return res + + +class JsonExporter(ExporterBase): + def __init__( + self, + database, + contact, + output_dir, + type_, # 导出文件类型 + message_types: set[MessageType] = None, # 导出的消息类型 + time_range=None, # 导出的日期范围 + group_members: set[str] = None, # 群聊中只导出这些人的聊天记录 + progress_callback=None, # 进度回调函数,func(progress:float) + finish_callback=None, # 导出完成回调函数 + json_config: JsonConfig = None + ): + super().__init__(database, contact, output_dir, type_, message_types, time_range, group_members, + progress_callback, finish_callback) # 调用父类的构造函数 + if json_config: + self.json_config: JsonConfig = json_config + else: + self.json_config = JsonConfig() + + def is_user(self, is_send): + """ + 判断一条消息是否是user角色发送的 + @param is_send: + @return: + """ + return is_send ^ (self.json_config.assistant == AssistantUser.SELF) + + def system_prompt(self): + system = { + "role": "system", + "content": self.json_config.prompt.replace( + '{{name}}', Me().name + ).replace( + '{{remark}}', self.contact.remark + ) + } + return system + + def message_to_conversion(self, group): + conversions = [self.system_prompt()] if self.json_config.prompt else [] + # 确保最后一条消息是assistant发出的 + while len(group) and self.is_user(group[-1].is_sender): + group.pop() + for message in group: + is_send = message.is_sender + text = remove_privacy_info(message.content) + # 确保第一条消息必须是user发出的 + if is_first_msg(conversions) and not self.is_user(is_send): + continue + if self.is_user(is_send): + json_msg = { + "role": "user", + "content": text + } + else: + json_msg = { + "role": "assistant", + "content": text + } + json_msg['str_time'] = message.str_time + conversions.append(json_msg) + if len(conversions) == 1: + return [] + return merge_content(conversions) + + def split_by_time(self, length=300): + """ + 通过第一条消息和最后一条消息的时间间隔分割数据集 + @param length: + @return: + """ + messages = self.database.get_messages_by_type(self.contact.wxid, type_=MessageType.Text, + time_range=self.time_range) + start_time = 0 + res = [] + i = 0 + while i < len(messages): + message = messages[i] + timestamp = message.timestamp + is_send = message.is_sender + group = [] + while i < len(messages) and timestamp - start_time < length: + group.append(message) + i += 1 + if i >= len(messages): + break + message = messages[i] + timestamp = message.timestamp + is_send = message.is_sender + while not self.is_user(is_send): + group.append(message) + i += 1 + if i >= len(messages): + break + message = messages[i] + timestamp = message.timestamp + is_send = message.is_sender + start_time = timestamp + if len(group) > 4: + res.append(group) + return res + + def split_by_intervals(self, max_diff_seconds=300): + """ + 通过相邻两条消息的时间间隔分割数据集 + @param max_diff_seconds: + @return: + """ + messages = self.database.get_messages_by_type(self.contact.wxid, type_=MessageType.Text, + time_range=self.time_range) + res = [] + i = 0 + current_group = [] + while i < len(messages): + message = messages[i] + timestamp = message.timestamp + is_send = message.is_sender + while not self.is_user(is_send) and i + 1 < len(messages): + i += 1 + message = messages[i] + is_send = message.is_sender + current_group = [messages[i]] + i += 1 + while i < len(messages) and messages[i].timestamp - current_group[-1].timestamp <= max_diff_seconds: + current_group.append(messages[i]) + i += 1 + while i < len(messages) and not self.is_user(messages[i].is_sender): + current_group.append(messages[i]) + i += 1 + if len(current_group) > 4: + res.append(current_group) + return res + + def split_by_window(self, window_size=10, step=3): + """ + 滑动窗口切分数据集 + @param window_size: + @param step: + @return: + """ + messages = self.database.get_messages_by_type(self.contact.wxid, type_=MessageType.Text, + time_range=self.time_range) + res = [] + i = 0 + while i < len(messages): + message = messages[i] + timestamp = message.timestamp + is_send = message.is_sender + current_group = [] + j = i + while not self.is_user(is_send) and j + 1 < len(messages) and j - i < window_size: + j += 1 + message = messages[j] + is_send = message.is_sender + current_group = [messages[j]] + j += 1 + while j < len(messages) and j - i < window_size: + current_group.append(messages[j]) + j += 1 + res.append(current_group) + i += step + return res + + def export(self): + print(f"【开始导出 json {self.contact.remark}】") + origin_path = self.origin_path + filename = os.path.join(origin_path, f"{self.contact.remark}.json") + filename = get_new_filename(filename) + messages_groups = [] + match self.json_config.strategy: + case JsonStrategy.SPLIT_BY_INTERVALS: + messages_groups = self.split_by_intervals(self.json_config.intervals) + case JsonStrategy.SPLIT_BY_TIME: + messages_groups = self.split_by_time(self.json_config.span) + case JsonStrategy.SLIDING_WINDOW: + messages_groups = self.split_by_window(self.json_config.window_size, self.json_config.step) + dataset = [] + self.update_progress_callback(0.5) + for group in messages_groups: + conversations = self.message_to_conversion(group) + if conversations: + if self.json_config.model == 'Alpaca': + has_system_prompt = conversations[0].get('role') == 'system' + dataset.append( + { + 'system': conversations[0].get('content') if has_system_prompt else '', + 'instruction': conversations[-2].get('content'), + 'input': '', + 'output': conversations[-1].get('content'), + 'history': conversion_to_history(conversations), + } + ) + else: + dataset.append({ + self.json_config.get_model_keys(): conversations + }) + if self.json_config.shuffle: + # 打乱列表顺序 + random.shuffle(dataset) + with open(filename, "w", encoding="utf-8") as f: + json.dump(dataset, f, ensure_ascii=False, indent=4) + print(f"【完成导出 json {self.contact.remark}】") + self.update_progress_callback(1) + self.finish_callback(self.exporter_id) diff --git a/exporter/exporter_markdown.py b/exporter/exporter_markdown.py new file mode 100644 index 0000000..a62f72e --- /dev/null +++ b/exporter/exporter_markdown.py @@ -0,0 +1,210 @@ +import os +import re + +from exporter.exporter import ExporterBase +from wxManager import MessageType, Message +from wxManager.model import QuoteMessage, LinkMessage + + +def parser_date(str_date): + # 2024-01-01 12:00:00 + return str_date[0:4], str_date[0:7], str_date[0:10] + + +def escape_markdown(text): + """ + 转义Markdown特殊字符。 + """ + if not text: + return '' + # 定义需要转义的特殊字符 + special_chars = r"([\\`*_{}[\]()#+\-.!|])" + # 使用正则表达式添加转义符 + escaped_text = re.sub(special_chars, r"\\\1", text) + return escaped_text + + +class MarkdownExporter(ExporterBase): + def title(self, message): + str_time = message.str_time + return f'**{str_time[11:]} {escape_markdown(message.display_name)}**:' + + def text(self, doc, message): + str_content = message.content + doc.write( + f'''{self.title(message)} {escape_markdown(str_content)}\n\n''' + ) + + def image(self, doc, message): + doc.write( + f'''{self.title(message)} \n\n''' + ) + + def audio(self, doc, message): + voice_to_text = self.database.get_audio_text(message.server_id) + doc.write( + f'''{self.title(message)} [语音] {voice_to_text}\n\n''' + ) + + def emoji(self, doc, message): + doc.write( + f'''{self.title(message)} [表情包][{message.description}]\n\n''' + ) + + def file(self, doc, message): + doc.write( + f'''{self.title(message)} [文件]\n\n''' + ) + + def refermsg(self, doc, message: QuoteMessage): + doc.write( + f'''{self.title(message)} {message.content} \n> {escape_markdown(message.quote_message.to_text())}\n\n''' + ) + + def system_msg(self, doc, message): + str_content = message.content + str_time = message.str_time + str_content = str_content.replace('重新编辑]]>', "") + doc.write( + f'''> {str_time} {str_content}\n\n''' + ) + + def video(self, doc, message): + doc.write( + f'''{self.title(message)}\n[视频]\n\n''' + ) + + def music_share(self, doc, message: LinkMessage): + doc.write( + f'''{self.title(message)} {message.description} [{message.description}]({message.href}) {message.app_name}\n\n''' + ) + + def share_card(self, doc, message: LinkMessage): + doc.write( + f'''{self.title(message)} [{escape_markdown(message.title)}]({message.href})\n\n''' + ) + + def transfer(self, doc, message): + doc.write( + f'''{self.title(message)} {message.to_text()}\n\n''' + ) + + def call(self, doc, message): + doc.write( + f'''{self.title(message)} {message.to_text()}\n\n''' + ) + + def personal_business_card(self, doc, message): + doc.write( + f'''{self.title(message)}{message.to_text()}\n\n''' + ) + + + def position(self, doc, message): + doc.write( + f'''{self.title(message)} {message.to_text()}\n\n''' + ) + + def relay(self, doc, message): + doc.write( + f'''{self.title(message)} {message.to_text()}\n\n''' + ) + + def applets(self, doc, message): + doc.write( + f'''{self.title(message)}【小程序】: {message.app_name}:[{message.title}]({message.href})\n\n''' + ) + + def media(self, doc, message): + doc.write( + f'''{self.title(message)} {message.to_text()}\n\n''' + ) + + def announcement(self, doc, message): + doc.write( + f'''{self.title(message)}{message.to_text()}\n\n''' + ) + + def add_year(self, doc, year): + doc.write(f'## {year}\n\n') + + def add_month(self, doc, month): + doc.write(f'### {month}\n\n') + + def add_day(self, doc, day): + doc.write(f'#### {day}\n\n') + + def export(self): + # 实现导出为txt的逻辑 + print(f"【开始导出 Markdown {self.contact.remark}】") + origin_path = self.origin_path + os.makedirs(origin_path, exist_ok=True) + filename = os.path.join(origin_path, self.contact.remark + '.md') + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + total_steps = len(messages) + num = 1 + years = set() + months = set() + days = set() + with open(filename, mode='w', newline='', encoding='utf-8') as f: + for index, message in enumerate(messages): + if not self._is_running: + break + if index and index % 1000 == 0: + self.update_progress_callback(index / total_steps) + if not self.is_selected(message): + continue + type_ = message.type + year, month, day = parser_date(message.str_time) + if year not in years: + self.add_year(f, year) + years.add(year) + if month not in months: + self.add_month(f, month) + months.add(month) + if day not in days: + self.add_day(f, day) + days.add(day) + + if self.contact.is_chatroom() and self.group_members_set: + contact = message[13] + if contact.wxid not in self.group_members_set: + continue + if type_ == MessageType.Text: + self.text(f, message) + elif type_ == MessageType.Image: + self.image(f, message) + elif type_ == MessageType.Audio: + self.audio(f, message) + elif type_ == MessageType.Video: + self.video(f, message) + elif type_ == MessageType.Emoji: + self.emoji(f, message) + elif type_ == MessageType.System: + self.system_msg(f, message) + elif type_ == MessageType.Quote: + self.refermsg(f, message) + elif type_ == MessageType.File: + self.file(f, message) + elif type_ == MessageType.LinkMessage: + self.share_card(f, message) + elif type_ == MessageType.Transfer: + self.transfer(f, message) + elif type_ == MessageType.MergedMessages: + self.relay(f, message) + elif type_ == MessageType.Applet: + self.applets(f, message) + elif type_ == MessageType.WeChatVideo: + self.media(f, message) + # elif type_ == MessageType.: + # self.announcement(f, message) + elif type_ == MessageType.Voip: + self.call(f, message) + elif type_ == MessageType.BusinessCard: + self.personal_business_card(f, message) + elif type_ == MessageType.Position: + self.position(f, message) + self.update_progress_callback(1) + print(f"【完成导出 Markdown {self.contact.remark}】") + self.finish_callback(self.exporter_id) diff --git a/exporter/exporter_txt.py b/exporter/exporter_txt.py new file mode 100644 index 0000000..a47d506 --- /dev/null +++ b/exporter/exporter_txt.py @@ -0,0 +1,37 @@ +import os +import traceback + +from wxManager import MessageType +from wxManager.model import Message +from exporter.exporter import ExporterBase, get_new_filename + + +class TxtExporter(ExporterBase): + def title(self, message: Message): + str_time = message.str_time + if message.type == MessageType.System: + return f'{str_time}' + display_name = message.display_name + return f'{str_time} {display_name}' + + def export(self): + # 实现导出为txt的逻辑 + print(f"【开始导出 TXT {self.contact.remark}】") + origin_path = self.origin_path + os.makedirs(origin_path, exist_ok=True) + filename = os.path.join(origin_path, self.contact.remark + '.txt') + filename = get_new_filename(filename) + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + total_steps = len(messages) + txt_res = [] + for index, message in enumerate(messages): + if index and index % 1000 == 0: + self.update_progress_callback(index / total_steps) + if not self.is_selected(message): + continue + txt_res.append(f'{self.title(message)}\n{message.to_text()}') + with open(filename, mode='w', newline='', encoding='utf-8') as f: + f.write('\n\n'.join(txt_res)) + self.update_progress_callback(1) + print(f"【完成导出 TXT {self.contact.remark}】") + self.finish_callback(self.exporter_id) diff --git a/exporter/exporter_xlsx.py b/exporter/exporter_xlsx.py new file mode 100644 index 0000000..241dc02 --- /dev/null +++ b/exporter/exporter_xlsx.py @@ -0,0 +1,526 @@ +import os +import time +import traceback + +from wxManager import Me, MessageType +from wxManager.decrypt.decrypt_dat import batch_decode_image_multiprocessing +from wxManager.log import logger +from wxManager.model import Message +from exporter.exporter import ExporterBase, copy_files, decode_audios, get_new_filename + +from PIL import JpegImagePlugin +from PIL import ImageFile + +from PIL import Image as PILImage + +from wxManager.parser.link_parser import wx_sport, wx_collection_data, wx_pay_data + +JpegImagePlugin._getmp = lambda x: None +ImageFile.LOAD_TRUNCATED_IMAGES = True + + +def add_hyperlink(doc, row, column, hyperlink): + from openpyxl.styles import Font + import openpyxl + from openpyxl.drawing.image import Image + from openpyxl.utils import get_column_letter + Image.MAX_IMAGE_PIXELS = None + cell = doc.cell(row=row, column=column) + cell.hyperlink = hyperlink + # 添加样式来改变超链接文本的颜色和下划线 + font = Font(color="0000FF", underline="single") # 蓝色和单下划线 + cell.font = font + + +def find_image_with_known_extensions(img_path): + # 常见的图片后缀名 + extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'] + directory = os.path.dirname(img_path) + filename = os.path.basename(img_path) + + for ext in extensions: + # 构造完整路径 + full_path = os.path.join(directory, f"{filename}{ext}") + # 检查文件是否存在 + if os.path.isfile(full_path): + return full_path + + return None + + +class ExcelExporter(ExporterBase): + row = 2 + + def add_member_info(self, sheet): + if self.contact.is_chatroom(): + columns = ['wxid', '微信号', '类型', '群昵称', '昵称', '头像地址', + '头像原图', '标签', '性别', '个性签名', '国家(地区)', '省份', '城市'] + self.group_contacts = self.database.get_chatroom_members(self.contact.wxid) + # 写入CSV文件 + sheet.append(columns) + for wxid, contact in self.group_contacts.items(): + sheet.append( + [ + contact.wxid, contact.alias, contact.flag, contact.remark, contact.nickname, + contact.small_head_img_url, contact.big_head_img_url, contact.label_name(), + contact.gender, contact.signature, *contact.region + ] + ) + else: + if self.contact.is_public(): + pass + else: + columns = ( + 'wxid', '微信号', '类型', '群昵称', '昵称', '头像地址', '头像原图', '标签', '性别', '电话', + '个性签名', '国家(地区)', '省份', '城市') + # 写入CSV文件 + sheet.append(columns) + contact = self.contact + sheet.append( + [ + contact.wxid, contact.alias, contact.flag, contact.remark, contact.nickname, + contact.small_head_img_url, contact.big_head_img_url, contact.label_name(), + contact.gender, contact.signature, *contact.region + ] + ) + + def message_to_list(self, message: Message): + remark = message.display_name + nickname = message.display_name + if self.contact.is_chatroom(): + contact = self.group_contacts.get(message.sender_id) + if contact: + remark = contact.remark + nickname = contact.nickname + else: + contact = Me() if message.is_sender else self.contact + remark = contact.remark + nickname = contact.nickname + res = [str(message.server_id), message.type_name(), message.display_name, message.str_time, message.to_text(), + remark, nickname, 'more'] + return res + + def to_excel(self): + from openpyxl.styles import Font + import openpyxl + from openpyxl.drawing.image import Image + from openpyxl.utils import get_column_letter + Image.MAX_IMAGE_PIXELS = None + print(f"【开始导出 XLSX {self.contact.remark}】") + os.makedirs(self.origin_path, exist_ok=True) + filename = os.path.join(self.origin_path, f"{self.contact.remark}.xlsx") + filename = get_new_filename(filename) + columns = ['消息ID', '类型', '发送人', '时间', '内容', '备注', '昵称', '更多信息'] + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + new_workbook = openpyxl.Workbook() + new_sheet = new_workbook.create_sheet("聊天记录", 0) + member_sheet = new_workbook.create_sheet("成员信息", 1) + self.add_member_info(member_sheet) + new_sheet.append(columns) + num = 1 + total_num = len(messages) + image_tasks = [] + video_tasks = [] + file_tasks = [] + audio_tasks = [] + image_dir = os.path.join(self.origin_path, 'image') + video_dir = os.path.join(self.origin_path, 'video') + audio_dir = os.path.join(self.origin_path, 'voice') + file_dir = os.path.join(self.origin_path, 'file') + image_index = {} + + def parser_merged(merged_message): + for msg in merged_message.messages: + type_ = msg.type + if type_ == MessageType.Image: + msg.set_file_name() + image_tasks.append( + ( + os.path.join(Me().wx_dir, msg.path), + os.path.join(image_dir, msg.str_time[:7]), + msg.file_name + ) + ) + image_tasks.append( + ( + os.path.join(Me().wx_dir, msg.thumb_path), + os.path.join(image_dir, msg.str_time[:7]), + msg.file_name + '_t' + ) + ) + msg.path = f"./image/{msg.str_time[:7]}/{msg.file_name}" + msg.thumb_path = f"./image/{msg.str_time[:7]}/{msg.file_name + '_t'}" + elif type_ == MessageType.File: + origin_file_path = os.path.join(Me().wx_dir, msg.path) + file_tasks.append( + ( + origin_file_path, + os.path.join(file_dir, msg.str_time[:7]), + '' + ) + ) + msg.path = f'./file/{msg.str_time[:7]}/{os.path.basename(origin_file_path)}' + elif type_ == MessageType.Video: + msg.set_file_name() + video_tasks.append( + ( + os.path.join(Me().wx_dir, msg.path), + os.path.join(video_dir, msg.str_time[:7]), + msg.file_name + ) + ) + ext = os.path.basename(msg.path).split('.')[-1] + msg.path = f'./video/{msg.str_time[:7]}/{msg.file_name}.{ext}' + elif type_ == MessageType.MergedMessages: + parser_merged(msg) + + for index, message in enumerate(messages): + if not self._is_running: + break + if index % 1000 == 0: + self.update_progress_callback(index / total_num) + if not self.is_selected(message): + continue + try: + new_sheet.append(self.message_to_list(message)) + self.row += 1 + except: + logger.error(traceback.format_exc()) + continue + type_ = message.type + if type_ == MessageType.Image: + message.set_file_name() + image_index[message.server_id] = self.row + image_tasks.append( + ( + os.path.join(Me().wx_dir, message.path), + os.path.join(image_dir, message.str_time[:7]), + message.file_name + ) + ) + image_tasks.append( + ( + os.path.join(Me().wx_dir, message.thumb_path), + os.path.join(image_dir, message.str_time[:7]), + message.file_name + '_t' + ) + ) + message.path = f"./image/{message.str_time[:7]}/{message.file_name}" + message.thumb_path = f"./image/{message.str_time[:7]}/{message.file_name + '_t'}" + elif type_ == MessageType.File: + origin_file_path = os.path.join(Me().wx_dir, message.path) + file_tasks.append( + ( + origin_file_path, + os.path.join(file_dir, message.str_time[:7]), + '' + ) + ) + if os.path.isfile(origin_file_path): + message.path = f'./file/{message.str_time[:7]}/{os.path.basename(origin_file_path)}' + add_hyperlink(new_sheet, self.row, 5, message.path) + elif type_ == MessageType.Video: + message.set_file_name() + video_tasks.append( + ( + os.path.join(Me().wx_dir, message.path), + os.path.join(video_dir, message.str_time[:7]), + message.file_name + ) + ) + ext = os.path.basename(message.path).split('.')[-1] + message.path = f'./video/{message.str_time[:7]}/{message.file_name}.{ext}' + add_hyperlink(new_sheet, self.row, 5, message.path) + elif type_ == MessageType.Audio: + message.set_file_name() + audio_tasks.append( + ( + self.database.get_media_buffer(message.server_id), + os.path.join(audio_dir, message.str_time[:7]), + message.file_name + ) + ) + message.path = f'./voice/{message.str_time[:7]}/{message.file_name + ".mp3"}' + add_hyperlink(new_sheet, self.row, 5, message.path) + elif type_ == MessageType.MergedMessages: + parser_merged(message) + # 使用多进程,导出所有图片 + batch_decode_image_multiprocessing(Me().xor_key, image_tasks) + + # 使用多线程,复制文件、视频到导出文件夹 + copy_files(video_tasks + file_tasks) + + decode_audios(audio_tasks) + if MessageType.Image in self.message_types: + for index, message in enumerate(messages): + if message.type == MessageType.Image: + if not self.is_selected(message): + continue + row = image_index[message.server_id] + img_path = find_image_with_known_extensions(os.path.join(self.origin_path, message.path)) + if not img_path: + img_path = find_image_with_known_extensions(os.path.join(self.origin_path, message.thumb_path)) + if not img_path: + continue + try: + # 打开图片以获取其尺寸 + with PILImage.open(img_path) as img: + width, height = img.size + max_height = 500 + # 计算缩放比例 + scale = min(1.0, max_height / height) + + # 缩放后的图片尺寸 + scaled_width = int(width * scale) + scaled_height = int(height * scale) + + # 插入图片 + img = Image(img_path) + img.width = scaled_width + img.height = scaled_height + + # 计算单元格的坐标 + cell = f"{get_column_letter(5)}{row}" + + # 将图片添加到工作表 + new_sheet.add_image(img, cell) + + # 设置行高 + new_sheet.row_dimensions[row].height = scaled_height * 0.75 # 0.75 是像素到 Excel 单位的转换因子 + except: + logger.error(traceback.format_exc()) + pass + # 获取列的字母表示(A、B、C...) + col_letter = get_column_letter(1) + # 设置整列的单元格格式为文本 + for cell in new_sheet[col_letter]: + cell.number_format = "@" # "@" 表示文本格式 + try: + new_workbook.save(filename) + except PermissionError: + filename = '.'.join(filename.split('.')[:-1]) + str(int(time.time())) + '.xlsx' + new_workbook.save(filename) + self.update_progress_callback(1) + self.finish_callback(self.exporter_id) + print(f"【完成导出 XLSX {self.contact.remark}】") + + def public_to_excel(self): + from openpyxl.styles import Font + import openpyxl + from openpyxl.drawing.image import Image + from openpyxl.utils import get_column_letter + Image.MAX_IMAGE_PIXELS = None + + print(f"【开始导出 XLSX {self.contact.remark}】") + os.makedirs(self.origin_path, exist_ok=True) + filename = os.path.join(self.origin_path, f"{self.contact.remark}.xlsx") + filename = get_new_filename(filename) + columns = ['日期', '时间', '标题', '描述', '链接', '更多信息'] + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + new_workbook = openpyxl.Workbook() + new_sheet = new_workbook.create_sheet("聊天记录", 0) + new_sheet.append(columns) + total_num = len(messages) + for index, message in enumerate(messages): + if not self._is_running: + break + if index % 1000 == 0: + self.update_progress_callback(index / total_num) + if not message.type in {MessageType.LinkMessage}: + continue + try: + new_sheet.append([*message.str_time.split(' '), message.title, message.description, message.href]) + except: + logger.error(traceback.format_exc()) + continue + # 获取列的字母表示(A、B、C...) + col_letter = get_column_letter(1) + # 设置整列的单元格格式为文本 + for cell in new_sheet[col_letter]: + cell.number_format = "@" # "@" 表示文本格式 + try: + new_workbook.save(filename) + except PermissionError: + filename = '.'.join(filename.split('.')[:-1]) + str(int(time.time())) + '.xlsx' + new_workbook.save(filename) + self.update_progress_callback(1) + self.finish_callback(self.exporter_id) + print(f"【完成导出 XLSX {self.contact.remark}】") + + def wx_pay(self): + from openpyxl.styles import Font + import openpyxl + from openpyxl.drawing.image import Image + from openpyxl.utils import get_column_letter + Image.MAX_IMAGE_PIXELS = None + print(f"【开始导出 XLSX {self.contact.remark}】") + os.makedirs(self.origin_path, exist_ok=True) + filename = os.path.join(self.origin_path, f"{self.contact.remark}.xlsx") + filename = get_new_filename(filename) + columns = ['类型', '收款单位', '日期', '时间', '金额', '付款方式', '收单机构', '更多信息'] + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + new_workbook = openpyxl.Workbook() + new_sheet = new_workbook.create_sheet("聊天记录", 0) + new_sheet.append(columns) + total_num = len(messages) + for index, message in enumerate(messages): + if not self._is_running: + break + if index % 1000 == 0: + self.update_progress_callback(index / total_num) + if not message.type in {MessageType.LinkMessage}: + continue + try: + card_data = wx_pay_data(message.xml_content) + date, str_time = message.str_time.split(' ') + if card_data.get('title') in {'记账日报', '「先享后付」服务使用通知', '转入零钱通,五一享收益', + '转入零钱通,端午享收益', '智能手表支付服务已启用', '优惠券领取提醒', + '清明假期收益规则', '「先享后付」服务完成通知', '礼包领取提醒', + '五一假期收益规则提醒', '端午节假期收益规则', '中秋节假期收益规则', + '元旦假期收益规则', '春节假期收益规则', '五一假期收益规则', + '中秋及国庆假期收益规则', '春节赚收益攻略', '「先享后付」服务取消通知', + '揭开骗局,远离诈骗'}: + continue + new_sheet.append( + [ + card_data.get('title'), card_data.get('display_name'), date, str_time, + card_data.get('money'), card_data.get('payment_type'), card_data.get('acquiring_institution'), + card_data.get('more') + ] + ) + except: + logger.error(traceback.format_exc()) + continue + # 获取列的字母表示(A、B、C...) + col_letter = get_column_letter(1) + # 设置整列的单元格格式为文本 + for cell in new_sheet[col_letter]: + cell.number_format = "@" # "@" 表示文本格式 + try: + new_workbook.save(filename) + except PermissionError: + filename = '.'.join(filename.split('.')[:-1]) + str(int(time.time())) + '.xlsx' + new_workbook.save(filename) + self.update_progress_callback(1) + self.finish_callback(self.exporter_id) + print(f"【完成导出 XLSX {self.contact.remark}】") + + def wx_collect(self): + from openpyxl.styles import Font + import openpyxl + from openpyxl.drawing.image import Image + from openpyxl.utils import get_column_letter + Image.MAX_IMAGE_PIXELS = None + + print(f"【开始导出 XLSX {self.contact.remark}】") + os.makedirs(self.origin_path, exist_ok=True) + filename = os.path.join(self.origin_path, f"{self.contact.remark}.xlsx") + filename = get_new_filename(filename) + columns = ['类型', '日期', '时间', '金额', '详细信息', '汇总', '备注', '更多信息'] + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + new_workbook = openpyxl.Workbook() + new_sheet = new_workbook.create_sheet("聊天记录", 0) + new_sheet.append(columns) + total_num = len(messages) + for index, message in enumerate(messages): + if not self._is_running: + break + if index % 1000 == 0: + self.update_progress_callback(index / total_num) + if not message.type in {MessageType.LinkMessage}: + continue + try: + card_data = wx_collection_data(message.xml_content) + date, str_time = message.str_time.split(' ') + new_sheet.append( + [ + card_data.get('title'), date, str_time, card_data.get('money'), card_data.get('display_name'), + card_data.get('summary'), card_data.get('more') + ] + ) + except: + logger.error(traceback.format_exc()) + continue + # 获取列的字母表示(A、B、C...) + col_letter = get_column_letter(1) + # 设置整列的单元格格式为文本 + for cell in new_sheet[col_letter]: + cell.number_format = "@" # "@" 表示文本格式 + try: + new_workbook.save(filename) + except PermissionError: + filename = '.'.join(filename.split('.')[:-1]) + str(int(time.time())) + '.xlsx' + new_workbook.save(filename) + self.update_progress_callback(1) + self.finish_callback(self.exporter_id) + print(f"【完成导出 XLSX {self.contact.remark}】") + + def wx_sport(self): + from openpyxl.styles import Font + import openpyxl + from openpyxl.drawing.image import Image + from openpyxl.utils import get_column_letter + Image.MAX_IMAGE_PIXELS = None + + + print(f"【开始导出 XLSX {self.contact.remark}】") + os.makedirs(self.origin_path, exist_ok=True) + filename = os.path.join(self.origin_path, f"{self.contact.remark}.xlsx") + filename = get_new_filename(filename) + columns = ['日期', '排名', '步数', '当日冠军', '当日冠军步数', '更多信息'] + messages = self.database.get_messages(self.contact.wxid, time_range=self.time_range) + new_workbook = openpyxl.Workbook() + new_sheet = new_workbook.create_sheet("聊天记录", 0) + new_sheet.append(columns) + total_num = len(messages) + for index, message in enumerate(messages): + if not self._is_running: + break + if index and index % 1000 == 0: + self.update_progress_callback(index / total_num) + if not message.type in {MessageType.LinkMessage}: + continue + try: + card_data = wx_sport(message.xml_content) + champion_name = '' + if not card_data.get('rank_list'): + champion = {} + else: + champion = card_data.get('rank_list')[0] + contact = self.database.get_contact_by_username(champion.get('username')) + champion_name = contact.remark + new_sheet.append( + [ + message.str_time.split(' ')[0], card_data.get('rank'), card_data.get('score'), + champion_name, champion.get('score') + ] + ) + except: + logger.error(traceback.format_exc()) + continue + # 获取列的字母表示(A、B、C...) + col_letter = get_column_letter(1) + # 设置整列的单元格格式为文本 + for cell in new_sheet[col_letter]: + cell.number_format = "@" # "@" 表示文本格式 + try: + new_workbook.save(filename) + except PermissionError: + filename = '.'.join(filename.split('.')[:-1]) + str(int(time.time())) + '.xlsx' + new_workbook.save(filename) + self.update_progress_callback(1) + self.finish_callback(self.exporter_id) + print(f"【完成导出 XLSX {self.contact.remark}】") + + def run(self): + if self.contact.is_public(): + if self.contact.wxid == 'gh_3dfda90e39d6': + self.wx_pay() + elif self.contact.wxid == 'gh_f0a92aa7146c': + self.wx_collect() + elif self.contact.wxid == 'gh_43f2581f6fd6': + self.wx_sport() + else: + self.public_to_excel() + else: + self.to_excel() diff --git a/app/resources/data/ffmpeg.exe b/exporter/ffmpeg.exe similarity index 100% rename from app/resources/data/ffmpeg.exe rename to exporter/ffmpeg.exe diff --git a/exporter/resources/default_avatar.png b/exporter/resources/default_avatar.png new file mode 100644 index 0000000..d69c77d Binary files /dev/null and b/exporter/resources/default_avatar.png differ diff --git a/exporter/resources/emoji/666.png b/exporter/resources/emoji/666.png new file mode 100644 index 0000000..ff6aa92 Binary files /dev/null and b/exporter/resources/emoji/666.png differ diff --git a/exporter/resources/emoji/Emm.png b/exporter/resources/emoji/Emm.png new file mode 100644 index 0000000..94f777d Binary files /dev/null and b/exporter/resources/emoji/Emm.png differ diff --git a/exporter/resources/emoji/OK.png b/exporter/resources/emoji/OK.png new file mode 100644 index 0000000..2a6e18e Binary files /dev/null and b/exporter/resources/emoji/OK.png differ diff --git a/exporter/resources/emoji/Whimper.png b/exporter/resources/emoji/Whimper.png new file mode 100644 index 0000000..742ac56 Binary files /dev/null and b/exporter/resources/emoji/Whimper.png differ diff --git a/exporter/resources/emoji/亲亲.png b/exporter/resources/emoji/亲亲.png new file mode 100644 index 0000000..2896ca1 Binary files /dev/null and b/exporter/resources/emoji/亲亲.png differ diff --git a/exporter/resources/emoji/便便.png b/exporter/resources/emoji/便便.png new file mode 100644 index 0000000..07126f8 Binary files /dev/null and b/exporter/resources/emoji/便便.png differ diff --git a/exporter/resources/emoji/偷笑.png b/exporter/resources/emoji/偷笑.png new file mode 100644 index 0000000..6c26f9b Binary files /dev/null and b/exporter/resources/emoji/偷笑.png differ diff --git a/exporter/resources/emoji/傲慢.png b/exporter/resources/emoji/傲慢.png new file mode 100644 index 0000000..9d56e08 Binary files /dev/null and b/exporter/resources/emoji/傲慢.png differ diff --git a/exporter/resources/emoji/再见.png b/exporter/resources/emoji/再见.png new file mode 100644 index 0000000..5f45eb5 Binary files /dev/null and b/exporter/resources/emoji/再见.png differ diff --git a/exporter/resources/emoji/凋谢.png b/exporter/resources/emoji/凋谢.png new file mode 100644 index 0000000..6b618a3 Binary files /dev/null and b/exporter/resources/emoji/凋谢.png differ diff --git a/exporter/resources/emoji/加油.png b/exporter/resources/emoji/加油.png new file mode 100644 index 0000000..4141148 Binary files /dev/null and b/exporter/resources/emoji/加油.png differ diff --git a/exporter/resources/emoji/加油加油.png b/exporter/resources/emoji/加油加油.png new file mode 100644 index 0000000..4a2976e Binary files /dev/null and b/exporter/resources/emoji/加油加油.png differ diff --git a/exporter/resources/emoji/勾引.png b/exporter/resources/emoji/勾引.png new file mode 100644 index 0000000..3e5137f Binary files /dev/null and b/exporter/resources/emoji/勾引.png differ diff --git a/exporter/resources/emoji/发呆.png b/exporter/resources/emoji/发呆.png new file mode 100644 index 0000000..7d52967 Binary files /dev/null and b/exporter/resources/emoji/发呆.png differ diff --git a/exporter/resources/emoji/发怒.png b/exporter/resources/emoji/发怒.png new file mode 100644 index 0000000..c7a208e Binary files /dev/null and b/exporter/resources/emoji/发怒.png differ diff --git a/exporter/resources/emoji/发抖.png b/exporter/resources/emoji/发抖.png new file mode 100644 index 0000000..41596e8 Binary files /dev/null and b/exporter/resources/emoji/发抖.png differ diff --git a/exporter/resources/emoji/可怜.png b/exporter/resources/emoji/可怜.png new file mode 100644 index 0000000..742ac56 Binary files /dev/null and b/exporter/resources/emoji/可怜.png differ diff --git a/exporter/resources/emoji/右哼哼.png b/exporter/resources/emoji/右哼哼.png new file mode 100644 index 0000000..49fa38c Binary files /dev/null and b/exporter/resources/emoji/右哼哼.png differ diff --git a/exporter/resources/emoji/叹气.png b/exporter/resources/emoji/叹气.png new file mode 100644 index 0000000..63c6a02 Binary files /dev/null and b/exporter/resources/emoji/叹气.png differ diff --git a/exporter/resources/emoji/吃瓜.png b/exporter/resources/emoji/吃瓜.png new file mode 100644 index 0000000..bd99b78 Binary files /dev/null and b/exporter/resources/emoji/吃瓜.png differ diff --git a/exporter/resources/emoji/合十.png b/exporter/resources/emoji/合十.png new file mode 100644 index 0000000..f92a569 Binary files /dev/null and b/exporter/resources/emoji/合十.png differ diff --git a/exporter/resources/emoji/吐.png b/exporter/resources/emoji/吐.png new file mode 100644 index 0000000..7a33ba0 Binary files /dev/null and b/exporter/resources/emoji/吐.png differ diff --git a/exporter/resources/emoji/吐舌.png b/exporter/resources/emoji/吐舌.png new file mode 100644 index 0000000..fb551ff Binary files /dev/null and b/exporter/resources/emoji/吐舌.png differ diff --git a/exporter/resources/emoji/呲牙.png b/exporter/resources/emoji/呲牙.png new file mode 100644 index 0000000..d615596 Binary files /dev/null and b/exporter/resources/emoji/呲牙.png differ diff --git a/exporter/resources/emoji/咒骂.png b/exporter/resources/emoji/咒骂.png new file mode 100644 index 0000000..549de88 Binary files /dev/null and b/exporter/resources/emoji/咒骂.png differ diff --git a/exporter/resources/emoji/咖啡.png b/exporter/resources/emoji/咖啡.png new file mode 100644 index 0000000..27f3ee8 Binary files /dev/null and b/exporter/resources/emoji/咖啡.png differ diff --git a/exporter/resources/emoji/哇.png b/exporter/resources/emoji/哇.png new file mode 100644 index 0000000..8b35491 Binary files /dev/null and b/exporter/resources/emoji/哇.png differ diff --git a/exporter/resources/emoji/啤酒.png b/exporter/resources/emoji/啤酒.png new file mode 100644 index 0000000..3d28da1 Binary files /dev/null and b/exporter/resources/emoji/啤酒.png differ diff --git a/exporter/resources/emoji/嘘.png b/exporter/resources/emoji/嘘.png new file mode 100644 index 0000000..18793fe Binary files /dev/null and b/exporter/resources/emoji/嘘.png differ diff --git a/exporter/resources/emoji/嘴唇.png b/exporter/resources/emoji/嘴唇.png new file mode 100644 index 0000000..77c8dff Binary files /dev/null and b/exporter/resources/emoji/嘴唇.png differ diff --git a/exporter/resources/emoji/嘿哈.png b/exporter/resources/emoji/嘿哈.png new file mode 100644 index 0000000..912dbe1 Binary files /dev/null and b/exporter/resources/emoji/嘿哈.png differ diff --git a/exporter/resources/emoji/囧.png b/exporter/resources/emoji/囧.png new file mode 100644 index 0000000..465da48 Binary files /dev/null and b/exporter/resources/emoji/囧.png differ diff --git a/exporter/resources/emoji/困.png b/exporter/resources/emoji/困.png new file mode 100644 index 0000000..6525e88 Binary files /dev/null and b/exporter/resources/emoji/困.png differ diff --git a/exporter/resources/emoji/坏笑.png b/exporter/resources/emoji/坏笑.png new file mode 100644 index 0000000..bb0d5bf Binary files /dev/null and b/exporter/resources/emoji/坏笑.png differ diff --git a/exporter/resources/emoji/大哭.png b/exporter/resources/emoji/大哭.png new file mode 100644 index 0000000..c0a63c1 Binary files /dev/null and b/exporter/resources/emoji/大哭.png differ diff --git a/exporter/resources/emoji/天啊.png b/exporter/resources/emoji/天啊.png new file mode 100644 index 0000000..95aefcd Binary files /dev/null and b/exporter/resources/emoji/天啊.png differ diff --git a/exporter/resources/emoji/太阳.png b/exporter/resources/emoji/太阳.png new file mode 100644 index 0000000..3f9226f Binary files /dev/null and b/exporter/resources/emoji/太阳.png differ diff --git a/exporter/resources/emoji/失望.png b/exporter/resources/emoji/失望.png new file mode 100644 index 0000000..28f031e Binary files /dev/null and b/exporter/resources/emoji/失望.png differ diff --git a/exporter/resources/emoji/奸笑.png b/exporter/resources/emoji/奸笑.png new file mode 100644 index 0000000..914890b Binary files /dev/null and b/exporter/resources/emoji/奸笑.png differ diff --git a/exporter/resources/emoji/好的.png b/exporter/resources/emoji/好的.png new file mode 100644 index 0000000..989ac10 Binary files /dev/null and b/exporter/resources/emoji/好的.png differ diff --git a/exporter/resources/emoji/委屈.png b/exporter/resources/emoji/委屈.png new file mode 100644 index 0000000..f04d409 Binary files /dev/null and b/exporter/resources/emoji/委屈.png differ diff --git a/exporter/resources/emoji/害羞.png b/exporter/resources/emoji/害羞.png new file mode 100644 index 0000000..88c8717 Binary files /dev/null and b/exporter/resources/emoji/害羞.png differ diff --git a/exporter/resources/emoji/尴尬.png b/exporter/resources/emoji/尴尬.png new file mode 100644 index 0000000..a01e450 Binary files /dev/null and b/exporter/resources/emoji/尴尬.png differ diff --git a/exporter/resources/emoji/庆祝.png b/exporter/resources/emoji/庆祝.png new file mode 100644 index 0000000..cfdf014 Binary files /dev/null and b/exporter/resources/emoji/庆祝.png differ diff --git a/exporter/resources/emoji/弱.png b/exporter/resources/emoji/弱.png new file mode 100644 index 0000000..306505c Binary files /dev/null and b/exporter/resources/emoji/弱.png differ diff --git a/exporter/resources/emoji/强.png b/exporter/resources/emoji/强.png new file mode 100644 index 0000000..3e88fa8 Binary files /dev/null and b/exporter/resources/emoji/强.png differ diff --git a/exporter/resources/emoji/得意.png b/exporter/resources/emoji/得意.png new file mode 100644 index 0000000..5de282b Binary files /dev/null and b/exporter/resources/emoji/得意.png differ diff --git a/exporter/resources/emoji/微笑.png b/exporter/resources/emoji/微笑.png new file mode 100644 index 0000000..ab81877 Binary files /dev/null and b/exporter/resources/emoji/微笑.png differ diff --git a/exporter/resources/emoji/心碎.png b/exporter/resources/emoji/心碎.png new file mode 100644 index 0000000..629224a Binary files /dev/null and b/exporter/resources/emoji/心碎.png differ diff --git a/exporter/resources/emoji/快哭了.png b/exporter/resources/emoji/快哭了.png new file mode 100644 index 0000000..ce59b72 Binary files /dev/null and b/exporter/resources/emoji/快哭了.png differ diff --git a/exporter/resources/emoji/恐惧.png b/exporter/resources/emoji/恐惧.png new file mode 100644 index 0000000..976b705 Binary files /dev/null and b/exporter/resources/emoji/恐惧.png differ diff --git a/exporter/resources/emoji/悠闲.png b/exporter/resources/emoji/悠闲.png new file mode 100644 index 0000000..1dd32cf Binary files /dev/null and b/exporter/resources/emoji/悠闲.png differ diff --git a/exporter/resources/emoji/惊恐.png b/exporter/resources/emoji/惊恐.png new file mode 100644 index 0000000..59ffdc0 Binary files /dev/null and b/exporter/resources/emoji/惊恐.png differ diff --git a/exporter/resources/emoji/惊讶.png b/exporter/resources/emoji/惊讶.png new file mode 100644 index 0000000..adc6d1d Binary files /dev/null and b/exporter/resources/emoji/惊讶.png differ diff --git a/exporter/resources/emoji/愉快.png b/exporter/resources/emoji/愉快.png new file mode 100644 index 0000000..f65c5e8 Binary files /dev/null and b/exporter/resources/emoji/愉快.png differ diff --git a/exporter/resources/emoji/憨笑.png b/exporter/resources/emoji/憨笑.png new file mode 100644 index 0000000..1881de3 Binary files /dev/null and b/exporter/resources/emoji/憨笑.png differ diff --git a/exporter/resources/emoji/打脸.png b/exporter/resources/emoji/打脸.png new file mode 100644 index 0000000..e2cf5e5 Binary files /dev/null and b/exporter/resources/emoji/打脸.png differ diff --git a/exporter/resources/emoji/抓狂.png b/exporter/resources/emoji/抓狂.png new file mode 100644 index 0000000..71d8df8 Binary files /dev/null and b/exporter/resources/emoji/抓狂.png differ diff --git a/exporter/resources/emoji/抠鼻.png b/exporter/resources/emoji/抠鼻.png new file mode 100644 index 0000000..4f3ab6d Binary files /dev/null and b/exporter/resources/emoji/抠鼻.png differ diff --git a/exporter/resources/emoji/抱拳.png b/exporter/resources/emoji/抱拳.png new file mode 100644 index 0000000..eb943e6 Binary files /dev/null and b/exporter/resources/emoji/抱拳.png differ diff --git a/exporter/resources/emoji/拥抱.png b/exporter/resources/emoji/拥抱.png new file mode 100644 index 0000000..9cd2f50 Binary files /dev/null and b/exporter/resources/emoji/拥抱.png differ diff --git a/exporter/resources/emoji/拳头.png b/exporter/resources/emoji/拳头.png new file mode 100644 index 0000000..4d40db2 Binary files /dev/null and b/exporter/resources/emoji/拳头.png differ diff --git a/exporter/resources/emoji/捂脸.png b/exporter/resources/emoji/捂脸.png new file mode 100644 index 0000000..c20cab8 Binary files /dev/null and b/exporter/resources/emoji/捂脸.png differ diff --git a/exporter/resources/emoji/握手.png b/exporter/resources/emoji/握手.png new file mode 100644 index 0000000..b625691 Binary files /dev/null and b/exporter/resources/emoji/握手.png differ diff --git a/exporter/resources/emoji/撇嘴.png b/exporter/resources/emoji/撇嘴.png new file mode 100644 index 0000000..623e454 Binary files /dev/null and b/exporter/resources/emoji/撇嘴.png differ diff --git a/exporter/resources/emoji/擦汗.png b/exporter/resources/emoji/擦汗.png new file mode 100644 index 0000000..2b12443 Binary files /dev/null and b/exporter/resources/emoji/擦汗.png differ diff --git a/exporter/resources/emoji/敲打.png b/exporter/resources/emoji/敲打.png new file mode 100644 index 0000000..9d2fdaa Binary files /dev/null and b/exporter/resources/emoji/敲打.png differ diff --git a/exporter/resources/emoji/无语.png b/exporter/resources/emoji/无语.png new file mode 100644 index 0000000..9bf0b26 Binary files /dev/null and b/exporter/resources/emoji/无语.png differ diff --git a/exporter/resources/emoji/旺柴.png b/exporter/resources/emoji/旺柴.png new file mode 100644 index 0000000..796419e Binary files /dev/null and b/exporter/resources/emoji/旺柴.png differ diff --git a/exporter/resources/emoji/晕.png b/exporter/resources/emoji/晕.png new file mode 100644 index 0000000..423ebfc Binary files /dev/null and b/exporter/resources/emoji/晕.png differ diff --git a/exporter/resources/emoji/月亮.png b/exporter/resources/emoji/月亮.png new file mode 100644 index 0000000..d1197e7 Binary files /dev/null and b/exporter/resources/emoji/月亮.png differ diff --git a/exporter/resources/emoji/机智.png b/exporter/resources/emoji/机智.png new file mode 100644 index 0000000..e802d4d Binary files /dev/null and b/exporter/resources/emoji/机智.png differ diff --git a/exporter/resources/emoji/汗.png b/exporter/resources/emoji/汗.png new file mode 100644 index 0000000..aa8e7e3 Binary files /dev/null and b/exporter/resources/emoji/汗.png differ diff --git a/exporter/resources/emoji/流泪.png b/exporter/resources/emoji/流泪.png new file mode 100644 index 0000000..236c98f Binary files /dev/null and b/exporter/resources/emoji/流泪.png differ diff --git a/exporter/resources/emoji/炸弹.png b/exporter/resources/emoji/炸弹.png new file mode 100644 index 0000000..cda16f8 Binary files /dev/null and b/exporter/resources/emoji/炸弹.png differ diff --git a/exporter/resources/emoji/烟花.png b/exporter/resources/emoji/烟花.png new file mode 100644 index 0000000..330aca8 Binary files /dev/null and b/exporter/resources/emoji/烟花.png differ diff --git a/exporter/resources/emoji/爆竹.png b/exporter/resources/emoji/爆竹.png new file mode 100644 index 0000000..f1a6a0b Binary files /dev/null and b/exporter/resources/emoji/爆竹.png differ diff --git a/exporter/resources/emoji/爱心.png b/exporter/resources/emoji/爱心.png new file mode 100644 index 0000000..6a9403d Binary files /dev/null and b/exporter/resources/emoji/爱心.png differ diff --git a/exporter/resources/emoji/猪头.png b/exporter/resources/emoji/猪头.png new file mode 100644 index 0000000..f44055c Binary files /dev/null and b/exporter/resources/emoji/猪头.png differ diff --git a/exporter/resources/emoji/玫瑰.png b/exporter/resources/emoji/玫瑰.png new file mode 100644 index 0000000..21638e2 Binary files /dev/null and b/exporter/resources/emoji/玫瑰.png differ diff --git a/exporter/resources/emoji/生病.png b/exporter/resources/emoji/生病.png new file mode 100644 index 0000000..6bca2f0 Binary files /dev/null and b/exporter/resources/emoji/生病.png differ diff --git a/exporter/resources/emoji/疑问.png b/exporter/resources/emoji/疑问.png new file mode 100644 index 0000000..aea4507 Binary files /dev/null and b/exporter/resources/emoji/疑问.png differ diff --git a/exporter/resources/emoji/發.png b/exporter/resources/emoji/發.png new file mode 100644 index 0000000..56c8806 Binary files /dev/null and b/exporter/resources/emoji/發.png differ diff --git a/exporter/resources/emoji/白眼.png b/exporter/resources/emoji/白眼.png new file mode 100644 index 0000000..f2cc5d8 Binary files /dev/null and b/exporter/resources/emoji/白眼.png differ diff --git a/exporter/resources/emoji/皱眉.png b/exporter/resources/emoji/皱眉.png new file mode 100644 index 0000000..e4e0ee1 Binary files /dev/null and b/exporter/resources/emoji/皱眉.png differ diff --git a/exporter/resources/emoji/睡.png b/exporter/resources/emoji/睡.png new file mode 100644 index 0000000..2506f2e Binary files /dev/null and b/exporter/resources/emoji/睡.png differ diff --git a/exporter/resources/emoji/破涕为笑.png b/exporter/resources/emoji/破涕为笑.png new file mode 100644 index 0000000..69b9a49 Binary files /dev/null and b/exporter/resources/emoji/破涕为笑.png differ diff --git a/exporter/resources/emoji/礼物.png b/exporter/resources/emoji/礼物.png new file mode 100644 index 0000000..2eb828c Binary files /dev/null and b/exporter/resources/emoji/礼物.png differ diff --git a/exporter/resources/emoji/社会社会.png b/exporter/resources/emoji/社会社会.png new file mode 100644 index 0000000..0770fa5 Binary files /dev/null and b/exporter/resources/emoji/社会社会.png differ diff --git a/exporter/resources/emoji/福.png b/exporter/resources/emoji/福.png new file mode 100644 index 0000000..9c9158b Binary files /dev/null and b/exporter/resources/emoji/福.png differ diff --git a/exporter/resources/emoji/笑脸.png b/exporter/resources/emoji/笑脸.png new file mode 100644 index 0000000..0c2200d Binary files /dev/null and b/exporter/resources/emoji/笑脸.png differ diff --git a/exporter/resources/emoji/红包.png b/exporter/resources/emoji/红包.png new file mode 100644 index 0000000..862fb6b Binary files /dev/null and b/exporter/resources/emoji/红包.png differ diff --git a/exporter/resources/emoji/翻白眼.png b/exporter/resources/emoji/翻白眼.png new file mode 100644 index 0000000..81ad985 Binary files /dev/null and b/exporter/resources/emoji/翻白眼.png differ diff --git a/exporter/resources/emoji/耶.png b/exporter/resources/emoji/耶.png new file mode 100644 index 0000000..ccb1b82 Binary files /dev/null and b/exporter/resources/emoji/耶.png differ diff --git a/exporter/resources/emoji/胜利.png b/exporter/resources/emoji/胜利.png new file mode 100644 index 0000000..8662127 Binary files /dev/null and b/exporter/resources/emoji/胜利.png differ diff --git a/exporter/resources/emoji/脸红.png b/exporter/resources/emoji/脸红.png new file mode 100644 index 0000000..164ae21 Binary files /dev/null and b/exporter/resources/emoji/脸红.png differ diff --git a/exporter/resources/emoji/色.png b/exporter/resources/emoji/色.png new file mode 100644 index 0000000..5e89b41 Binary files /dev/null and b/exporter/resources/emoji/色.png differ diff --git a/exporter/resources/emoji/苦涩.png b/exporter/resources/emoji/苦涩.png new file mode 100644 index 0000000..7b2314d Binary files /dev/null and b/exporter/resources/emoji/苦涩.png differ diff --git a/exporter/resources/emoji/菜刀.png b/exporter/resources/emoji/菜刀.png new file mode 100644 index 0000000..37714d9 Binary files /dev/null and b/exporter/resources/emoji/菜刀.png differ diff --git a/exporter/resources/emoji/蛋糕.png b/exporter/resources/emoji/蛋糕.png new file mode 100644 index 0000000..4ecd31b Binary files /dev/null and b/exporter/resources/emoji/蛋糕.png differ diff --git a/exporter/resources/emoji/衰.png b/exporter/resources/emoji/衰.png new file mode 100644 index 0000000..49ff6a8 Binary files /dev/null and b/exporter/resources/emoji/衰.png differ diff --git a/exporter/resources/emoji/裂开.png b/exporter/resources/emoji/裂开.png new file mode 100644 index 0000000..16d0001 Binary files /dev/null and b/exporter/resources/emoji/裂开.png differ diff --git a/exporter/resources/emoji/让我看看.png b/exporter/resources/emoji/让我看看.png new file mode 100644 index 0000000..0ccee49 Binary files /dev/null and b/exporter/resources/emoji/让我看看.png differ diff --git a/exporter/resources/emoji/调皮.png b/exporter/resources/emoji/调皮.png new file mode 100644 index 0000000..72e3f1c Binary files /dev/null and b/exporter/resources/emoji/调皮.png differ diff --git a/exporter/resources/emoji/跳跳.png b/exporter/resources/emoji/跳跳.png new file mode 100644 index 0000000..e211b8c Binary files /dev/null and b/exporter/resources/emoji/跳跳.png differ diff --git a/exporter/resources/emoji/转圈.png b/exporter/resources/emoji/转圈.png new file mode 100644 index 0000000..f81e84f Binary files /dev/null and b/exporter/resources/emoji/转圈.png differ diff --git a/exporter/resources/emoji/鄙视.png b/exporter/resources/emoji/鄙视.png new file mode 100644 index 0000000..bf22463 Binary files /dev/null and b/exporter/resources/emoji/鄙视.png differ diff --git a/exporter/resources/emoji/闭嘴.png b/exporter/resources/emoji/闭嘴.png new file mode 100644 index 0000000..b568ecc Binary files /dev/null and b/exporter/resources/emoji/闭嘴.png differ diff --git a/exporter/resources/emoji/阴脸.png b/exporter/resources/emoji/阴脸.png new file mode 100644 index 0000000..f49563a Binary files /dev/null and b/exporter/resources/emoji/阴脸.png differ diff --git a/exporter/resources/emoji/阴险.png b/exporter/resources/emoji/阴险.png new file mode 100644 index 0000000..1995dbf Binary files /dev/null and b/exporter/resources/emoji/阴险.png differ diff --git a/exporter/resources/emoji/难过.png b/exporter/resources/emoji/难过.png new file mode 100644 index 0000000..927c134 Binary files /dev/null and b/exporter/resources/emoji/难过.png differ diff --git a/exporter/resources/emoji/難受.png b/exporter/resources/emoji/難受.png new file mode 100644 index 0000000..7b2314d Binary files /dev/null and b/exporter/resources/emoji/難受.png differ diff --git a/exporter/resources/emoji/骷髅.png b/exporter/resources/emoji/骷髅.png new file mode 100644 index 0000000..7542615 Binary files /dev/null and b/exporter/resources/emoji/骷髅.png differ diff --git a/exporter/resources/emoji/鼓掌.png b/exporter/resources/emoji/鼓掌.png new file mode 100644 index 0000000..4039892 Binary files /dev/null and b/exporter/resources/emoji/鼓掌.png differ diff --git a/exporter/resources/ffmpeg.exe b/exporter/resources/ffmpeg.exe new file mode 100644 index 0000000..e576a7c Binary files /dev/null and b/exporter/resources/ffmpeg.exe differ diff --git a/exporter/resources/template.html b/exporter/resources/template.html new file mode 100644 index 0000000..013321a --- /dev/null +++ b/exporter/resources/template.html @@ -0,0 +1,5435 @@ + + + + + + + + + + +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ chart.title_opts.title }}
-{{ chart.title_opts.subtitle }}
- {{ chart.html_content }} -{{ chart.title_opts.title }}
-{{ chart.title_opts.subtitle }}
-