12306 火车票余票查询脚本开发

12306 余票查询开发初衷

可能很多同学有疑惑都2024年了,怎么还需要自己开发这个余票查询功能呢?毕竟当前市面太多自动化抢票工具了,只需要填写基本信息就可以完成从抢票到下单等一系列操作,确实,现在有很多抢票软件做的都很完美,但是有一个痛点目前是我遇到的,那就是

1.我想查例如 北京—> 石家庄的票,在这样的大型节假日的时候,我们都会选择抢票,但是抢票软件都是选择你关注的的,虽然抢票软件可以自动抢新开列车,但是如果新开的列车不是你所期望的时间,这时间把钱付了,你还要退票,那么就要面临手续费!

2.另一方面,我想看当前那些有余票的,我就需要每次打开软件去查询,没有一个很方便的直接告诉我,哪列车当前有票!会感觉很麻烦,如果有一个程序可以帮我自动查询然后告诉我,那么就很完美了!

那么我开发的这个脚本就可以完美的解决上述的两个问题,但是目前仅限于查询余票,其余还是做不到的!后续功能可以慢慢加上!

12306 网站接口剖析

如果我们想查询余票,各位同学肯定会想到的时候通过接口查询,那么我们首先观察12306 网站在查询的时候都用了那些接口呢?假如我们这时候查询 北京—> 石家庄 2024年10月9题的火车票,通过查询NetWork面板我们发现一共请求了2个接口!

那么这两个接口具体是干嘛的呢? 下面我们就先剖析第一个接口

url_1: https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2024-10-09&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=SJP&purpose_codes=ADULT

