使用Python编写一个微信机器人

最近心血来潮,想通过Python编写一个微信机器人。网上搜索了一下可以使用itchat这个包来完成:
https://github.com/why2lyj/ItChat-UOS
需要注意的时普通的itchat包已经用不了了,因为微信在很久以前就关闭了通过网络登录的途径,但是上面这个UOS的版本,在我博客发布的时候还是可以正常使用的:

pip install itchat-uos==1.5.0.dev0

注意需要安装这个1.5.0版本,直接安装是1.4版本实测已经无法使用(会提示wxid相关报错)

同时需要参考这里的链接,对源码进行下修改,否则会出现无限刷新二维码无法登录的问题
https://github.com/zhayujie/chatgpt-on-wechat/issues/8

由于微信如果登录第二个账号会自己退出,所以需要第二个手机号。同时为了保证正常登录,需要第二个手机号进行实名认证和绑定银行卡

我是使用了雷电云手机买了个虚拟的手机终端,专门用于微信机器人的扫码登录。

在这里插入图片描述

我全部的微信机器人代码在github上:
https://github.com/CaLlMeErIC/WechatBot

以下是我主要部分的关键代码,直接让gpt-o1帮忙写的。主要有以下几点:
1.只处理私聊和@的信息
2.通过多线程和队列处理信息,使用锁确保一时间同一人只能占用一个线程。因为后面会涉及到进行游戏和计算游戏豆的模块
3.不同的模块使用同一的类名和方法,仅文件名不同,这样可以通过动态导入的方式往里面加自定义的模块。通过不同的文字命令来调用不同的功能模块。
4.在初始化的时候需要扫描module文件夹下的所有模块,这些模块会以单例模式初始化,然后把对象保存在字典里,通过命令调用指定的模块
5.数据库的部分用sqlite3解决
6.为了防止掉线,加了重连功能。因为重连的时候没办法扫码所以使用的是直接登录(直接登录不如扫码登录稳定),在初次使用的时候是扫码登录。

"""
启动微信机器人并导入不同的功能模块
"""
import traceback
import threading
import queue
import logging
import itchat
from itchat.content import TEXT
from utils.scan_module import get_command_module_dict

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(threadName)s %(message)s')


