如今的平常生活已經離不開微信,不免會生出微信有沒有什麼API可使用的想法。php
那樣就能夠拿本身微信作個消息聚合、開個投票什麼的,能夠顯然沒有這種東西。html
不過還好,有網頁版微信不就等於有了API麼,這個項目就是出於這個想法出現的。node
看完這一系列教程,你就能從頭開始實現本身關於微信以及相似工具的想法,例如一個完善的微信機器人。python
固然,若是你只對使用微信的API感興趣,能夠直接跳到下一篇教程,直接使用我已經完成的API。git
本文爲該教程的第一部分,主要講述抓包與僞造,將會以最簡單的方法介紹使用Python模擬登錄抓取數據等內容。github
Python與基本的網絡基礎都不困難,因此即便沒有這方面基礎輔助搜索引擎也徹底能夠學習本教程。web
關於本教程有任何建議或者疑問,都歡迎郵件與我聯繫,或者在github上提出(i7meavnktqegm1b@qq.com)正則表達式
教程將會從如何分析微信協議開始,第一部分將教你如何從零開始獲取並模擬擴展我的微信號所須要的協議。json
第二部分將會就這些協議進行利用,以微信機器人爲例介紹我給出的項目基本框架與存儲、任務識別等功能。瀏覽器
第三部分就項目基本框架開發插件,以消息聚合等功能爲例對框架作進一步介紹與擴展。
目前的樣例微信號被擴展爲了可以完成信息上傳下載的機器人,用於展現信息交互功能。
其支持文件、圖片、語音的上傳下載,能夠掃碼嘗試使用。
本文是這一教程的第一部分,須要配置抓包與Python環境。
本教程使用的環境以下:
Windows 8.1
Python 2.7.11 (安裝Image, requests)
Wireshark 2.0.2
微信版本6.3.15
Wireshark是常見的抓包軟件,這裏經過一些配置抓取微信網頁端的流量。
因爲微信網頁端使用https,須要特殊的配置才能看到有意義的內容,具體的配置見這裏。
配置完成之後開始抓包,載入https://www.baidu.com
後若能看到http請求則配置成功。
微信網頁端登錄分爲不少步,這裏以第一步掃碼爲例講解如何從抓包開始完成模擬。
在抓包之前,咱們須要先想清楚這是一個什麼樣的過程。
咱們都登陸過網頁端微信,沒有的話能夠如今作一個嘗試:微信網頁端。
這個過程簡單而言能夠分爲以下幾步:
向服務器提供一些用於獲取二維碼的數據
服務器返回二維碼
向服務器詢問二維碼掃描狀態
服務器返回掃描狀態
有了這些概念之後就能夠開始將這四步和包對應起來。
開啓wireshark抓包後登錄網頁端微信,完成掃碼登錄,而後關閉wireshark抓包。
篩選http請求(就是菜單欄下面輸入的那個http),能夠看到這樣的界面。
這裏須要講的就是第一列「No.」列的數字就是後文說的幾號包,例如第一行就是30號包。數據包的類型則在Info列中能夠看到,是GET,POST或是別的請求。
那麼咱們能夠開始分析抓到的包了,咱們先粗略的瀏覽一下數據包。
第325號包引發了個人注意,由於登錄過程中很是有特徵的一個過程是二維碼的獲取,因此咱們嘗試打開這一數據包的圖片的內容。
325號包是由292號包的請求獲取的,292號包又是一個普通的get請求,因此咱們嘗試直接在瀏覽器中訪問這一網址。(訪問本身抓到的網址)
具體的網址經過雙擊打開292號包便可找到。如須要能夠點擊這裏看圖。
咱們發現直接在瀏覽器中獲取了一張二維碼,因此這頗有可能就是上述1、二步的過程了。
那麼咱們是向服務器提供了哪些數據獲取了二維碼呢?
每次咱們登陸的二維碼會變化,且沒有隨二維碼傳回的標識,因此咱們確定提供了每次不一樣的信息
網址中最後一部分看起來比較像標識:https://login.weixin.qq.com/qrcode/4ZtmDT6OPg==
爲了進一步驗證猜測,再次抓包,發現相似292號包的請求url僅最後一部分存在區別
因此咱們提供了4ZtmDT6Opg==
獲取到了這一二維碼。
那麼這一標識是隨機生成的仍是服務器獲取的呢?
從最近的包開始分析服務器傳回的數據(Source是服務器地址的數據),發現就在上一行,286號包有咱們感興趣的數據。
打開這個包,能夠看到其返回的數據爲window.QRLogin.code = 200; window.QRLogin.uuid = "4ZtmDT6OPg==";
(見下方截圖)
顯然致使服務器返回這一請求的284號包就是咱們獲取標識(下稱uuid)所須要僞造的包。
那麼284號包須要傳遞給服務器哪些數據?
這是一個get請求,因此咱們分析其請求的url:https://login.weixin.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_=1453725386008
。
能夠發現須要給出五個量appid, redirect_uri, fun, lang, _
。
其中除了appid其他都顯然是固定的量(_的格式顯然爲時間戳)。
然而搜索284號包以前的包也沒有發現這一數值的來源,因此暫且認爲其也是固定的量,模擬時若是出現問題再作嘗試。
到了這裏,1,2步的過程咱們已經可以對應上相應的包了。
3,4部的最顯著特徵是在掃描成功之後會獲取掃描用的微信號的頭像。
咱們仍是首先大體的瀏覽一下服務器返回的數據包,試圖找到包含圖片的數據包。
從325號包(微信頭像確定在二維碼以後獲取)開始瀏覽。
咱們發現338號包中包含一個base64加密的圖片,解壓後能夠看到本身的頭像。
因此這個數據包就是服務器返回的掃描成功的數據包了,而前面那部分window.code=201
顯然就是表示狀態的代碼。(見下方截圖)
通過嘗試與再次抓包,咱們理解狀態碼的涵義:200:登錄成功 201:掃描成功 408:圖片過時
那麼第四部咱們已經可以徹底的理解
咱們很容易的找到了在登陸過程中不斷出現的請求,那麼要怎麼模擬呢?
首先這是一個簡單的get請求,url爲:https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=4ZtmDT6OPg==&tip=1&r=-2026440414&_=1453725386009
能夠發現須要給出五個量loginicon, uuid, tip, r, _
經過屢次抓包發現除了r之外均可以找到簡單的規律,那麼r的規律等待模擬時再嘗試處理
至此你應該已經能將四個過程所有與具體的數據包對應。爲了不有遺漏的過程,咱們將沒有使用到的與服務器交互的數據包標識出來(右鍵Mark)。通過簡單的瀏覽,認爲其中並無必須的數據包交互。但值得注意的是,若是以後模擬數據包沒有問題卻沒法登錄的話應當再回到這些數據包中搜尋。
這裏作一個簡單的小結,這一部分簡單的介紹了分析數據包的基本思路,以及一些小的技巧。固然這些僅供參考,在具體的抓包中徹底能夠根據具體的交互過程自由發揮。而目前留下來的問題有:第一步時的appid與第三步時的r,留待模擬時在作研究。
這一部分咱們使用python的requests模塊,能夠經過pip install requests
安裝。
咱們先來簡單的講述一下這個包。
import requests # 新建一個session對象(就像開了一個瀏覽器同樣) session = requests.Session() # 使用get方法獲取https://www.baidu.com/s?wd=python url = 'https://www.baidu.com/s' params = { 'wd': 'python', } r = session.get(url = url, params = params) with open('baidu.htm') as f: f.write(r.content) # 存入文件,可使用瀏覽器嘗試打開 # 舉例使用post方法 import json url = 'https://www.baidu.com' data = { 'wd': 'python', } r = session.get(url = url, data = json.dumps(data)) with open('baidu.htm') as f: f.write(r.content) # 以上代碼與下面的代碼不連續
若是想要更多的瞭解這個包,能夠瀏覽requests快速入門。
你能夠嘗試獲取一個你熟悉的網站來測試使用requests,在測試時能夠打開抓包,查看你發送的數據包與想要發送的數據包是否同樣。
那麼咱們開始模擬第1、二個過程,向服務器提供一些用於獲取二維碼的數據,服務器返回二維碼。
向服務器提交284,292號包
從服務器返回數據中提取出uuid與二維碼圖片
284號包
咱們須要模擬的地址爲:https://login.weixin.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_=1453725386008 ,因此咱們模擬的代碼以下:
#coding=utf8 import time, requests session = requests.Session() url = 'https://login.weixin.qq.com/jslogin' params = { 'appid': 'wx782c26e4c19acffb', 'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage', 'fun': 'new', 'lang': 'en_US', '_': int(time.time()), } r = session.get(url, params = params) print('Content: %s'%r.text)
固然,將模擬的地址所有寫在url裏面效果徹底同樣。
值得一提的是requests會幫咱們自動urlencode,若是不須要urlencode(/變爲了%2F)能夠將全部內容都寫在url裏面。
提取出uuid
這裏使用re,若是不瞭解正則表達式的話能夠直接拿來用,畢竟和這一個教程並不相關。
# 上接上一段程序 import re regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' # 咱們能夠看到返回的量是上述的格式,括號內的內容被提取了出來 data = re.search(regx, r.text) if data and data.group(1) == '200': uuid = data.group(2) print('uuid: %s'%uuid)
若是沒能成功獲取到uuid能夠嘗試再運行一次。
292號包
咱們須要模擬的url爲:https://login.weixin.qq.com/qrcode/4ZtmDT6OPg== ,因此咱們模擬的代碼以下:
# 上接上一段程序 url = 'https://login.weixin.qq.com/qrcode/' + uuid r = session.get(url, stream = True) with open('QRCode.jpg', 'wb') as f: f.write(r.content) # 如今你能夠在你存儲代碼的位置發現一張存下來的圖片,用下面的代碼打開它 import platform, os, subprocess if platform.system() == 'Darwin': subprocess.call(['open', 'QRCode.jpg']) elif platform.system() == 'Linux': subprocess.call(['xdg-open', 'QRCode.jpg']) else: os.startfile('QR.jpg')
因爲咱們須要獲取圖像,因此須要以二進制數據流的形式獲取服務器返回的數據包,因此增長stream = True
。
而將二進制數據流寫入也須要在打開文件時設定二進制寫入,即open('QRCode.jpg', 'wb')
。
固然,若是獲取失敗能夠再運行一次。
同理的3、四步也能夠按照這個方法寫出,這裏就再也不贅述,只給出代碼。
而通過測試咱們發現,第一步時的appid實際是一個固定的量,第三步時的r甚至不輸入也能夠登陸。
# 上接上一段代碼 import time while 1: url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login' # 這裏演示一下不使用自帶的urlencode params = 'tip=1&uuid=%s&_=%s'%(uuid, int(time.time())) r = session.get(url, params = params) regx = r'window.code=(\d+)' data = re.search(regx, r.text) if not data: continue if data.group(1) == '200': # 下面一段是爲了以後獲取登陸信息作準備 uriRegex = r'window.redirect_uri="(\S+)";' redirectUri = re.search(uriRegex, r.text).group(1) r = session.get(redirectUri, allow_redirects=False) redirectUri = redirectUri[:redirectUri.rfind('/')] baseRequestText = r.text break elif data.group(1) == '201': print('You have scanned the QRCode') time.sleep(1) elif data.group(1) == '408': raise Exception('QRCode should be renewed') print('Login successfully')
當你看到Login successfully時,說明至此咱們已經成功從零開始,經過抓包分析,用python成功模擬了python登錄。
不過是否是看上去沒有什麼反饋呢?那是由於咱們尚未模擬會產生反饋的包,但其實差的只是研究發文字、發圖片什麼的包了。
爲了體現咱們已經登錄了,加上後面這段代碼就能夠看到登錄的帳號信息:
# 上接上一段代碼 import xml.dom.minidom def get_login_info(s): baseRequest = {} for node in xml.dom.minidom.parseString(s).documentElement.childNodes: if node.nodeName == 'skey': baseRequest['Skey'] = node.childNodes[0].data.encode('utf8') elif node.nodeName == 'wxsid': baseRequest['Sid'] = node.childNodes[0].data.encode('utf8') elif node.nodeName == 'wxuin': baseRequest['Uin'] = node.childNodes[0].data.encode('utf8') elif node.nodeName == 'pass_ticket': baseRequest['DeviceID'] = node.childNodes[0].data.encode('utf8') return baseRequest baseRequest = get_login_info(baseRequestText) url = '%s/webwxinit?r=%s' % (redirectUri, int(time.time())) data = { 'BaseRequest': baseRequest, } headers = { 'ContentType': 'application/json; charset=UTF-8' } r = session.post(url, data = json.dumps(data), headers = headers) dic = json.loads(r.content.decode('utf-8', 'replace')) print('Log in as %s'%dic['User']['NickName'])
這裏作一個簡單的小結:
模擬數據包整體而言是以尋找未知的必須數據爲線索,輔助一些技巧,串聯起整個過程。
首先須要用python初始化一個session,不然登陸過程的存儲將會比較麻煩。
模擬數據包的時候首先區分get與post請求,對應session的get與post方法。
get的數據爲url後半部分的內容,post是數據包最後一部分的內容。
get方法中傳入數據的標示爲params, post方法中傳入數據的標示爲data。
session的get,post方法返回一個量,能夠經過r.text自動編碼顯示。
存儲圖片有特殊的方式與配置。
到如今爲止我展現了一個完整的抓包、分析、模擬的過程完成了模擬登錄,其餘一些事情其實也都是相似的過程,想清楚每一步要作些什麼便可。
這裏用到的軟件都只介紹了最簡單的一些方法,進一步的內容這裏給出一些建議:
wireshark能夠直接瀏覽官方文檔,有空能夠作一個瞭解。
requests包的使用經過搜索引擎便可,特殊的功能建議直接閱讀源碼。
那麼作一個小練習好了,測試一下學到的東西:讀取命令行的輸入併發送給本身。(這部分的源碼放在了文末)
在分析包的過程當中記得抓好位置的必要數據這個線索,練習以前提到過的一些技巧。
把大的過程拆分紅一個一個小的任務可能會讓分析簡單不少。
若是發現登陸過程意料以外的斷了,分析不出緣由,能夠嘗試多抓幾回包再比較分析。
這是由於微信網頁端存在心跳機制,一段時間不交互將會斷開鏈接。
另外,每次獲取數據時(webwxsync)記得更新SyncKey。
在項目中已經模擬好了幾乎全部的請求,你能夠經過參考個人方法與數據包。
若是以後微信網頁版出現更新我會在本項目中及時更新。
項目中的微信網頁端接口見這裏
這是由於使用requests包會自動將中文文件名編碼爲服務器端沒法識別的格式,因此須要修改requests包或者使用別的方法上傳文件。
最簡單的方法即將requests包的packages/urlib3中的fields.py中的format_header_param
方法改成以下內容:
def format_header_param(name, value): if not any(ch in value for ch in '"\\\r\n'): result = '%s="%s"' % (name, value) try: result.encode('ascii') except UnicodeEncodeError: pass else: return result if not six.PY3: # Python 2: value = value.encode('utf-8') value = email.utils.encode_rfc2231(value, 'utf-8') value = '%s="%s"' % (name, value.decode('utf8')) return value
建議更新Python版本至2.7.11
源碼可在該地址下載:這裏
但願讀完這篇文章能對你有幫助,有什麼不足之處萬望指正(鞠躬)。
有什麼想法或者想要關注個人更新,歡迎來Github上Star或者Fork。
160426
LittleCoder
EOF