mirror of
https://github.com/LC044/WeChatMsg
synced 2025-02-22 10:52:18 +08:00
新增”统计信息“功能
This commit is contained in:
parent
120a47009c
commit
7048cbf170
@ -240,6 +240,13 @@ class Msg:
|
|||||||
return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result
|
return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result
|
||||||
|
|
||||||
def get_messages_by_type(self, username_, type_, year_='all', time_range=None):
|
def get_messages_by_type(self, username_, type_, year_='all', time_range=None):
|
||||||
|
"""
|
||||||
|
@param username_:
|
||||||
|
@param type_:
|
||||||
|
@param year_:
|
||||||
|
@param time_range: Tuple(timestamp:开始时间戳,timestamp:结束时间戳)
|
||||||
|
@return:
|
||||||
|
"""
|
||||||
if not self.open_flag:
|
if not self.open_flag:
|
||||||
return None
|
return None
|
||||||
if time_range:
|
if time_range:
|
||||||
@ -260,11 +267,11 @@ class Msg:
|
|||||||
lock.release()
|
lock.release()
|
||||||
else:
|
else:
|
||||||
sql = '''
|
sql = '''
|
||||||
select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent,DisplayContent
|
select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent,DisplayContent
|
||||||
from MSG
|
from MSG
|
||||||
where StrTalker=? and Type=? and strftime('%Y', CreateTime, 'unixepoch', 'localtime') = ?
|
where StrTalker=? and Type=? and strftime('%Y', CreateTime, 'unixepoch', 'localtime') = ?
|
||||||
order by CreateTime
|
order by CreateTime
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
lock.acquire(True)
|
lock.acquire(True)
|
||||||
self.cursor.execute(sql, [username_, type_, year_])
|
self.cursor.execute(sql, [username_, type_, year_])
|
||||||
|
@ -73,6 +73,60 @@ def wordcloud(wxid, is_Annual_report=False, year='2023', who='1'):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def wordcloud_(wxid, is_Annual_report=False, time_range=None):
|
||||||
|
import jieba
|
||||||
|
txt_messages = msg_db.get_messages_by_type(wxid, MsgType.TEXT, time_range=time_range)
|
||||||
|
if not txt_messages:
|
||||||
|
return {
|
||||||
|
'chart_data': None,
|
||||||
|
'keyword': "没有聊天你想分析啥",
|
||||||
|
'max_num': "0",
|
||||||
|
'dialogs': []
|
||||||
|
}
|
||||||
|
# text = ''.join(map(lambda x: x[7], txt_messages))
|
||||||
|
text = ''.join(map(lambda x: x[7], txt_messages)) # 1“我”说的话,0“Ta”说的话
|
||||||
|
|
||||||
|
total_msg_len = len(text)
|
||||||
|
# 使用jieba进行分词,并加入停用词
|
||||||
|
words = jieba.cut(text)
|
||||||
|
# 统计词频
|
||||||
|
word_count = Counter(words)
|
||||||
|
# 过滤停用词
|
||||||
|
stopwords_file = './app/data/stopwords.txt'
|
||||||
|
with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
|
||||||
|
stopwords1 = set(stopword_file.read().splitlines())
|
||||||
|
# 构建 FFmpeg 可执行文件的路径
|
||||||
|
stopwords = set()
|
||||||
|
stopwords_file = './app/resources/data/stopwords.txt'
|
||||||
|
if not os.path.exists(stopwords_file):
|
||||||
|
resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
|
||||||
|
stopwords_file = os.path.join(resource_dir, 'app', 'resources', 'data', 'stopwords.txt')
|
||||||
|
with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
|
||||||
|
stopwords = set(stopword_file.read().splitlines())
|
||||||
|
stopwords = stopwords.union(stopwords1)
|
||||||
|
filtered_word_count = {word: count for word, count in word_count.items() if len(word) > 1 and word not in stopwords}
|
||||||
|
|
||||||
|
# 转换为词云数据格式
|
||||||
|
data = [(word, count) for word, count in filtered_word_count.items()]
|
||||||
|
# text_data = data
|
||||||
|
data.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
text_data = data[:100] if len(data) > 100 else data
|
||||||
|
# 创建词云图
|
||||||
|
keyword, max_num = text_data[0]
|
||||||
|
w = (
|
||||||
|
WordCloud(init_opts=opts.InitOpts(width=f"{wordcloud_width}px", height=f"{wordcloud_height}px"))
|
||||||
|
.add(series_name="聊天文字", data_pair=text_data, word_size_range=[5, 100])
|
||||||
|
)
|
||||||
|
# return w.render_embed()
|
||||||
|
return {
|
||||||
|
'chart_data': w.dump_options_with_quotes(),
|
||||||
|
'keyword': keyword,
|
||||||
|
'max_num': str(max_num),
|
||||||
|
'dialogs': msg_db.get_messages_by_keyword(wxid, keyword, num=5, max_len=12)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def wordcloud_christmas(wxid, year='2023'):
|
def wordcloud_christmas(wxid, year='2023'):
|
||||||
import jieba
|
import jieba
|
||||||
txt_messages = msg_db.get_messages_by_type(wxid, MsgType.TEXT, year)
|
txt_messages = msg_db.get_messages_by_type(wxid, MsgType.TEXT, year)
|
||||||
@ -99,7 +153,7 @@ def wordcloud_christmas(wxid, year='2023'):
|
|||||||
stopwords_file = './app/resources/data/stopwords.txt'
|
stopwords_file = './app/resources/data/stopwords.txt'
|
||||||
if not os.path.exists(stopwords_file):
|
if not os.path.exists(stopwords_file):
|
||||||
resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
|
resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
|
||||||
stopwords_file = os.path.join(resource_dir, 'app', 'resources', 'data','stopwords.txt')
|
stopwords_file = os.path.join(resource_dir, 'app', 'resources', 'data', 'stopwords.txt')
|
||||||
with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
|
with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
|
||||||
stopwords = set(stopword_file.read().splitlines())
|
stopwords = set(stopword_file.read().splitlines())
|
||||||
stopwords = stopwords.union(stopwords1)
|
stopwords = stopwords.union(stopwords1)
|
||||||
|
1
app/resources/icons/down.svg
Normal file
1
app/resources/icons/down.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706096167224" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7281" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M907.1 299.9c-14.6-14.6-38.4-14.6-53 0L513 640.5 172 299.9c-14.6-14.6-38.4-14.6-53 0-14.6 14.6-14.6 38.4 0 53l364.7 364.2c0.8 1 1.7 2 2.7 3 7.3 7.3 17 11 26.7 10.9 9.7 0 19.3-3.6 26.7-10.9 1-1 1.8-1.9 2.7-3l364.7-364.2c14.4-14.6 14.4-38.4-0.1-53z" fill="" p-id="7282"></path></svg>
|
After Width: | Height: | Size: 612 B |
1
app/resources/icons/up.svg
Normal file
1
app/resources/icons/up.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706096206963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8303" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M936.170667 669.952L534.613333 268.394667a32 32 0 0 0-42.986666-2.069334l-2.282667 2.069334L87.829333 669.952a8.533333 8.533333 0 0 0-2.496 6.037333v66.346667a8.533333 8.533333 0 0 0 14.570667 6.058667l412.074667-412.096 412.117333 412.096a8.533333 8.533333 0 0 0 14.570667-6.037334v-66.346666a8.533333 8.533333 0 0 0-2.496-6.058667z" fill="#333333" p-id="8304"></path></svg>
|
After Width: | Height: | Size: 706 B |
1
app/resources/icons/update.svg
Normal file
1
app/resources/icons/update.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706094276810" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5249" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M549.546667 320v176.64l124.586666 124.586667a21.333333 21.333333 0 0 1 0 30.293333l-22.613333 22.613333a21.333333 21.333333 0 0 1-30.293333 0l-140.373334-140.373333a22.613333 22.613333 0 0 1-6.4-14.933333V320a21.333333 21.333333 0 0 1 21.333334-21.333333h32.426666a21.333333 21.333333 0 0 1 21.333334 21.333333z m346.453333 85.333333v-213.333333a21.333333 21.333333 0 0 0-21.333333-21.333333h-12.373334a20.906667 20.906667 0 0 0-15.36 6.4l-63.573333 63.573333A384 384 0 1 0 896 534.613333a21.333333 21.333333 0 0 0-5.546667-15.786666 22.186667 22.186667 0 0 0-15.36-6.826667h-42.666666a21.333333 21.333333 0 0 0-21.333334 20.053333A298.666667 298.666667 0 1 1 512 213.333333a295.68 295.68 0 0 1 210.346667 88.32l-75.946667 75.946667a20.906667 20.906667 0 0 0-6.4 15.36v12.373333a21.333333 21.333333 0 0 0 21.333333 21.333334h213.333334a21.333333 21.333333 0 0 0 21.333333-21.333334z" p-id="5250" fill="#d81e06"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -60,7 +60,11 @@ class ContactInfo(QWidget, Ui_Form):
|
|||||||
self.toolButton_output.showMenu()
|
self.toolButton_output.showMenu()
|
||||||
|
|
||||||
def analysis(self):
|
def analysis(self):
|
||||||
QDesktopServices.openUrl(QUrl("https://memotrace.lc044.love/"))
|
# QDesktopServices.openUrl(QUrl("https://memotrace.lc044.love/"))
|
||||||
|
self.report_thread = ReportThread(self.contact)
|
||||||
|
# self.report_thread.okSignal.connect(lambda x: QDesktopServices.openUrl(QUrl("http://127.0.0.1:21314")))
|
||||||
|
self.report_thread.start()
|
||||||
|
QDesktopServices.openUrl(QUrl("http://127.0.0.1:21314/charts"))
|
||||||
|
|
||||||
def annual_report(self):
|
def annual_report(self):
|
||||||
if 'room' in self.contact.wxid:
|
if 'room' in self.contact.wxid:
|
||||||
|
@ -199,7 +199,7 @@ QComboBox::down-arrow:on
|
|||||||
QLineEdit
|
QLineEdit
|
||||||
{
|
{
|
||||||
background:transparent;
|
background:transparent;
|
||||||
border-radius:15px;
|
border-radius:10px;
|
||||||
border-top: 0px solid #b2e281;
|
border-top: 0px solid #b2e281;
|
||||||
border-bottom: 1px solid rgb(227,228,222);
|
border-bottom: 1px solid rgb(227,228,222);
|
||||||
border-right: 1px solid rgb(227,228,222);
|
border-right: 1px solid rgb(227,228,222);
|
||||||
|
@ -6,12 +6,11 @@ from typing import List
|
|||||||
from PyQt5 import QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
from PyQt5.QtCore import QTimer, QThread, pyqtSignal, QObject
|
from PyQt5.QtCore import QTimer, QThread, pyqtSignal, QObject
|
||||||
from PyQt5.QtGui import QTextCursor
|
from PyQt5.QtGui import QTextCursor
|
||||||
from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox, QCalendarWidget
|
from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox
|
||||||
|
|
||||||
from app.DataBase import micro_msg_db, misc_db
|
from app.DataBase import micro_msg_db, misc_db
|
||||||
from app.DataBase.output_pc import Output
|
from app.DataBase.output_pc import Output
|
||||||
from app.components import ScrollBar
|
from app.components import ScrollBar
|
||||||
from app.components.calendar_dialog import CalendarDialog
|
|
||||||
from app.components.export_contact_item import ContactQListWidgetItem
|
from app.components.export_contact_item import ContactQListWidgetItem
|
||||||
from app.person import Contact
|
from app.person import Contact
|
||||||
from app.ui.menu.exportUi import Ui_Dialog
|
from app.ui.menu.exportUi import Ui_Dialog
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from PyQt5 import QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
from PyQt5.QtCore import QTimer, QThread, pyqtSignal
|
from PyQt5.QtCore import QTimer, QThread, pyqtSignal, Qt, QPoint
|
||||||
|
from PyQt5.QtGui import QMouseEvent
|
||||||
from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox, QCalendarWidget, QWidget, QVBoxLayout, QLabel
|
from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox, QCalendarWidget, QWidget, QVBoxLayout, QLabel
|
||||||
|
|
||||||
from app.components.calendar_dialog import CalendarDialog
|
from app.components.calendar_dialog import CalendarDialog
|
||||||
@ -33,6 +34,7 @@ class TimeRangeDialog(QDialog, Ui_Dialog):
|
|||||||
self.calendar = None
|
self.calendar = None
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setWindowTitle('选择日期范围')
|
self.setWindowTitle('选择日期范围')
|
||||||
|
# self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
|
||||||
self.setStyleSheet(Stylesheet)
|
self.setStyleSheet(Stylesheet)
|
||||||
self.toolButton_start_time.clicked.connect(self.select_date_start)
|
self.toolButton_start_time.clicked.connect(self.select_date_start)
|
||||||
self.toolButton_end_time.clicked.connect(self.select_date_end)
|
self.toolButton_end_time.clicked.connect(self.select_date_end)
|
||||||
@ -77,6 +79,21 @@ class TimeRangeDialog(QDialog, Ui_Dialog):
|
|||||||
self.calendar.set_end_date()
|
self.calendar.set_end_date()
|
||||||
self.calendar.show()
|
self.calendar.show()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, e: QMouseEvent): # 重写移动事件
|
||||||
|
if self._tracking:
|
||||||
|
self._endPos = e.pos() - self._startPos
|
||||||
|
self.move(self.pos() + self._endPos)
|
||||||
|
|
||||||
|
def mousePressEvent(self, e: QMouseEvent):
|
||||||
|
if e.button() == Qt.LeftButton:
|
||||||
|
self._startPos = QPoint(e.x(), e.y())
|
||||||
|
self._tracking = True
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, e: QMouseEvent):
|
||||||
|
if e.button() == Qt.LeftButton:
|
||||||
|
self._tracking = False
|
||||||
|
self._startPos = None
|
||||||
|
self._endPos = None
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
|
37
app/web_ui/templates/charts.html
Normal file
37
app/web_ui/templates/charts.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>微信年度聊天报告</title>
|
||||||
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@latest/dist/echarts.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://assets.pyecharts.org/assets/v5/echarts-wordcloud.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{my_nickname}}——{{ta_nickname}}</h1>
|
||||||
|
<div id="echarts-container" style="width: 600px; height: 400px;"></div>
|
||||||
|
<div id="echarts-wordcloud" style="width: 800px; height: 800px;"></div>
|
||||||
|
<script>
|
||||||
|
// 使用 AJAX 请求获取 Pyecharts 生成的图表配置项数据
|
||||||
|
fetch('/get_chart_options')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(chartOptions => {
|
||||||
|
// 在这里使用 ECharts 渲染图表
|
||||||
|
var myChart = echarts.init(document.getElementById('echarts-container'));
|
||||||
|
var option = chartOptions.chart_data;
|
||||||
|
myChart.setOption(JSON.parse(option));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// 使用 AJAX 请求获取 Pyecharts 生成的图表配置项数据
|
||||||
|
fetch('/wordcloud')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(chartOptions => {
|
||||||
|
// 在这里使用 ECharts 渲染图表
|
||||||
|
var myChart = echarts.init(document.getElementById('echarts-wordcloud'));
|
||||||
|
var option = chartOptions.chart_data;
|
||||||
|
myChart.setOption(JSON.parse(option));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,8 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask import Flask, render_template, send_file, jsonify, make_response
|
from flask import Flask, render_template, send_file, jsonify, make_response
|
||||||
|
from pyecharts.charts import Bar
|
||||||
|
|
||||||
from app.DataBase import msg_db
|
from app.DataBase import msg_db
|
||||||
from app.analysis import analysis
|
from app.analysis import analysis
|
||||||
@ -200,5 +202,47 @@ def get_image(filename):
|
|||||||
return send_file(os.path.join(f"{os.getcwd()}/data/avatar/", filename), mimetype='image/png')
|
return send_file(os.path.join(f"{os.getcwd()}/data/avatar/", filename), mimetype='image/png')
|
||||||
|
|
||||||
|
|
||||||
|
# 示例数据,实际应用中你需要替换成从数据库或其他数据源获取的数据
|
||||||
|
echarts_data = {
|
||||||
|
'categories': ['Category A', 'Category B', 'Category C', 'Category D', 'Category E'],
|
||||||
|
'data': [20, 50, 80, 45, 60]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_chart():
|
||||||
|
# 使用 Pyecharts 生成图表
|
||||||
|
bar = Bar()
|
||||||
|
bar.add_xaxis(echarts_data['categories'])
|
||||||
|
bar.add_yaxis('Data', echarts_data['data'])
|
||||||
|
return bar.dump_options_with_quotes()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/get_chart_options')
|
||||||
|
def get_chart_options():
|
||||||
|
chart_options = generate_chart()
|
||||||
|
data = {
|
||||||
|
'chart_data': chart_options
|
||||||
|
}
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/wordcloud')
|
||||||
|
def get_wordcloud():
|
||||||
|
|
||||||
|
time_range = (0, time.time())
|
||||||
|
print(time_range)
|
||||||
|
world_cloud_data = analysis.wordcloud_(contact.wxid, time_range=time_range)
|
||||||
|
return jsonify(world_cloud_data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/charts')
|
||||||
|
def charts():
|
||||||
|
data = {
|
||||||
|
'my_nickname':Me().name,
|
||||||
|
'ta_nickname':contact.remark,
|
||||||
|
}
|
||||||
|
return render_template('charts.html',**data)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, host='0.0.0.0')
|
app.run(debug=True, host='0.0.0.0')
|
||||||
|
Loading…
Reference in New Issue
Block a user