这个url_1 实现了我们的查询功能就是查询北京—>石家庄 10月09日的票,可以看返回体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"httpstatus": 200,
"data": {
"result": [
"IN8i2mjU4pcjjt%2FuzxOnHM4uHCelK7W%2FFIJqW4sNvITAIkDPEQXWDCZRPUOlCQDyzvC2TBDcOaw8%0AHDSJMjOYT%2FZUGmDFoxGjVSeCZ0m3vuq11L0d5I8BhBL%2FcWpvvt%2FQzFHg57AgDF%2B24iCwm1rNRfvx%0A8WqPrdaBsIa7ffyQ9%2FNMxitJVEF9AAv0kc8VjDK0wqGAqS%2F6QlXILrQTUeh830ahknr0%2F017eIrI%0A67tFo0WX2HUhszbtLo7mEk%2FFWu%2BovNguW%2BOG7%2BB%2Bh1GBf61lRTktO03u0XOz3Di3NY%2FUKARr7vE5%0AnqcGhjzJ9uFZrXc2Q2gMq04L2uz43U0jxNqtkb9ZmnXO%2BeYC|预订|25000K772836|K7725|QTP|HDP|FTP|SJP|05:10|08:44|03:34|Y|Jz1NB7nb1uVJBAoXtjLOZGJ20tRCNaJVqDZwRJdiCitslCNE|20241008|3|P4|05|10|1|0|||||||有||有|有|||||1030W0|131|1|0||100415002130087500211004153540|0|||||1|0#0#0#0#z#0#3#z||7|CHN,CHN|||N#N#|330087531009553200925||202409250800|",
"4y1ZRp%2BUDU05TDqqGv5db0B8Ihr2U1k%2FUeqFOAFBuD7D4figNevZqpZ6BhdLoptjhZm0o1VkTITk%0A3ls1ZBfEW%2BM3gjtMX5jVAt7kltUotA8BR7mIkjeKOO1p0%2F6xN13lF5egt%2BVI3l5OH2HCdNox%2BJR0%0AXYaq%2BW%2BIJfFMsylSFQ4VzXzuZT2LZIgB4vJi2L5xnxBbUMWNMVC%2BE2uLm8%2BYUSLVjtA23UYLJCvZ%0AotZroMKl5%2FWYzSTg5t%2BsDf0IWJflfP9oUFRJQ71joO5iAhRO7K2ztONUGpXeegX8iLvFAgGyEy9h%0Af3bbljnIxuSs8earw%2FBRl4QpP5%2F9vcCPe7%2BgkeRHKIA%3D|预订|24000G67010K|G6701|BXP|HPP|BXP|SJP|05:34|06:52|01:18|Y|gM8t0dYOT81RRsMTXqzAZBNqGbCT0vYlvM9nnLI%2F6hDnhC7c|20241009|3|P2|01|03|1|0|||||||||||有|有|9||90M0O0|9MO|1|0||9040700009M016300021O010200021|0|||||1|0#1#0#0#z#0#z#z||7|CHN,CHN|||N#N#||90076M0066O0066|202409250800|",
"aJBRnhEtmXAILBbtHPZE5sGXxKlGgBMT4oHXCbUtcPuZ3qg3wWoFgPftibMhjuGeeMyWLj%2FOMpA4%0A1S0xBnV93r8DZd%2BjeZ4ctKY8mrxeTZqQS2hC%2FQdik20TK%2FDIeoXiTcCkZvD6gVUqMgIDBkb21q1J%0Alo5s%2BHidvNoHsOGvFqN4LHScUsfRvT8bWgW0gjuHs%2F%2FD2LRYdoWI4huvq1ymE112foQZCBFBaMTG%0AerGlG%2BMeFF6Yuk8nfv0jFZ90L3M9LmjZ3juZOklCSAl1bTGx5R31iszI%2BmrAd1NPGazE7VglF5Rz%0A0dxDkY27GSnazC6iVP40G4koVDk8EhXfl81rLTHtopE%3D|预订|24000G67030M|G6703|BXP|SJP|BXP|SJP|06:09|07:38|01:29|Y|3xnb0KoZxbzyVOsX6Abi3Bt1wPSt82%2FVpCTzaT73xMwj4JKw|20241009|3|P3|01|04|1|0|||||||||||有|有|有||90M0O0|9MO|1|0||9040700021M013600021O008500021|0|||||1|0#0#0#0#z#0#z#z||7|CHN,CHN|||N#N#||90076M0056O0055|202409250800|",
"Ogwl8J7aObJfs%2F3CMJhpSdQGFSiYlTW3BFiMMI6XfFv9oGNvbzSdPewztg4FaP7YVypFXeKH4IVI%0AjZs0qTz2jKFKllmW2I3Vb9VQMW6WYOcNk1lX3rJF7vnglUOsQQPQnXeYWPlasqgPchyVoX0lcab3%0AUJJ8p4PRgz1%2BDV11X%2FxqrgBMajeH3oceMve6xFuZPx0hv1ULSLmqzs4soBc4aAqXG1NhnyUn8Cle%0A%2BfIbM%2FGoWvBxny3boSaGHqG0RtOqkSz57MCHny4lhKzj0unAJk9PHWuEKD9pBcZKzMco2FQAZa81%0ABn2yKz%2Fj%2Ff5pL0P93yXy5RrXg9soKhdEzYp40T0DTL8%3D|预订|240000G55920|G559|BXP|JGF|BXP|SJP|06:15|07:26|01:11|Y|IItRSsrNtZYpYkCJw54WZ92Lec8RbTDQ%2BJpF4yOQO%2Bv1U21q|20241009|3|P4|01|02|1|0|||||||||||有|5|无||90M0O0|9MO|0|1||9040700000M020600005O012900021|0|||||1|0#0#0#0#z#0#z#z||7|CHN,CHN|||N#N#||90076M0084O0084|202409250800|"
],
"flag": "1",
"level": "0",
"sametlc": "Y",
"map": {
"VVP": "石家庄北",
"IFP": "北京朝阳",
"FTP": "北京丰台",
"SJP": "石家庄",
"BJP": "北京",
"QIP": "清河",
"BXP": "北京西",
"GIP": "高邑"
}
},
"messages": "",
"status": true
}

上述的返回体中我截取了result中的部分数据,因为返回太多了我们只需要对一个观察就可以找到具体的规律,此时我们看到

1
"IN8i2mjU4pcjjt%2FuzxOnHM4uHCelK7W%2FFIJqW4sNvITAIkDPEQXWDCZRPUOlCQDyzvC2TBDcOaw8%0AHDSJMjOYT%2FZUGmDFoxGjVSeCZ0m3vuq11L0d5I8BhBL%2FcWpvvt%2FQzFHg57AgDF%2B24iCwm1rNRfvx%0A8WqPrdaBsIa7ffyQ9%2FNMxitJVEF9AAv0kc8VjDK0wqGAqS%2F6QlXILrQTUeh830ahknr0%2F017eIrI%0A67tFo0WX2HUhszbtLo7mEk%2FFWu%2BovNguW%2BOG7%2BB%2Bh1GBf61lRTktO03u0XOz3Di3NY%2FUKARr7vE5%0AnqcGhjzJ9uFZrXc2Q2gMq04L2uz43U0jxNqtkb9ZmnXO%2BeYC|预订|25000K772836|K7725|QTP|HDP|FTP|SJP|05:10|08:44|03:34|Y|Jz1NB7nb1uVJBAoXtjLOZGJ20tRCNaJVqDZwRJdiCitslCNE|20241008|3|P4|05|10|1|0|||||||有||有|有|||||1030W0|131|1|0||100415002130087500211004153540|0|||||1|0#0#0#0#z#0#3#z||7|CHN,CHN|||N#N#|330087531009553200925||202409250800|"

