12306搶票腳本開(kāi)發(fā)(五)中文火車站名到火車站代號(hào)的轉(zhuǎn)換


文章地址 :

12306搶票腳本開(kāi)發(fā)(一)提綱
12306搶票腳本開(kāi)發(fā)(二)解析火車站代號(hào)并分析查詢的HTTP請(qǐng)求
12306搶票腳本開(kāi)發(fā)(三)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的查詢腳本
12306搶票腳本開(kāi)發(fā)(四)完善上節(jié)課的代碼并面向?qū)ο?/a>
12306搶票腳本開(kāi)發(fā)(五)更友好的使用方式
12306搶票腳本開(kāi)發(fā)(六)更友好的時(shí)間輸入方式
12306搶票腳本開(kāi)發(fā)(七)將前幾節(jié)課的成果結(jié)合起來(lái)實(shí)現(xiàn)一個(gè)完整的工具


簡(jiǎn)介 :

為了能讓上節(jié)課的代碼能適合更多的人使用 , 這里需要做幾件事 :

1. 能解析火車站中文名
2. 能解析更友好的時(shí)間
3. 默認(rèn)查詢的是成人票 , 當(dāng)用戶指定要查詢學(xué)生票的時(shí)候才查詢學(xué)生票
4. 將交互的方式做以調(diào)整 (接收命令行參數(shù))

首先看第一個(gè)功能 : 解析火車站的中文名

我們首先將之前分析中用到的那個(gè)保存中文名和代號(hào)的文件下載到本地
然后嘗試解析這個(gè)文件 , 并和用戶的輸入進(jìn)行匹配 , 這里為了方便就先編寫(xiě)一個(gè)簡(jiǎn)單的函數(shù)

下載這個(gè)文件 : 
https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8997
我們可以看到這個(gè) URL 是帶參數(shù)的 , 就是說(shuō) , 隨著時(shí)間的推移
火車站的數(shù)據(jù)可能會(huì)有更新 , 因此這里需要用一個(gè)版本信息來(lái)控制下載的文件
這里的版本是 station_version=1.8997
那么我們將來(lái)的程序運(yùn)行的時(shí)候應(yīng)該要保證每一次的這個(gè)文件都是最新的
因此我們首先要獲取最新的版本號(hào) , 然后再根據(jù)版本號(hào)去下載這個(gè)文件
我們知道 HTML 中可以引用外部的 JS 代碼 , 需要將這個(gè) JS 文件的 URL 填寫(xiě)在 : 
<script>標(biāo)簽的 src 屬性中 , 當(dāng)瀏覽器解析到這個(gè)標(biāo)簽的時(shí)候 , 就會(huì)發(fā)起一個(gè) HTTP 請(qǐng)求來(lái)向服務(wù)器請(qǐng)求這個(gè)文件
那么只要我們能得到主頁(yè)的 HTML , 解析這個(gè) HTML 文檔 , 去尋找鏈接 station_name.js 的 script 標(biāo)簽
這個(gè)標(biāo)簽的 src 屬性就是我們要請(qǐng)求的文件 , 這個(gè)時(shí)候就可以保證使用的火車站的信息是和 12306 官網(wǎng)是一致的了

tools.py

#!/usr/bin/env python
# encoding:utf-8

import requests
import bs4
import logging

def getStationNamesVersion():
    '''
    獲取 station_names.js 這個(gè)文件最新的版本號(hào)
    '''
    logging.captureWarnings(True)
    url = "https://kyfw.12306.cn/otn/"
    station_name_version = "" # 先初始化為 0 , 防止沒(méi)有獲取到的時(shí)候不能正常返回
    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 標(biāo)簽的 src 屬性
    for i in scripts:
        try: # 這里使用 try 是因?yàn)橛械?script 標(biāo)簽并沒(méi)有 src 這個(gè)屬性
            src = i['src']
            srcs.append(src)
        except:
            pass
    for i in srcs: # 這里設(shè)計(jì)地比較有擴(kuò)展性 , 如果還要獲取別的某個(gè)文件的版本 , 只需要在循環(huán)中添加判斷即可
        if "station_name" in i: # 找到含有 station_names 的一條 src
            station_name_version = i.split("station_version=")[1] # 截取版本號(hào)
            # print "成功獲取到車站信息版本 :" , station_name_version # 打印日志
    return station_name_version

