文章目录
摘要
本文实现了一种使用文字识别的方法来构建的微信群聊机器人,调用openai来实现智能聊天。众所周知,微信机器人的项目现在由于网页版微信被ban,大部分都已经不可用了。我的常用微信号和小号都无法登录网页版微信,于是寻找能够通过pc端来实现微信机器人的方法,找到诸如wechaty等项目。这些项目通过dll注入的方式调用微信客户端的内部方法来接受和发送消息。但可惜的是这些项目都是收费的,本身这就是个伪需求,只是做着玩的,要花钱就不太值当了。既然找不到现成的框架,那么试试自己能不能实现吧!本身我也没学过逆向,想要破解微信来实现dll注入对我来说太过困难。而且就算凭我的三脚猫功夫能实现需求,估计不超过一周就能被封号😥。只能想个更安全的方式了,就是使用模拟操作来直接操作微信客户端,当然这种方式实现的功能有着非常大的局限性,不过只是做着玩的话也勉强可以用。
实现思路
使用python来实现,有着强大的第三方库的支持,避免自己造轮子。
- 我们首先需要能够监听到群里发送的消息。使用
pyautogui
库的screenshot()
方法对屏幕进行截图。 - 之后要从截图中获得最后一条消息框的位置,并对消息框进行截图。可知消息框的左边界是固定的位置(只要你不缩放微信界面)。那就从这条线最底下往上找rgb为(255,255,255)白色的坐标。就可以获得消息框左下角坐标。在从该坐标分别向上找和向右找白色的部分,就可以得到消息框的大小了。
3. 获得消息框的截图后,对其使用文字识别吧。一开始使用了tesseract来进行文字识别,但其中文的识别不出正确文字,不知到是我配置有问题吗?当把图片放大8倍的时候好歹能识别出几个字了😑。总之又换成了
CnOcr
来进行文字识别了,虽然有时候也识别错误,但也足够用了。
4. 获得新消息后,调用openai的api来获得要回复的文本。在用pyperclip
的copy()
方法复制文字进剪切板,pyautogui
来实现ctrl+v
粘贴,alt+s
发送消息。
实现代码
废话不多说了,这个代码是花了两天写的,仍有很多不足,但小心点使用还是可以的。
import pyautogui
from pynput.keyboard import Key, Controller
from pyperclip import copy,paste
from PIL import Image, ImageChops
import time
import io
from cnocr import CnOcr
from queue import Queue
import openai
import threading
import logging
"""
向队列中添加元素,并保持队列元素的个数。
args:
queue:队列
limit:限制队列元素的个数
item:要添加的元素
"""
def queue_limit_put(queue,limit,item):
queue.put(item)
if queue.qsize()>limit:
queue.get()
def send_msg_on_wechat(message,response):
send_msg=message+"\n--------\n"+response
copy(send_msg)
pyautogui.hotkey('ctrl', 'v')
pyautogui.hotkey('alt','s')
class WeChatRobot:
def __init__(self) -> None:
self.snap_position=[300,60,980,825]#截图框的位置:left,top,length,height
self.dialog_left=340#消息框的左边界
self.ocr=CnOcr()
self.message_list=Queue()#新消息队列
self.robot_name="%小马"#机器人的唤醒名字
self.using_name=True#True时要带机器人名字的消息才会回复,False所有消息都回复
self.snap_error_check=True
"""
对屏幕区域进行截图
args:
x,y:左上点的坐标
m:长度
n:高度
"""
def snap(self,x,y,m,n):
try:
im=pyautogui.screenshot(region=(x,y,m,n))
self.snap_error_check=True
except Exception as e:
if self.snap_error_check:
self.snap_error_check=False#防止发送太多的崩溃消息
send_msg_on_wechat('我崩溃啦!','截图失败了!')
return im
#对比两张图片相似度
def compare_image(self,screenshot1,screenshot2):
image1=screenshot1
image2=screenshot2
diff=ImageChops.difference(image1,image2)
diff_image = diff.convert('RGBA')
diff_count = diff.getbbox()
if diff_count is None:#与上一张截图相同
return False
similarity = 1 - ((diff_count[2]-diff_count[0]) * (diff_count[3]-diff_count[1])) / (image1.width * image1.height)
if __debug__:
print(f"相似度:{similarity}")
if similarity>=0.95:#相似度大于0.95判定为相同,低于0.95判定为有新消息
return False
return True
#寻找对话框的坐下点坐标
def find_dialog_leftbott(self,im):
x=80
y=self.snap_position[3]-1
while y>0:
if im.getpixel((x,y))==(255,255,255):
return (x,y)
else:
y=y-1
return (-1,-1)
#寻找对话框的长与高,(x,y)为左下点坐标
def find_dialog_lenhei(self,im,x,y):
right=x
top=y
while right<self.snap_position[2]:
if im.getpixel((right,y))!=(255,255,255):
break
right=right+1
while top>0:
if im.getpixel((x,top))!=(255,255,255):
break
top=top-1
if right==self.snap_position[2] or top==0:
return (-1,-1)
return (right-x,y-top)
# def test_ai_reply(self):
# time.sleep(1)
# for i in range(2):
# self.message_list.put(f"你是谁")
def run(self):
privous_screenshot=None
time.sleep(5)#等我切换到微信页面
while True:
#对微信界面进行截图作为原始图
im=self.snap(self.snap_position[0],self.snap_position[1],self.snap_position[2],self.snap_position[3])
#寻找消息框并截图
(dialog_x,dialog_y)=self.find_dialog_leftbott(im)
if dialog_x==-1 or dialog_y==-1:
logger.warning("查找对话框位置失败!")
if __debug__:
print(f"dialog_x,dialog_y:{dialog_x,dialog_y}")
(dialog_len,dialog_hei)=self.find_dialog_lenhei(im,dialog_x,dialog_y)
if dialog_len==-1 or dialog_hei==-1:
logger.warning("查找对话框长宽失败!")
if __debug__:
print(f"dialog_len,dialog_hei{dialog_len,dialog_hei}")
#根据获得的参数截取对话框的图片
dialog_image=im.crop((dialog_x,dialog_y-dialog_hei,dialog_x+dialog_len,dialog_y))
hasNews=False
if privous_screenshot is not None:
#与上一张截图进行对比,相似度是否在阈值内
#此处只对比了消息框的内容,会导致用户连续发送相同的内容不会认为其是新消息,暂时没解决办法
hasNews=self.compare_image(privous_screenshot,dialog_image)
if hasNews:
#使用cnocr进行识别,部分文字仍识别不正确,但正确率仍可以接受
result=self.ocr.ocr(dialog_image)
res_text=""
for element in result:#拼接识别的文字
res_text=res_text+element.get('text')
logger.info(f"识别新消息:{res_text}")
if self.using_name:
if res_text.startswith(self.robot_name):
res_text=res_text.replace(self.robot_name,"")#识别出机器人名字,然后从消息中去除机器人名字
self.message_list.put(res_text)
else:
self.message_list.put(res_text)
privous_screenshot=dialog_image
time.sleep(0.5)
class AI_reply:
def __init__(self,queue) -> None:
self.queue=queue
self.api_key='abcdefghijklmnopqrstuvwxyz'#换成你的openai api key
openai.api_key=self.api_key
openai.proxy = 'http://127.0.0.1:1234'#设置下代理,否则连不上网络
self.model='gpt-3.5-turbo'
self.temperature=0.7
self.max_tokens=200#设置200回复字数上限,担心我的额度啊
self.n=1
self.msg_context=Queue()#保存的上下文
self.msg_context_limit=5#上下文大小,影响ai能记住几句之前的内容
self.keyborad=Controller()
# def test_api(self):
# messages = [{ "role": "user", "content": '欢迎使用openai!' }]
# model = 'gpt-3.5-turbo'
# response = openai.ChatCompletion.create(
# model=model,
# messages=messages,
# temperature=self.temperature,
# n=self.n
# )
# queue_limit_put(self.msg_context,self.msg_context_limit,response.choices[0].message)
# if __debug__:
# print(f'\033[31m收到的数据:{response.choices[0].message.content}\033[0m')
#制作消息放入上下文中
def create_message(self,content):
message={
"role":"user",
"content":content
}
queue_limit_put(self.msg_context,self.msg_context_limit,message)
return self.msg_context
#请求api,获得返回结果
def get_response(self,message):
try:
response=openai.ChatCompletion.create(
model=self.model,
messages=message,
temperature=self.temperature,
max_tokens=self.max_tokens
)
except openai.APIConnectionError as e:
logger.error(e)
return ('error','网络又抽风了,这也是没办法的事。')
queue_limit_put(self.msg_context,self.msg_context_limit,response.choices[0].message)#将返回的结果放入上下文中,让ai能记住说了什么
logger.info(f"从openai获得回复:{response.choices[0].message.content}")
return ('success',response.choices[0].message.content)
def run(self):
while True:
logger.info("等待新消息。")
msg=self.queue.get()
logger.info(f"获得新消息,正在请求openai:{msg}")
self.create_message(msg)
logger.info(f"token:{list(self.msg_context.queue)}")
(statue,res)=self.get_response(list(self.msg_context.queue))
if statue=='success':
self.send_msg_on_wechat(msg,res)
elif statue=='error':
self.send_msg_on_wechat('链接不上openai了',res)
logger=logging.getLogger('robot_logger')
logger.setLevel("INFO")
file_handler = logging.FileHandler("C:\\Users\\22308\\Documents\\python file\\log\\robot.txt")
file_handler.setLevel("INFO")
fmt =logging.Formatter('%(filename)s-%(lineno)d-%(asctime)s-%(levelname)s-%(message)s')
file_handler.setFormatter(fmt)
logger.addHandler(file_handler)
logger.info("开始运行!")
robot=WeChatRobot()
ai=AI_reply(robot.message_list)
logger.info("初始化完成")
#创建线程监控是否有新消息,然后请求api
thread = threading.Thread(target=ai.run)
thread.start()
robot.run()
thread.join()
logger.info("运行结束!")
遇到问题
写一下遇到的一些小问题吧。
pymouse报错缺少windows和win32api
虽然最后也没用到这个库,但遇到了就写一下吧。
缺少windows:
pip install PyUserinput
缺少win32api:
pip install pypiwin32
module ‘PIL.Image’ has no attribute ‘ANTIALIAS’
在pillow的10.0.0版本中,ANTIALIAS方法被删除了,改为10.0版本许可的参数即可。
-
方案一,修改ddddocr的_init_.py文件,将其中的ANTIALIAS替换为新方法:
image = image.resize((int(image.size[0] * (64 / image.size[1])), 64), Image.LANCZOS).convert('L')
-
方案二,降级Pillow的版本
pip uninstall -y Pillow
pip install Pillow==9.5.0
获取openai api key
在openai的官网上注册好后,到API文档中激活api key,每个月有5美刀的额度,持续三个月。不幸的是我的key不知道怎么回事已经申请过了,现在已经过期了😅。只好去买一个账号了,好在也不贵。
pip install CnOcr提示需要c++编译环境
找解决方法时有说安装anaconda可以,安装visual cpp build tool的。但总有地方卡住我。最后还是直接安装了virtual sudio,下了6个G😅。
总结
除了接入chatgpt以外,其实还可以使用一些固定指令来实现天气,咨询等内容,有了框架后这些也好写了。现在这些内容放到相亲相爱一家人里装个逼足够了。当然如果大家有更好用的技术或者其他建议的话欢迎告诉我。最后放一下我老婆。