上次寫(xiě)的如何給小孩約馬術(shù)課過(guò)程,見(jiàn)這里 Python 約課[1], 本想一勞永逸,但是好景不長(zhǎng),預(yù)約系統(tǒng)升級(jí)了,而且還換了服務(wù)商,從之前的公眾號(hào) H5 應(yīng)用,換成了小程序,之前編寫(xiě)的方式直接失效,孩子又沒(méi)馬騎了
誰(shuí)叫他遇到一個(gè)程序員老爸呢?這點(diǎn)事兒難不倒我,開(kāi)干
小程序的不同之處
與訪問(wèn) H5 不同的是,小程序相當(dāng)于一個(gè) App,其上的操作是經(jīng)過(guò)微信的封裝的,所以無(wú)法直接獲取到請(qǐng)求鏈接和數(shù)據(jù),同樣也無(wú)法獲得返回的數(shù)據(jù)
就像一個(gè) app,他的請(qǐng)求都是內(nèi)置在程序內(nèi)的
對(duì)于這種情況,就需要使用抓包工具,比如 Charles
它的原理是,作為請(qǐng)求的代理,即小程序 或 app 發(fā)送請(qǐng)求時(shí),先將請(qǐng)求發(fā)送給代理,然后再由代理將請(qǐng)求發(fā)送給服務(wù)器,返回的過(guò)程也一樣
這也是著名的 中間人攻擊
中間人攻擊
如果要獲取 小程序或者 app 的具體請(qǐng)求,就需要用這種方式,讓代理獲取請(qǐng)求和相應(yīng)的數(shù)據(jù)
具體這么玩呢?直接參考 Charles 教程或者在網(wǎng)上一搜,就知道了,這里推薦一篇Android抓包-Charles[2],供各位參考
飛越 Https 協(xié)議
如果配置好了之后,可能發(fā)現(xiàn) Charles 抓的包全是亂碼,這是因?yàn)?小程序必須使用 Https 協(xié)議
也就是在 Http 協(xié)助之上對(duì)請(qǐng)求數(shù)據(jù)做一次加密,以防止中間人攻擊
Https 的原理也很簡(jiǎn)單,就是目標(biāo)網(wǎng)址申請(qǐng)一個(gè) https 證書(shū),然后將其對(duì)稱(chēng)密鑰的公鑰發(fā)布在頒發(fā)證書(shū)的網(wǎng)站上
當(dāng)由請(qǐng)求訪問(wèn)目標(biāo)服務(wù)器時(shí),目標(biāo)服務(wù)器會(huì)要求其進(jìn)行加滿請(qǐng)求,這是客戶(hù)端程序會(huì)自動(dòng)去證書(shū)頒發(fā)網(wǎng)址下載目標(biāo)網(wǎng)站的公鑰,也就是證書(shū)
然后對(duì)請(qǐng)求的數(shù)據(jù)用公鑰加密,再發(fā)送到目標(biāo)服務(wù)器上,目標(biāo)服務(wù)器收到請(qǐng)求后,會(huì)用自己的私鑰解密請(qǐng)求數(shù)據(jù),轉(zhuǎn)化為明文繼續(xù)處理
當(dāng)返回響應(yīng)時(shí)也是一樣的,不過(guò)目標(biāo)服務(wù)器用自己的私鑰加密,客戶(hù)端用公鑰解密
詳細(xì)說(shuō)明可參考 圖解HTTP[3]
這里只需要按照 Charles 的說(shuō)明,在手機(jī)端按照 Charles 頒發(fā)的證書(shū)就可以了
不過(guò)如果用的是 Android 系統(tǒng)的話,需要注意 Android 7.0 之后 谷歌升級(jí)了安全策略,不再支持用戶(hù)自主安裝的證書(shū)
有兩個(gè)解決辦法:
- 對(duì)手機(jī)做root,然后修改手機(jī)的安全策略,詳細(xì)可參考: 通過(guò)Charles抓取Android的Https鏈接數(shù)據(jù)[4]
- 找一個(gè)未升級(jí)到 Android 7.0 的手機(jī)
翻出了一臺(tái)幾年前的手機(jī),充電,開(kāi)機(jī),查看版本,是 Android 6,哈哈,太幸運(yùn)了
安裝好證書(shū)后,再次抓包,就可以看見(jiàn)請(qǐng)求的數(shù)據(jù)了
Charles 抓包
輕車(chē)熟路
得到了請(qǐng)求鏈接和請(qǐng)求數(shù)據(jù),就可以像上一次一樣編寫(xiě)成 Python 腳本了
上一次是通過(guò)瀏覽器中請(qǐng)求的方式獲取的請(qǐng)求數(shù)據(jù),在 Charles 中,獲取也很方便,如下圖
Charles 獲取請(qǐng)求
通過(guò)快捷菜單,獲取 curl 命令的請(qǐng)求數(shù)據(jù),然后復(fù)制到 網(wǎng)站
Charles 獲取請(qǐng)求
然后將 python 代碼拷出到文件里,執(zhí)行即可,夠簡(jiǎn)單吧,具體可以參考之前的文章: 這才是使用Python的正確姿勢(shì)![6] 的文章描述
更進(jìn)一步
這里還需要解決一個(gè)問(wèn)題,可能是我這個(gè)做老爸的實(shí)在太懶了
因?yàn)檎滴逡患倨?,假期結(jié)束后的一個(gè)周六是工作日,而之前的程序會(huì)預(yù)約每周六的課程,如果是工作日的話,剛好沖突了
所以需要避開(kāi)工作日,那么首先想到的是有沒(méi)有判斷節(jié)假日的庫(kù)可用,找了一圈,發(fā)現(xiàn)有些 api 可以,但是不是需要付費(fèi)就是需要注冊(cè),比較麻煩,于是直接去萬(wàn)年歷中去抓取
鎖定的一個(gè)萬(wàn)能歷史網(wǎng)站,標(biāo)記清晰,數(shù)據(jù)準(zhǔn)確,而且免費(fèi)
萬(wàn)年歷
分析請(qǐng)求,是通過(guò)鏈接獲取一個(gè)月的數(shù)據(jù),獲取的結(jié)果是 xml 格式的數(shù)據(jù)
分析發(fā)現(xiàn),日期類(lèi)型是通過(guò) css 的類(lèi)來(lái)標(biāo)記的,分別是 wnrl_riqi_ban,wnrl_riqi_mo,wnrl_riqi_xiu,表示 上班,周末 和 休息
所以只需要對(duì)獲取的 xml 進(jìn)行解析就好了
這里我又再進(jìn)一步 —— 因?yàn)楂@取的是一個(gè)月的,每次請(qǐng)求獲取又點(diǎn)費(fèi),而且是在搶預(yù)約,所以需要更高的效率(哈哈,實(shí)際上是想炫炫技而已),于是做了一個(gè)小緩存,每次看看有沒(méi)有當(dāng)月的 xml 文件,如果有直接讀取,沒(méi)有則獲取,并存儲(chǔ)起來(lái)
實(shí)現(xiàn)了節(jié)假日判斷后,在主預(yù)約程序里加一個(gè)判斷,如果要預(yù)約的日子是工作日,再后延一日,繼續(xù)判斷,直到遇到一個(gè)非工作日
這里展示一下判斷日期類(lèi)型的代碼:
import requests
from lxml import etree
import datetime
import os
def getDaysInfo(ym):
cacheName = ym + ".html"
if os.path.exists(cacheName):
content = open(cacheName).read()
else:
content = requestsDayInfo(ym)
saveFile(cacheName, content)
return content
def requestsDayInfo(ym=None):
headers = {
'sec-ch-ua': '"google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"',
'Referer': 'https://wannianrili.bmcx.com/',
'sec-ch-ua-mobile': '?0',
'User-Agent': 'Mozilla/5.0 (windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
}
params = (
('q', ym),
('v', '20031912'),
)
response = requests.get('https://wannianrili.bmcx.com/ajax/', headers=headers, params=params)
return response.text
def saveFile(name, content):
print(name)
f = open(name,'w')
f.write(content)
f.close()
def parse(content, d):
html = etree.HTML(content)
dayclass = html.xpath('//*[@id="wnrl_riqi_id_'+str(int(d)-1)+'"]')[0].attrib.get('class')
if dayclass is None or dayclass == 'wnrl_riqi_ban':
return 1
elif dayclass == 'wnrl_riqi_mo':
return 2
elif dayclass == 'wnrl_riqi_xiu':
return 3
else:
return 0
def getDayType(date):
str_date = date.strftime('%Y-%m-%d')
ymd = str_date.split("-")
ym = ymd[0] + '-' + ymd[1]
d = ymd[2]
return parse(getDaysInfo(ym), d)
if __name__ == "__main__":
delta = 1 # 探索步長(zhǎng)為一日
date = datetime.date.today()
while(getDayType(date)<2):
delta += 1
date = datetime.date.today() + datetime.timedelta(days=delta)
總結(jié)
好了,現(xiàn)在又可以做優(yōu)雅的老爸了哈哈,對(duì)孩子最好的教育就是陪孩子一起成長(zhǎng),無(wú)論是什么方面,如果你恰巧喜歡編程,會(huì)編程的話,可以嘗試和孩子一起做些有意思的東西,比如 做個(gè)擲骰子游戲[7]