def getUrlForStationNames(station_name_version):
    '''
    構(gòu)建用于下載 station_names.js 這個(gè)文件的地址
    '''
    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()

然后我們來(lái)實(shí)現(xiàn)一個(gè)測(cè)試上述函數(shù)的腳本 :

#!/usr/bin/env python
# encoding:utf-8

import tools
import os


# 獲取官網(wǎng)的這個(gè)文件的版本
print "正在獲取官網(wǎng)的火車站信息文件版本..."
station_names_version = tools.getStationNamesVersion()
print "獲取成功 !"
print "官網(wǎng)版本號(hào) : [",(station_names_version),"]"

# 比對(duì)本地文件
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 "正在解析本地文件版本號(hào)..."
    local_file_version = local_file_name.split("_")[0]
    print "本地版本號(hào) : [", local_file_version, "]"
else:
    print "本地沒(méi)有緩存文件 , 準(zhǔn)備開(kāi)始下載..."

# 下載文件 , 保存文件名以版本開(kāi)始 (便于下次運(yùn)行的時(shí)候比對(duì))
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 "正在解析站點(diǎn)信息..."
stations = content.split("@")
print "解析成功 ! 總站點(diǎn)數(shù) : [ " + str(len(stations)) + " ]"

運(yùn)行效果 :

Paste_Image.png

現(xiàn)在我們已經(jīng)能下載這個(gè)文件了 , 我們接下來(lái)就要解析這個(gè)文件 :
根據(jù)上幾節(jié)課的分析 , 我們已經(jīng)知道了 :

1. 這是一個(gè) js 文件 , 其中只定義了一個(gè)字符串變量 , 而我們只需要關(guān)注這個(gè)字符串 , 因此需要對(duì)這個(gè) js 文件的內(nèi)容進(jìn)行處理
2. 所有的火車站之間用 '@' 分隔
3. 每一單獨(dú)的火車站的字段應(yīng)該是有 6 個(gè) , 每一個(gè)之間都以 '|' 分隔
4. 其中 : ("@bjb|北京北|VAP|beijingbei|bjb|0")
    字段0 : 火車站名稱漢語(yǔ)拼音首字母
    字段1 : 火車站名稱漢語(yǔ)
    字段2 : 在查票的時(shí)候火車站的代碼 (比如說(shuō) : 上海的代碼即為 SHH)
    字段3 : 火車站名稱漢語(yǔ)拼音
    字段4 : 火車站名稱漢語(yǔ)拼音首字母 (模糊匹配 : 比如說(shuō)輸入 北京南站 , 那么有可能也有 北京站 的信息)
    字段5 : 火車站編號(hào)(數(shù)字的序號(hào) , 應(yīng)該是鐵道部或者網(wǎng)站自己定義的 , 應(yīng)該是用于唯一標(biāo)識(shí)某一個(gè)火車站)

那么我們接下來(lái)要實(shí)現(xiàn)的代碼的功能就是 : 輸入火車站名稱漢語(yǔ)(字段1) , 能返回火車站查詢時(shí)用的代碼(字段2)
開(kāi)始寫(xiě)吧~

#!/usr/bin/env python
# encoding:utf-8

import tools
import os


# 獲取官網(wǎng)的這個(gè)文件的版本
print "正在獲取官網(wǎng)的火車站信息文件版本..."
station_names_version = tools.getStationNamesVersion()
print "獲取成功 !"
print "官網(wǎng)版本號(hào) : [",(station_names_version),"]"

# 比對(duì)本地文件
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 "正在解析本地文件版本號(hào)..."
    local_file_version = local_file_name.split("_")[0]
    print "本地版本號(hào) : [", local_file_version, "]"
else:
    print "本地沒(méi)有緩存文件 , 準(zhǔn)備開(kāi)始下載..."

# 下載文件 , 保存文件名以版本開(kāi)始 (便于下次運(yùn)行的時(shí)候比對(duì))
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 "正在解析站點(diǎn)信息..."
    stations = content.split("@")[1:] # 由于這個(gè)文件開(kāi)頭就是 '@' , 因此需要去掉第一個(gè)元素
    print "解析成功 ! 總站點(diǎn)數(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ùn)行效果


Paste_Image.png

這里其實(shí)還有可以優(yōu)化的地方
比如說(shuō) :

1. 函數(shù)每調(diào)用一次就有一次 IO , 應(yīng)該優(yōu)化成只進(jìn)行一次 IO
2. 暫時(shí)不支持模糊查詢
3. 暫時(shí)不支持通過(guò)拼音首字母查詢