首先是一堆乱码,后面我们看到有中文的预定,然后我们继续观察看到有 K7725 这个火车编号的 信息,然后后面后有部分时间信息,以及后续的票务信息:|||||||有||有|有|||||, 那么有人要问了这些|表示什么呢?先不管,这里我们认为是票务的信息,那么我们发现也就这些是我们能看懂的,那么我们怎么利用现有信息来实现余票查询呢?别着急后续继续解释。

url_2:https://kyfw.12306.cn/lcquery/queryG?train_date=2024-10-09&from_station_telecode=BJP&to_station_telecode=SJP&result_index=0&can_query=Y&isShowWZ=Y&sort_type=2&purpose_codes=00&is_loop_transfer=S&channel=E&_json_att=

url_2 是查询中转的接口,也就是查询北京—>石家庄 10月09日的可以乘坐哪些中转的车,一起看返回体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
{
"data": {
"flag": true,
"result_index": 1,
"can_query": "N",
"middleList": [
{
"all_lishi": "3小时52分钟",
"all_lishi_minutes": 232,
"arrive_date": "2024-10-09",
"arrive_time": "12:14",
"end_station_code": "SJP",
"end_station_name": "石家庄",
"first_train_no": "24000G780703",
"from_station_code": "VNP",
"from_station_name": "北京南",
"fullList": [
{
"arrive_time": "08:43",
"bed_level_info": "",
"controlled_train_flag": "0",
"country_flag": "CHN,CHN",
"day_difference": "0",
"dw_flag": "5#1#0#S#z#0#z#z",
"end_station_name": "廊坊",
"end_station_telecode": "LJP",
"from_station_name": "北京南",
"from_station_no": "01",
"from_station_telecode": "VNP",
"gg_num": "--",
"gr_num": "--",
"is_support_card": "1",
"lishi": "00:21",
"local_arrive_time": "",
"local_start_time": "",
"qt_num": "--",
"rw_num": "--",
"rz_num": "--",
"seat_discount_info": "90075M0080O0076",
"seat_types": "9MO",
"srrb_num": "--",
"start_station_name": "北京南",
"start_station_telecode": "VNP",
"start_time": "08:22",
"start_train_date": "20241009",
"station_train_code": "G7807",
"swz_num": "2",
"to_station_name": "廊坊",
"to_station_no": "02",
"to_station_telecode": "LJP",
"train_no": "24000G780703",
"train_seat_feature": "3",
"trms_train_flag": "0",
"tz_num": "--",
"wz_num": "--",
"yb_num": "--",
"yp_info": "9008700002M004200018O002500021",
"yw_num": "--",
"yz_num": "--",
"ze_num": "有",
"zy_num": "18"
}
],
"isHeatTrain": "N",
"isOutStation": "0",
"lCWaitTime": "0",
"lishi_flag": "0",
"middle_date": "2024-10-09",
"middle_station_code": "LJP",
"middle_station_name": "廊坊",
"same_station": "0",
"same_train": "N",
"score": 386,
"score_str": "276+100+10=386##100#100",
"scretstr": "MjAyNC0xMC0wOSMwMCNHNzgwNyMwMDoyMSMwODoyMiMyNDAwMEc3ODA3MDMjVk5QI0xKUCMwODo0MyPljJfkuqzljZcj5buK5Z2KIzAxIzAyI2RSWkVHSk9HdTRZdDlVNE0vVkhGaHdRckk5TXpaNGlaYkpYaFJHS1hlNi9RMXo5ZiNQMyMxNCMxMjMwIzMjMzEjMDM1NyMwMSMwMzU4IzAjVk5QI0xKUCMxNzI4MzY4MzQwMjcwIzE3MjcyMzg2MDAwMDAjNSwxLDAsUyx6LDAseix6IyMjQ0hOLENITiM6OjoyMDI0LTEwLTA5IzAwI0c3ODA1IzAyOjQyIzA5OjMyIzJkMDAwRzc4MDUwMyNMSlAjU0pQIzEyOjE0I%2BW7iuWdiiPnn7PlrrbluoQjMDEjMDYjQ1pxYm94dlFlM1Z4Y3ZoYzd4TFlWNnNFN3BjTmNkS2VMSUQ5eXJUWldGTjhxK1k4I1A0IzE0IzE2MDAjMyMwMSMwMzU4IzAxIzAzNDkjMCNMSlAjSFBQIzE3MjgzNjgzNDAyNzAjMTcyNzI1MTIwMDAwMCM1LDEsMCxTLHosMCx6LHojIyNDSE4sQ0hOIzo6Om51bGwjMDo6OjI3RDdGMjg5ODcxODdFNUM3RkJBMEUyMjRDNjkxOTUzOTNCMUY1N0FENThDOUQwMUQ3RkNCN0ZEOjo6MDo6Olk6OjpZ",
"second_train_no": "2d000G780503",
"start_time": "08:22",
"train_count": 2,
"train_date": "2024-10-09",
"use_time": "",
"wait_time": "49分钟",
"wait_time_minutes": 49
}
]
},
"status": true,
"errorMsg": ""
}

