整理 | 小耕家的喵大仙
出品 | CSDN(ID:lichao19897314)
Q Q | 978124155
关联源码及工具下载https://download.csdn.net/download/lichao19897314/90096681
往期知识回顾
(1)开启探索微信自动化之路-微信UI自动化(.Net+C#)
(2)初始化微信窗体UI自动化实例-微信UI自动化(.Net+C#)
(3)采用热键终止微信采集任务-微信UI自动化(.Net+C#)
(4)采集微信通讯录和联系人-微信UI自动化(.Net+C#)
(5)实现对微信窗体元素静默操作-微信UI自动化(.Net+C#)
(6)搜索特定微信通讯录联系人-微信UI自动化(.Net+C#)
(7)定时群发微信图文消息-微信UI自动化(.Net+C#)
(8)监控微信进程运行状态-微信UI自动化(.Net+C#)
(9)监控微信网络连接状态-微信UI自动化(.Net+C#)
(10)实现微信窗体自动跟随移动-微信UI自动化(.Net+C#)
(11)实现微信窗体尺寸跟随自动调整-微信UI自动化(.Net+C#)
(12)采集微信消息记录及历史消息-微信UI自动化(.Net+C#)
(13)自动回复微信聊天消息-微信UI自动化(.Net+C#)
(14)微信窗体元素截图操作-微信UI自动化(.Net+C#)
(15)针对微信主窗体的行为控制-微信UI自动化(.Net+C#)
(17)自动采集微信聊天信息中的文件-微信UI自动化(.Net+C#)
(18)采集微信群成员信息-微信UI自动化(.Net+C#)
(20)批量将微信群成员添加为好友-微信UI自动化(.Net+C#)
(21)批量删除微信联系人-微信UI自动化(.Net+C#)
(22)采集微信通讯录详情面板-微信UI自动化(.Net+C#)
(23)实时采集微信消息(基于主窗体)--微信UI自动化(.Net+C#)
(24)实时采集微信消息(基于独立窗体)-微信UI自动化(.Net+C#)
👆😀以上文章是以往使用自动化方案操作微信的一些案例!如有兴趣请点击浏览!
因为文章可能无法满足读者要求,如需源码和支持请联系本人 QQ 978124155
本篇目的
有同学在看了(23)实时采集微信消息(基于主窗体)--微信UI自动化(.Net+C#) 和(24)采集微信实时消息(基于独立窗体)-微信UI自动化(.Net+C#)文章两篇后,一位做公安系统的朋友联系我并获取了采用链表方式获取独立聊天窗体的消息的源码后,后经过测试发现在消息出现撤回的情况下会造成链表断裂,数据比对接不上,导致功能异常。针对这个问题,我也是不好解决,当遇到消息撤回重新构造链表?好像也不那么准确,其实归根到底还是数据分析维度不够导致UI消息构造不精准,因为主要的原因在于消息重复和消息撤回导致我们程序不好分析。经过两天的研究发现,微信在聊天记录窗体中存在每条消息的发送时间,虽然精度是分钟级别的,但是对UI自动化来说已经又是一到曙光,经过两天时间的编码和调试,已经形成了初步成果并记录在此。
所以针对采集聊天记录窗体这个需求,我们分析如下
- 首先我们采集的对象还是针对处于独立聊天窗体状态下的无人值守的微信子窗体,如下图一示例则代表采集3个群的实时聊天消息。
- 针对某个单独的聊天窗体我们需要自动打开聊天记录窗体。
- 用最后一次采集时间跟当前时间做对比,比如1点29分是最后一次采集,现在是1点31分,得到中间的时间差为2分钟,那么我们就滚动聊天记录到1点29分最开始的一条聊天消息处。滚动读取这两分钟内全部的聊天记录,一般实时监控任务一直开着都是读取最近一分钟内的。所以消息的实时性是很高的。
经过对上述分析,我们在针对微信的UI自动化的过程中在软件执行之前可以手动(当然也可以自动 ,如需自动请参考 (6)搜索特定微信通讯录联系人-微信UI自动化(.Net+C#) 该文章)打开需要实时采集的群或者好友窗体,记住是以独立窗口的方式打开,这样才能保证消息的准确性和会话消息完整性,因为每个窗体的会话信息都完全呈现在我们的可视区域范围内。
该种方式并不适合在需要和微信交互的情况下使用,所以针对无人值守的场景才适用
软件视频和部分截图
各位朋友如果时间允许可观看视频直观感受下软件的执行过程,会更加直观清晰,本人将自动化速度调节的慢些,以便更加清晰的感受到自动化带来的魅力。
采集微信实时消息(通过聊天记录面板)
以下两张图分别为软件运行过程中的采集截图和微信独立窗体的截图,对比两张图可以发现中间的内容和顺序是一致的,说明该方法经过验证是可行并达到了实际效果。
图一
图二
实现思路
- 获取已经打开的微信独立聊天窗口,如果没有则继续监听,这里我们用【微信UI自动化学习】窗体为示例。
- 将【微信UI自动化学习】聊天窗体设置为焦点后,点击【聊天记录】按钮。
- 搜索和【微信UI自动化学习】标题一致的【聊天记录】窗体,如果没有则代表打开失败,重试步骤2。如果存在则将【微信UI自动化学习】聊天记录窗体设置成焦点状态。
- 从我们的任务缓存中找到采集【微信UI自动化学习】实时消息的最后一次采集时间跟当前时间做对比,比如1点29分是最后一次采集,当前时间是1点31分,得到中间的时间差为2分钟。
- 针对【微信UI自动化学习】的聊天记录窗体的消息列表执行滚动条向上滚动事件,滚动到聊天记录到1点29分最开始的一条聊天消息处。
- 接下来我们开始读取聊天记录,开始向下滚动消息记录列表,读取这两分钟内全部的聊天记录,并删除最近2分钟的缓存使用最新的数据做替换。
- 采集完成后我们需要自动关闭【微信UI自动化学习】的聊天记录窗体,因为聊天记录窗体中的消息不会实时更新。所以需要主动光笔,等待下一次采集时在打开。
技术实现
获取打开的微信独立聊天窗口
/// <summary>
/// 获取正在打开的聊天窗口(不包含主窗体)
/// </summary>
/// <returns></returns>
public List<AutomationElement> GetWeChatWindows()
{
try
{
List<AutomationElement> weChat = new List<AutomationElement>();
var source = WindowApi.GetAllDesktopWindows().ToList();
foreach (var item in source)
{
if (item.szClassName == "chatwnd")
{
var cc = automation.FromHandle(item.hWnd);
weChat.Add(cc);
}
}
return weChat;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
聊天记录窗体的打开关闭管理
using FlaUI.Core.AutomationElements;
using System.Threading;
namespace FlaUI.FrameWork.WinForm.SimpleEnhance
{
/// <summary>
/// 聊天记录窗体管理
/// </summary>
public class ChatHistoryFormController
{
/// <summary>
/// 打开历史窗体
/// </summary>
/// <param name="window"></param>
public bool Open(AutomationElement window)
{
if (window == null)
return false;
window.Focus();
var chatRecordButton = window.FindFirstByXPath(@"/Pane[1]/Pane/Pane[1]/Pane/Pane/Pane[2]/Pane[2]/Pane[2]/Pane/ToolBar/Button[5]");
if (chatRecordButton != null)
{
chatRecordButton.Click();
Thread.Sleep(300);
return true;
}
return false;
}
/// <summary>
/// 获取当前聊天窗口对应的聊天记录窗口
/// </summary>
/// <param name="window"></param>
/// <returns></returns>
public AutomationElement GetChatHistoryForm(AutomationElement window)
{
if (window == null)
return null;
var source = UI_WX_Window.Current.GetWeChatHisWindows();
foreach (var hisWindow in source)
{
if (hisWindow.Name == window.Name)
{
return hisWindow;
}
}
return null;
}
/// <summary>
/// 关闭历史窗体
/// </summary>
/// <param name="window"></param>
public bool Close(AutomationElement window)
{
if (window == null)
return false;
var hisWindow = GetChatHistoryForm(window);
if (hisWindow == null)
return false;
var closeButton = hisWindow.FindFirstByXPath(@"/Pane[1]/Pane[2]/Button[2]");
if (closeButton != null)
{
closeButton.Click();
Thread.Sleep(300);
return true;
}
return false;
}
}
}
定位到某个时间点
/// <summary>
/// 定位到某个时间点
/// </summary>
/// <param name="historyWindow"></param>
/// <param name="lastCollectionTime"></param>
private void LocationTime(AutomationElement historyWindow,DateTime lastCollectionTime) {
var list = historyWindow.FindFirstByXPath("/Pane[1]/Pane[3]/Pane/Pane[2]/Pane/List");
if (list == null)
return;
while (true)
{
var messageElements = list.FindAllChildren().ToList();
if (messageElements.Count <= 0)
break;
var currentMsg = new ChatMessageBuildCotroller().IsToday(messageElements[0]);
if (!currentMsg)
break;
var time = new ChatMessageBuildCotroller().GetReceiveTime(messageElements[0]);
if ( time < lastCollectionTime)
{
break; ;
}
contactScroll.SetScrollPercent(0, 0);
Thread.Sleep(100);
}
}
从某个时间点构建消息到底部
/// <summary>
/// 从某个时间点构建消息到底部
/// </summary>
/// <param name="historyWindow"></param>
private List<WeChatMessageDto> BuildMessage(AutomationElement historyWindow)
{
List<WeChatMessageDto> source = new List<WeChatMessageDto>();
var list = historyWindow.FindFirstByXPath("/Pane[2]/Pane[3]/Pane/Pane[2]/Pane/List");
if (list == null)
return source;
while (true)
{
var messageElements = list.FindAllChildren().ToList();
if (messageElements.Count <= 0)
break;
foreach (var messageElement in messageElements)
{
if (messageElement.ControlType == Core.Definitions.ControlType.ListItem)
{
var currentMsg = new ChatMessageBuildCotroller().Get(messageElement);
if (currentMsg != null)
{
if (source.Count(item => item.Message == currentMsg.Message && item.ReceiveTime == currentMsg.ReceiveTime && item.UserName == currentMsg.UserName) <= 0)
source.Add(currentMsg);
}
}
if (messageElement.ControlType == Core.Definitions.ControlType.Pane)
return source;
}
Thread.Sleep(100);
}
return source;
}
消息记录的创建根据UI对象
using FlaUI.Core.AutomationElements;
using System;
using System.Threading;
namespace FlaUI.FrameWork.WinForm.SimpleEnhance
{
/// <summary>
/// 聊天记录生成
/// </summary>
public class ChatMessageBuildCotroller
{
public WeChatMessageDto Get(AutomationElement chatItem)
{
WeChatMessageDto currentMessage = null;
var name = chatItem.Name;
if (string.IsNullOrEmpty(name))
{
currentMessage = BuildChatMessage(chatItem);
}
else if (name == "[图片]")
{
currentMessage = BuildImageMessage(chatItem);
}
else if (name == "[文件]")
{
currentMessage = BuildFileMessage(chatItem);
}
return currentMessage;
}
/// <summary>
/// 获取消息接收
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public DateTime? GetReceiveTime(AutomationElement item)
{
var time = item.FindFirstByXPath("/Pane/Pane/Pane[1]/Text[2]");
var timeStr = time?.Name;
if (string.IsNullOrEmpty(timeStr))
return null;
if (timeStr.Contains(":"))
{
return DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd ") + timeStr + ":00");
}
return null;
}
/// <summary>
/// 是否是今天的消息
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public bool IsToday(AutomationElement item)
{
var time = item.FindFirstByXPath("/Pane/Pane/Pane[1]/Text[2]");
var timeStr = time?.Name;
if (string.IsNullOrEmpty(timeStr))
return false;
if (timeStr.Contains(":"))
return true;
return false;
}
/// <summary>
/// 是否Ended
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public bool IsEnd(AutomationElement item)
{
return item.ControlType == Core.Definitions.ControlType.Pane;
}
private WeChatMessageDto BuildChatMessage(FlaUI.Core.AutomationElements.AutomationElement item)
{
var time = GetReceiveTime(item);
if (time == null)
return null;
var message = item.FindFirstByXPath("/Pane/Pane/Pane[1]/Pane[1]/Pane/Text");
var button = item.FindFirstByXPath("/Pane/Button");
if (message != null)
{
return new WeChatMessageDto { Message = message.Name.ToString(), ReceiveTime = time.Value, UserName = button.Name.ToString(), UserType = 1 };
}
return null;
}
private WeChatMessageDto BuildImageMessage(FlaUI.Core.AutomationElements.AutomationElement item)
{
var time = GetReceiveTime(item);
if (time == null)
return null;
var button = item.FindFirstByXPath("/Pane/Button");
if (button != null)
{
return new WeChatMessageDto { MessageType = 1, ReceiveTime = time.Value, UserName = button.Name, Message = "[图片]", UserType = 2 };
}
return null;
}
private WeChatMessageDto BuildFileMessage(FlaUI.Core.AutomationElements.AutomationElement item)
{
var time = GetReceiveTime(item);
if (time == null)
return null;
var message = item.FindFirstByXPath("/Pane/Pane/Pane[1]/Pane[1]/Pane/Text");
var button = item.FindFirstByXPath("/Pane/Button");
if (message != null)
{
var fileName = item.FindFirstByXPath(@"/Pane/Pane/Pane[1]/Pane[1]/Pane/Pane/Pane[1]/Text[1]");
var size = item.FindFirstByXPath(@"/Pane/Pane/Pane[1]/Pane[1]/Pane/Pane/Pane[1]/Text[2]");
return new WeChatMessageDto { MessageType = 3, UserName = button.Name, ReceiveTime = time.Value, Message = "[文件]" + fileName?.Name + " " + size?.Name, UserType = 2 };
}
return null;
}
}
}
因为文章可能无法满足读者要求,如需源码和支持请联系本人 QQ 978124155