新增”统计信息“功能

This commit is contained in:
shuaikangzhou 2024-01-27 00:12:55 +08:00
parent 120a47009c
commit 7048cbf170
11 changed files with 176 additions and 11 deletions

View File

@ -240,6 +240,13 @@ class Msg:
return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result
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:
return None
if time_range:
@ -260,11 +267,11 @@ class Msg:
lock.release()
else:
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
from MSG
where StrTalker=? and Type=? and strftime('%Y', CreateTime, 'unixepoch', 'localtime') = ?
order by CreateTime
'''
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
where StrTalker=? and Type=? and strftime('%Y', CreateTime, 'unixepoch', 'localtime') = ?
order by CreateTime
'''
try:
lock.acquire(True)
self.cursor.execute(sql, [username_, type_, year_])

View File

@ -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'):
import jieba
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'
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')
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)

View 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

View 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

View 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

View File

@ -60,7 +60,11 @@ class ContactInfo(QWidget, Ui_Form):
self.toolButton_output.showMenu()
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):
if 'room' in self.contact.wxid:

View File

@ -199,7 +199,7 @@ QComboBox::down-arrow:on
QLineEdit
{
background:transparent;
border-radius:15px;
border-radius:10px;
border-top: 0px solid #b2e281;
border-bottom: 1px solid rgb(227,228,222);
border-right: 1px solid rgb(227,228,222);

View File

@ -6,12 +6,11 @@ from typing import List
from PyQt5 import QtWidgets
from PyQt5.QtCore import QTimer, QThread, pyqtSignal, QObject
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.output_pc import Output
from app.components import ScrollBar
from app.components.calendar_dialog import CalendarDialog
from app.components.export_contact_item import ContactQListWidgetItem
from app.person import Contact
from app.ui.menu.exportUi import Ui_Dialog

View File

@ -1,6 +1,7 @@
from datetime import datetime
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 app.components.calendar_dialog import CalendarDialog
@ -33,6 +34,7 @@ class TimeRangeDialog(QDialog, Ui_Dialog):
self.calendar = None
self.setupUi(self)
self.setWindowTitle('选择日期范围')
# self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
self.setStyleSheet(Stylesheet)
self.toolButton_start_time.clicked.connect(self.select_date_start)
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.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__':
import sys

View 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>

View File

@ -1,8 +1,10 @@
import os
import sys
import time
import requests
from flask import Flask, render_template, send_file, jsonify, make_response
from pyecharts.charts import Bar
from app.DataBase import msg_db
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')
# 示例数据,实际应用中你需要替换成从数据库或其他数据源获取的数据
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__":
app.run(debug=True, host='0.0.0.0')