这里返回的信息也依旧截取了部分返回信息,那么可以看到这个返回体更加清晰,描述了中转车量的信息,那么由于我们这里暂时不考虑中转,首先就关心这个接口了!

针对url_1 接口的返回体解析

那么从上面我们看到了返回信息里面包含了乘车的编号信息,票务信息,那么就可以实现查询了,首先我们观察第一个也是上面遗留的问题:|||||||有||有|有|||||, 这些 | 表示什么意思?下面我来解释一下,通过观察 我们对比该车次的返回在12306页面的表达看

观察上述信息发现,硬卧 二等卧、 硬座、 无座 均是有,而其他类型的例如 一等座、二等座 二等包座等 都是 --,那么现在很明显了,| 表示的是无,有票以及票有数据,则是显示 有或者具体数据,那么此时我们已经解刨这个接口完毕了,找到了我们想要的信息,那么接下来的难点就是如何从这些返回体中取出想要的数据!

编写返回体提取票务信息逻辑

首先观察返回信息:

1
"IN8i2mjU4pcjjt%2FuzxOnHM4uHCelK7W%2FFIJqW4sNvITAIkDPEQXWDCZRPUOlCQDyzvC2TBDcOaw8%0AHDSJMjOYT%2FZUGmDFoxGjVSeCZ0m3vuq11L0d5I8BhBL%2FcWpvvt%2FQzFHg57AgDF%2B24iCwm1rNRfvx%0A8WqPrdaBsIa7ffyQ9%2FNMxitJVEF9AAv0kc8VjDK0wqGAqS%2F6QlXILrQTUeh830ahknr0%2F017eIrI%0A67tFo0WX2HUhszbtLo7mEk%2FFWu%2BovNguW%2BOG7%2BB%2Bh1GBf61lRTktO03u0XOz3Di3NY%2FUKARr7vE5%0AnqcGhjzJ9uFZrXc2Q2gMq04L2uz43U0jxNqtkb9ZmnXO%2BeYC|预订|25000K772836|K7725|QTP|HDP|FTP|SJP|05:10|08:44|03:34|Y|Jz1NB7nb1uVJBAoXtjLOZGJ20tRCNaJVqDZwRJdiCitslCNE|20241008|3|P4|05|10|1|0|||||||有||有|有|||||1030W0|131|1|0||100415002130087500211004153540|0|||||1|0#0#0#0#z#0#3#z||7|CHN,CHN|||N#N#|330087531009553200925||202409250800|"

我们看到在上面中有一个预定的汉字,那么我就可以先以 预定 进行分割,此时可以获取后面的信息,那么就是

1
train_info = train_txt.split("预定")[1]

那么我们拿到信息如下:

1
|25000K772836|K7725|QTP|HDP|FTP|SJP|05:10|08:44|03:34|Y|Jz1NB7nb1uVJBAoXtjLOZGJ20tRCNaJVqDZwRJdiCitslCNE|20241008|3|P4|05|10|1|0|||||||有||有|有|||||1030W0|131|1|0||100415002130087500211004153540|0|||||1|0#0#0#0#z#0#3#z||7|CHN,CHN|||N#N#|330087531009553200925||202409250800|