關(guān)于問(wèn)題 2 , 我們可以這樣解決 :

如果一個(gè)城市有好多個(gè)火車站 , 那么它們的命名一定是這樣的 : (例如 北京)
北京站
北京北站
北京南站
北京東站
北京西站
哈 , 有規(guī)律了吧 , 都是以北京開(kāi)頭的
那么我們要實(shí)現(xiàn)這樣的查詢 , 只需要很簡(jiǎn)單地將上述代碼中判斷函數(shù)參數(shù)和文件中的火車站名是否相等的函數(shù)改成 startswith
這樣就可以進(jìn)行模糊查詢 , 但是這樣做的話 , 就需要遍歷完整個(gè)文件
而且需要返回一個(gè)列表

完整的代碼在下方 , 運(yùn)行結(jié)果為 :

Paste_Image.png

代碼 :

tools.py

#!/usr/bin/env python
# encoding:utf-8

import requests
import bs4
import logging

def getStationNamesVersion():
    '''
    獲取 station_names.js 這個(gè)文件最新的版本號(hào)
    '''
    logging.captureWarnings(True)
    url = "https://kyfw.12306.cn/otn/"
    station_name_version = "" # 先初始化為 0 , 防止沒(méi)有獲取到的時(shí)候不能正常返回
    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 標(biāo)簽的 src 屬性
    for i in scripts:
        try: # 這里使用 try 是因?yàn)橛械?script 標(biāo)簽并沒(méi)有 src 這個(gè)屬性
            src = i['src']
            srcs.append(src)
        except:
            pass
    for i in srcs: # 這里設(shè)計(jì)地比較有擴(kuò)展性 , 如果還要獲取別的某個(gè)文件的版本 , 只需要在循環(huán)中添加判斷即可
        if "station_name" in i: # 找到含有 station_names 的一條 src
            station_name_version = i.split("station_version=")[1] # 截取版本號(hào)
            # print "成功獲取到車站信息版本 :" , station_name_version # 打印日志
    return station_name_version

def getUrlForStationNames(station_name_version):
    '''
    構(gòu)建用于下載 station_names.js 這個(gè)文件的地址
    '''
    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)的這個(gè)文件的版本
print "正在獲取官網(wǎng)的火車站信息文件版本..."
station_names_version = tools.getStationNamesVersion()
print "獲取成功 !"
print "官網(wǎng)版本號(hào) : [",(station_names_version),"]"

# 比對(duì)本地文件
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 "正在解析本地文件版本號(hào)..."
    local_file_version = local_file_name.split("_")[0]
    print "本地版本號(hào) : [", local_file_version, "]"
else:
    print "本地沒(méi)有緩存文件 , 準(zhǔn)備開(kāi)始下載..."

# 下載文件 , 保存文件名以版本開(kāi)始 (便于下次運(yùn)行的時(shí)候比對(duì))
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 "正在解析站點(diǎn)信息..."
    stations = content.split("@")[1:] # 由于這個(gè)文件開(kāi)頭就是 '@' , 因此需要去掉第一個(gè)元素
    print "解析成功 ! 總站點(diǎn)數(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)可以完成從中文的火車站名到火車站代號(hào)的轉(zhuǎn)換了
也就是說(shuō) , 我們最開(kāi)始提出的問(wèn)題中的問(wèn)題 1 已經(jīng)解決
這樣的話 , 其實(shí)我們大部分的內(nèi)容已經(jīng)完成了, 這就已經(jīng)可以制作成一個(gè)很友好的小工具了
如果再能配合 Linux 的定時(shí)任務(wù)就可以實(shí)現(xiàn)對(duì)火車票進(jìn)行輪詢的功能
當(dāng)然在查到火車票的時(shí)候要對(duì)用戶進(jìn)行通知 , 這我們會(huì)在以后的課程中進(jìn)行介紹


總結(jié) :

本節(jié)課我們主要實(shí)現(xiàn)了 : 中文火車站名到火車站代號(hào)的轉(zhuǎn)換
既可以進(jìn)行精確的查詢
也可以進(jìn)行模糊的查詢


預(yù)告 :

下節(jié)課我們來(lái)實(shí)現(xiàn)讓程序能接受更多格式的時(shí)間輸入 , 感謝大家的支持~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容