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

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