那么我们继续观察发现在上述信息中有一个|Y|,继续观察其他的发现要不就是|Y|要不就是|N|,那么就可以按照这个进行分割。

1
train_info = train_txt.split("预定")[1]split("|Y|")[0] 

也就得到了

1
|25000K772836|K7725|QTP|HDP|FTP|SJP|05:10|08:44|03:34

此时拿到了具体的车次信息,那么想要后续的信息就可以取

1
train_info = train_txt.split("预定")[1]split("|Y|")[1] 

那么拿到的是:

1
Jz1NB7nb1uVJBAoXtjLOZGJ20tRCNaJVqDZwRJdiCitslCNE|20241008|3|P4|05|10|1|0|||||||有||有|有|||||1030W0|131|1|0||100415002130087500211004153540|0|||||1|0#0#0#0#z#0#3#z||7|CHN,CHN|||N#N#|330087531009553200925||202409250800|

通过观察,在|20241008| 前面的乱码发现基本上长度一致,那么我就可以指定长度取截取

至此,我们实现了信息的采集和分割,那么我们能实现拿到具体车次,是否有票这些信息。

判断信息中是否有票

那么拿到数据之后,我们就要判断是否有票,我们可以根据是否有有以及是否有数字存在即可!

1
2
3
4
5
6
7
8
9
def contains_number_or_you(s):
# 检查是否包含数字
if any(char.isdigit() for char in s):
return True
# 检查是否包含“有”
if "有" in s:
return True
# 如果都不包含,则返回False
return False

函数通过检查是否有数字以及是否有有,返回布尔值。

组装返回信息

目前计划是通过邮件将信息返回,那么此时已经可以判断车次,是否有票,我们就需要通过将我们拿到的数据进行重写组装,然后将其转化为一个html格式的表格便于我们查看,具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def list_to_html(data):
print("+++++++++++++++++++")
# print(data)
# print(type(data))
print("++++++++++++++++++")
headers = [
"列车号", "起点站", "途经站1", "途经站2", "终点站",
"出发时间", "到达时间", "耗费时间", "暂无", "暂无",
"暂无", "暂无", "暂无", "座位数目", "优选一等座",
"座位数目", "座位数目", "软卧/动卧", "座位数目", "座位数目", "无座", "座位数目", "硬卧,二等卧", "硬座", "二等座", "一等座", "商务座,特等座", "座位数目"
]

# 构造HTML表格的开头部分,包括CSS样式
html_table = '''
<style>
.table-container {
height: 90vh; /* 根据视口高度设置,以适应不同屏幕 */
overflow-y: auto; /* 允许垂直滚动 */
display: block; /* 可能需要,取决于布局 */
width: 100%; /* 或固定宽度 */
}
th {
position: sticky;
top: 0;
background-color: #f9f9f9; /* 表头背景色 */
}
table {
width: 100%; /* 确保表格宽度与容器相同 */
border-collapse: collapse; /* 边框合并 */
}
th, td {
border: 1px solid #ddd; /* 边框样式 */
padding: 8px; /* 单元格内边距 */
text-align: left; /* 文本对齐方式 */
}
</style>
<div class="table-container">
<table>
<thead>
<tr>
'''
for header in headers:
html_table += f'<th>{header}</th>'
html_table += '''
</tr>
</thead>
<tbody>
'''

# 遍历数据,并为每一项添加一行到表格体(tbody)中
for item in data:
columns = item.split('|')
# 填充缺失的列
columns += [''] * (len(headers) - len(columns))
html_row = '<tr>' + ''.join(f'<td>{column}</td>' for column in columns) + '</tr>\n'
html_table += html_row

# 添加表格的结尾部分
html_table += '''
</tbody>
</table>
</div>
'''

# 返回完整的HTML字符串
print(html_table)
return html_table

上面的函数接收一个list,然后进行转化生成一个html信息。

邮件发送

我们拿到html信息后需要第一时间通知给我,那么就要用到邮件通知,下面是邮件通知代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
async def send_mail(mail_body, receivers):
# 发件人邮箱账号
sender = '[email protected]'
# 发件人邮箱授权码(不是QQ密码)
password = '' # 此处填写你的授权码
# SMTP服务器地址
smtp_server = 'smtp.qq.com'
# SMTP服务器端口,对于QQ邮箱,使用SSL的465端口
smtp_port = 465

# 邮件内容
subject = '抢到火车票了!'
# HTML邮件正文
html_body = mail_body

