工具和环境
- pycharm
- import requests
- import json
- from prettytable import PrettyTable
分析12306 官网
-
https://www.12306.cn/index/ 进入官网
-
填好参考的出发地 、到达地、出发时间
-
点击查询
-
看到如上画面
-
F12 找到 Network一栏,ctrl+R 刷新一次,在Name列表里找到
-
query?leftTicket…… 文件 查看他的 Response或者Preview,查看data
-
如未找到 继续刷新
-
result 的值即为我们所要获取的值
-
经过上面的分析,我们要得到的内容即为该网页(query?leftTicket……)的 response
-
查看 Headers,查看 Requests headers
建立爬虫基础模型
-
通过 heardes 内容 我们写出基本爬虫代码:(仔细看注释)
import requests # 下面我们是对 headers 内容的复制 和建立 字典以便传入请求 # 注意:有一些键值对 对我们的结果没有影响,不考虑 cookies = { 'JSESSIONID': '0F137F1CCFEF0D45BCE1395B91E3AF02', '_jc_save_wfdc_flag': 'dc', # 下面是我们的出发地, '_jc_save_fromStation': '%u6DF1%u5733%2CSZQ', 'BIGipServerotn': '1859715338.64545.0000', 'RAIL_EXPIRATION': '1591478460680', 'RAIL_DEVICEID': 'KUmHXGT8KZoXi1oy1CXW0M61gVb55jGcLbYj_BrzfzCDxsdqe70FMmeOoqXPO884_8IBHLo0l_aLRM5USvx_L1_Vhz6w12yNVDkpkrl5tbZILluFoGC74mpkWllOaZyGRU3V1XcbMgbz9jWFxTL4KmlKSIQLex_M', 'BIGipServerpassport': '971505930.50215.0000', 'route': '9036359bb8a8a461c164a04f8f50b252', # 时间,即为我们输入的查询时间 '_jc_save_toDate': '2020-06-06', '_jc_save_fromDate': '2020-06-06', # 下面使我们的到达地 '_jc_save_toStation': '%u90B5%u9633%2CSYQ', }
-
我们看到地址已经被编码,这里考虑解码:
-
通过 在线编码转换
-
我们可以发现 %2C后面部分为站点代码,后面我们会解释到,根据多次实验发现,我们删掉逗号和后面的内容请求结果依然正确
-
我们也发现,在这运用的编码方式,因而我们可以类似的写出编码函数:
def cn_unicode(station): return str(station.encode('unicode_escape')).replace("\\\\",'%') # 代码检验: print(cn_unicode('深圳')) # 结果如下,大小写不影响请求结果的正确性,读者可以自行将其转换成大写
-
-
说明:为何需要得到编码函数,因为我们后面查找车次需要输入不同的地址,将其的编码结果我们替换即可请求
-
在headers中我们也看到了被编码的站点名称
headers = { 'Connection': 'keep-alive', 'Cache-Control': 'no-cache', 'Accept': '*/*', 'Sec-Fetch-Dest': 'empty', 'X-Requested-With': 'XMLHttpRequest', 'If-Modified-Since': '0', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Referer': 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs=%E6%B7%B1%E5%9C%B3,SZQ&ts=%E9%82%B5%E9%98%B3,SYQ&date=2020-06-03&flag=N,N,Y', # 这一部分中 %E6%B7%B1%E5%9C%B3,SZQ %E9%82%B5%E9%98%B3,SYQ # 我们同样可以发现这就是我们的站点 'Accept-Language': 'zh-CN,zh;q=0.9', }
-
我们同样可以得到他的编码函数:
def cn_utf8(station): return str(st.encode('utf-8')).replace("\\x",'%') # 代码检验 print(cn_utf8('深圳')) # 结果如下
- 对于params 部分我们主要解决站点代码替换问题
- 我们可以在 Sources 栏 通过一下目录找到文件
- kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9143
- 提取 station_names 内容我们做成字典,以便通过站点名得到其代码
params = ( ('leftTicketDTO.train_date', '2020-06-06'),# 时间 ('leftTicketDTO.from_station', 'SZQ'),#出发地代码 ('leftTicketDTO.to_station', 'SYQ'),#到达地代码 ('purpose_codes', 'ADULT'),# 成人票 #('purpose_codes','0X00'), # 学生票 ) response = requests.get('https://kyfw.12306.cn/otn/leftTicket/query', headers=headers, params=params, cookies=cookies)
-
def station_dict(): # 截取部分作为参考 # 实际请求我们需要传入所有站点 station_names = "@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2@bjn|北京南|VNP|beijingnan|bjn|3@bjx|北京西|BXP|beijingxi|bjx|4@gzn|广州南|IZQ|guangzhounan|gzn|5@cqb|重庆北|CUW|chongqingbei|cqb|6@cqi|重庆|CQW|chongqing|cq|7@cqn|重庆南|CRW|chongqingnan|cqn|8@cqx|重庆西|CXW|chongqingxi|cqx|9@gzd|广州东|GGQ|guangzhoudong|gzd|10" result_list = station_names.split("@") # print(result_list) del result_list[0] # print(result_list) dict = {} for each in result_list: dict[each.split("|")[1]] = each.split("|")[2] return dict # 通过上面函数我们得到有效的数据: dict = station_dict()
-
-
通过上面的分析我们得到完整的请求模型,读者可以优化,封装。
#coding:utf-8 import requests # 中文转 unicode 传给 cookies def cn_unicode(st): return str(st.encode('unicode_escape')).replace("\\\\",'%') # 中文转 utf8 传给headers def cn_utf8(st): return str(st.encode('utf-8')).replace("\\x",'%') # 参数的传递没有足够严谨 def get_response(st_dict): # 参数传给 params cookies = { 'JSESSIONID': '0F137F1CCFEF0D45BCE1395B91E3AF02', '_jc_save_wfdc_flag': 'dc', '_jc_save_fromStation': cn_unicode(fs), 'BIGipServerotn': '1859715338.64545.0000', 'RAIL_EXPIRATION': '1591478460680', 'RAIL_DEVICEID': 'KUmHXGT8KZoXi1oy1CXW0M61gVb55jGcLbYj_BrzfzCDxsdqe70FMmeOoqXPO884_8IBHLo0l_aLRM5USvx_L1_Vhz6w12yNVDkpkrl5tbZILluFoGC74mpkWllOaZyGRU3V1XcbMgbz9jWFxTL4KmlKSIQLex_M', 'BIGipServerpassport': '971505930.50215.0000', 'route': '9036359bb8a8a461c164a04f8f50b252', '_jc_save_toDate': date, '_jc_save_fromDate': date, '_jc_save_toStation': cn_unicode(ts), } headers = { 'Connection': 'close', 'Cache-Control': 'no-cache', 'Accept': '*/*', 'Sec-Fetch-Dest': 'empty', 'X-Requested-With': 'XMLHttpRequest', 'If-Modified-Since': '0', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Referer': 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs={0},SZQ&ts={1},SYQ&date={2}&flag=N,N,Y'.format(cn_utf8(fs),cn_utf8(ts),date), 'Accept-Language': 'zh-CN,zh;q=0.9', } params = ( ('leftTicketDTO.train_date', date), ('leftTicketDTO.from_station', st_dict[fs]), ('leftTicketDTO.to_station', st_dict[ts]), ('purpose_codes', 'ADULT'), ) response = requests.get('https://kyfw.12306.cn/otn/leftTicket/query', headers=headers,params=params, cookies = cookies) response.encoding = 'utf-8' return response.text
结果分析和提取有用成分
-
运行上面的模型:
-
提取出 response 部分结果集:
{"httpstatus":200,"data":{"result":["2dE%2ByheYtEEN5J%2BISuDPWgjB%2Bn%2BfI0u%2FxjDCheFHi50zqdaKD6ajsl5hYjYjfITHpjGM%2BFwG2Ubh%0Aqb7P72EE18fdODM1pgMx%2BcPvAa6f89dN4tKzfkb3uZF3k0I3IvVmu6pGFJ0qqbsJaavVB3hhuyLv%0A36ufwuMKTZUzOOIQlvxX%2B86fVvm1SUcIRAaGSoEbpyc4kxsbaNNRqGUBfDk5nCA4PB2s9SigRWqX%0AvzWMT4edxjnXKVLw0iLqqo4g1G%2BeWnJLxZz84s%2BZdmZ%2F%2Fy9wAQu7AumQJ%2F%2F1VKIP3fOMaFxfl1u1%0AUNtdClzmlw8%3D|预订|64000K900530|K9005|XFA|BJQ|CSQ|BJQ|00:05|09:46|09:41|N|oSMfXgi%2Bz7E77Ucay%2FknFz1891F0CeCvRjj9xHBgJyNsJs%2Fp6oaeXaTtZ%2BE%3D|20200617|3|Q6|09|14|0|0||||无|||无||无|无|||||304010W0|3411|1|1||3026900000404200000010142000001014203000|||||||0","zpzis8Y%2FlhFToFfW4knUBU3%2Fn9gWgS61E6GOqJfuzjgbGwZb0XTfTiPr84XkNTSrSzQi0CoRWiEk%0Aw4cEhUoOOIMyQDq5PK8gDSBLBiNnq1uaxkUJKxZBz09sa8i%2FT5Rpg6BwuLyyQeJwf5edID5BdY7A%0Af%2Fb6BPDB%2FcC5mfEM7kM0eSsc1WIWHpp8Kq2WQ2W2iLc0TOAIoXuzrscc7emMuFRfq7MZ%2BybXk0pF%0A%2BKzUidwUOTNPZ7SLoL9pwT531X9%2FfFWCZnwUcQ4oELkFZBFCYY%2FyM09m3CoX5luoH8TphWsChGyV%0AUf%2Bokw%3D%3D|预订|6a000K90750F|K9075|VGQ|SZQ|CSQ|SZQ|00:26|10:28|10:02|N|jpcUdDsEdkXWsZd1QWzPwfqp0VH7TsnbKGiUha9adOhgdekpJ65XcaDbjAk%3D|20200617|3|Q7|03|07|0|0||||无|||无||无|无|||||403010W0|4311|1|1||4042400000302730000010146000001014603000|||||||",后面省略未给出],"flag":"1","map":{"CSQ":"长沙","NZQ":"福田","BJQ":"深圳东","IOQ":"深圳北","SZQ":"深圳","OSQ":"深圳西","CWQ":"长沙南"}},"messages":"","status":true}
-
我可以看到他的结果集类似 我们的 字典
-
但是这里我们如果想提取出来 通过 response[“data”][“result”] 按照正常思维我们可以提取出车次信息
-
我们得到 response 后进行 以下测试:
try: print(response['data']['result']) except Exception as e: print("测试代码出错,出错信息:",e) finally: print((type(response))) response = json.loads(response) # import json print(type(response)) print(response["data"]["result"])
-
-
根据结果我们可以看到 我们得到的 看似为 dict 的 response 其实是 str
-
我们通过 json 模块的 loads 方法即可转换
-
我们看到 response 结果集的:
- response[‘data’][‘result’] 信息即为车次信息
结果制表输出
-
接 基础模型代码 我们将 get_response() 得到的 response 传入函数 make_table()
-
def make_table(response): result_dict = json.loads(response) # 将 str 转换为 dict result_list = result_dict["data"]["result"] # 创建表格 pt = PrettyTable() # from prettytable import PrettyTable # 添加表头 对照官网车票表格制表 pt.field_names = ( ["车次", "出发站", "到达站", "出发时间", "到达时间", "历时", "商务座/特等座", "一等座", "二等座/二等包座", "高级软卧", "软卧一等座", "动卧", "硬卧二等座", "软座", "硬座", "无座", "其他"]) for each in result_list: # 对提取的车次结果进行 逐趟 分析 res = each.split("|") # 可以发现车次结果信息由 ”|“ 区分,即通过”|“ 切割制成列表,方便操作 del res[0] del res[11] del res[1] del res[3:5] del res[7:15] del res[-13:-1] del res[0] # 对照官网车票表格删除无用信息 high_train = ["" for _ in range(17)] # 高铁/动车 生成列表占位 low_train = ["" for _ in range(17)] # 普通火车 # if 语句判断原因,高铁动车只有商务 一等 二等 无座,其他通过占位列表无需更改 # 可能因疫情期间 暂时 无座 均为 无,及为默认 空,不进行更改 if res[0][0] == 'G' or 'D':# 判断 是否为 高铁/动车 high_train[0:6] = res[0:6] high_train[6] = res[18] high_train[7] = res[17] high_train[8] = res[16] pt.add_row(high_train) # 从头到尾 填充表格 else: low_train[0:6] = res[0:6] low_train[9] = res[7] low_train[10] = res[9] low_train[11] = res[-2] low_train[12] = res[-7] low_train[14] = res[-6] low_train[15] = res[12] pt.add_row(low_train) return pt
-
调用 main:
if __name__ == '__main__':
dict = get_abbreviation()
fs = input("请输入你初始站:")
ts = input("请输入你终点站:")
date = input("你要查询哪天的车次:")
response = get_response(dict)
pt = make_table(response)
print(pt)
- 我们看到 出发站、到达站 均为站点代码,此时我们可以在 make_table 函数里面 调用我们的 station_dict的代码值来查找中文名称键
def make_table(response,dict):
result_dict = json.loads(response)
result_list = result_dict["data"]["result"]
# map_ = dict["data"]["map"]
# 创建表格
pt = PrettyTable()
# 添加表头
pt.field_names = (
["车次", "出发站", "到达站", "出发时间", "到达时间", "历时", "商务座/特等座", "一等座", "二等座/二等包座", "高级软卧", "软卧一等座", "动卧", "硬卧二等座", "软座", "硬座",
"无座", "其他"])
for each in result_list:
res = each.split("|")
del res[0]
del res[11]
del res[1]
del res[3:5]
del res[7:15]
del res[-13:-1]
del res[0]
# 添加如下部分 即可
for i in range(len(res)):
for k,v in dict.items():
if res[i] == v: # 通过字典的值,来得到键
res[i] = k
high_train = ["" for _ in range(17)]
low_train = ["" for _ in range(17)]
if res[0][0] == 'G':
high_train[0:6] = res[0:6]
high_train[6] = res[18]
high_train[7] = res[17]
high_train[8] = res[16]
pt.add_row(high_train)
else:
low_train[0:6] = res[0:6]
low_train[9] = res[7]
low_train[10] = res[9]
low_train[11] = res[-2]
low_train[12] = res[-7]
low_train[14] = res[-6]
low_train[15] = res[12]
pt.add_row(low_train)
return pt
- 此时,我们可以看到如下结果
- 注意:我们写的出发地,可能是我们查询结果的 经停站
输出结果的美观,我们可以熟练运用 prettytable 模块 进行修改
说明
各位读者:
部分代码冗余,解释啰嗦,或不得当之处
多多指教
欢迎留言!