有同学看了(23)实时采集微信消息(基于主窗体)--微信UI自动化(.Net+C#) 文章后,在了解到这种方式的利弊后,联系我说他想在无人值守的环境下,实时采集某些特定的群或者联系人的消息,并且确保消息不丢失。
所以针对这个需求,我们分析如下
无人值守的情况下我们可用和微信进行UI交互,不会影响到用户正常操作。
该同学只需要采集某些特定的群或者联系人,所以我们只需要监控特定的窗体对象即可。
需要确保消息的完整性,不能丢失对话消息。
经过对上述分析,我们在针对微信的UI自动化的过程中在软件执行之前可以手动(当然也可以自动 ,如需自动请参考 (6)搜索特定微信通讯录联系人-微信UI自动化(.Net+C#) 该文章)打开需要实时采集的群或者好友窗体,记住是以独立窗口的方式打开,这样才能保证消息的准确性和会话消息完整性,因为每个窗体的会话信息都完全呈现在我们的可视区域范围内。
该种方式并不适合在需要和微信交互的情况下使用,所以针对无人值守的场景才适用。
软件视频和部分截图
各位朋友如果时间允许可观看视频直观感受下软件的执行过程,会更加直观清晰,本人将自动化速度调节的慢些,以便更加清晰的感受到自动化带来的魅力。
该截图为微信会话独立窗体的对话消息记录
该截图为软件抓取微信独立会话窗体的截图,通过两张截图对比内容发现微信发送的消息和软件采集的消息是一致的。
实现思路
- 获取当前微信号下面所有打开的独立微信窗体。
- 创建DTO对象,包含消息内容,消息类型,消息发送人,上一条消息(链路结构),用户头像,这里我们要重点强调为什么要定义【上一条消息(链路结构)】这个属性,因为我们UI对象是没有包含消息的唯一标识符的,在采集过程中你无法确定消息是不是已经采集或者是不是重复。增加一个这样的结构和维度来保证消息的准确性。
- 如果某个独立会话窗口时第一次采集则初始化消息缓存。
- 如果不是第一次采集则采集最近的15条消息(根据自己需要)形成消息链路,使用发送人,消息内容做对比,如果在缓存中不存在则视为新消息,如果存在则使用链路进行对比,如果链路中的消息和缓存中的链路消息不一致也视为新消息,链路一致则视为旧消息。
关于链路的解释,我们用一个实际的微信聊天截图来直观的描述,如下描述了一个消息的链路
在比较一个消息是否是新消息,有两种情况,一种是发送人+消息内容不存在缓存,一种是发送人+消息内容存在缓存中,那么这种存在缓存中的消息难道就不是新消息吗?请看下面的图片1,【你好啊】这个消息内容在下图出现了两次,第二次的【你好啊】从缓存中搜索是存在的,但是它又是一条新消息,你不能忽略这条消息,所有面对这种情况你需要采用消息链路的机制,如果链路中的消息中有比对不上的则视为新消息, 参考图2。
图1
图2
技术细节
获取当前微信号下面所有打开的独立微信窗体。
/// <summary>
/// 获取正在打开的聊天窗口(不包含主窗体)
/// </summary>
/// <returns></returns>
public List<AutomationElement> GetWeChatWindows()
{
List<AutomationElement> weChat = new List<AutomationElement>();
var source = WindowApi.GetAllDesktopWindows().ToList();
foreach (var item in source)
{
if (item.szClassName == "ChatWid")
{
var cc = automation.FromHandle(item.hWnd);
if (cc.ControlType == FlaUI.Core.Definitions.ControlType.Window)
weChat.Add(cc );
}
}
return weChat;
}
DTO对象定义
/// <summary>
/// 微信聊天消息模型
/// </summary>
public class WeChatContractMessageDto
{
public WeChatContractMessageDto() {
MessageType = 0;
}
/// <summary>
/// 消息内容
/// </summary>
public string Message { get; set; }
/// <summary>
/// 0消息 1图片 2动画表情
/// </summary>
public int MessageType { get; set; }
/// <summary>
/// 图片二进制对象
/// </summary>
public Bitmap ImageBitMap { get; set; }
/// <summary>
/// 用户头像(准确度不行)
/// </summary>
public Bitmap UserHeader { get; set; }
/// <summary>
/// 时间
/// </summary>
public DateTime Time { get; set; }
/// <summary>
/// 1发送方 2接收方 3时间
/// </summary>
public int UserType { get; set; }
/// <summary>
/// 微信聊天用户
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 消息处理状态
/// </summary>
public MessageExecuteStatus ExecuteStatus { get; set; }
/// <summary>
/// 上一条消息(链路结构)
/// </summary>
public WeChatContractMessageDto PreMessage { get; set; }
public static bool Equals(WeChatContractMessageDto t1, WeChatContractMessageDto t2)
{
if (t1 == null && t2 == null) return true;
if (t1 == null || t2 == null) return false;
if (t1 != null && t2 != null)
{
return t1.UserName == t2.UserName && t1.Message == t2.Message;
}
return false;
}
}
构建消息的方法
private WeChatContractMessageDto BuildChatMessage(FlaUI.Core.AutomationElements.AutomationElement item)
{
var my = item.FindFirstByXPath("/Pane[2]/Pane/Pane/Text");
var to = my == null ? item.FindFirstByXPath("/Pane[1]/Pane/Pane/Text") : null;
if (my != null)
{
var button = item.FindFirstByXPath("/Pane[1]/Button");
var map = button.Capture();
return new WeChatContractMessageDto { UserHeader = map, Message = my.Name.ToString(), UserName = button.Name.ToString(), UserType = 1 };
}
else if (to != null)
{
var button = item.FindFirstByXPath("/Pane[1]/Button");
var map = button.Capture();
return new WeChatContractMessageDto { UserHeader= map, Message = to.Name.ToString(), UserName = button.Name.ToString(), UserType = 2 };
}
return null;
}
第一次初始化消息缓存
if (dto.Init == false)
{
#region 初始化第一次打开的聊天窗口记录
foreach (var chatItem in chatElementSource)
{
if (chatItem.ControlType != FlaUI.Core.Definitions.ControlType.ListItem)
continue;
WeChatContractMessageDto currentMessage = null;
if (chatItem.Name != "[图片]" && chatItem.Name != "[动画表情]")
currentMessage = BuildChatMessage(chatItem);
if (currentMessage == null || currentMessage.UserType == 3)
continue;
var preMessage= dto.Messages.Count > 0 ? dto.Messages[dto.Messages.Count - 1] : null;
//如果是一个人连续发送多条重复记录则忽略本条
if (WeChatContractMessageDto.Equals(preMessage, currentMessage))
continue;
//如果未初始化则全部添加到原始集合中
currentMessage.PreMessage = preMessage;//上一条记录 构造消息链
dto.Messages.Add(currentMessage);
currentMessage.ExecuteStatus = MessageExecuteStatus.Complete;
ExecuteWeChatMessageRemind(dto, currentMessage);
currentMessage.ExecuteStatus = MessageExecuteStatus.Complete;
}
dto.Init = true;//设置为初始化完毕
#endregion
}
消息链路对比
#region 构造最近15条有效记录作为比较的依据
List<WeChatContractMessageDto> tempSource = new List<WeChatContractMessageDto>();
var cnt = chatElementSource.Count-1;
for(int i= cnt ; i >=0; i--)
{
var chatItem = chatElementSource[i];
WeChatContractMessageDto currentMessage = null;
if (chatItem.BoundingRectangle.Bottom == 0&&
chatItem.BoundingRectangle.Height == 0 && chatItem.BoundingRectangle.Width == 0 &&
chatItem.BoundingRectangle.Top == 0 )
continue;
if (chatItem.Name == "以下是新消息")
continue;
if (chatItem.Name != "[图片]" && chatItem.Name != "[动画表情]")
{
currentMessage = BuildChatMessage(chatItem);
}
if (currentMessage == null || currentMessage.UserType == 3)
continue;
if (tempSource.Count >= 15)
break;
var temp= tempSource.Count>0? tempSource[tempSource.Count-1]:null;
if (!WeChatContractMessageDto.Equals(currentMessage, temp))
{
if (temp != null)
temp.PreMessage = currentMessage;
tempSource.Add(currentMessage);
}
}
tempSource.Reverse();
#endregion
#region 分析最近15条记录-如果不存在则视为新消息,如果最近3条记录不匹配也视为新消息
foreach (var ansyTemp in tempSource)
{
//比较最近三条记录链路 如果相等则代表消息是历史消息
var hisEqualMsg = dto.Messages.Where(s => s.UserName == ansyTemp.UserName && s.Message == ansyTemp.Message);
if (hisEqualMsg == null)
{
ansyTemp.PreMessage = dto.Messages.Count > 0 ? dto.Messages[dto.Messages.Count - 1] : null;
dto.Messages.Add(ansyTemp);
//执行新消息记录
ExecuteWeChatMessageRemind(dto, ansyTemp);
Repay(current, ansyTemp);
ansyTemp.ExecuteStatus = MessageExecuteStatus.Complete;
}
else
{
var newMessage = true;
foreach (var hisMsg in hisEqualMsg.ToList())
{
//递归比较
var result= MsgEqual(ansyTemp, hisMsg);
if(result)
newMessage=false;
}
if (newMessage)
{
ansyTemp.PreMessage = dto.Messages.Count > 0 ? dto.Messages[dto.Messages.Count - 1] : null;
dto.Messages.Add(ansyTemp);
//执行新消息记录
ExecuteWeChatMessageRemind(dto, ansyTemp);
Repay(current, ansyTemp);
ansyTemp.ExecuteStatus = MessageExecuteStatus.Complete;
}
}
}
#endregion