樹莓派實戰:微信機器人(itchat實現)

2022-07-11 06:48:53

背景

樓主有一臺樹莓派4B開發板(8G記憶體版),是目前的頂配機型。這一年來的業餘時間,除了寫Java、架構方面的文章,也陸續折騰了不少樹莓派上的好玩小專案,在此新開一個樹莓派實戰的文章系列,分享給粉絲和讀者。

什麼是樹莓派?樹莓派是一個信用卡大小的單板計算機,ARM架構CPU,可以7×24跑Linux伺服器,連線各種擴充套件硬體,所以發揮想象力,就能做很多有意思的事情。

需求

你有沒有想過,擁有一個微信機器人,可以自動回覆、AI聊天、定時傳送天氣預報、控制攝像頭等等。使用樹莓派+開源庫itchat,就能實現上述所有需求。
為什麼強調要用樹莓派呢?因為它能7×24線上,可以把itchat使用者端當作一個不停服的server。
特別說明:本文僅供學習用,請勿用於任何商業和其它用途。

itchat簡介

itchat是一個開源的微信個人號介面,使用不到三十行的程式碼,就可以完成一個能夠處理所有資訊的微信機器人。
github地址:https://github.com/littlecodersh/ItChat

你一定對原理感到好奇。其實可以概括為一句話:itchat本質上是一個微信網頁版使用者端,它實現了微信網頁版的協定/語意,通過http來通訊。具體原始碼可以看components包裡的檔案。

下面分點介紹如何實現有趣的功能。

功能實現

1、自動回覆

首先得註冊訊息處理常式,即對不同型別的訊息做處理。微信訊息分為文字、圖片、語音、視訊、好友申請等,可通過itchat的Python語法糖來註冊不同型別訊息的處理常式,有點類似Java裡的註解。
如果是文字訊息,可以識別其中的關鍵字,不同的關鍵字對應不同的邏輯處理。
預設是處理單聊的訊息,也可以處理群聊的訊息。
下面給出一個demo,並加以註釋。

import itchat, time
from itchat.content import *

# 註冊訊息處理常式,回覆文字、地圖、名片、備註、分享型別的訊息
@itchat.msg_register([TEXT, MAP, CARD, NOTE, SHARING])
def text_reply(msg):
    # 回覆以下訊息:訊息型別,訊息內容文字
    itchat.send('%s: %s' % (msg.type, msg.text))
    # 根據不同的關鍵字,回覆不同的訊息
    if '你好' in msg.text:
        itchat.send('你好啊')
    elif '拜拜' in msg.text:
        itchat.send('下次聊')

# 註冊訊息處理常式,當收到圖片、語音、附件、視訊型別的訊息時,下載內容
@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO])
def download_files(msg):
    # 下載檔案
    msg.download(msg.fileName)
    typeSymbol = {
        PICTURE: 'img',
        VIDEO: 'vid', }.get(msg.type, 'fil')
    return '@%s@%s' % (typeSymbol, msg.fileName)

# 註冊訊息處理常式,處理好友申請訊息
@itchat.msg_register(FRIENDS)
def add_friend(msg):
    # 自動通過對方的好友申請
    msg.user.verify()
    # 然後傳送問候語
    msg.user.send('Nice to meet you!')

# 上面幾個都是單聊,加上isGroupChat=True就能處理群聊訊息
@itchat.msg_register(TEXT, isGroupChat=True)
def text_reply(msg):
    # 當在群聊被at時才回復,一般都會加上此條件,否則可能回覆群內所有訊息
    if msg.isAt:
        # 回覆時,也at對應的人訊息
        msg.user.send(u'@%s\u2005I received: %s' % (
            msg.actualNickName, msg.text))

# 登入
itchat.auto_login(True)
# 執行itchat使用者端,debug=True會列印紀錄檔
itchat.run(True)

2、AI聊天

有了第1步的基礎,要實現AI聊天,就需要引入另外的AI本地庫、或者線上API了,使用線上API更簡單,只需要控制傳參、解析響應即可。樓主使用了一個叫青雲客的API,可免費使用(自己簡單試用的前提下,非商用),帶關鍵字命令的AI對話還是不錯的,如果是自由對話,那大概率前言不搭後語。