class WeChatBot:
    """
    微信机器人
    """

    def __init__(self):
        # 功能模块映射,根据消息前缀映射到对应的模块名
        self.module_mapping = get_command_module_dict()

        # 定义消息队列
        self.message_queue = queue.Queue()
        self.num_worker_threads = 5  # 工作线程数

        # 初始化发送者锁字典和锁
        self.sender_locks = {}
        self.sender_locks_lock = threading.Lock()

        self.handle_private_message = None
        self.handle_group_message = None

        # 初始化 itchat
        itchat.auto_login(hotReload=False)

        # 注册消息处理函数
        self.register_handlers()

        # 启动消息处理线程
        self.start_worker_threads()

    def register_handlers(self):
        """
        注册消息处理器
        """

        # 由于装饰器的使用,我们需要将函数定义在这里,并使用 self 作为参数

        @itchat.msg_register(TEXT, isFriendChat=True)
        def handle_private_message(msg):
            # 将消息放入队列,不直接处理
            self.message_queue.put(('private', msg))

        @itchat.msg_register(TEXT, isGroupChat=True)
        def handle_group_message(msg):
            # 将消息放入队列,不直接处理
            if msg['IsAt']:
                self.message_queue.put(('group', msg))

        # 将函数绑定到实例
        self.handle_private_message = handle_private_message
        self.handle_group_message = handle_group_message

    def start_worker_threads(self):
        """
        启动工作线程
        """
        for i in range(self.num_worker_threads):
            thread_pool = threading.Thread(target=self.message_worker, name=f'Worker-{i + 1}')
            thread_pool.daemon = True
            thread_pool.start()

    def message_worker(self):
        """
        通过多线程和队列处理信息,使用锁确保一时间
        同一人只能占用一个线程
        """
        while True:
            try:
                msg_type, msg_data = self.message_queue.get(timeout=1)
                sender_id = msg_data['FromUserName']

                # 获取或创建发送者的锁
                with self.sender_locks_lock:
                    if sender_id not in self.sender_locks:
                        self.sender_locks[sender_id] = threading.Lock()
                    sender_lock = self.sender_locks[sender_id]

                # 使用发送者的锁,确保同一时间只有一个线程处理该发送者的消息
                with sender_lock:
                    if msg_type == 'private':
                        self.handle_private_message_worker(msg_data)
                    elif msg_type == 'group':
                        self.handle_group_message_worker(msg_data)

                self.message_queue.task_done()
            except queue.Empty:
                continue
            except Exception as exception:
                logging.error("消息处理时发生异常:%s", exception)

    def handle_private_message_worker(self, msg):
        """
        处理私聊消息
        """
        sender = msg['User']
        nickname = sender['NickName']
        content = msg['Text']
        logging.info("私聊消息 - 来自 %s:%s", nickname, content)
        reply = self.generate_reply(nickname, content)
        itchat.send(reply, toUserName=sender['UserName'])

    def handle_group_message_worker(self, msg):
        """
        处理群消息
        """
        if msg['IsAt']:
            group_name = msg['User']['NickName']
            sender_nickname = msg['ActualNickName']
            actual_content = msg['Content']
            # 获取自己的昵称
            my_nickname = itchat.search_friends()['NickName']
            # 去除@信息,提取实际内容
            content = actual_content.replace(f'@{my_nickname}', '').strip()
            logging.info("群聊消息 - %s 中 @%s 说:%s", group_name, sender_nickname, content)
            reply_content = self.generate_reply(sender_nickname, content)
            reply = f"@{sender_nickname} {reply_content}"
            itchat.send(reply, toUserName=msg['FromUserName'])

    def generate_reply(self, nickname, content):
        """
        调用不同的功能模块,处理消息生成回复
        """
        try:
            command_sign = content.split(" ")[0]
            if command_sign in self.module_mapping:
                module_instance = self.module_mapping[command_sign]
                reply = module_instance.process_messages(nickname, content)
            else:
                reply = "对不起,暂时还没有这个功能"
            return reply
        except Exception as exception:
            print(traceback.format_exc())
            logging.error("处理模块时发生异常:%s", exception)
            return '抱歉,出现了一些错误。'

    @staticmethod
    def run():
        """
        开始运行机器人
        """
        while True:
            try:
                itchat.run(blockThread=True)
            except KeyboardInterrupt:
                # 如果用户手动中断,退出循环
                logging.info("微信机器人已停止")
                break
            except Exception as exception:
                logging.error("主循环发生异常:%s", exception)
                if 'request' in str(exception) or 'Logout' in str(exception) or 'login' in str(exception).lower():
                    try:
                        itchat.auto_login(hotReload=True)
                        logging.info("重新登录成功")
                    except Exception as login_exception:
                        logging.error("重新登录失败:%s", login_exception)
                        print("无法重新登录,程序即将退出")
                        break  # 退出循环,不再尝试重新登录
                else:
                    logging.error("无法识别的异常,未能重新登录:%s", exception)
                    print("遇到无法识别的异常,程序即将退出")
                    break  # 退出循环,不再尝试


if __name__ == '__main__':
    bot = WeChatBot()
    bot.run()

比如以下就是一个自定义的21点游戏模块:

import random
# 请确保 BeanManager 类的代码已经正确导入或者定义
from utils.bean_actions import BeanManager  # 导入你的 BeanManager 类


