在移動社交時代,微信已經成為我們生活不可或缺的一部分。2017年的《微信資料包告》中顯示:每天有380億條訊息從微信上發出,其中6億條是語音訊息,有350萬個活躍的公眾賬號,並存在著8億活躍粉絲。在其他諸如支付、出行等方面,也更是與我們的生活休憩相關。正如它的slogan那樣:微信,是一個生活方式。
和微信有關的第三方框架很多,比如「wxpy」「wxBot」等,今天要使用的是其中著名的「itchat」Python模組,它是由第三方開發者@LittleCoder等架構實現的介面,該API的使用場景可以不僅僅只是一個微信機器人,更多的功能可以由我們自己去發現擴充。「itchat」的功能非常強大,呼叫卻十分簡單,同時,將「itchat」結合「matplotlib」「jieba」等模組,可以對我們的微信好友的資訊進行一些收集與分析,讓我們能更直觀的瞭解我們自己的圈子。
一個自動回覆機器人
微信自上線以來,一直沒有自動回覆的功能,想必是有他們的理念。但是有些人群,確實對此功能有一定需求,我舉兩個栗子:
- 不願時刻被訊息打擾的人
- 訊息需要批量處理的人們(比如微商)
對此,我設計了幾個功能:
功能列表:
- [x] 收到訊息立即自動回覆
- [x] 收到訊息延遲指定時間回覆
- [x] 對不同好友定製不同的回覆內容
- [x] 在手機端隨時進行控制
itchat的原理是通過掃碼進行遠端微信網頁端的登入,然後通過在移動端進行操作,網頁端進行響應,來實現一些功能,雖然似乎繞了個彎子,但是在微信的限制下,這似乎已經是一種最佳的方式了,猶如戴著鐐銬起舞。
首先我們需要考慮別的問題是:程式如何在微信端接收到你的指令。此時出現了一個非常重要的角色:檔案管理器。(當網頁端微信登陸後,訊息列表會出現)此時,檔案管理器充當了移動端和web端的橋樑。比如你給檔案管理器傳一個字串作為指令,網頁端接收到資料後就能根據你的預定來執行相應操作。
我們可以先通過設定幾個全域性變數來做作為功能的開關和儲存資料的容器。
#自動回覆開關
SWITCH_REPLY=True
#延遲迴復開關
SWITCH_DELAY=False
#延遲時間
DELAY_TIME=120
#訊息字首開關
SWITCH_PREFIX=True
#訊息字首內容
PREFIX_CONTENT="[自動回覆]"
#回覆內容字典
REPLY_DICT={}
#延遲迴復字典
DELAY_REPLY_DICT={}
複製程式碼
然後通過判斷web端在”檔案管理器“中接收到的字串指令來進行不同操作,這一部分的程式碼比較簡單且冗長,這裡就不貼出來了,完整原始碼地址將會在文末給出。
假如此時我們收到了朋友的訊息,需要程式給出自動回覆。
#獲取傳送訊息的朋友的資訊
target_friend=itchat.search_friends(userName = msg['FromUserName'])
if target_friend:
#獲取ta的暱稱
nickName=target_friend['NickName']
if not REPLY_DICT.__contains__(nickName):
#設定預設回覆
REPLY_DICT[nickName]="抱歉我有事暫未看到訊息,稍後回覆,若有急事可以電話聯絡(•ω•`)"
reply_content=REPLY_DICT[nickName]
#判斷自動回覆開關
if SWITCH_REPLY:
#判斷延時回覆開關
if SWITCH_DELAY:
localtime = time.time()
DELAY_REPLY_DICT[nickName]=[localtime,msg['FromUserName']]
print (DELAY_REPLY_DICT)
if not SWITCH_DELAY:
#判斷訊息字首開關
if SWITCH_PREFIX:
reply_content = PREFIX_CONTENT + REPLY_DICT[nickName]
else:
reply_content = REPLY_DICT[nickName]
#傳送訊息
itchat.send(reply_content, toUserName=msg['FromUserName'])
複製程式碼
收到朋友訊息即時進行自動回覆是很簡單的,但是如何去做延時傳送回覆訊息呢?(至於做這個功能有沒有必要的問題可以先擱置,不過我認為在很多場景下是需要這個功能的,大家也可以在評論區討論在什麼場景下需要延遲自動回覆)現在就回到技術的問題,如何實現可設定時間的延時自動回覆。
我先談一談我的想法,拋磚引玉。一般傳送訊息需要用到佇列,進行入隊和出隊,我在這裡設定了一個字典來儲存訊息傳送者的資料。鍵為訊息傳送者的暱稱,值是一個長度為2的陣列,分別儲存訊息傳送者的微信id和接收訊息時的時間戳。這樣我將每條傳送過來的朋友資訊儲存在這個字典中,再通過將設定延遲時間同訊息時間戳求和與當前時間戳進行對比,若當前時間戳較大,那麼執行傳送訊息的操作。
此時再開啟一個執行緒作為定時任務,定時去檢測字典中每條資料是否到達了傳送的臨界要求(當前時間戳>=訊息時間戳+設定的延遲時間)Python中有個專門做定時任務的模組叫sched,但是我嘗試了一下,sched會阻塞當前主執行緒,也會阻塞itchat的執行緒,所以並不合適。這裡我還是採用了threading的Timer來充當定時器,不過要注意使用遞迴,否則將會出現執行一次就結束的情況。
#延遲傳送訊息的函式
def delay_reply():
#print("開始執行")
global DELAY_REPLY_DICT
if SWITCH_DELAY:
while len(DELAY_REPLY_DICT)>0:
localtime = time.time()
# print (localtime)
# print (DELAY_REPLY_DICT[item][0])
# print (int(DELAY_TIME))
for item in list(DELAY_REPLY_DICT.keys()):
if SWITCH_REPLY:
reply_content = item + "," + str(round(int(DELAY_TIME) / 60, 1)) + "分鐘過去了," + REPLY_DICT[item]
itchat.send(reply_content, toUserName=DELAY_REPLY_DICT[item][1])
# print ("傳送訊息")
del DELAY_REPLY_DICT[item]
print (DELAY_REPLY_DICT)
global timer1
timer1=threading.Timer(DELAY_TIME,delay_reply)
timer1.start()
複製程式碼
到此為止,主要的功能已經實現了,我用一個測試賬號對我的微信進行了各種測試,看一下以下截圖:
這時功能基本已經完成了,這就結束了嗎?彆著急,再想想有沒有需要完善一下的地方?用過微信web端的同學應該知道,當web端長期處於未操作的狀態下會失去連線。在我們這個情況下,假如你長時間未收到微信訊息,後臺程式將會與微信失去連線,再次開啟需要登上伺服器重啟程式,這顯然非常麻煩。有沒有什麼簡單的解決辦法呢?我想到一些應用的後臺通常會做一道心跳檢測機制,那我就模仿這個思路,定時給我的”檔案管理器“發一個字串,來保持連線。
def keep_alive():
text="保持登入"
itchat.send(text, toUserName="filehelper")
global timer2
timer2 = threading.Timer(60*60,keep_alive)
timer2.start()
複製程式碼
最後,我們需要將這個程式釋出在伺服器上,讓它全天候為我的微信服務。
這裡需要注意,如果僅用python xxxx.py
來執行的話,關閉shell會導致程式結束,所以我們需要使用nohup python xxxx.py &
來全方位守護程式,這裡囉嗦一句,nohup和&的功能是不一樣的,很多人容易混淆,感興趣的話可以去查下資料區分一下。
到此,微信經過了我們的稍稍調教,已經乖了一點。但是,這遠遠不夠,思路可以繼續擴充,比如實現通過手機微信來控制電腦的開關機,電腦軟體的啟動關閉,都是可以的,甚至實現控制家中的空調,理論上也未嘗不可,只是比較有難度。
簡單分析微信好友資訊
上文提到,既然我們能通過itchat來獲取好友的資訊,那讓我們看看都有哪些好玩的資訊。這是以json形式返回的我的資訊,同理我的好友的這些公開資訊我也能獲取到。
我們就來進行一些簡單的的資料抓取,清洗與呈現。我看中的欄位是:Sex、City、Province、Signature。我想做的是通過圖表來直觀地展示我微信好友中的性別比例,家鄉分佈(當然不一定準確,很多人都是胡亂設定的),我的江蘇朋友的市級分佈,以及好友個性簽名的詞雲。這部分內容主要是需要熟悉諸如pyecharts、jieba、wordcloud模組的API調取,難度不大,但需要細心除錯。我這裡就不詳細贅述了,有疑問可以在評論區討論。
性別比例
def get_sex():
# 獲取好友資料
my_friends = itchat.get_friends(update=True)[0:]
sex = {"male": 0, "female": 0, "other": 0}
for item in my_friends[1:]:
s = item["Sex"]
if s == 1:
sex["male"] += 1
elif s == 2:
sex["female"] += 1
else:
sex["other"] += 1
total = len(my_friends[1:])
複製程式碼
好友省級分佈
def get_data(type):
result=[]
my_friends = itchat.get_friends(update=True)[0:]
for item in my_friends:
result.append(item[type])
return result
def friends_province():
# 獲取好友省份
province= get_data("Province")
# 分類
province_distribution = {}
for item in province:
#刪除英文省份,因為中國地圖表中沒有
if bool(re.search('[a-z]',item)):
continue
elif not province_distribution.__contains__(item):
province_distribution[item] = 1
else:
province_distribution[item] += 1
#將省份名為空的刪除
province_distribution.pop('')
#提取地圖介面需要的資料格式
province_keys=province_distribution.keys()
province_values=province_distribution.values()
return province_keys,province_values
if __name__ == '__main__':
itchat.auto_login(True)
微信好友省份分佈
attr,value=friends_province()
map = Map("我的微信好友分佈", "@寒食君",width=1200, height=600)
map.add("", attr, value, maptype='china', is_visualmap=True,
visual_text_color='#000')
map.render()
複製程式碼
省內分佈
def friends_jiangsu():
# 獲取好友城市
city_distribution={}
city = get_data("City")
jiangsu_city=["南通市","常州市","淮安市","連雲港市","南京市","蘇州市","宿遷市","泰州市","無錫市","徐州市","鹽城市","揚州市","鎮江市"]
for item in city:
item=item+"市"
if item in jiangsu_city:
if not city_distribution.__contains__(item):
city_distribution[item]=1
else:
city_distribution[item]+=1
# 提取地圖介面需要的資料格式
city_keys=city_distribution.keys()
city_values=city_distribution.values()
return city_keys,city_values
if __name__ == '__main__':
itchat.auto_login(True)
微信江蘇好友分佈
attr,value=friends_jiangsu()
map = Map("江蘇好友分佈","@寒食君", width=1200, height=600)
map.add("", attr, value, maptype='江蘇', is_visualmap=True,
visual_text_color='#000')
map.render()
複製程式碼
個性簽名詞雲
def friends_signature():
signature = get_data("Signature")
wash_signature=[]
for item in signature:
#去除emoji表情等非文字
if "emoji" in item:
continue
rep = re.compile("1f\d+\w*|[<>/=【】『』♂ω]")
item=rep.sub("", item)
wash_signature.append(item)
words="".join(wash_signature)
wordlist = jieba.cut(words, cut_all=True)
word_space_split = " ".join(wordlist)
coloring = np.array(Image.open("C:/Users/casua/Desktop/test1.JPG"))
my_wordcloud = WordCloud(background_color="white", max_words=800,
mask=coloring, max_font_size=80, random_state=30, scale=2,font_path="C:/Windows/Fonts/STKAITI.ttf").generate(word_space_split)
image_colors = ImageColorGenerator(coloring)
plt.imshow(my_wordcloud.recolor(color_func=image_colors))
plt.imshow(my_wordcloud)
plt.axis("off")
plt.show()
複製程式碼
看來我的朋友們,大多是對自己的人生充滿著夢想的青年,既痴迷於技術,也熱愛著街舞,他們喜歡通過努力與自律堅守初心。The world makes me pain,but it is worth fighting for chance. (我實在編不下去了