# 調API來進行AI聊天,只有一個文字引數
def ai_chat(msg):
    url = 'http://api.qingyunke.com/api.php?key=free&appid=0&msg=%s' % msg
    response = requests.get(url)
    return response.json()["content"].replace('{br}', '\n') # 響應裡的換行是{br},替換為微信可識別的\n換行

3、定時傳送天氣預報

有了第2步的基礎,要獲取天氣預報資訊,只需要在AI聊天的請求裡傳某地天氣即可,比如:上海天氣、北京天氣。當然,你也可以通過爬天氣預報網頁的欄位,得到更詳盡的天氣預報資訊,此處就不多討論了。
定時發天氣預報,要解決2個關鍵問題。

  • 一是如何執行定時任務。此處選用Python庫apscheduler。當然,也可以寫一個Python指令碼,然後通過作業系統的crontab在指定的時間執行該指令碼,不過還有更優雅的方式,在Python主程式內啟動定時任務。可以使用Python庫apscheduler來實現定時任務的排程,類似於Java的ScheduledThreadPool。
  • 二是如何傳送訊息到指定的群。itchat已經提供了便捷的API來根據群名搜尋具體的群。
from apscheduler.schedulers.blocking import BlockingScheduler

# 傳送天氣預報資訊到群裡
def weather_report():
    msg = ai_chat('上海天氣')
    # 獲取所有群聊
    itchat.get_chatrooms(update=True)
    # 根據群名,搜尋具體的群
    chatrooms = itchat.search_chatrooms(name='<此處改為實際的群名>')
    chatroom = itchat.update_chatroom(chatrooms[0]['UserName'])
    # 傳送訊息,到指定的群
    itchat.send_msg(msg=msg,toUserName=chatroom['UserName'])

if __name__ == '__main__':
    itchat.auto_login(hotReload=True)
    # itchat啟動後是否阻塞,此處改為否(預設為是),相當於itchat在新啟動的執行緒中執行,不阻塞主程式
    itchat.run(blockThread=False)

    # 定時任務
    scheduler = BlockingScheduler()
    # 指定在每天早上9點呼叫weather_report函數
    scheduler.add_job(weather_report, 'cron', day_of_week='*', hour=9, minute=0, second=0)
    scheduler.start()

4、控制攝像頭,拍照、視訊看看家裡

樹莓派4B有2個USB 3.0高速介面、2個USB 2.0介面,只需要其中一個連線上USB攝像頭即可,一般2.0介面即可,3.0介面留給外接硬碟。
想要通過攝像頭看到家裡,要解決的關鍵問題是,使用什麼拍照軟體?使用什麼視訊聊天軟體?

拍照

可以使用fswebcam來拍照,可以指定影象解析度,也可以不指定,預設的解析度較低。
安裝:sudo apt install fswebcam

    img_file = '%d.jpg' % timestamp
    # 呼叫fswebcam拍照
    os.system('fswebcam %s' % img_file)
    # 傳送照片至自己的檔案傳輸助手,因為通常發給自己會失敗
    itchat.send_image(img_file, toUserName='filehelper')

發起視訊

樓主嘗試了幾個常見的免費視訊聊天軟體,都無法支援,主要原因是樹莓派是ARM CPU架構,主流軟體基本上只在amd64、x86 CPU架構下發行。比如QQ、Skype、網頁版Jitsi Meet等都無法發起視訊聊天。
最終,樓主發現了一個較為完美的解決方案,就是使用linphone:

  • 發起視訊:在樹莓派上安裝並開啟linphone程式,也在手機上安裝並開啟linphone app。這樣通過微信就可以讓樹莓派上的linphone發起視訊通話,手機端就能接到電話了。
  • 結束通話視訊:需要通過微信機器人,在樹莓派上主動退出linphone,否則後續不能繼續發起視訊。
    下載最新的linphone可能無法正常工作,得使用sudo apt install linphone來安裝舊的穩定版。
    # 先退出linphone(如當前有在執行),再啟動linphone
    os.system('linphonecsh exit; linphonecsh init -V -c .linphonerc')
    time.sleep(1)
    # 使用linphone命令列撥打視訊通話
    os.system('linphonecsh generic "call <替換成實際的linphone賬號,需註冊>"')

