python編程實踐(3):python+selenium實現12306搶票腳本

又到了一年一度的春運時節,搶個票?

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開始搶票。


Github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章