又到了一年一度的春運時節,搶個票?
1、設計思路
如果我們要買一張火車票,我們會怎麼做?打開12306,登陸,輸入出發地和目的地,選擇出行日期,然後點擊查詢,有餘票的話就下單購買,沒有票就點刷新或者等待一會再去看下,爲了能搶到票,你時不時就得放下手頭的工作,登陸12306看看有沒有票。很枯燥很繁瑣不是嗎?因此我們希望寫一個腳本來代替我們做這些事情。
腳本是一段程序,能夠自動幫我們完成上述枯燥的工作,遺憾的是,這個腳本並不智能,無法像人一樣識別複雜的圖像、邏輯,它只會執行我們交待給它的事情(也就是一堆的if…else、while),因此我們不得不打開瀏覽器,分析下構成一個頁面的元素(html標籤,css,JavaScript),而這些元素纔是腳本能夠識別的,我們用腳本來解析這些元素,判斷這是否是我們想要的數據,從而決定是否進入下一步。
幸好,有很多對開發者友好的瀏覽器可以幫助我們分析一個網頁。打開Firefox瀏覽器,進入12306官網,點擊鼠標右鍵->“查看元素”,彈出的控制檯包含很多功能,左上角的箭頭可以選取頁面元素,“查看器”可以查看網頁元素,“控制檯”可以調試JavaScript腳本,“網絡”可以對網絡通信進行抓包,看一看訪問一個網頁都加載了哪些資源。通過分析12306的頁面,確定哪些信息是我們需要的(車次、出發時間、餘票信息),以及確定下單時提交的表單。
2、工具準備
- 開發環境爲win10
- 在windows中安裝python3.6.8,並且將python的可執行文件所在目錄添加到環境變量。
- 用pip安裝selenium庫
- 下載Firefox的webdriver,並且將可執行文件所在目錄添加到環境變量。
- 用pip安裝playsound庫
3、代碼
代碼拆分到兩個文件,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=
4、運行腳本開始搶票
在cmd命令行輸入python 12306.py開始搶票。