完整程式碼

以下是樓主寫的幾個實用例子,並加以註釋。
完整程式碼已上傳至github:https://github.com/topcoding/wechat_robot
除了上面提到的幾個功能實現,還增加了健身打卡、睡覺打卡的功能。現在,微信機器人的功能已經越來越豐富了。

# -*- coding: utf-8 -*-

import itchat
import sqlite3
import os
import time
import requests
from apscheduler.schedulers.blocking import BlockingScheduler


PUNCH_TYPE_WORKOUT = 1
PUNCH_TYPE_SLEEP = 2

ai_chat_switch = True

AI_CHATROOM_WHITELIST = ['<替換成實際的群名>']


def save_db(punch_type, owner, timestamp = None):
    conn = sqlite3.connect('punch-card.db')
    cursor = conn.cursor()
    if timestamp is None:
        punch_time = (int) (time.time())
    else:
        punch_time = timestamp
    cursor.execute("insert into punch_card(punch_type, owner, updated_at) values(%d, '%s', %d)"
                   % (punch_type, owner, punch_time))
    conn.commit()
    conn.close()

@itchat.msg_register(itchat.content.TEXT)
def text_reply(msg):
    print(msg)
    timestamp = (int) (time.time())
    global ai_chat_switch
    if msg.text == '健身打卡':
        save_db(PUNCH_TYPE_WORKOUT, msg.User.NickName, timestamp)
        itchat.send('%s,您好,您於%s健身打卡成功' % (msg.User.NickName, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())), toUserName='filehelper')
    elif msg.text == '睡覺打卡':
        save_db(PUNCH_TYPE_SLEEP, msg.User.NickName, timestamp)
        itchat.send('%s,您好,您於%s睡覺打卡成功' % (msg.User.NickName, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())), toUserName='filehelper')
    elif msg.text == '拍照':
        img_file = '%d.jpg' % timestamp
        os.system('fswebcam %s' % img_file)
        itchat.send_image(img_file, toUserName='filehelper')
    elif msg.text == '看看家裡':
        os.system('linphonecsh exit; linphonecsh init -V -c .linphonerc')
        time.sleep(1)
        os.system('linphonecsh generic "call <替換成實際的linphone賬號,需註冊>"')
    elif msg.text == '結束通話視訊':
        os.system('linphonecsh exit')
    elif msg.text == '群聊':
        ai_chat_switch = True
    elif msg.text == '群聊取消':
        ai_chat_switch = False
    else:
        # do nothing
        pass

@itchat.msg_register('Text', isGroupChat = True)
def group_reply(msg):
    if ai_chat_switch and msg['isAt'] and msg['User']['NickName'] in AI_CHATROOM_WHITELIST:
        print(msg)
        return u'@%s\u2005%s' % (msg['ActualNickName'], ai_chat(msg))

def ai_chat(msg):
    url = 'http://api.qingyunke.com/api.php?key=free&appid=0&msg=%s' % msg
    response = requests.get(url)
    return response.json()["content"].replace('{br}', '\n')

def weather_report():
    msg = ai_chat('上海天氣')
    itchat.get_chatrooms(update=True)
    chatrooms = itchat.search_chatrooms(name='<替換成實際的群名>')
    chatroom = itchat.update_chatroom(chatrooms[0]['UserName'])
    itchat.send_msg(msg=msg,toUserName=chatroom['UserName'])


if __name__ == '__main__':
    itchat.auto_login(hotReload=True)
    itchat.run(blockThread=False)

    scheduler = BlockingScheduler()
    scheduler.add_job(weather_report, 'cron', day_of_week='*', hour=9, minute=0, second=0)
    scheduler.start()