又到了一年一度的春运时节,抢个票?
如果我们要买一张火车票,我们会怎么做?打开12306,登陆,输入出发地和目的地,选择出行日期,然后点击查询,有余票的话就下单购买,没有票就点刷新或者等待一会再去看下,为了能抢到票,你时不时就得放下手头的工作,登陆12306看看有没有票。很枯燥很繁琐不是吗?因此我们希望写一个脚本来代替我们做这些事情。
脚本是一段程序,能够自动帮我们完成上述枯燥的工作,遗憾的是,这个脚本并不智能,无法像人一样识别复杂的图像、逻辑,它只会执行我们交待给它的事情(也就是一堆的if…else、while),因此我们不得不打开浏览器,分析下构成一个页面的元素(html标签,css,JavaScript),而这些元素才是脚本能够识别的,我们用脚本来解析这些元素,判断这是否是我们想要的数据,从而决定是否进入下一步。
幸好,有很多对开发者友好的浏览器可以帮助我们分析一个网页。打开Firefox浏览器,进入12306官网,点击鼠标右键->“查看元素”,弹出的控制台包含很多功能,左上角的箭头可以选取页面元素,“查看器”可以查看网页元素,“控制台”可以调试JavaScript脚本,“网络”可以对网络通信进行抓包,看一看访问一个网页都加载了哪些资源。通过分析12306的页面,确定哪些信息是我们需要的(车次、出发时间、余票信息),以及确定下单时提交的表单。
代码拆分到两个文件,12306.py文件是程序的入口文件,里面是整个程序的运行逻辑,funcs12306.py包含辅助函数。此外还需一个配置文件setting.ini,以及一个mp3文件kc.mp3(下单成功之后播放提醒用户)。
12306.py:
# -*- coding: UTF-8 -*-
# python购票脚本
import time
import random
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains
import funcs12306 as fc
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
# 读取配置文件
config = fc.read_setting()
# print(config)
# input()
# 打开浏览器
driver = webdriver.Firefox()
# 记录查询次数
query_times = 1
# 等待5秒
driver.implicitly_wait(5)
print("正在打开12306登录页")
# 进入登录页
driver.get("https://kyfw.12306.cn/otn/resources/login.html")
time.sleep(1)
# 登录
fc.login(driver, config["username"], config["password"])
print("=========================抢票中=============================")
'''进入购票流程'''
# 读取常用联系人,选择要购票的乘客,乘客姓名保存到列表里
if config["passerger"] == '':
driver.get('https://kyfw.12306.cn/otn/view/passengers.html')
passengers = fc.choose_passenger(driver)
else:
passengers = config["passerger"].split()
# 输入出发日期
travel_dates = config["travel_date"].split();
# 进入车票查询页
driver.get('https://kyfw.12306.cn/otn/leftTicket/init')
# 设置出发地
s = driver.find_element_by_id('fromStationText')
ActionChains(driver).move_to_element(s)\
.click(s)\
.send_keys_to_element(s, config["s_station"])\
.move_by_offset(20,50)\
.click()\
.perform()
# 设置目的地
e = driver.find_element_by_id('toStationText')
ActionChains(driver).move_to_element(e)\
.click(e)\
.send_keys_to_element(e, config["e_station"])\
.move_by_offset(20,50)\
.click()\
.perform()
fc.query_tickets(driver,
travel_dates[random.randint(0,len(travel_dates)-1)])
# 选择车次
if config["train_number"] == '':
trains = fc.choose_train(driver)
else:
trains = config["train_number"].split()
# 座位
seat_level = config["seat_level"].split()
while True:
print("查询次数:{0}".format(query_times))
# 判断能否购买,可以购买进入选择乘客页
fc.can_buy(driver,fc.list_to_string(trains),
str(len(passengers)),
fc.list_to_string(seat_level))
if driver.current_url=='https://kyfw.12306.cn/otn/confirmPassenger/initDc':
fc.confirm_buy(driver, fc.list_to_string(passengers))
# 发送邮件通知
fc.mail("已为您预订{0},请在半小时之内登录12306完成支付。"\
.format(ticket.text),
config["mail_sender"],
config["mail_sender_password"],
config["mail_receiver"])
# 播放音乐
while True:
playsound('kc.mp3')
break;
fc.query_tickets(driver, travel_dates[random.randint(0,len(travel_dates)-1)])
query_times+=1
funcs12306.py:
import time
import random
import re
import smtplib
import mail
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from email.mime.text import MIMEText
from email.utils import formataddr
from selenium import webdriver
# 从配置文件读取配置
def read_setting():
s_file = open("setting.ini", encoding='UTF-8')
config = {}
lines = s_file.readlines()
for x in lines:
if re.match(r'[^;]+=.*', x) != None :
x = x.strip('\n')
s = x.split("=")
config[s[0].strip()] = s[1].strip()
return config
# 登录函数
# 图形验证码不太好破解,这里手动登陆
def login(driver,username,password):
# 执行js脚本选择账号密码登陆
try:
WebDriverWait(driver, 6).until(EC.presence_of_element_located((By.CLASS_NAME,"login-hd")))
driver.execute_script('var c = document.querySelectorAll\
(".login-hd-account > a:nth-child(1)");c[0].click();')
except Exception as e:
print(e)
# 在表单中填入用户名和密码
driver.find_element_by_id('J-userName').send_keys(username)
driver.find_element_by_id('J-password').send_keys(password)
#验证码图片
# img_code = driver.find_element_by_id('J-loginImg')
# 输入验证码选择
# select = list(map(int,input("请选择验证码图片(输入1-8,多张用空格分隔):").split()))
"""将选择的图片序号转换为坐标,共有八张图片,
第一张图片坐标大约为(40,50),左右、上下间隔大约为70,下面是八张图片
的近似点击坐标"""
# site = {
# 1 :(40,68),
# 2 :(110,67),
# 3:(180,65),
# 4:(250,59),
# 5:(40,132),
# 6:(110,129),
# 7:(183,135),
# 8:(259,132),
# }
input("登陆成功后,按任意键继续")
# 逐个点击图片
# for x in select:
# webdriver.ActionChains(driver).move_to_element_with_offset(img_code,
# site[x][0],site[x][1]).click().perform()
# 选择乘客
def choose_passenger(driver):
while not ('https://kyfw.12306.cn/otn/view/passengers.html' in driver.current_url):
driver.get('https://kyfw.12306.cn/otn/view/passengers.html')
# 保存常用联系人
passengers = []
choose = []
while True:
try:
print("search passengers...")
# 找到展示姓名的元素
name_element = driver.find_elements_by_class_name('name-yichu')
for x in name_element:
passengers.append(x.text)
# 写一段js进行翻页
js = 'var next = document.getElementsByClassName("next");\
next[0].click();'
driver.execute_script(js)
time.sleep(1)
except Exception as e:
print(e)
print("乘客信息如下:")
for i in range(len(passengers)):
print('{0:3} {1:5}'.format(i,passengers[i]))
choose = list(map(int,input("选择乘客(输入名字前的序号,多个用空格分隔):")\
.split()))
name = []
for x in choose:
name.append(passengers[x])
return name
# 查询车票
def query_tickets(driver, travel_date):
# 设置出发日
driver.execute_script('document.getElementById("train_date").removeAttribute("readonly");')
date = driver.find_element_by_id('train_date')
date.clear()
date.send_keys(travel_date)
# 点击查询
driver.execute_script('document.getElementById("query_ticket").click();')
time.sleep(1)
# 选择车次
def choose_train(driver):
trains = {}
train_number = driver.find_elements_by_class_name('number')
s_time = driver.find_elements_by_class_name('start-t')
length = len(train_number)
for i in range(length):
trains[train_number[i].text] = s_time[i].text
print("{0:6} {1:6}".format("车次","出发时间"))
for x in trains.items():
print("{0:6} {1:6}".format(x[0],x[1]))
return list(input("选择车次,多个用空格分隔:").split())
# 判断是否有票
def can_buy(driver,train_number,passenger_num,seat_level):
if driver.current_url != 'https://kyfw.12306.cn/otn/leftTicket/init':
return
js ='var tb = document.getElementById("queryLeftTable");\
var rows = tb.children;\
var train_number = '+train_number+';\
var passenger_num = '+passenger_num+';\
var seat_level = '+seat_level+';\
var length = rows.length;\
for (var i = 0; i <length; i++) {\
if(rows[i].children.length==0)continue;\
var number = rows[i].children[0].children[0]\
.children[0].children[0].textContent.trim();\
if(train_number.indexOf(number)==-1)\
continue;\
for (var j = seat_level.length - 1; j >= 0; j--) {\
if(rows[i].children[seat_level[j]].textContent == "有"){\
rows[i].lastElementChild.firstChild.click();\
}\
if(rows[i].children[seat_level[j]].textContent >=passenger_num){\
rows[i].lastElementChild.firstChild.click();\
}\
}\
}'
driver.execute_script(js)
# 点击下单,确认购买
def confirm_buy(driver, passengers):
try:
ticket = WebDriverWait(driver, 6).until(EC.presence_of_element_located((By.ID,"ticket_tit_id")))
except Exception as e:
print(e)
print("为您预订:{0}".format(ticket.text))
js = 'var passengers='+passengers+';\
console.log(passengers);\
var passengers_list = document.getElementById("normal_passenger_id");\
var li = passengers_list.children;\
for(var i = 0; i<li.length; i++){\
if(passengers.indexOf(li[i].children[1].textContent)==-1){\
continue;\
}\
li[i].children[0].click();\
}\
document.getElementById("submitOrder_id").click();\
'
# 等待
time.sleep(3)
driver.execute_script(js)
time.sleep(1)
driver.execute_script('document.getElementById("qr_submit_id").click()')
print("订单已提交,请登录12306完成支付")
def list_to_string(li):
t_n = ""
for x in li:
t_n += '"'+str(x)+'",'
t_n = '['+t_n+']'
return t_n
# 发送邮件
def mail(msg_body, my_sender, my_pass, my_user):
try:
msg=MIMEText(msg_body,'plain','utf-8')
msg['From']=formataddr(["ldy",my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号
msg['To']=formataddr(["亲爱的用户",my_user]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号
msg['Subject']="12306抢票通知" # 邮件的主题,也可以说是标题
server=smtplib.SMTP_SSL("smtp.qq.com", 465) # 发件人邮箱中的SMTP服务器,端口是25
server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码
server.sendmail(my_sender,[my_user,],msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
server.quit() # 关闭连接
except Exception as e:
print(e)
setting.ini
;配置文件
;配置抢票信息,旅行日期、乘客、车次等有多个值的用空格分隔
;出发地
s_station=北京
;目的地
e_station=上海
;出行日期,有多个日期请用空格分隔
travel_date = 2020-01-21
;乘客姓名,多个值用空格分隔,需要先在12306后台添加
passerger =
;车次,为空则在命令行中选择
train_number =
;登陆用户名
username=
;登陆密码
password=
;票种:
;1:商务座
;2:一等座
;3:二等座
;4:高级软卧
;5:软卧一等卧
;6:动卧
;7:硬卧二等卧
;8:软座
;9:硬座
;10:无座
;选择前面的数字,多个值用空格分隔
seat_level=9 7
;发送通知邮箱
mail_sender =
;发送通知邮箱 密码
mail_sender_password=
;接收通知邮箱
mail_receiver=
在cmd命令行输入python 12306.py开始抢票。
Github