# 邮件接收者
# receiver = '[email protected]'

# 创建邮件对象,指定内容为HTML
message = MIMEText(html_body, 'html', 'utf-8')
# 设置From头部,使用formataddr函数来正确设置完整的From头部信息
message['From'] = formataddr(('发件人姓名', sender))
# 设置To头部,同样使用Header
# message['To'] = Header(receiver, 'utf-8')
# 设置Subject头部,对于中文主题也使用Header
message['Subject'] = Header(subject, 'utf-8')

try:
# 创建SMTP SSL连接
smtpObj = smtplib.SMTP_SSL(smtp_server, smtp_port)
smtpObj.login(sender, password) # 登录到SMTP服务器

# 如果receivers是单个字符串,则转换为列表
if isinstance(receivers, str):
receivers = [receivers]

# 发送邮件给多个接收者
smtpObj.sendmail(sender, receivers, message.as_string())
print("邮件发送成功")
smtpObj.quit()
except smtplib.SMTPException as e:
print("邮件发送失败:", e)

这里我们声明是一个协程方法也就是发送邮件是异步的,我无需等待邮件发送后才执行下一个逻辑。

一些固定规律的解决

在继续查看返回体的时候发现里面会有固定的 W M F 的返回信息,此处为

1
Jz1NB7nb1uVJBAoXtjLOZGJ20tRCNaJVqDZwRJdiCitslCNE|20241008|3|P4|05|10|1|0|||||||有||有|有|||||1030W0|131|1|0||100415002130087500211004153540|0|||||1|0#0#0#0#z#0#3#z||7|CHN,CHN|||N#N#|330087531009553200925||202409250800|

也就是我想去掉 |1030W0| 后面的信息由于后续都是无规律的,但是肯定会有M W F 出现,那么我们可以获取当前位置然后进行分割,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def remove_after_wm(s):
# 查找'W'或'M'首次出现的位置
# 使用min函数和find方法确保我们找到的是两者中较早出现的一个
# 如果'W'或'M'都不存在,find会返回-1,而min(-1, -1)也是-1
pos_w = s.find('W')
pos_m = s.find('M')
pos_f = s.find('F')

if pos_w != -1 and pos_m != -1 and pos_f != -1:
pos = min(pos_w, pos_m, pos_f)
elif pos_w != -1:
pos = pos_w
elif pos_m != -1:
pos = pos_m
elif pos_f != -1:
pos = pos_f
else:
pos = -1 # 表示'W'和'M'都不存在

return s[:pos]

整体规划

到现在我们已经拿到了具体的信息,那么就可以规划我们的具体实现,首先我们可以支持 邮件多人发送,请求12306延迟设置防止IP访问频繁被封,用户关注的火车信息,假如我关注的车次只想他有票的时候才会通知也行支持多个车次的设置,假如我们不想一直接收邮件的发送,那么我们可以设置邮件接收的延迟时间,目前支持设置分钟,小时级别的延迟。

那么上面就是我们要实现的功能,下面就是已经实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def train_info(url, address, receivers, sleep_time=10, user_focus_train=None, email_delay_time=None):
"""
=========== dreamshao 12306查询余票函数调用指南=========

支持多线程查询火车票余票,使用教程:
登录12306 网站 https://www.12306.cn/index/
选择起点,终点,时间 点击查询
此时页面跳转到列车详情页面,打开network 面板,再次点击查询抓取查询接口
注意: 此时可能会用2个接口请求,一个是火车列表信息, 一个是中转信息
我们需要的url 是这样的: https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2024-09-30&leftTicketDTO.from_station=SJP&leftTicketDTO.to_station=XTP&purpose_codes=ADULT
然后将其作为Url 参数传入,然后选择改url对应的COOKIE, 在程序中替换之前的即可!

=========== dreamshao 12306查询余票函数调用指南=========
:param url: 请求url
:param address: 火车起点重点: 北京-----> 邯郸
:param user_focus_train: 你关注的列车有票才会发送邮件,格式是list ['G3433','K333']
:param sleep_time: 请求接口休眠时间
:param email_delay_time 邮件延迟发送时间,就是当前不希望每次都收到通知,只是希望间隔多久通知一次, 目前是 minutes, hours 格式是(1, 0.5) 是 小时, 其余是按照分钟处理
:param receivers 邮件接收人 可以传递一个列表例如 ['[email protected]','[email protected]] 多人接收邮件
:return:
"""
if url == "" or address == "" or receivers == "":
return "please input right url or address or receivers"
print(
f"当前请求地址是{url},请求火车票方向是{address},邮件接收人是{receivers},请求间隔时间是{sleep_time}秒,用户关注的火车列表是{user_focus_train},邮件间隔报警时间是{email_delay_time}")
time_info = url.split('train_date=')[1].split("&")[0] # 出发时间
global send_info_type, send_numbers, send_email_all_type, send_all_numbers, email_alert_delay_type, email_alert_delay_time # 关注的火车发送邮件状态
send_info_type = False # 关注火车列表发送状态
send_email_all_type = False # 所有火车列表发送状态
send_numbers = 0
send_all_numbers = 0
numbers = 0
email_alert = []
email_alert_delay_time = delay_time(email_delay_time)

