計(jì):消息監(jiān)聽與定時(shí)匯總實(shí)戰(zhàn))
1. 從群里接龍到自動點(diǎn)名這個(gè)需求到底卡在哪做過社群運(yùn)營的人都有一個(gè)共同的痛每天早上的健康打卡、作業(yè)提交、活動報(bào)名全靠群成員手動接龍。人少還好幾十個(gè)人的群消息刷得飛快誰交了誰沒交翻聊天記錄翻到眼花。更麻煩的是總有人格式不統(tǒng)一——有人寫張三已打卡有人寫1. 張三 已提交還有人直接甩一張截圖。你要是靠肉眼去核對不出三天就得崩潰。這個(gè)項(xiàng)目的核心思路很直接用 Python 的 itchat 庫登錄微信網(wǎng)頁版監(jiān)聽指定群聊的消息把符合打卡格式的消息自動記錄下來再定時(shí)輸出一份已打卡名單和未打卡名單。說白了就是給微信群配了一個(gè)不知疲倦的點(diǎn)名員。它適合誰用三類人最需要一是社群運(yùn)營者每天要統(tǒng)計(jì)幾十上百人的提交情況二是班級或團(tuán)隊(duì)的小負(fù)責(zé)人需要定期匯總成員反饋三是想學(xué) Python 自動化辦公的初學(xué)者這個(gè)場景足夠真實(shí)代碼量也不大拿來練手非常合適。但我要先把丑話說在前面itchat 依賴的是網(wǎng)頁版微信協(xié)議而網(wǎng)頁版微信的可用性這些年一直在收緊。很多賬號已經(jīng)無法登錄網(wǎng)頁版這是這個(gè)方案最大的不確定性。所以這篇文章不會只講怎么跑起來更會講清楚什么情況下它跑不起來以及跑不起來時(shí)怎么換思路。這才是真實(shí)項(xiàng)目里最有價(jià)值的部分。2. itchat 的工作機(jī)制與登錄環(huán)節(jié)的真實(shí)門檻2.1 itchat 到底做了什么itchat 本質(zhì)上是對網(wǎng)頁版微信 HTTP 接口的一層 Python 封裝。你調(diào)用itchat.auto_login()它會在本地起一個(gè)服務(wù)彈出一個(gè)二維碼圖片你用手機(jī)掃碼確認(rèn)后它就拿到了一組登錄憑證包括 cookie 和同步密鑰。之后它通過長輪詢的方式不斷向服務(wù)器請求有沒有新消息拿到消息后回調(diào)你注冊的處理函數(shù)。理解這一點(diǎn)很關(guān)鍵itchat 不是破解微信它走的是官方曾經(jīng)開放的網(wǎng)頁版通道。所以它的能力邊界完全取決于這個(gè)通道是否對你的賬號開放。這也是為什么同一段代碼有人跑得好好的有人掃碼后直接提示無法登錄。2.2 登錄環(huán)節(jié)最容易踩的三個(gè)坑第一個(gè)坑是賬號類型限制。實(shí)測下來很多近期注冊的賬號、以及部分被系統(tǒng)判定為風(fēng)險(xiǎn)較高的賬號是無法登錄網(wǎng)頁版的。這不是代碼問題換多少臺電腦都一樣。判斷方法很簡單你先用瀏覽器打開網(wǎng)頁版微信如果瀏覽器都登不上itchat 必然也登不上。第二個(gè)坑是二維碼顯示不出來。auto_login()默認(rèn)會嘗試用系統(tǒng)圖片查看器打開二維碼在服務(wù)器環(huán)境或者某些精簡系統(tǒng)上會失敗。穩(wěn)妥的做法是加上enableCmdQR參數(shù)直接在終端里用字符畫顯示二維碼import itchat # enableCmdQR2 表示用兩個(gè)字符寬度渲染一個(gè)像素塊終端里更清晰 itchat.auto_login(enableCmdQR2, hotReloadTrue)hotReloadTrue這個(gè)參數(shù)值得單獨(dú)說。它會把登錄狀態(tài)緩存到本地文件短時(shí)間內(nèi)再次運(yùn)行程序時(shí)不用重新掃碼。調(diào)試階段這個(gè)能省下大量時(shí)間不然你每改一行代碼就要掃一次碼心態(tài)很容易崩。第三個(gè)坑是登錄后立刻掉線。常見原因是同一賬號在多個(gè)地方登錄網(wǎng)頁版或者程序運(yùn)行環(huán)境網(wǎng)絡(luò)不穩(wěn)定導(dǎo)致長輪詢超時(shí)。我的經(jīng)驗(yàn)是調(diào)試期間手機(jī)端不要頻繁切換網(wǎng)絡(luò)程序盡量跑在穩(wěn)定的有線網(wǎng)絡(luò)環(huán)境里。提示如果你發(fā)現(xiàn)掃碼后程序卡住不動先別急著改代碼。打開調(diào)試日志看看是不是卡在get_contact或web_init這一步這通常意味著賬號權(quán)限受限而不是代碼寫錯了。3. 群消息監(jiān)聽的實(shí)現(xiàn)細(xì)節(jié)從注冊回調(diào)到精準(zhǔn)匹配3.1 消息回調(diào)的注冊與群聊定位itchat 的消息處理靠裝飾器注冊回調(diào)函數(shù)。核心是itchat.msg_register你需要指定監(jiān)聽的消息類型。群聊文本消息的類型是itchat.content.TEXT但光這樣還不夠因?yàn)槟J(rèn)情況下群消息里的發(fā)送者信息是不完整的你需要額外開啟群聊成員信息獲取。import itchat from itchat.content import TEXT # 開啟群聊消息中攜帶發(fā)送者昵稱 itchat.msg_register(TEXT, isGroupChatTrue) def handle_group_msg(msg): # msg[ActualNickName] 是群內(nèi)昵稱 # msg[Content] 是消息正文 # msg[FromUserName] 是群聊的ID pass這里有個(gè)非常關(guān)鍵的細(xì)節(jié)群聊的FromUserName是一串以開頭的 ID不是群名稱。你不能直接用群名去匹配得先拿到群 ID。獲取方法是遍歷itchat.get_chatrooms()按群名找到對應(yīng)的UserName然后緩存起來。def find_group_id(group_name): chatrooms itchat.get_chatrooms(updateTrue) for room in chatrooms: if room[NickName] group_name: return room[UserName] return NoneupdateTrue會強(qiáng)制從服務(wù)器拉取最新的群列表第一次運(yùn)行時(shí)建議加上之后可以緩存結(jié)果減少請求。3.2 打卡消息的匹配策略匹配邏輯是整個(gè)程序的大腦。最粗糙的做法是判斷消息里有沒有打卡兩個(gè)字但這樣誤判率極高——有人發(fā)我今天不想打卡也會被算進(jìn)去。我的做法是設(shè)計(jì)一個(gè)結(jié)構(gòu)化匹配規(guī)則用正則表達(dá)式同時(shí)約束關(guān)鍵詞和格式。假設(shè)打卡格式約定為姓名已打卡可以這樣寫import re # 匹配 張三 已打卡 或 張三已打卡 或 張三: 已打卡 pattern re.compile(r^([\u4e00-\u9fa5A-Za-z]{2,10})\s*[:]?\s*(已打卡|打卡完成|已完成)$) def parse_checkin(content): text content.strip() match pattern.match(text) if match: return match.group(1) # 返回姓名 return None這個(gè)正則做了幾件事限制姓名是 2 到 10 個(gè)中英文字符允許中間有冒號或空格結(jié)尾必須是約定的打卡詞之一。^和$保證整條消息完全符合格式避免張三已打卡但是李四沒打這種句子被誤判。實(shí)際運(yùn)營中你很難要求所有人都嚴(yán)格按格式來。所以更實(shí)用的策略是分級匹配先嘗試嚴(yán)格匹配匹配不上再走寬松匹配比如只提取消息開頭的姓名最后把寬松匹配的結(jié)果單獨(dú)列出來人工復(fù)核。這樣既不漏人也不會把臟數(shù)據(jù)混進(jìn)正式名單。3.3 去重與時(shí)間窗口同一個(gè)人可能重復(fù)打卡或者手滑發(fā)了兩遍。所以記錄的時(shí)候要用集合去重并且加上時(shí)間窗口——只統(tǒng)計(jì)當(dāng)天某個(gè)時(shí)間段內(nèi)的消息。時(shí)間窗口的判斷用消息自帶的CreateTime字段它是 Unix 時(shí)間戳。import time from datetime import datetime def is_in_window(msg_time, start_hour6, end_hour12): dt datetime.fromtimestamp(msg_time) return start_hour dt.hour end_hour把去重集合和時(shí)間窗口結(jié)合起來就能保證統(tǒng)計(jì)結(jié)果干凈可靠。這里我踩過一個(gè)坑CreateTime是服務(wù)器時(shí)間戳如果你的運(yùn)行環(huán)境時(shí)區(qū)設(shè)置不對算出來的小時(shí)數(shù)會偏。穩(wěn)妥做法是顯式指定時(shí)區(qū)或者干脆用本地時(shí)間做二次校驗(yàn)。4. 名單生成、定時(shí)任務(wù)與結(jié)果推送4.1 已打卡與未打卡名單的比對統(tǒng)計(jì)的核心是全集減去已打卡集合。全集從哪來兩個(gè)來源一是群成員列表itchat.get_chatrooms()里每個(gè)群的MemberList二是你預(yù)先維護(hù)的一份花名冊。我更推薦后者因?yàn)槿撼蓡T會變動而且很多人群昵稱和真實(shí)姓名對不上。# 預(yù)置花名冊 roster [張三, 李四, 王五, 趙六] def build_report(checked_in): done [name for name in roster if name in checked_in] undone [name for name in roster if name not in checked_in] return done, undone用列表推導(dǎo)式做比對代碼簡潔邏輯也一目了然。注意checked_in是集合查找效率是 O(1)花名冊哪怕有幾百人比對也是瞬間完成。4.2 定時(shí)輸出與消息推送統(tǒng)計(jì)不能只靠手動觸發(fā)得定時(shí)跑。最簡單的方案是用threading.Timer或者schedule庫起一個(gè)后臺線程每隔一段時(shí)間生成一次報(bào)告。但要注意itchat 的消息監(jiān)聽本身是阻塞的定時(shí)任務(wù)必須跑在獨(dú)立線程里否則會把監(jiān)聽循環(huán)卡死。import threading import schedule def job(): done, undone build_report(checked_in_set) report f已打卡 {len(done)} 人{(lán)、.join(done)}\n report f未打卡 {len(undone)} 人{(lán)、.join(undone)} # 把報(bào)告發(fā)回群里或發(fā)給指定聯(lián)系人 itchat.send(report, toUserNamegroup_id) def run_scheduler(): schedule.every().day.at(12:00).do(job) while True: schedule.run_pending() time.sleep(1) # 在獨(dú)立線程里啟動定時(shí)器 threading.Thread(targetrun_scheduler, daemonTrue).start()daemonTrue保證主程序退出時(shí)定時(shí)線程也跟著結(jié)束不會留下僵尸進(jìn)程。發(fā)送報(bào)告用itchat.sendtoUserName填群 ID 就能發(fā)回群里填個(gè)人 ID 就能私發(fā)給負(fù)責(zé)人。4.3 數(shù)據(jù)持久化別讓統(tǒng)計(jì)結(jié)果只活在內(nèi)存里程序一重啟內(nèi)存里的打卡記錄就沒了。所以必須落盤。最輕量的方案是寫 CSV每天一個(gè)文件方便用 Excel 打開核對。import csv from datetime import date def save_record(name, timestamp): filename fcheckin_{date.today()}.csv with open(filename, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([name, datetime.fromtimestamp(timestamp).strftime(%H:%M:%S)])注意encodingutf-8-sig這個(gè)帶 BOM 的編碼能讓 Excel 正確識別中文不然打開就是亂碼。這個(gè)細(xì)節(jié)很多教程都不提但實(shí)際用起來差別巨大。5. 實(shí)測中的意外情況與排查鏈路5.1 消息收不到從回調(diào)注冊查起程序跑起來了群里也發(fā)了打卡消息但日志里什么都沒有。這時(shí)候按下面的順序排查第一步確認(rèn)回調(diào)函數(shù)真的被注冊了。裝飾器寫錯、函數(shù)名重復(fù)、或者msg_register的參數(shù)類型不對都會導(dǎo)致回調(diào)不觸發(fā)??梢栽诨卣{(diào)函數(shù)第一行加一句print(收到消息)看有沒有輸出。第二步確認(rèn)群消息類型。如果你只注冊了TEXT那圖片、語音、表情都不會觸發(fā)。打卡如果允許發(fā)截圖就得額外注冊PICTURE類型。第三步確認(rèn)群 ID 匹配正確。前面說過群 ID 是開頭的字符串如果你拿群名去比對FromUserName永遠(yuǎn)匹配不上。打印出來看一眼就清楚了。5.2 中文亂碼編碼問題的三種表現(xiàn)中文亂碼在不同環(huán)節(jié)表現(xiàn)不一樣。終端打印亂碼通常是終端編碼不是 UTF-8寫文件亂碼是沒加utf-8-sig發(fā)送消息亂碼多半是字符串拼接時(shí)混入了非 UTF-8 的內(nèi)容。我的習(xí)慣是在程序入口統(tǒng)一設(shè)置import sys import io sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)這樣終端輸出就穩(wěn)定了。文件寫入按前面說的加 BOM發(fā)送消息前對內(nèi)容做一次encode(utf-8).decode(utf-8)的清洗基本能覆蓋絕大多數(shù)亂碼場景。5.3 賬號被限制登錄這個(gè)方案的天花板這是最需要正視的問題。當(dāng) itchat 提示無法登錄網(wǎng)頁版時(shí)說明你的賬號已經(jīng)不在網(wǎng)頁版通道的開放范圍內(nèi)。這時(shí)候繼續(xù)折騰代碼沒有意義得換思路。替代方案有幾個(gè)方向一是改用企業(yè)微信或釘釘?shù)臋C(jī)器人接口它們對自動化更友好有官方 API二是用群機(jī)器人 webhook讓成員通過點(diǎn)擊卡片或填表的方式打卡數(shù)據(jù)直接進(jìn)后臺三是如果只是內(nèi)部小范圍使用可以考慮用共享文檔加腳本讀取的方式成員在文檔里填腳本定時(shí)抓取。每個(gè)方案都有取舍。官方 API 穩(wěn)定但需要配置webhook 簡單但交互形式受限共享文檔靈活但依賴成員自覺。選哪個(gè)取決于你的群規(guī)模和成員的技術(shù)接受度。6. 幾個(gè)讓程序更耐用的工程習(xí)慣6.1 把配置抽出來別硬編碼群名、花名冊、打卡關(guān)鍵詞、統(tǒng)計(jì)時(shí)間窗口這些東西都會變。如果全寫在代碼里每次調(diào)整都要改代碼、重啟程序。更好的做法是放到一個(gè)config.json里{ group_name: 某打卡群, roster: [張三, 李四, 王五], keywords: [已打卡, 打卡完成], window: {start: 6, end: 12}, report_time: 12:00 }程序啟動時(shí)讀一次配置改規(guī)則只需要改 JSON 文件代碼一行不動。這個(gè)習(xí)慣在長期運(yùn)行的項(xiàng)目里價(jià)值極高。6.2 日志要分級別只 print調(diào)試階段print夠用但程序一旦長期跑你需要知道什么時(shí)候收到了什么消息哪條消息匹配失敗了定時(shí)任務(wù)有沒有正常執(zhí)行。用logging模塊分級別記錄import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(checkin.log, encodingutf-8), logging.StreamHandler() ] )匹配失敗的消息用WARNING級別記下來方便事后分析是不是格式約定需要調(diào)整。定時(shí)任務(wù)的執(zhí)行用INFO記錄出問題時(shí)能快速定位是任務(wù)沒跑還是跑了但結(jié)果不對。6.3 異常兜底別讓一個(gè)錯誤搞崩整個(gè)程序消息處理函數(shù)里任何一行拋異常都可能導(dǎo)致回調(diào)中斷。所以核心邏輯一定要包在 try 里def handle_group_msg(msg): try: # 處理邏輯 pass except Exception as e: logging.error(f處理消息出錯: {e}, exc_infoTrue)exc_infoTrue會把完整堆棧寫進(jìn)日志排查時(shí)能直接看到是哪一行出的問題。這個(gè)習(xí)慣看起來啰嗦但在無人值守的場景下它是程序能連續(xù)跑幾周不崩的關(guān)鍵。7. 關(guān)于這類自動化工具的一點(diǎn)個(gè)人體會我前后用類似思路做過好幾個(gè)群統(tǒng)計(jì)工具最大的感受是技術(shù)實(shí)現(xiàn)從來不是最難的部分難的是讓規(guī)則被所有人接受并執(zhí)行。你代碼寫得再漂亮如果群里一半人不按格式打卡統(tǒng)計(jì)結(jié)果照樣一團(tuán)糟。所以我的建議是工具上線前先在群里做三天雙軌運(yùn)行——人工統(tǒng)計(jì)和自動統(tǒng)計(jì)同時(shí)跑對比結(jié)果找出格式歧義的地方把規(guī)則打磨清楚再正式切換。這個(gè)過程看起來慢但能省掉后面無數(shù)次的返工。另外itchat 這類依賴非官方通道的方案本質(zhì)上是在借用一個(gè)隨時(shí)可能關(guān)閉的入口。用它做臨時(shí)工具、做個(gè)人練手項(xiàng)目都沒問題但如果是要長期穩(wěn)定運(yùn)行的業(yè)務(wù)系統(tǒng)還是盡早遷移到有官方支持的平臺上。把 itchat 當(dāng)作學(xué)習(xí)消息監(jiān)聽、回調(diào)機(jī)制、定時(shí)任務(wù)的練手項(xiàng)目它的價(jià)值就已經(jīng)足夠了。真正沉淀下來的是你對消息驅(qū)動規(guī)則匹配定時(shí)匯總這套模式的理解換任何平臺都能復(fù)用。