上次寫的如何給小孩約馬術課過程,見這里 Python 約課[1], 本想一勞永逸,但是好景不長,預約系統升級了,而且還換了服務商,從之前的公眾號 H5 應用,換成了小程序,之前編寫的方式直接失效,孩子又沒馬騎了
誰叫他遇到一個程序員老爸呢?這點事兒難不倒我,開干
小程序的不同之處
與訪問 H5 不同的是,小程序相當於一個 app,其上的操作是經過微信的封裝的,所以無法直接獲取到請求鏈接和數據,同樣也無法獲得返回的數據
就像一個 app,他的請求都是內置在程序內的
對於這種情況,就需要使用抓包工具,比如 Charles
它的原理是,作為請求的代理,即小程序 或 app 發送請求時,先將請求發送給代理,然后再由代理將請求發送給服務器,返回的過程也一樣
這也是著名的 中間人攻擊
中間人攻擊
如果要獲取 小程序或者 app 的具體請求,就需要用這種方式,讓代理獲取請求和相應的數據
具體這么玩呢?直接參考 Charles 教程或者在網上一搜,就知道了,這里推薦一篇Android抓包-Charles[2],供各位參考
飛越 Https 協議
如果配置好了之后,可能發現 Charles 抓的包全是亂碼,這是因為 小程序必須使用 Https 協議
也就是在 Http 協助之上對請求數據做一次加密,以防止中間人攻擊
Https 的原理也很簡單,就是目標網址申請一個 https 證書,然后將其對稱密鑰的公鑰發布在頒發證書的網站上
當由請求訪問目標服務器時,目標服務器會要求其進行加滿請求,這是客戶端程序會自動去證書頒發網址下載目標網站的公鑰,也就是證書
然后對請求的數據用公鑰加密,再發送到目標服務器上,目標服務器收到請求后,會用自己的私鑰解密請求數據,轉化為明文繼續處理
當返回響應時也是一樣的,不過目標服務器用自己的私鑰加密,客戶端用公鑰解密
詳細說明可參考 圖解HTTP[3]
這里只需要按照 Charles 的說明,再手機端按照 Charles 頒發的證書就可以了
不過如果用的是 Android 系統的話,需要注意 Android 7.0 之后 谷歌升級了安全策略,不再支持用戶自主安裝的證書
有兩個解決辦法:
- 對手機做root,然后修改手機的安全策略,詳細可參考: 通過Charles抓取Android的Https鏈接數據[4]
- 找一個未升級到 Android 7.0 的手機
翻出了一台幾年前的手機,充電,開機,查看版本,是 Android 6,哈哈,太幸運了
安裝好證書后,再次抓包,就可以看見請求的數據了
Charles 抓包
輕車熟路
得到了請求鏈接和請求數據,就可以像上一次一樣編寫成 Python 腳本了
上一次是通過瀏覽器中請求的方式獲取的請求數據,在 Charles 中,獲取也很方便,如下圖
Charles 獲取請求
通過快捷菜單,獲取 curl 命令的請求數據,然后復制到 網站 https://curl.trillworks.com/[5]
Charles 獲取請求
然后將 python 代碼拷出到文件里,執行即可,夠簡單吧,具體可以參考之前的文章: 這才是使用Python的正確姿勢![6] 的文章描述
更進一步
這里還需要解決一個問題,可能是我這個做老爸的實在太懶了
因為正直五一假期,假期結束后的一個周六是工作日,而之前的程序會預約每周六的課程,如果是工作日的話,剛好沖突了
所以需要避開工作日,那么首先想到的是有沒有判斷節假日的庫可用,找了一圈,發現有些 api 可以,但是不是需要付費就是需要注冊,比較麻煩,於是直接去萬年歷中去抓取
鎖定的一個萬能歷網站 https://wannianrili.bmcx.com,標記清晰,數據准確,而且免費
萬年歷
分析請求,是通過鏈接 https://wannianrili.bmcx.com/ajax/ 獲取一個月的數據,獲取的結果是 xml 格式的數據
分析發現,日期類型是通過 css 的類來標記的,分別是 wnrl_riqi_ban
,wnrl_riqi_mo
,wnrl_riqi_xiu
,表示 上班,周末 和 休息
所以只需要對獲取的 xml 進行解析就好了
這里我又再進一步 —— 因為獲取的是一個月的,每次請求獲取又點費,而且是在搶預約,所以需要更高的效率(哈哈,實際上是想炫炫技而已),於是做了一個小緩存,每次看看有沒有當月的 xml 文件,如果有直接讀取,沒有則獲取,並存儲起來
實現了節假日判斷后,在主預約程序里加一個判斷,如果要預約的日子是工作日,再后延一日,繼續判斷,直到遇到一個費工作日
這里展示一下判斷日期類型的代碼:
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 # 探索步長為一日
date = datetime.date.today()
while(getDayType(date)<2):
delta += 1
date = datetime.date.today() + datetime.timedelta(days=delta)
總結
好了,現在又可以做優雅的老爸了哈哈,對孩子最好的教育就是陪孩子一起成長,無論是什么方面,如果你恰巧喜歡編程,會編程的話,可以嘗試和孩子一起做些有意思的東西,比如 做個擲骰子游戲[7]
筆芯
參考資料
[1]
Python 約課: https://mp.weixin.qq.com/s/XqICwC9_cRBhua-6-lbjWw
[2]
Android抓包: https://www.jianshu.com/p/8385a13b0e5c
[3]
圖解HTTP: https://book.douban.com/subject/25863515/
[4]
Android 7.0 安裝證書: https://bbs.huaweicloud.com/blogs/245014
[5]
Curl 轉化為 Python 請求: https://curl.trillworks.com/
[6]
這才是使用Python的正確姿勢!: https://mp.weixin.qq.com/s/XqICwC9_cRBhua-6-lbjWw
[7]
做個擲骰子游戲: https://mp.weixin.qq.com/s/czcGKk6RTrZVi6-KRUAR0w