文章地址 :
12306搶票腳本開發(fā)(一)提綱
12306搶票腳本開發(fā)(二)解析火車站代號并分析查詢的HTTP請求
12306搶票腳本開發(fā)(三)實現(xiàn)一個簡單的查詢腳本
12306搶票腳本開發(fā)(四)完善上節(jié)課的代碼并面向?qū)ο?/a>
12306搶票腳本開發(fā)(五)更友好的使用方式
12306搶票腳本開發(fā)(六)更友好的時間輸入方式
12306搶票腳本開發(fā)(七)將前幾節(jié)課的成果結(jié)合起來實現(xiàn)一個完整的工具
簡介 :
為了能讓上節(jié)課的代碼能適合更多的人使用 , 這里需要做幾件事 :
1. 能解析火車站中文名
2. 能解析更友好的時間
3. 默認查詢的是成人票 , 當(dāng)用戶指定要查詢學(xué)生票的時候才查詢學(xué)生票
4. 將交互的方式做以調(diào)整 (接收命令行參數(shù))
首先看第一個功能 : 解析火車站的中文名
我們首先將之前分析中用到的那個保存中文名和代號的文件下載到本地
然后嘗試解析這個文件 , 并和用戶的輸入進行匹配 , 這里為了方便就先編寫一個簡單的函數(shù)
下載這個文件 :
https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8997
我們可以看到這個 URL 是帶參數(shù)的 , 就是說 , 隨著時間的推移
火車站的數(shù)據(jù)可能會有更新 , 因此這里需要用一個版本信息來控制下載的文件
這里的版本是 station_version=1.8997
那么我們將來的程序運行的時候應(yīng)該要保證每一次的這個文件都是最新的
因此我們首先要獲取最新的版本號 , 然后再根據(jù)版本號去下載這個文件
我們知道 HTML 中可以引用外部的 JS 代碼 , 需要將這個 JS 文件的 URL 填寫在 :
<script>標簽的 src 屬性中 , 當(dāng)瀏覽器解析到這個標簽的時候 , 就會發(fā)起一個 HTTP 請求來向服務(wù)器請求這個文件
那么只要我們能得到主頁的 HTML , 解析這個 HTML 文檔 , 去尋找鏈接 station_name.js 的 script 標簽
這個標簽的 src 屬性就是我們要請求的文件 , 這個時候就可以保證使用的火車站的信息是和 12306 官網(wǎng)是一致的了
tools.py
#!/usr/bin/env python
# encoding:utf-8
import requests
import bs4
import logging
def getStationNamesVersion():
'''
獲取 station_names.js 這個文件最新的版本號
'''
logging.captureWarnings(True)
url = "https://kyfw.12306.cn/otn/"
station_name_version = "" # 先初始化為 0 , 防止沒有獲取到的時候不能正常返回
response = requests.get(url, verify=False)
content = response.text.encode("UTF-8")
soup = bs4.BeautifulSoup(content, "html.parser")
scripts = soup.findAll("script")
srcs = [] # 保存 HTML 中所有的 script 標簽的 src 屬性
for i in scripts:
try: # 這里使用 try 是因為有的 script 標簽并沒有 src 這個屬性
src = i['src']
srcs.append(src)
except:
pass
for i in srcs: # 這里設(shè)計地比較有擴展性 , 如果還要獲取別的某個文件的版本 , 只需要在循環(huán)中添加判斷即可
if "station_name" in i: # 找到含有 station_names 的一條 src
station_name_version = i.split("station_version=")[1] # 截取版本號
# print "成功獲取到車站信息版本 :" , station_name_version # 打印日志
return station_name_version
def getUrlForStationNames(station_name_version):
'''
構(gòu)建用于下載 station_names.js 這個文件的地址
'''
return "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=" + station_name_version
def downloadFile(url, filename):
'''
下載文件并保存到本地
'''
logging.captureWarnings(True)
f = open(filename, "a");
f.write(requests.get(url, verify=False).text.encode("UTF-8"))
f.close()
然后我們來實現(xiàn)一個測試上述函數(shù)的腳本 :
#!/usr/bin/env python
# encoding:utf-8
import tools
import os
# 獲取官網(wǎng)的這個文件的版本
print "正在獲取官網(wǎng)的火車站信息文件版本..."
station_names_version = tools.getStationNamesVersion()
print "獲取成功 !"
print "官網(wǎng)版本號 : [",(station_names_version),"]"
# 比對本地文件
print "正在獲取本地緩存文件文件名..."
local_file_name = ""
local_file_version = ""
for filename in os.listdir("./"):
if filename.endswith("_station_names.dat"):
local_file_name = filename
if local_file_name != "":
print "獲取成功 ! 本地文件名 : [", local_file_name, "]"
print "正在解析本地文件版本號..."
local_file_version = local_file_name.split("_")[0]
print "本地版本號 : [", local_file_version, "]"
else:
print "本地沒有緩存文件 , 準備開始下載..."
# 下載文件 , 保存文件名以版本開始 (便于下次運行的時候比對)
if local_file_version == "":
print "官網(wǎng)火車站文件更新 , 正在下載..."
tools.downloadFile(tools.getUrlForStationNames(tools.getStationNamesVersion()), station_names_version+"_"+"station_names.dat")
else:
if local_file_version != station_names_version:
print "官網(wǎng)火車站文件更新 , 正在下載..."
tools.downloadFile(tools.getUrlForStationNames(tools.getStationNamesVersion()), station_names_version+"_"+"station_names.dat")
else:
print "本地文件已最新 , 直接使用!"
# 讀取文件
print "正在讀取文件..."
station_names = open("./" + station_names_version + "_" + "station_names.dat", "r")
content = station_names.read()
content = content[20:-2] # 去掉多余的 js 關(guān)鍵字 , 只提取出字符串內(nèi)容
print "正在解析站點信息..."
stations = content.split("@")
print "解析成功 ! 總站點數(shù) : [ " + str(len(stations)) + " ]"
運行效果 :
現(xiàn)在我們已經(jīng)能下載這個文件了 , 我們接下來就要解析這個文件 :
根據(jù)上幾節(jié)課的分析 , 我們已經(jīng)知道了 :
1. 這是一個 js 文件 , 其中只定義了一個字符串變量 , 而我們只需要關(guān)注這個字符串 , 因此需要對這個 js 文件的內(nèi)容進行處理
2. 所有的火車站之間用 '@' 分隔
3. 每一單獨的火車站的字段應(yīng)該是有 6 個 , 每一個之間都以 '|' 分隔
4. 其中 : ("@bjb|北京北|VAP|beijingbei|bjb|0")
字段0 : 火車站名稱漢語拼音首字母
字段1 : 火車站名稱漢語
字段2 : 在查票的時候火車站的代碼 (比如說 : 上海的代碼即為 SHH)
字段3 : 火車站名稱漢語拼音
字段4 : 火車站名稱漢語拼音首字母 (模糊匹配 : 比如說輸入 北京南站 , 那么有可能也有 北京站 的信息)
字段5 : 火車站編號(數(shù)字的序號 , 應(yīng)該是鐵道部或者網(wǎng)站自己定義的 , 應(yīng)該是用于唯一標識某一個火車站)
那么我們接下來要實現(xiàn)的代碼的功能就是 : 輸入火車站名稱漢語(字段1) , 能返回火車站查詢時用的代碼(字段2)
開始寫吧~
#!/usr/bin/env python
# encoding:utf-8
import tools
import os
# 獲取官網(wǎng)的這個文件的版本
print "正在獲取官網(wǎng)的火車站信息文件版本..."
station_names_version = tools.getStationNamesVersion()
print "獲取成功 !"
print "官網(wǎng)版本號 : [",(station_names_version),"]"
# 比對本地文件
print "正在獲取本地緩存文件文件名..."
local_file_name = ""
local_file_version = ""
for filename in os.listdir("./"):
if filename.endswith("_station_names.dat"):
local_file_name = filename
if local_file_name != "":
print "獲取成功 ! 本地文件名 : [", local_file_name, "]"
print "正在解析本地文件版本號..."
local_file_version = local_file_name.split("_")[0]
print "本地版本號 : [", local_file_version, "]"
else:
print "本地沒有緩存文件 , 準備開始下載..."
# 下載文件 , 保存文件名以版本開始 (便于下次運行的時候比對)
if local_file_version == "":
print "官網(wǎng)火車站文件更新 , 正在下載..."
tools.downloadFile(tools.getUrlForStationNames(station_names_version), station_names_version+"_"+"station_names.dat")
else:
if local_file_version != station_names_version:
print "官網(wǎng)火車站文件更新 , 正在下載..."
tools.downloadFile(tools.getUrlForStationNames(station_names_version), station_names_version+"_"+"station_names.dat")
else:
print "本地文件已最新 , 直接使用!"
def getStationCode(station_name):
result = ""
# 讀取文件
print "正在讀取文件..."
station_names = open("./" + station_names_version + "_" + "station_names.dat", "r")
content = station_names.read()
station_names.close()
content = content[20:-2] # 去掉多余的 js 關(guān)鍵字 , 只提取出字符串內(nèi)容
print "正在解析站點信息..."
stations = content.split("@")[1:] # 由于這個文件開頭就是 '@' , 因此需要去掉第一個元素
print "解析成功 ! 總站點數(shù) : [ " + str(len(stations)) + " ]"
for station in stations:
fields = station.split("|")
# station_name_pinyin_simple = fields[0]
station_name_standard = fields[1]
station_code = fields[2]
# station_name_pinyin = fields[3]
# station_name_pinyin_simple_fuzz = fields[4]
# station_num = fields[5]
if station_name == station_name_standard:
result = station_code
return result
return result
print "[ 北京 ] -> [" + getStationCode("北京") + "]"
print "[ 上海 ] -> [" + getStationCode("上海") + "]"
print "[ 廣州 ] -> [" + getStationCode("廣州") + "]"
print "[ 深圳 ] -> [" + getStationCode("深圳") + "]"
print "[ 成都 ] -> [" + getStationCode("成都") + "]"
print "[ 哈爾濱 ] -> [" + getStationCode("哈爾濱") + "]"
print "[ 西安 ] -> [" + getStationCode("西安") + "]"
運行效果
這里其實還有可以優(yōu)化的地方
比如說 :
1. 函數(shù)每調(diào)用一次就有一次 IO , 應(yīng)該優(yōu)化成只進行一次 IO
2. 暫時不支持模糊查詢
3. 暫時不支持通過拼音首字母查詢
關(guān)于問題 2 , 我們可以這樣解決 :
如果一個城市有好多個火車站 , 那么它們的命名一定是這樣的 : (例如 北京)
北京站
北京北站
北京南站
北京東站
北京西站
哈 , 有規(guī)律了吧 , 都是以北京開頭的
那么我們要實現(xiàn)這樣的查詢 , 只需要很簡單地將上述代碼中判斷函數(shù)參數(shù)和文件中的火車站名是否相等的函數(shù)改成 startswith
這樣就可以進行模糊查詢 , 但是這樣做的話 , 就需要遍歷完整個文件
而且需要返回一個列表
完整的代碼在下方 , 運行結(jié)果為 :
代碼 :
tools.py
#!/usr/bin/env python
# encoding:utf-8
import requests
import bs4
import logging
def getStationNamesVersion():
'''
獲取 station_names.js 這個文件最新的版本號
'''
logging.captureWarnings(True)
url = "https://kyfw.12306.cn/otn/"
station_name_version = "" # 先初始化為 0 , 防止沒有獲取到的時候不能正常返回
response = requests.get(url, verify=False)
content = response.text.encode("UTF-8")
soup = bs4.BeautifulSoup(content, "html.parser")
scripts = soup.findAll("script")
srcs = [] # 保存 HTML 中所有的 script 標簽的 src 屬性
for i in scripts:
try: # 這里使用 try 是因為有的 script 標簽并沒有 src 這個屬性
src = i['src']
srcs.append(src)
except:
pass
for i in srcs: # 這里設(shè)計地比較有擴展性 , 如果還要獲取別的某個文件的版本 , 只需要在循環(huán)中添加判斷即可
if "station_name" in i: # 找到含有 station_names 的一條 src
station_name_version = i.split("station_version=")[1] # 截取版本號
# print "成功獲取到車站信息版本 :" , station_name_version # 打印日志
return station_name_version
def getUrlForStationNames(station_name_version):
'''
構(gòu)建用于下載 station_names.js 這個文件的地址
'''
return "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=" + station_name_version
def downloadFile(url, filename):
'''
下載文件并保存到本地
'''
logging.captureWarnings(True)
f = open(filename, "a");
f.write(requests.get(url, verify=False).text.encode("UTF-8"))
f.close()
test.py
#!/usr/bin/env python
# encoding:utf-8
import tools
import os
# 獲取官網(wǎng)的這個文件的版本
print "正在獲取官網(wǎng)的火車站信息文件版本..."
station_names_version = tools.getStationNamesVersion()
print "獲取成功 !"
print "官網(wǎng)版本號 : [",(station_names_version),"]"
# 比對本地文件
print "正在獲取本地緩存文件文件名..."
local_file_name = ""
local_file_version = ""
for filename in os.listdir("./"):
if filename.endswith("_station_names.dat"):
local_file_name = filename
if local_file_name != "":
print "獲取成功 ! 本地文件名 : [", local_file_name, "]"
print "正在解析本地文件版本號..."
local_file_version = local_file_name.split("_")[0]
print "本地版本號 : [", local_file_version, "]"
else:
print "本地沒有緩存文件 , 準備開始下載..."
# 下載文件 , 保存文件名以版本開始 (便于下次運行的時候比對)
if local_file_version == "":
print "官網(wǎng)火車站文件更新 , 正在下載..."
tools.downloadFile(tools.getUrlForStationNames(station_names_version), station_names_version+"_"+"station_names.dat")
else:
if local_file_version != station_names_version:
print "官網(wǎng)火車站文件更新 , 正在下載..."
tools.downloadFile(tools.getUrlForStationNames(station_names_version), station_names_version+"_"+"station_names.dat")
else:
print "本地文件已最新 , 直接使用!"
def getStationCodes(station_name):
results = []
# 讀取文件
print "正在讀取文件..."
station_names = open("./" + station_names_version + "_" + "station_names.dat", "r")
content = station_names.read()
station_names.close()
content = content[20:-2] # 去掉多余的 js 關(guān)鍵字 , 只提取出字符串內(nèi)容
print "正在解析站點信息..."
stations = content.split("@")[1:] # 由于這個文件開頭就是 '@' , 因此需要去掉第一個元素
print "解析成功 ! 總站點數(shù) : [ " + str(len(stations)) + " ]"
for station in stations:
fields = station.split("|")
# station_name_pinyin_simple = fields[0]
station_name_standard = fields[1]
station_code = fields[2]
# station_name_pinyin = fields[3]
# station_name_pinyin_simple_fuzz = fields[4]
# station_num = fields[5]
if station_name_standard.startswith(station_name):
results.append({"station_code":station_code, "station_name":station_name_standard})
return results
def printStationInfo(station_info):
for result in station_info:
print "[ %s ] -> [ %s ]" % (result["station_name"], result["station_code"])
print "=" * 18 + " [ 北京 ] " + "=" * 18
printStationInfo(getStationCodes("北京"))
print "=" * 18 + " [ 上海 ] " + "=" * 18
printStationInfo(getStationCodes("上海"))
print "=" * 18 + " [ 天津 ] " + "=" * 18
printStationInfo(getStationCodes("天津"))
print "=" * 18 + " [ 成都 ] " + "=" * 18
printStationInfo(getStationCodes("成都"))
print "=" * 18 + " [ 哈爾濱 ] " + "=" * 18
printStationInfo(getStationCodes("哈爾濱"))
print "=" * 18 + " [ 西安 ] " + "=" * 18
printStationInfo(getStationCodes("西安"))
現(xiàn)在我們已經(jīng)可以完成從中文的火車站名到火車站代號的轉(zhuǎn)換了
也就是說 , 我們最開始提出的問題中的問題 1 已經(jīng)解決
這樣的話 , 其實我們大部分的內(nèi)容已經(jīng)完成了, 這就已經(jīng)可以制作成一個很友好的小工具了
如果再能配合 Linux 的定時任務(wù)就可以實現(xiàn)對火車票進行輪詢的功能
當(dāng)然在查到火車票的時候要對用戶進行通知 , 這我們會在以后的課程中進行介紹
總結(jié) :
本節(jié)課我們主要實現(xiàn)了 : 中文火車站名到火車站代號的轉(zhuǎn)換
既可以進行精確的查詢
也可以進行模糊的查詢
預(yù)告 :
下節(jié)課我們來實現(xiàn)讓程序能接受更多格式的時間輸入 , 感謝大家的支持~