while True:
# 获取当前时间
now = datetime.now()
# 是否有票的标志
has_ticket = False
print(f"当前正在抢{address}的火车票")
payload = {}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
'Cookie': 'JSESSIONID=6571054C8C61F63A5F32C82A1A340BED; guidesStatus=off; _big_fontsize=0; highContrastMode=defaltMode; cursorStatus=off; BIGipServerpool_index=821035530.43286.0000; route=6f50b51faa11b987e576cdb301e545c4; BIGipServerotn=670040586.24610.0000'

}

time.sleep(sleep_time)
response = requests.request("GET", url, headers=headers, data=payload)
if check_http_code(response.status_code):
re_json = json.loads(response.text)
len_nums = len(re_json['data']['result'])
# print(re_json['data']['result'])
for i in (re_json['data']['result']):
numbers += 1
try:
if "|N|" in i:
# print(i.split("预订")[1][14:].split("|N|")[0])
train_info = i.split("预订")[1][14:].split("|N|")[0]
print(train_info)
else:
# print(i.split("预订")[1][14:].split("|Y|")[0])
train_info = i.split("预订")[1][14:].split("|Y|")[0]
print(train_info)
info = remove_after_wm(str((i.split("预订")[1][14:]).split("2024")[1]))
# print(info)
# print(info[20:][:17])
print(numbers)
print(len_nums)
if contains_number_or_you(info[20:][:17]):
print(info)
print("有票啦!")
email_info = train_info + " " + info
if email_info not in email_alert:
email_alert.append(email_info)
if numbers == len_nums:
# 替换成表格
email_alert_update = list_to_html(email_alert)
# print(email_alert_update)
# print(type(email_alert_update))
if user_focus_train:
print("===================")
print(f"用户开启了筛选火车发送邮件,当前选择的是{user_focus_train}")
print("===================")
for user_train in user_focus_train:
for train_single_info in email_alert:
if user_train in train_single_info:
send_info_type = True
if send_info_type and send_numbers == 0:
print("===================")
print(f"现在正在查看{address},现在时间{now}")
print(f"用户开启了筛选火车发送邮件,存在相同列车信息,将会发送邮件!")
print("===================")
send_numbers = 1
asyncio.run(send_mail(mail_body=f"抢到了{time_info},{address}的票{email_alert_update}",
receivers=receivers))
email_alert.clear()

elif send_numbers != 0 and send_info_type and email_alert_delay_time:
print("===================")
print("当前设置了邮件发送延迟")
print(f"现在正在查看{address},现在时间{now},定时清空列表时间{email_alert_delay_time}")
print(
f"当前用户开启了筛选火车发送邮件,存在相同列车信息, 但是在当前设置的延迟发送邮件中, 下次发送时间是{email_alert_delay_time}之后, 所以此次不会发送邮件!")
print("===================")
email_alert.clear()
if compare_time(now, email_alert_delay_time):
email_alert.clear()
send_numbers = 0 # 专注火车列表发送次数归零
# send_all_numbers = 0 # 已经发送所有火车有票的记录
# send_email_all_type = True
print("清空列表,重新开始发送邮件")
asyncio.run(send_mail(mail_body=f"抢到了{time_info},{address}的票{email_alert_update}",
receivers=receivers))
email_alert_delay_time = delay_time(email_delay_time)
print(f"现在正在查看{address},现在时间{now},定时清空列表时间{email_alert_delay_time}")
else:
print("===================")
print(f"现在正在查看{address},现在时间{now}")
print(f"用户开启了筛选火车发送邮件,不存在相同列车信息,不会发送邮件!")
email_alert.clear()
print("===================")
elif not send_email_all_type and send_all_numbers == 0 and email_alert_delay_time:
"""
首次发送全部有票的火车, 且当前存在邮件延迟发送时间设置
"""
print("===================")
print("首次发送全部有票的火车,当前设置了邮件发送延迟")
print(f"现在正在查看{address},现在时间{now},定时清空列表时间{email_alert_delay_time}")
print("====================")
send_all_numbers += 1
send_email_all_type = True
asyncio.run(send_mail(mail_body=f"抢到了{time_info},{address}的票{email_alert_update}",
receivers=receivers))
email_alert.clear()

