Android無障礙服務 x itchat 打造微信半自動機器人
em…是我,那個『敲最屌的碼,輸最多的錢』的傻雕開發仔,故事的最後:沒有暴富,沒有嫩模,也沒有穴深妹…
再次奉勸各位一句:遠離投機倒把,保持身心健康!(當然,後面如果學到機器學習的東西,可能會有續集 ~(╯▽╰ )真香~~)
好的,碎碎唸的那麼多,說回本節,寫這一篇原因是,我的Py交易羣裏,童鞋問的最多的問題都是和機器人有關,基本都是下面這類問題:
- 1.羣主,你的機器人怎麼實現的?
- 2.羣主,你的apk怎麼運行了沒反應?
- 3.羣主,支持自動添加手機號爲好友嗎?
- …等等
一開始我還是比較熱衷幫忙解決問題的,但是耐心這種東西呢,是最容易被消磨殆盡的。而且學習Python之後,我變得越來越懶,一些繁瑣重複的操作,我都會想辦法自動化…當然,如果你是給我打錢或者是妹子,我也是很樂意的。
其實Android無障礙服務和itchat都是一些老生常談的東西了,我也寫過好幾篇文章了:
- 妙用AccessibilityService黑科技實現微信自動加好友拉人進羣聊
- 自動搶紅包,點贊朋友圈,AccessibilityService解放你的雙手
- 利用itchat搭建微信機器人詳解(附三個實用示例)
- 小豬的Python學習之旅 —— 18.Python微信轉發小宇宙早報
- 小豬的Python學習之旅 —— 19.Python微信自動好友驗證,自動回覆,發送羣聊鏈接
東西就那些,套路也是那些,大部分時候,你只需要一點**『靈性』**!
就好像七巧板一樣,只有七塊,但是卻能拼接出許多圖形。要有靈性!本文先說下無障礙服務和itchat的一些核心要點,然後以讀者編寫的機器人腳本爲例,教你實現一波,希望可以給你帶來一些提示,然後去擴展自己想要的功能。
開源的微信個人號接口庫——itchat
itchat庫基於微信網頁版,出了好幾年的了,微信正在慢慢收窄網頁端的功能(因爲濫用的微信機器人),很多以前能用的接口慢慢都不能用了,比如:拉人進羣,添加好友等,反正網頁端沒有的,現在都不能用。丟幾個鏈接:
- 微信網頁端登錄:https://wx2.qq.com/
- itchat的倉庫地址:https://github.com/littlecodersh/ItChat
- itchat官方文檔:https://itchat.readthedocs.io/zh/latest/
itchat現在能做的:監聽加好友的信息,監聽聊天信息(包括羣聊),發送信息。
基本上常用的而且可用的就這三個。當然如果你願意掏錢的話,你不需要折騰那麼多,網上有其他付費的途徑:微友助手,王二狗機器人,還有xposed插件等。基於其他協議且可用的免費開源庫目前還沒見過,有知道的歡迎在評論區告知下!
使用要點提煉
如果可以,我希望你,儘可能的學會使用『正則表達式處理字符串』(基本功),丟個以前寫過的文章:
小豬的Python學習之旅 —— 3.正則表達式,正則對於字符串匹配,提取非常實用!
1.監聽並通過加好友的請求
@itchat.msg_register(itchat.content.FRIENDS)
def deal_with_friend(msg):
# 自動將新好友的消息錄入,不需要重載通訊錄
itchat.add_friend(**msg['Text'])
通過上面的add_friend
函數,就可以完成添加好友的操作了。但是,有時我們可能需要做一些過濾,
不然亂七八糟的人都能加你了,是吧,比如對添加內容進行過濾,包含某些字眼才通過驗證,或者
獲取加你的人的相關信息,比如姓名,驗證信息,個性簽名,性別等。我們直接把上面的msg打印出來,
內容如下:
{'MsgId': '6930655840618917667', 'FromUserName': 'fmessage', 'ToUserName': '@64fc6691440834f2dfba5489d652e5dbae06da4d57d550757403094424f7ec9c', 'MsgType': 37, 'Content': '<msg fromusername="wxid_gvr9a3le939h22" encryptusername="v1_6a48a18b8d6164b69dfdd3949311cd4bc58d96dfe00636d5e9814ff36b8d107164e768ccd9b2fb911ea9b87ccac42978@stranger" fromnickname="Robot Pig" content="我是Robot Pig" shortpy="ROBOTPIG" imagestatus="3" scene="30" country="AD" province="" city="" sign="(´v`o)♡" percard="1" sex="2" alias="" weibo="" albumflag="0" albumstyle="0" albumbgimgid="" snsflag="1" snsbgimgid="http://mmsns.qpic.cn/mmsns/icDH6NcE3zNVBleeQZUzzlnhWk16tIfPKyvsmqWIpUwAxHkvricuNCL2RvGPjS3pVq7miaZQoju8TU/0" snsbgobjectid="12478275406675193980" mhash="197adbfd7de1668f30895d20dfb09b67" mfullhash="197adbfd7de1668f30895d20dfb09b67" bigheadimgurl="http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/0" smallheadimgurl="http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/96" ticket="v2_4ed4b6a5ac5ccf04c68c7bd64e4b543ba5babde23ce2985d4317bfc4bb62dcdfd78bb701551e1410fe64836f9bd147199de3e4493a031ea2daf52187816d9207@stranger" opcode="2" googlecontact="" qrticket="" chatroomusername="" sourceusername="" sourcenickname=""><brandlist count="0" ver="688441058"></brandlist></msg>', 'Status': 3, 'ImgStatus': 1, 'CreateTime': 1541558757, 'VoiceLength': 0, 'PlayLength': 0, 'FileName': '', 'FileSize': '', 'MediaId': '', 'Url': '', 'AppMsgType': 0, 'StatusNotifyCode': 0, 'StatusNotifyUserName': '', 'RecommendInfo': {'UserName': '@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3', 'NickName': 'Robot Pig', 'QQNum': 0, 'Province': '', 'City': '', 'Content': '我是Robot Pig', 'Signature': '(´v`o)♡', 'Alias': '', 'Scene': 30, 'VerifyFlag': 0, 'AttrStatus': 50467109, 'Sex': 2, 'Ticket': 'v2_4ed4b6a5ac5ccf04c68c7bd64e4b543ba5babde23ce2985d4317bfc4bb62dcdfd78bb701551e1410fe64836f9bd147199de3e4493a031ea2daf52187816d9207@stranger', 'OpCode': 2}, 'ForwardFlag': 0, 'AppInfo': {'AppID': '', 'Type': 0}, 'HasProductId': 0, 'Ticket': '', 'ImgHeight': 0, 'ImgWidth': 0, 'SubMsgType': 0, 'NewMsgId': 6930655840618917667, 'OriContent': '', 'EncryFileName': '', 'User': <User: {'UserName': '@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3', 'MemberList': <ContactList: []>}>, 'Type': 'Friends', 'Text': {'status': 3, 'userName': '@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3', 'verifyContent': '', 'autoUpdate': {'UserName': '@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3', 'NickName': 'Robot Pig', 'QQNum': 0, 'Province': '', 'City': '', 'Content': '我是Robot Pig', 'Signature': '(´v`o)♡', 'Alias': '', 'Scene': 30, 'VerifyFlag': 0, 'AttrStatus': 50467109, 'Sex': 2, 'Ticket': 'v2_4ed4b6a5ac5ccf04c68c7bd64e4b543ba5babde23ce2985d4317bfc4bb62dcdfd78bb701551e1410fe64836f9bd147199de3e4493a031ea2daf52187816d9207@stranger', 'OpCode': 2}}}
看上去和Json有點類似是吧,但是不是Json,你用Json格式化工具試試就知道了,而是一種類似於字典的東東
(跟下源碼就知道了,msg的類:itchat.storage.messagequeue.Message)一大串有點亂,用PyCharm新建一個json文件,
複製粘貼格式化下:
因爲類似於字典,你可以通過鍵的形式獲取所需的值,比如打印下msg[‘Content’]:
<msg fromusername="wxid_gvr9a3le939h22" encryptusername="v1_6a48a18b8d6164b69dfdd3949311cd4bc58d96dfe00636d5e9814ff36b8d107164e768ccd9b2fb911ea9b87ccac42978@stranger" fromnickname="Robot Pig" content="我是Robot Pig" shortpy="ROBOTPIG" imagestatus="3" scene="30" country="AD" province="" city="" sign="(´v`o)♡" percard="1" sex="2" alias="" weibo="" albumflag="0" albumstyle="0" albumbgimgid="" snsflag="1" snsbgimgid="http://mmsns.qpic.cn/mmsns/icDH6NcE3zNVBleeQZUzzlnhWk16tIfPKyvsmqWIpUwAxHkvricuNCL2RvGPjS3pVq7miaZQoju8TU/0" snsbgobjectid="12478275406675193980" mhash="197adbfd7de1668f30895d20dfb09b67" mfullhash="197adbfd7de1668f30895d20dfb09b67" bigheadimgurl="http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/0" smallheadimgurl="http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/96" ticket="v2_4ed4b6a5ac5ccf04c68c7bd64e4b543b5c95f344b73e23e05f71c527683d8693969fcef8a893316431a660ac382032b022ec8af4d8ae5372e680931064da1ce3@stranger" opcode="2" googlecontact="" qrticket="" chatroomusername="" sourceusername="" sourcenickname=""><brandlist count="0" ver="688441086"></brandlist></msg>
同樣新建一個xml文件,複製粘貼格式化下:
裏面有我們想要的信息,接着我們用正則來提取這些想要的數據:
msg_pattern = re.compile('<msg fromusername="(.*?)".*?fromnickname="(.*?)" content="(.*?)".*?sign="(.*?)".*?sex="(\d)".*?bigheadimgurl="(.*?)"',re.S)
接着修改一波代碼:
@itchat.msg_register(itchat.content.FRIENDS)
def deal_with_friend(msg):
result = msg_pattern.search(msg['Content'])
if result is not None:
print('添加人微信id:', result.group(1))
print('添加人用戶名', result.group(2))
print('驗證內容', result.group(3))
print('添加人個性簽名', result.group(4))
print('添加人性別', result.group(5))
print('添加人頭像大圖', result.group(6))
添加試試,打印結果如下:
行吧,什麼加你人的信息都拿到了,你想幹嘛就幹嘛!
2.監聽聊天信息
這個也很簡單,你可以監聽多種類型的信息,如下表所示:
信息類型 | 解釋 |
---|---|
itchat.content.TEXT | 文本內容 |
itchat.content.MAP | 位置文本 |
itchat.content.Card | 名片 |
itchat.content.Note | 通知文本 |
itchat.content.Sharing | 分享名稱 |
itchat.content.RECORDING | 錄音 |
itchat.PICTURE | 圖片/表情 |
itchat.content.VOICE | 錄音 |
itchat.content.ATTACHMENT | 附件 |
itchat.content.VIDEO | 短視頻 |
itchat.content.FRIENDS | 好友邀請 |
itchat.content.SYSTEM | 系統信息 |
可註冊多個信息監聽,後註冊的信息優先級高於先註冊信息,帶參數信息高於不帶參數信息。
核心代碼示例如下:
@itchat.msg_register(itchat.content.TEXT)
def reply_msg(msg):
if msg['Content'] == u'你好':
itchat.send_msg(msg['User']['NickName'] + "你好啊!", msg['FromUserName'])
和上面的監聽並通過加好友的請求玩法一樣,根據鍵拿值,或者正則提取需要的數據。這裏提供幾個常用的鍵:
msg['Content'] # 獲取用戶發送的內容,後面的匹配值建議加上u,代表Unicode編碼
msg['User']['NickName'] # 發送信息的用戶名
msg['FromUserName'] # 接收信息的用戶名,這個不是直接的用戶暱稱或微信號!!!
3.發送信息
itchat支持下述幾種類型的信息(不支持語音):
函數名 | 作用 |
---|---|
send_msg() | 發送文字信息 |
send_file() | 發送文件 |
send_video() | 發送視頻 |
send_image() | 發送圖片 |
核心代碼示例如下:
user_info = itchat.search_friends(name='一朵死去的花')
if len(user_info) > 0:
# 拿到用戶名
user_name = user_info[0]['UserName']
# 發送文字信息
itchat.send_msg('培傑你好啊!', user_name)
# 發送圖片
time.sleep(10)
itchat.send_image('cat.jpg', user_name)
# 發送文件
time.sleep(10)
itchat.send_file('19_2.py', user_name)
# 發送視頻
time.sleep(10)
itchat.send_video('sport.mp4', user_name)
建議加入延時,避免信息發送過於頻繁,導致賬號被封!
4.獲得羣聊成員列表
核心代碼示例如下:
@itchat.msg_register(itchat.content.TEXT, isGroupChat=True)
def reply_msg(msg):
print("收到一條羣信息:", msg['ActualNickName'], msg['Content'])
玩法和之前的一樣,另外,還可以調用**msg.isAt
**判斷是否有人@自己。
5.監控加羣信息
核心代碼示例如下:
@itchat.msg_register([NOTE], isGroupChat=True)
def revoke_msg(msg):
if '邀請' in str(msg['Text']):
# 進行相關操作
就是判斷提示信息裏是否有加羣字眼,好吧,常用的大概就這些,其他的自行查閱文檔。
Android無障礙服務——AccessibilityService
其實就是一個自動點點點的東西,沒什麼技術含量,真的!!!在開始講解AccessibilityService之前,先要明確一點:
什麼是自動化?
下面是我個人的理解:
把本該人做的,重複性高,單調,機械化的操作,交給程序去完成。
舉個例子,小豬每天都要用微信拉人進羣,所需的操作步驟如下所示:
是的,你拉一個人,需要20多秒,每次拉人的操作都是機械重複的。如果每天有30個人
進羣,你需要花費:600s,10分鐘我都夠開一把王者榮耀的了,別人在上分,我還在拉人???
多撈哦,用AccessibilityService寫個自動點點點的工具就可以把我從中解放出來。
1.自定義Service繼承AccessibilityService
自定義一個AccessibilityService類,重寫兩個主要方法:onInterrupt( ):輔助功能中斷的回調,基本不用理,
核心還是: onAccessibilityEvent(AccessibilityEvent event) 。
當界面發生改變,比如頂部Notification,界面更新,內容變化等,就會觸發**onAccessibilityEvent
方法。
點開AccessibilityEvent**類可以看到一堆的事件類型:
上面一大堆,其實並沒有什麼用,我一般是習慣直接把event.toString()給打印出來,然後自行判斷:
代碼示例如下:
這裏做的事情就是,當無障礙相關的Event觸發時,去判斷Event類型以及觸發事件的類名,再去執行相關操作:點擊,滾動,填充文本等。
2.獲取結點的幾個方法
可以通過resource-id,text來定位到結點,如果可以,建議使用後者,因爲一般APP更新後,這個id都會發生變化,(所以微信更新後都需要做適配,就是更新這個id)
1)通過UI Automator來查看佈局層次
舊版的Android Studio,Ctrl + alt + A,輸入 monitor 可以找到,新版的
Android Studio是找不到的,你需要來到**android-sdk/tools
**目錄下:
連接手機後,點擊頂部的:
接着可以看到當前頁面的層次結構圖:
有一點務必注意: resource-id不一定是唯一!!!
getRootInActiveWindow( ):獲取當前整個活動窗口的根節點,返回的是一個AccessibilityNodeInfo類,代表View的狀態信息, 提供了下述幾個非常實用的方法:
findAccessibilityNodeInfosByViewId
:通過視圖id查找節點元素。findAccessibilityNodeInfosByText
:通過字符串查找節點元素。getParent
:獲取父節點。getChild
:獲取子節點。
另外,找結點要注意判空,找不到對應結點直接調用其他方法是會空指針異常的!!!
找到結點然後就是一些動作了,常用的點擊,長按,滾動和輸入文字。代碼示例如下:
/* 點擊 */
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
/* 長按 */
node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
/* 滾動 */
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) //向上滾動
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) //向下滾動
/* 輸入文字 */
val arguments = Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,"xxx")
editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
/* 通過粘貼板輸入文字 */
public static void sendTextForEditText(Context context, AccessibilityNodeInfo editNode, String text) {
if (editNode != null) {
ClipboardManager clipboard = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text", text);
clipboard.setPrimaryClip(clip);
//獲得焦點
editNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
//粘貼內容
edittext.performAction(AccessibilityNodeInfo.ACTION_PASTE);
}
}
除此之外,還有AccessibilityService本身特有的方法,如模擬回退鍵,Home鍵等。
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) //回退
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME) //Home鍵
performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS) //點擊notification
大概的玩法就這些,除了通過UI Automator獲得id外,還可以通過其他幾種方式來獲取id。
2)開發者助手
如果你手機root了的話,可以安裝一個**『開發者助手』
**,點擊當前界面分析,點擊想查看的節點即可,如圖所示。
3)通過adb命令
依次鍵入:
adb shell uiautomator dump /mnt/sdcard/window_dump.xml
adb pull /mnt/sdcard/window_dump.xml
運行結果如圖所示:
接着可以把這個xml文件丟到as裏,格式化下,摺疊下一層層拆開,然後去找對應的結點:
這種方法是不怎麼推薦的,除非這個結點很明顯,比如文本啊,之類的,層級很多的時候,可能會找死你…
3.AccessibilityService注意事項
在使用AccessibilityService服務時,有幾點要注意:
首先需要手動開啓無障礙服務!!!程序轉了跑,沒反應,多半是因爲沒有開啓無障礙服務!
無障礙服務一般在:輔助功能->無障礙,(不同的手機可能不同)找到自己的點點點APK,開啓,如圖所示:
另外,有一點要注意,有時可能因爲異常導致程序意外終止了,你需要到無障礙中關掉對應的服務,然後重啓。
還有一點最重要的無障礙服務的適用範圍:
原生的Android APP!!! 是的原生!!!現在很多應用都是混合應用,對於H5的頁面,無障礙服務是無能爲力的!因爲此時的控件點擊事件不是通過onClick來產生的,而是直接判斷TouchEvent。而Android的無障礙服務沒有提供發送down,move,up事件的api。而替代方案只能使用root後的手機,向系統發送全局點擊命令。
一般是拿到結點,然後獲得結點所在的區域,然後執行相關的命令,比如點擊,常用代碼示例如下:
/* 執行Shell命令 */
public static void execShellCmd(String cmd) {
try {
// 申請獲取root權限,這一步很重要,不然會沒有作用
Process process = Runtime.getRuntime().exec("su");
// 獲取輸出流
OutputStream outputStream = process.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeBytes(cmd);
dataOutputStream.flush();
dataOutputStream.close();
outputStream.close();
} catch (Throwable t) {
t.printStackTrace();
}
}
/* 點擊某個結點 */
public static void perforGlobalClick(AccessibilityNodeInfo info) {
Rect rect = new Rect();
info.getBoundsInScreen(rect);
perforGlobalClick(rect.centerX(), rect.centerY());
}
/* 點擊某個座標點 */
public static void perforGlobalClick(int x, int y) {
execShellCmd("input tap " + x + " " + y);
}
/* 全局滑動 */
public static void perforGlobalSwipe(int x0, int y0, int x1, int y1) {
execShellCmd("input swipe " + x0 + " " + y0 + " " + x1 + " " + y1);
}
/* 全局返回 */
public static void perforGlobalHome(long delay) {
execShellCmd("input keyevent " + KeyEvent.KEYCODE_HOME);
}
/* 全局Home鍵 */
public static void perforGlobalHome(long delay) {
execShellCmd("input keyevent " + KeyEvent.KEYCODE_BACK);
}
行吧,關於AccessibilityService的玩法大概就這些了,接着教大家擼一個我的半自動微信機器人。
3.動手擼一個自己的微信機器人
先羅列下我的需求:
- 1.自動通過別人加好友的驗證,發送歡迎圖和歡迎信息;
- 2.監聽用戶發送的信息,響應對應的信息
- 菜單:返回菜單回覆詞
- 1:加入「Python學習交流羣」
- 2:加入「Android學習交流羣」
- 3:加入「閒聊扯淡羣」
- 4:加入「摳腚男孩的妙妙屋」
- 5:關注公衆號「摳腚男孩」
- 6:小豬的「個人博客」
- 7:小豬的「Github」
- 8:給小豬「打賞」
- 9:小豬的「微信」(不閒聊!)
- 其他,默認回覆黑人問號圖。
好的,問題來了,itchat現在不支持拉人進羣,怎麼辦?一個折中的方法,就是利用Android AccessibilityService,
完成自動拉人進羣,我們可以採集發送進羣的人的用戶名,然後定時(比如兩小時)發送一次到文件傳輸助手,然後
複製粘貼下用戶名到我們編寫的無障礙腳本里,完成自動拉人的操作,因爲還要人去複製粘貼,所以只能算半自動!
接着第二個問題,數據的傳輸格式,如果只是一個羣的話,最簡單的,用戶名拼接,回車作爲分隔:
小A
小B
小C
這裏的話,因爲我有多個羣,讀者可以選擇加入自己想加入的羣,回車換行或者添加分隔符的方式顯得有點low,
而且不方便擴展,這裏,我決定使用Json字符串,存儲加每個羣的人的用戶名,最後拼接成一個Json,示例如下:
{
"Python": [],
"Python2": [
"朱偉",
"程命沆(hàng)",
"雙槍老漢"
]
"Speak": [
"程命沆(hàng)"
],
"Android": [
"朱偉",
"程命沆(hàng)"
],
"Guy": []
}
PS:這裏有Python2的原因是,一羣滿了,所以要做下判斷,如果人數達到495的,把加羣的人添加到二羣。
所以還需要監控羣羣人員變化的信息,有新的人進羣,獲取一下一羣當前的,加入到二羣中。還有,要對
加羣的人做下判斷,如果已經在羣裏了,就不要添加到列表中。邏輯都弄清楚了,那就直接上代碼吧:
# -*- coding:utf-8 -*-
# 微信小宇宙助手
import datetime
import re
import time
import random
import json
import itchat
from itchat.content import *
from apscheduler.schedulers.blocking import BlockingScheduler
# 羣聊人員列表
member_python_list = []
member_python_list_2 = []
member_android_list = []
member_speak_list = []
# 加羣人員的列表
group_python_list = [] # Python
group_python_list_2 = [] # Python 2羣
group_android_list = [] # Android
group_speak_list = [] # 閒聊
# 獲取羣聊人員的列表的正則
nickname_compile = re.compile(r"\<ChatroomMember:.*?'NickName': '(.*?)'", re.S)
# 獲取羣聊名稱的正則
group_name_compile = re.compile("'NickName': '(.{1,40})', 'HeadImgUrl':", re.S)
# 添加好友通過歡迎詞
welcome_words = '(˶ᵔᵕᵔ˶)嚶嚶嚶,😘😘😘\n我是智障機器人小Pig,發送關鍵字:「菜單」 \n 查看更多小Pig的更多功能!'
# 菜單回覆詞
menu_answer = '(˶ᵔᵕᵔ˶)鏘鏘鏘~🎉🎉🎉,\n' \
'可用關鍵詞如下(輸入對應數字,比如1):\n' \
' 🐷 1.加入「Python學習交流羣」\n' \
' 🐷 2.加入「Android學習交流羣」\n' \
' 🐷 3.加入「閒聊扯淡羣」\n' \
' 🐷 4.關注公衆號「摳腚男孩」\n' \
' 🐷 5.小豬的「個人博客」\n' \
' 🐷 6.小豬的「GitHub」\n' \
' 🐷 7.給小豬「打賞」\n' \
' 🐷 8.小豬「微信」(不閒聊哦~)\n' \
'注:請不要回復過於頻繁,智障機器人不會聊天哦!🐶'
# 加羣統一回複詞
add_group_answer = '🚫🚫🚫FBI Warning!🚫🚫🚫\n(`・ω・´)ゞ非常抱歉的通知您:\n\n微信粑粑把拉人接口禁掉了,你的加羣請求已收到,小豬童鞋會盡快把你拉到羣中。\n\nヾノ≧∀≦)o 麻煩耐心等候哦!'
# 重複加羣回覆詞
add_repeat_answer = '<(`^´)>哼,敲生氣,你都在羣裏了,加什麼羣鴨!😠😠😠'
# 捐獻回覆詞
donate_answer = '(˶ᵔᵕᵔ˶)您的打賞,會讓小豬更有動力肝♂出更Interesting的文章,謝謝支持~😊😊😊'
# 小豬回覆詞
pig_answer = '(˶ᵔᵕᵔ˶)小豬童鞋不閒聊哦,有問題歡迎到羣裏討論哦~'
# 404回覆詞
no_match_answer = '!!!非常抱歉,您輸入的關鍵詞粗錯了,請發送「菜單」查看支持的數字關鍵字ヽ(・ω・´メ)'
msg_pattern = re.compile(
'<msg fromusername="(.*?)".*?fromnickname="(.*?)" content="(.*?)".*?sign="(.*?)".*?sex="(\d)".*?bigheadimgurl="(.*?)"',
re.S)
# 自動通過加好友
@itchat.msg_register(itchat.content.FRIENDS)
def deal_with_friend(msg):
result = msg_pattern.search(msg['Content'])
if result is not None:
print('添加人微信id:', result.group(1))
print('添加人用戶名', result.group(2))
print('驗證內容', result.group(3))
print('添加人個性簽名', result.group(4))
print('添加人性別', result.group(5))
print('添加人頭像大圖', result.group(6))
# itchat.add_friend(**msg['Text']) # 自動將新好友的消息錄入,不需要重載通訊錄
# time.sleep(random.randint(1, 3))
# itchat.send_msg(welcome_words, msg['RecommendInfo']['UserName'])
# time.sleep(random.randint(1, 3))
# itchat.send_image('welcome.png', msg['RecommendInfo']['UserName'])
# 自動回覆配置
@itchat.msg_register([TEXT])
def deal_with_msg(msg):
text = msg['Content']
if text == u'菜單':
time.sleep(random.randint(1, 3))
itchat.send(menu_answer, msg['FromUserName'])
# 加入Python交流羣
elif text == u'1':
time.sleep(random.randint(1, 3))
nickname = msg['User']['NickName']
if nickname not in member_python_list and nickname not in member_python_list_2:
itchat.send_msg("【" + nickname + "】童鞋\n" + add_group_answer, msg['FromUserName'])
if nickname is not None:
# 人數超過閥值拉入二羣
if len(member_python_list) >= 495:
if nickname not in group_python_list_2:
group_python_list_2.append(nickname)
else:
if nickname not in group_python_list:
group_python_list.append(nickname)
else:
itchat.send_msg(add_repeat_answer, msg['FromUserName'])
# 加入Android交流羣
elif text == u'2':
time.sleep(random.randint(1, 3))
nickname = msg['User']['NickName']
if nickname not in member_android_list:
itchat.send_msg("【" + nickname + "】童鞋\n" + add_group_answer, msg['FromUserName'])
if nickname is not None and nickname not in group_android_list:
group_android_list.append(nickname)
else:
itchat.send_msg(add_repeat_answer, msg['FromUserName'])
# 加入閒聊羣
elif text == u'3':
time.sleep(random.randint(1, 3))
nickname = msg['User']['NickName']
if nickname not in member_speak_list:
itchat.send_msg("【" + nickname + "】童鞋\n" + add_group_answer, msg['FromUserName'])
if nickname is not None and nickname not in group_speak_list:
group_speak_list.append(nickname)
else:
itchat.send_msg(add_repeat_answer, msg['FromUserName'])
# 公衆號
elif text == u'4':
time.sleep(random.randint(1, 3))
itchat.send_image('gzh.jpg', msg['FromUserName'])
# 個人博客
elif text == u'5':
time.sleep(random.randint(1, 3))
return 'coder-pig的個人主頁-掘金:https://juejin.im/user/570afb741ea493005de84da3'
# GitHub
elif text == u'6':
time.sleep(random.randint(1, 3))
return 'https://github.com/coder-pig'
# 打賞
elif text == u'7':
time.sleep(random.randint(1, 3))
itchat.send_image('ds.gif', msg['FromUserName'])
time.sleep(random.randint(1, 3))
itchat.send_msg(donate_answer, msg['FromUserName'])
time.sleep(random.randint(1, 3))
itchat.send_image('wxpay.png', msg['FromUserName'])
# 小豬微信
elif text == u'8':
time.sleep(random.randint(1, 3))
itchat.send_msg(pig_answer, msg['FromUserName'])
time.sleep(random.randint(1, 3))
itchat.send_image('scan_code.png', msg['FromUserName'])
# 其他默認回覆:
else:
time.sleep(random.randint(1, 3))
itchat.send_image('hrwh.png', msg['FromUserName'])
time.sleep(random.randint(1, 3))
itchat.send_msg(no_match_answer, msg['FromUserName'])
@itchat.msg_register([NOTE], isGroupChat=True)
def revoke_msg(msg):
result = group_name_compile.search(str(msg))
if result is not None:
group_name = result.group(1)
if '邀請' in str(msg['Text']):
results = nickname_compile.findall(str(msg))
if group_name == '小豬的Python學習交流羣':
member_python_list.clear()
for result in results:
member_python_list.append(result)
elif group_name == '小豬的Android學習交流羣':
member_python_list.clear()
results = nickname_compile.findall(str(msg))
for result in results:
member_android_list.append(result)
elif group_name == '技♂術交流🈲':
member_python_list.clear()
results = nickname_compile.findall(str(msg))
for result in results:
member_speak_list.append(result)
# 發送加羣人信息列表
def send_friend_group():
friend_dict = {"Python": [], "Android": [], "Speak": [], "Python2": []}
for p in group_python_list:
friend_dict['Python'].append(p)
for a in group_android_list:
friend_dict['Android'].append(a)
for s in group_speak_list:
friend_dict['Speak'].append(s)
for p2 in group_python_list_2:
friend_dict['Python2'].append(p2)
if len(friend_dict['Python']) > 0 or len(friend_dict['Android']) > 0 or len(friend_dict['Speak']) > 0 or len(
friend_dict['Python2']) > 0:
itchat.send_msg(str(json.dumps(friend_dict, ensure_ascii=False, indent=4)), toUserName="filehelper")
group_python_list.clear()
group_python_list_2.clear()
group_android_list.clear()
group_speak_list.clear()
# 登陸成功後開啓定時任務
def after_login():
sched.add_job(send_friend_group, 'interval', hours=2)
sched.start()
# 登陸時先獲取羣聊的UserName,獲取羣成員暱稱會用到
def get_member_list():
python_chat_rooms = itchat.search_chatrooms(name='小豬的Python學習交流1羣')
if len(python_chat_rooms) > 0:
group_username = python_chat_rooms[0]['UserName']
result = itchat.update_chatroom(group_username, detailedMember=True)
member_python_list.clear()
results = nickname_compile.findall(str(result))
for result in results:
member_python_list.append(result)
python_chat_rooms_2 = itchat.search_chatrooms(name='小豬的Python學習交流2羣')
if len(python_chat_rooms_2) > 0:
group_username = python_chat_rooms_2[0]['UserName']
result = itchat.update_chatroom(group_username, detailedMember=True)
member_python_list_2.clear()
results = nickname_compile.findall(str(result))
for result in results:
python_chat_rooms_2.append(result)
android_chat_rooms = itchat.search_chatrooms(name='小豬的Android學習交流羣')
if len(android_chat_rooms) > 0:
group_username = android_chat_rooms[0]['UserName']
result = itchat.update_chatroom(group_username, detailedMember=True)
member_android_list.clear()
results = nickname_compile.findall(str(result))
for result in results:
member_android_list.append(result)
speak_chat_rooms = itchat.search_chatrooms(name='技♂術交流🈲')
if len(android_chat_rooms) > 0:
group_username = speak_chat_rooms[0]['UserName']
result = itchat.update_chatroom(group_username, detailedMember=True)
member_speak_list.clear()
results = nickname_compile.findall(str(result))
for result in results:
member_speak_list.append(result)
if __name__ == '__main__':
sched = BlockingScheduler()
itchat.auto_login(loginCallback=get_member_list, enableCmdQR=1)
itchat.run(blockThread=False)
after_login()
運行後可以測試下我們的自動回覆:
可以,自動回覆的功能就做好了,接着是搭配着無障礙服務自動拉人。
先是五個羣名稱:
接着寫一個Bean類,用來放Json數據。
接着就是無障礙服務類了,感覺沒什麼好講的,直接上代碼吧:
package com.coderpig.wechathelper
import android.accessibilityservice.AccessibilityService
import android.app.Notification
import android.app.PendingIntent
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.orhanobut.hawk.Hawk
/**
* 描述:無障礙服務類
*
* @author CoderPig on 2018/04/12 13:47.
*/
class HelperService : AccessibilityService() {
private val TAG = "HelperService"
private val handler = Handler()
private var curGroup = ""
private var mMember = Member()
override fun onInterrupt() {}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
val eventType = event.eventType
val classNameChr = event.className
val className = classNameChr.toString()
Log.d(TAG, event.toString())
when (eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
if (Hawk.get(Constant.ADD_FRIENDS, false)) {
when (className) {
"com.tencent.mm.ui.LauncherUI" -> openGroup()
"com.tencent.mm.ui.contact.ChatroomContactUI" -> searchGroup()
"com.tencent.mm.ui.chatting.ChattingUI" -> openGroupSetting()
"com.tencent.mm.chatroom.ui.ChatroomInfoUI" -> openSelectContact()
"com.tencent.mm.ui.contact.SelectContactUI" -> addMembers()
}
}
if (className == "com.tencent.mm.ui.widget.a.c") {
dialogClick()
}
}
}
}
//1.打開羣聊
private fun openGroup() {
mMember = Hawk.get<Member>(Constant.MEMBER)
if(mMember.python_1.size != 0 || mMember.android.size != 0 || mMember.speak.size != 0
|| mMember.python_2.size != 0 || mMember.guy.size != 0) {
curGroup = when {
mMember.python_1.size > 0 -> Constant.GROUP_NAME_1
mMember.python_2.size > 0 -> Constant.GROUP_NAME_2
mMember.android.size > 0 -> Constant.GROUP_NAME_3
mMember.speak.size > 0 -> Constant.GROUP_NAME_4
mMember.guy.size > 0 -> Constant.GROUP_NAME_5
else -> ""
}
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val tabNodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/cw2")
for (tabNode in tabNodes) {
if (tabNode.text.toString() == "通訊錄") {
tabNode.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
handler.postDelayed({
val newNodeInfo = rootInActiveWindow
if (newNodeInfo != null) {
val tagNodes = newNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/lv")
for (tagNode in tagNodes) {
if (tagNode.text.toString() == "羣聊") {
tagNode.parent.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break
}
}
}
}, 500L)
}
}
}
}
}
//2.搜索羣聊
private fun searchGroup() {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/m6")
for (info in nodes) {
if (info.text.toString() == curGroup) {
info.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break
}
}
}
}
//3.打開羣聊設置
private fun openGroupSetting() {
when (curGroup) {
Constant.GROUP_NAME_1 -> {
if(mMember.python_1.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j1")[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_2 -> {
if(mMember.python_2.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j1")[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_3 -> {
if(mMember.android.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j1")[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_4 -> {
if(mMember.speak.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j1")[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_5 -> {
if(mMember.guy.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j1")[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
else -> {
performBackClick()
}
}
}
//4.滾動後點擊添加按鈕,打開添加成員頁面
private fun openSelectContact() {
if(curGroup != "") {
var members = arrayListOf<String>()
when (curGroup) {
Constant.GROUP_NAME_1 -> members = mMember.python_1
Constant.GROUP_NAME_2 -> members = mMember.python_2
Constant.GROUP_NAME_3 -> members = mMember.android
Constant.GROUP_NAME_4 -> members = mMember.speak
Constant.GROUP_NAME_5 -> members = mMember.guy
}
if (members.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val numText = nodeInfo.findAccessibilityNodeInfosByViewId("android:id/text1")[0].text.toString()
val memberCount = numText.substring(numText.indexOf("(") + 1,numText.indexOf(")")).toInt()
val listNode = nodeInfo.findAccessibilityNodeInfosByViewId("android:id/list")[0]
if(memberCount > 100) {
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}
val scrollNodeInfo = rootInActiveWindow
if (scrollNodeInfo != null) {
handler.postDelayed({
val nodes = scrollNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/dnm")
for (info in nodes) {
if (info.contentDescription.toString() == "添加成員") {
info.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break
}
}
}, 1000L)
}
}
}
} else {
performBackClick()
}
}
//5.添加成員
private fun addMembers() {
var members = arrayListOf<String>()
//最後一次的時候清空記錄,並且點擊頂部確定按鈕
when (curGroup) {
Constant.GROUP_NAME_1 -> members = mMember.python_1
Constant.GROUP_NAME_2 -> members = mMember.python_2
Constant.GROUP_NAME_3 -> members = mMember.android
Constant.GROUP_NAME_4 -> members = mMember.speak
Constant.GROUP_NAME_5 -> members = mMember.guy
}
if (members.size > 0) {
for (i in 0 until members.size) {
handler.postDelayed({
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val editNodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/b26")
if (editNodes != null && editNodes.size > 0) {
val editNode = editNodes[0]
val arguments = Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, members[i])
editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
}
}
}, 500L * (i + 1))
handler.postDelayed({
val cbNodes = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/om")
if (cbNodes != null) {
val cbNode: AccessibilityNodeInfo?
if (cbNodes.size > 0) {
cbNode = cbNodes[0]
cbNode?.parent?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
//最後一次的時候清空記錄,並且點擊頂部確定按鈕
if (i == members.size - 1) {
val m = Hawk.get<Member>(Constant.MEMBER)
when (curGroup) {
Constant.GROUP_NAME_1 -> m.python_1 = arrayListOf()
Constant.GROUP_NAME_2 -> m.python_2 = arrayListOf()
Constant.GROUP_NAME_3 -> m.android = arrayListOf()
Constant.GROUP_NAME_4 -> m.speak = arrayListOf()
Constant.GROUP_NAME_5 -> m.guy = arrayListOf()
}
Hawk.put(Constant.MEMBER, m)
curGroup = ""
val sureNodes = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j0")
if (sureNodes != null && sureNodes.size > 0) {
sureNodes[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}, 800L * (i + 1))
}
}
}
//對話框自動點擊
private fun dialogClick() {
val inviteNode = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/au_")[0]
inviteNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
private fun performBackClick() {
handler.postDelayed({ performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) }, 1300L)
}
}
接着複製itchat返回的加羣人的數據,寫入後,點擊打開微信,接下來就是享受自動加羣了,如動圖所述(加速過…)
行吧,關於Android無障礙服務 X itchat打造微信半自動機器人,就說這麼多,如果你看完
還不會,我是真的沒辦法了…無障礙服務不止可以應用於微信,其他原生APP也可以做,比如最常見的
自動打卡,自動簽到等,讀者學會了方法後,可以自行拓展~
4.倉庫地址
-
ItChatWXHelper:配合無障礙服務器拉人用的基於itchat的機器人
-
WechatHelper:利用Android AccessibilityService 實現自動加好友,拉人進羣聊