class FunctionModule:
    """
    FunctionModule 类,实现线程安全的单例模式。
    """

    _instance = None
    # 命令标识,用于标注什么样的命令开头会调用这个功能模块
    # 如@机器人 21点 或者@机器人 blackjack就会触发这个模块
    _command_sign = ["21点", "blackjack", "要牌", "停牌"]
    # 如果未激活那么不会使用
    is_active = True

    def __new__(cls):
        """
        线程安全的单例实现。
        """
        if cls._instance is None:
            cls._instance = super(FunctionModule, cls).__new__(cls)
        return cls._instance

    def __init__(self):
        """
        初始化方法。
        """

        if not hasattr(self, '_initialized'):
            self._initialized = True
            # 用户游戏状态存储,key为用户昵称,value为玩家状态
            self.user_states = {}
            # 初始化一副牌
            self.deck = self.create_deck()
            self.help_commands_set = set()
            for command in self._command_sign:
                # 构建帮助命令的各种可能格式
                help_variants = [
                    f"{command} 介绍",
                    f"{command} 帮助",
                    f"{command} 说明",
                    f"{command} help",
                    f"{command} 功能"
                ]
                # 将所有变体转换为小写并添加到集合中,确保大小写不敏感
                for variant in help_variants:
                    self.help_commands_set.add(variant.lower())
            # 初始化 BeanManager 实例
            self.bean_manager = BeanManager()

    def get_command_sign(self):
        """
        返回当前模块的命令标识
        """
        return self._command_sign

    @staticmethod
    def get_simple_description():
        """
        返回简单的功能描述
        """
        return "21点游戏"

    @staticmethod
    def get_detail_description():
        """
        返回详细的功能描述
        """
        description_string = """一个简单的21点(Blackjack)游戏。你可以发送'开始21点'来开始游戏,'要牌'来获取一张新牌,'停牌'来结束当前回合。
玩家与庄家比较牌面点数,最接近21点而不爆牌(超过21点)的玩家获胜。庄家在16点或以下必须继续要牌,17点或以上则停牌。
A:两种方式,可以作为11点(软手),亦作为1点(硬手)。2-10:牌面点数即其数值。J、Q、K:每张牌的点数为10点。"""
        return description_string.strip()

    def process_messages(self, sender_nickname, content):
        """
        根据发消息人的昵称和发消息的内容,制作回复
        """
        # 初始化玩家状态,如果不存在
        if sender_nickname not in self.user_states:
            self.user_states[sender_nickname] = {
                'player_hand': [],
                'dealer_hand': [],
                'in_game': False,
                'deck': [],
                'bet_amount': None  # 押注金额,None 表示未押注
            }

        user_state = self.user_states[sender_nickname]
        reply_string = ""
        content_lower = content.strip().lower()

        if content_lower in self.help_commands_set:
            return self.get_detail_description()

        # 解析用户输入,检查是否包含押注金额
        if any(content_lower.startswith(cmd) for cmd in ['21点', 'blackjack']):
            # 解析押注金额
            parts = content.strip().split()
            bet_amount = None
            if len(parts) >= 2:
                # 尝试解析押注金额
                bet_input = parts[1]
                try:
                    # 支持多种押注指令,如'押1000','赌1000',或直接'1000'
                    if bet_input.startswith(('押', '赌')):
                        bet_amount = int(bet_input[1:])
                    else:
                        bet_amount = int(bet_input)
                except ValueError:
                    reply_string = "请输入有效的押注金额,例如:'21点 押1000',或直接发送'21点'开始游戏。"
                    return reply_string

            if user_state['in_game']:
                reply_string = "你已经在游戏中了!"
            else:
                if bet_amount is not None:
                    # 用户选择押注,检查豆子余额
                    total_beans = self.bean_manager.get_bean_count(sender_nickname)
                    if total_beans < bet_amount:
                        reply_string = f"你的豆子不足!当前豆子:{total_beans},需要押注:{bet_amount}"
                        return reply_string
                    elif bet_amount <= 0:
                        reply_string = "押注金额必须大于0!"
                        return reply_string
                    # 扣除押注金额
                    self.bean_manager.add_beans(sender_nickname, -bet_amount)
                    user_state['bet_amount'] = bet_amount
                else:
                    # 用户未选择押注,设置押注金额为 None
                    user_state['bet_amount'] = None

                # 重新创建并洗牌
                user_state['deck'] = self.create_deck()
                random.shuffle(user_state['deck'])
                user_state['player_hand'] = []
                user_state['dealer_hand'] = []
                user_state['in_game'] = True
                # 玩家发两张初始牌
                user_state['player_hand'].append(self.draw_card(user_state['deck']))
                user_state['player_hand'].append(self.draw_card(user_state['deck']))
                # 庄家发两张牌,一张明牌,一张暗牌
                user_state['dealer_hand'].append(self.draw_card(user_state['deck']))
                user_state['dealer_hand'].append(self.draw_card(user_state['deck']))
                player_total = self.calculate_total(user_state['player_hand'])
                dealer_visible_card = user_state['dealer_hand'][0]

                if bet_amount is not None:
                    potential_win = bet_amount * 2 * 0.95  # 计算可能的赢取金额,扣除5%抽水
                    reply_string = (
                        f"游戏开始!你押注了 {bet_amount} 豆子,可赢取 {int(potential_win)} 豆子(扣除5%抽水)。\n"
                    )
                else:
                    reply_string = "游戏开始!\n"

                reply_string += (
                    f"你的手牌是:{self.format_hand(user_state['player_hand'])},总点数:{player_total}。\n"
                    f"庄家明牌:{dealer_visible_card}。\n"
                    "你可以选择'要牌'或者'停牌'。"
                )

        elif content_lower == '要牌':
            if not user_state['in_game']:
                reply_string = "你还没有开始游戏,请发送'21点'或'21点 押注金额'来开始游戏。"
            else:
                user_state['player_hand'].append(self.draw_card(user_state['deck']))
                player_total = self.calculate_total(user_state['player_hand'])
                if player_total > 21:
                    reply_string = (
                        f"你抽到了 {self.format_hand([user_state['player_hand'][-1]])},你的手牌是:{self.format_hand(user_state['player_hand'])},总点数 {player_total}。\n"
                        "爆掉了!你输了!\n"
                    )
                    if user_state['bet_amount'] is not None:
                        reply_string += f"你失去了 {user_state['bet_amount']} 豆子。\n"
                    reply_string += "游戏结束。"
                    user_state['in_game'] = False
                    user_state['bet_amount'] = None
                elif player_total == 21:
                    reply_string = (
                        f"你抽到了 {self.format_hand([user_state['player_hand'][-1]])},你的手牌是:{self.format_hand(user_state['player_hand'])},总点数 {player_total}。\n"
                        "恭喜你,Blackjack!你赢了!\n"
                    )
                    if user_state['bet_amount'] is not None:
                        winnings = int(user_state['bet_amount'] * 2 * 0.95)
                        self.bean_manager.add_beans(sender_nickname, winnings)
                        reply_string += f"你赢得了 {winnings} 豆子(扣除5%抽水)。\n"
                    reply_string += "游戏结束。"
                    user_state['in_game'] = False
                    user_state['bet_amount'] = None
                else:
                    reply_string = (
                        f"你抽到了 {self.format_hand([user_state['player_hand'][-1]])},你的手牌是:{self.format_hand(user_state['player_hand'])},总点数 {player_total}。\n"
                        "你可以继续选择'要牌'或者'停牌'。"
                    )

        elif content_lower == '停牌':
            if not user_state['in_game']:
                reply_string = "你还没有开始游戏,请发送'21点'或'21点 押注金额'来开始游戏。"
            else:
                player_total = self.calculate_total(user_state['player_hand'])
                # 庄家揭开暗牌并进行操作
                dealer_hand = user_state['dealer_hand']
                dealer_total = self.calculate_total(dealer_hand)
                while dealer_total < 17:
                    dealer_card = self.draw_card(user_state['deck'])
                    dealer_hand.append(dealer_card)
                    dealer_total = self.calculate_total(dealer_hand)

                reply_string = (
                    f"你的手牌是:{self.format_hand(user_state['player_hand'])},总点数 {player_total}。\n"
                    f"庄家的手牌是:{self.format_hand(dealer_hand)},总点数 {dealer_total}。\n"
                )
                if dealer_total > 21 or player_total > dealer_total:
                    reply_string += "恭喜你,你赢了!\n"
                    if user_state['bet_amount'] is not None:
                        winnings = int(user_state['bet_amount'] * 2 * 0.95)
                        self.bean_manager.add_beans(sender_nickname, winnings)
                        reply_string += f"你赢得了 {winnings} 豆子(扣除5%抽水)。\n"
                elif player_total < dealer_total:
                    reply_string += "很遗憾,你输了!\n"
                    if user_state['bet_amount'] is not None:
                        reply_string += f"你失去了 {user_state['bet_amount']} 豆子。\n"
                else:
                    reply_string += "平局!\n"
                    if user_state['bet_amount'] is not None:
                        # 返还押注金额
                        self.bean_manager.add_beans(sender_nickname, user_state['bet_amount'])
                        reply_string += f"你的押注 {user_state['bet_amount']} 豆子已返还。\n"
                reply_string += "游戏结束。"
                user_state['in_game'] = False
                user_state['bet_amount'] = None
            # 清空手牌,不清空牌库
            user_state['player_hand'] = []
            user_state['dealer_hand'] = []

        else:
            reply_string = "无法识别的指令。你可以发送'21点'或'21点 押注金额'来开始游戏,'要牌'来获取一张新牌,'停牌'来结束当前回合。"

        return reply_string

    @staticmethod
    def create_deck():
        """
        创建一副52张的扑克牌
        """
        suits = ['♠️', '♥️', '♣️', '♦️']
        ranks = ['A'] + [str(n) for n in range(2, 11)] + ['J', 'Q', 'K']
        deck = []
        for suit in suits:
            for rank in ranks:
                deck.append(f"{suit}{rank}")
        return deck

    def draw_card(self, deck):
        """
        从牌堆中抽一张牌
        """
        if len(deck) == 0:
            # 如果牌堆没牌了,重新创建并洗牌
            deck.extend(self.create_deck())
            random.shuffle(deck)
        return deck.pop()

    @staticmethod
    def calculate_total(hand):
        """
        计算手牌的总点数,处理A的情况(A可以是1也可以是11)
        """
        total = 0
        aces = 0
        for card in hand:
            rank = card[2:] if card[1] in ['️'] else card[1:]  # 处理可能的特殊字符
            if rank in ['J', 'Q', 'K']:
                total += 10
            elif rank == 'A':
                aces += 1
                total += 11
            else:
                total += int(rank)
        # 如果总点数超过21,且有A,把A当1处理
        while total > 21 and aces > 0:
            total -= 10
            aces -= 1
        return total

    @staticmethod
    def format_hand(hand):
        """
        格式化手牌输出
        """
        return '、'.join(hand)

    @staticmethod
    def get_card_value(card):
        """
        获取单张牌的点数, 用于显示庄家的明牌点数
        """
        rank = card[2:] if card[1] in ['️'] else card[1:]
        if rank in ['J', 'Q', 'K']:
            return 10
        elif rank == 'A':
            return 11
        else:
            return int(rank)

之后就需要部署到服务器上,我是直接用的阿里云服务器,现在做活动一年只要79块,真的是太便宜辣:
在这里插入图片描述
在部署的时候选择windows镜像,这样直接就有图像化界面,很方便。之后就是登录,安装anconda,安装解压缩软件,直接装完直接跑就完成了。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值