elif send_email_all_type and send_all_numbers == 1 and email_alert_delay_time:
"""
当前存在邮件延迟发送时间设置
"""
print("===================")
print("当前设置了邮件发送延迟")
print(f"现在正在查看{address},现在时间{now},定时清空列表时间{email_alert_delay_time}")
print("====================")
email_alert.clear()
if compare_time(now, email_alert_delay_time):
email_alert.clear()
# # send_numbers = 0 # 专注火车列表发送次数归零
# send_all_numbers = 1 # 已经发送所有火车有票的记录
# send_email_all_type = T # 回到首次发送的状态
print("清空列表,重新开始发送邮件")
asyncio.run(send_mail(mail_body=f"抢到了{time_info},{address}的票{email_alert_update}",
receivers=receivers))
email_alert_delay_time = delay_time(email_delay_time)
print(f"现在正在查看{address},现在时间{now},定时清空列表时间{email_alert_delay_time}")

else:
print("===================")
print(f"现在正在查看{address},现在时间{now}")
print("当前未设置延迟发送邮件,将即可发送邮件")
print("===================")
asyncio.run(send_mail(mail_body=f"抢到了{time_info},{address}的票{email_alert_update}",
receivers=receivers))
email_alert.clear()

numbers = 0 # 控制整体的数目归零
except Exception as e:
print(e)
print(remove_after_wm(str((i.split("起售")[1][14:]).split("2024")[1])))
print(">>>>>")
else:
return f"12306接口请求失败, 状态码是{response.status_code}"

多线程和多进程的选择

刚开始我选择的是多线程,但是在实践中发现在传递信息的时候会发生 A 线路的信息 流转到了 B 线路中,后来查询资料发现,多线程的内存信息共享的可能会发生数据错乱,但是多进程是内存独立的不共享,此时在实现的时候选择了 进程池来实现!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if __name__ == "__main__":
"""
train_info 函数传递顺序(url, address, receivers, sleep_time, user_focus_train, email_delay_time)
"""
query_list = [
(
"https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2024-10-07&leftTicketDTO.from_station=SJP&leftTicketDTO.to_station=HDP&purpose_codes=ADULT",
"石家庄---->邯郸", ["[email protected]", "[email protected]"], 10, ['K7734','G543'], (2, 1)), # 到邯郸
(
"https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2024-10-07&leftTicketDTO.from_station=SJP&leftTicketDTO.to_station=XTP&purpose_codes=ADULT",
"石家庄---->邢台", ["[email protected]", "[email protected]"], 20, [], (2, 2)) # 到邢台
]

with ProcessPoolExecutor(max_workers=2) as executor:
# 提交任务,每个任务都接收一个args元组,自动解包为多个参数
futures = [executor.submit(train_info, *args) for args in query_list]
# 等待所有任务完成
try:
for future in as_completed(futures):
# 处理可能的异常
result = future.result()
print(f"Received result: {result}")
except Exception as e:
print(f"进程出错了{e}")

上述代码采用了进程池的方式进行请求,最大设置了2个,可以自由设置,通过submit 将任务提交返回一个futures对象,然后我们需要看是否每个都完成了调用as_completed这个方法即可!

效果查看

运行后如下:

可以发现我们实现了我们上述的所有功能,下面看邮件效果:

可以看到邮箱收到了我们的邮件以及票务信息!

总结

通过编写这个脚本我也学到了很多知识,最大就是利用了进程池来实现,也感受到了多进程和多线程在使用上的差距,那么源码已经共享到github 地址是:https://github.com/dreamshao/12306
在使用过程中,如果无法查询可以将接口请求的最新cookie替换一下本地接口请求的cookie,如果有更好的建议或者有疑问可以联系我哦!


12306 火车票余票查询脚本开发
https://dreamshao.github.io/2024/10/08/12306余票查询/
作者
Yun Shao
发布于
2024年10月8日
许可协议