摘要
微信4.1.x.x版本的UI采用新的框架开发,能够获取到的信息有限,目前只能获取到消息列表的控件内容。
代码
运行后需要安装2个库,根据提示安装。
如何运行?
- 登录微信电脑版4.1.7.30,不能完全点击❌关闭界面,可以最小化;
- 运行脚本;
- UI界面启动即可自动获取聊天列表的最新的那条消息;
如果无法获取到,请退出微信重新登录再试试。
# -*- coding: utf-8 -*-
# pip install pyqt5 uiautomation
import sys
import re
import datetime
import uiautomation as auto
from PyQt5 import QtCore, QtWidgets
TARGET_DEPTH = 14
def is_time_line(text: str):
text = text.strip()
return bool(re.match(r'^\d{1,2}:\d{2}$', text) or re.match(r'^\d{2}/\d{2}$', text))
def parse_session(name_block: str):
lines = [l.strip() for l in name_block.splitlines()]
lines = [l for l in lines if l]
if not lines:
return None
session_name = lines[0]
time_text = None
for l in reversed(lines):
if is_time_line(l):
time_text = l
break
ignore_keywords = ['已置顶', '消息免打扰', '撤销']
message_line = None
for l in lines[1:]:
if l == time_text:
continue
if any(k in l for k in ignore_keywords):
continue
message_line = l
if not message_line:
return None
msg_type = "文本"
if ':' in message_line:
sender, content = message_line.split(':', 1)
return {
"group_name": session_name,
"sender": sender.strip().strip('"'),
"content": content.strip(),
"time": time_text,
"msg_type": msg_type
}
return {
"group_name": session_name,
"sender": "我",
"content": message_line,
"time": time_text,
"msg_type": msg_type
}
def time_to_sort_key(time_str):
"""把 HH:MM 或 MM/DD 转成 datetime,用于排序"""
if not time_str:
return datetime.datetime.min
try:
if re.match(r'^\d{1,2}:\d{2}$', time_str):
h, m = map(int, time_str.split(":"))
now = datetime.datetime.now()
return datetime.datetime(now.year, now.month, now.day, h, m)
elif re.match(r'^\d{2}/\d{2}$', time_str):
month, day = map(int, time_str.split("/"))
now = datetime.datetime.now()
return datetime.datetime(now.year, month, day)
except:
return datetime.datetime.min
return datetime.datetime.min
class FetchThread(QtCore.QThread):
data_signal = QtCore.pyqtSignal(list)
def __init__(self, interval=1.5):
super().__init__()
self.interval = interval
self._running = False
auto.SetGlobalSearchTimeout(10)
def run(self):
self._running = True
while self._running:
try:
results = self.fetch_data()
if results:
# 按时间倒序排序
results.sort(key=lambda x: time_to_sort_key(x.get("time")), reverse=True)
self.data_signal.emit(results)
except:
pass
self.msleep(int(self.interval * 1000))
def stop(self):
self._running = False
def set_interval(self, interval):
self.interval = interval
def fetch_data(self):
result_list = []
root = auto.GetRootControl()
target = root.Control(searchDepth=5, ClassName='mmui::MainWindow')
if not target.Exists(2):
return result_list
def dump(control, depth=0):
if depth == TARGET_DEPTH:
try:
if control.ClassName == 'mmui::ChatSessionCell' and control.Name:
result = parse_session(control.Name)
if result:
result_list.append(result)
except:
pass
return
for child in control.GetChildren():
dump(child, depth + 1)
dump(target)
return result_list
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.resize(1300, 690)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.main_layout = QtWidgets.QVBoxLayout(self)
self.main_layout.setContentsMargins(8, 8, 8, 8)
self.container = QtWidgets.QFrame()
self.container.setObjectName("container")
self.container_layout = QtWidgets.QVBoxLayout(self.container)
self.container_layout.setContentsMargins(20, 20, 20, 20)
self.main_layout.addWidget(self.container)
# 顶部栏
title_bar = QtWidgets.QHBoxLayout()
self.container_layout.addLayout(title_bar)
self.title = QtWidgets.QLabel("微信会话实时监听")
self.title.setStyleSheet("font-size:18px;font-weight:bold;")
self.close_btn = QtWidgets.QPushButton("✕")
self.close_btn.setFixedSize(36, 32)
self.close_btn.clicked.connect(self.close)
title_bar.addWidget(self.title)
title_bar.addStretch()
title_bar.addWidget(self.close_btn)
# 切换按钮
self.toggle_btn = QtWidgets.QPushButton()
self.toggle_btn.setFixedSize(120, 60)
self.toggle_btn.setStyleSheet("font-size:18px;font-weight:bold;")
self.container_layout.addWidget(self.toggle_btn, alignment=QtCore.Qt.AlignLeft)
# 表格
self.table = QtWidgets.QTableWidget()
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(
["昵称", "发送者", "消息类型", "内容", "时间"]
)
self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.table.verticalHeader().setVisible(False)
self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setWordWrap(True)
self.container_layout.addWidget(self.table)
# 线程
self.thread = FetchThread(interval=1.5)
self.thread.data_signal.connect(self.update_table)
self.toggle_btn.clicked.connect(self.toggle_listen)
self.setStyleSheet("""
QWidget {
font-family: "Microsoft YaHei";
color: #e6e6e6;
}
#container {
background-color: #1e1f26;
border-radius: 14px;
}
QPushButton {
border-radius: 10px;
font-size:18px;
font-weight:bold;
}
QTableWidget {
background-color: #232530;
gridline-color: #2f3240;
}
QHeaderView::section {
background-color: #2d2f3a;
border: none;
padding: 6px;
}
""")
self._drag_pos = None
self.listening = False
self.start_listen()
self.shown_msgs = set()
# 拖动
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self._drag_pos = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
if self._drag_pos and event.buttons() == QtCore.Qt.LeftButton:
self.move(event.globalPos() - self._drag_pos)
event.accept()
def mouseReleaseEvent(self, event):
self._drag_pos = None
# 监听控制
def toggle_listen(self):
if self.listening:
self.stop_listen()
else:
self.start_listen()
def start_listen(self):
if not self.thread.isRunning():
self.thread.start()
self.listening = True
self.toggle_btn.setText("停止监听")
self.toggle_btn.setStyleSheet("""
QPushButton {
background-color: #c0392b;
border-radius: 10px;
font-size:18px;
font-weight:bold;
}
QPushButton:hover {
background-color: #e74c3c;
}
""")
def stop_listen(self):
self.thread.stop()
self.listening = False
self.toggle_btn.setText("开始监听")
self.toggle_btn.setStyleSheet("""
QPushButton {
background-color: #27ae60;
border-radius: 10px;
font-size:18px;
font-weight:bold;
}
QPushButton:hover {
background-color: #2ecc71;
}
""")
# 表格追加,最新在上
def add_center_item(self, row, col, text):
item = QtWidgets.QTableWidgetItem(text)
item.setTextAlignment(QtCore.Qt.AlignCenter)
self.table.setItem(row, col, item)
# 修改 update_table 方法
def update_table(self, data):
for item in data:
msg_key = (item["group_name"], item["sender"], item["content"], item["time"])
if msg_key in self.shown_msgs:
continue # 已渲染过就跳过
self.shown_msgs.add(msg_key)
self.table.insertRow(0) # 最新消息插在最上面
self.add_center_item(0, 0, item["group_name"])
self.add_center_item(0, 1, item["sender"])
self.add_center_item(0, 2, item["msg_type"])
content_item = QtWidgets.QTableWidgetItem(item["content"])
content_item.setTextAlignment(QtCore.Qt.AlignCenter)
content_item.setToolTip(item["content"])
self.table.setItem(0, 3, content_item)
self.add_center_item(0, 4, item["time"] or "")
self.table.setRowHeight(0, 60)
self.table.setColumnWidth(3, 300)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())界面
更新4.1.8.29版本的
import sys
import time
import re
import uiautomation as auto
from PyQt5.QtWidgets import (
QApplication, QWidget, QVBoxLayout,
QTableWidget, QTableWidgetItem, QHeaderView
)
from PyQt5.QtCore import QThread, pyqtSignal, Qt
TARGET_DEPTH = 16
# ================== 后台监听线程 ==================
class MessageListener(QThread):
new_messages = pyqtSignal(list)
def run(self):
last_snapshot = set()
while True:
try:
window = auto.WindowControl(searchDepth=1, ClassName='mmui::MainWindow')
raw_results = []
def collect(ctrl, level=0):
if level == TARGET_DEPTH:
raw_results.append(ctrl.Name)
for c in ctrl.GetChildren():
collect(c, level + 1)
collect(window)
parsed = []
current_snapshot = set()
for text in raw_results:
text = (text or "").strip()
if not text:
continue
lines = text.splitlines()
# 时间
time_str = ""
time_index = None
for i, line in enumerate(lines):
if re.match(r'\d{1,2}:\d{2}', line):
time_str = line
time_index = i
break
name = lines[0] if lines else ""
msg_lines = lines[1:time_index] if time_index is not None else lines[1:]
msg_lines += lines[time_index+1:] if time_index is not None else []
msg_raw = "\n".join(msg_lines)
# 提取真实消息
if "已置顶" in msg_raw and "消息免打扰" in msg_raw:
real_msg = msg_raw.split("已置顶",1)[1].rsplit("消息免打扰",1)[0].strip()
elif "已置顶" in msg_raw:
real_msg = msg_raw.split("已置顶",1)[1].strip()
else:
real_msg = msg_raw.strip()
key = f"{name}_{time_str}_{real_msg}"
current_snapshot.add(key)
# ✅ 只取“新消息”
if key not in last_snapshot:
parsed.append({
"Name": name,
"Message": real_msg,
"Time": time_str
})
last_snapshot = current_snapshot
if parsed:
self.new_messages.emit(parsed)
except Exception as e:
print("监听失败:", e)
time.sleep(2)
# ================== UI ==================
class MessageTableWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("消息监听")
self.resize(900, 600)
self.layout = QVBoxLayout()
self.table = QTableWidget()
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["Name", "Message", "Time"])
# 表格优化
self.table.setWordWrap(True)
header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.Stretch)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
self.layout.addWidget(self.table)
self.setLayout(self.layout)
# 启动监听
self.listener = MessageListener()
self.listener.new_messages.connect(self.add_messages_top)
self.listener.start()
def add_messages_top(self, messages):
# ✅ 倒序插入,保证最新在最上面
for msg in reversed(messages):
self.table.insertRow(0)
name_item = QTableWidgetItem(msg['Name'])
msg_item = QTableWidgetItem(msg['Message'])
time_item = QTableWidgetItem(msg['Time'])
msg_item.setTextAlignment(Qt.AlignLeft | Qt.AlignTop)
self.table.setItem(0, 0, name_item)
self.table.setItem(0, 1, msg_item)
self.table.setItem(0, 2, time_item)
# 自适应高度
self.table.resizeRowsToContents()
# ================== 启动 ==================
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MessageTableWindow()
win.show()
sys.exit(app.exec_())本文作者
TANKING
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。