自从12306登录新增了滑块验证码以后,很多抢票软件就无法正常使用了。
博主最近研究使用selenium实现自动登录功能

准备

  1. Python 3.7 - 3.9
  2. Chrome Driver: 下载地址
  3. 打码服务: 打码服务使用12306_code_server,可以自建打码服务,也可以使用云打码服务。
  4. Pip 依赖: pip install requests==2.25.1 selenium==3.141.0

初始化

初始化 selenium 打开 12306 登录页面

Browser主要目的是在浏览器打开网页的时候,注入脚本修改 webdriver 的值, 否则滑块验证码会验证失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
from selenium import webdriver
# ChromeDriver 路径 - ChromeDriver 配置版本只要和 Chrome 的大版本匹配就行
CHROME_PATH = '/opt/google/chrome/chromedriver'
options = webdriver.ChromeOptions()
options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36')


class Browser(webdriver.Chrome):
def __init__(self):
super().__init__(chrome_options=options, executable_path=CHROME_PATH)

def get(self, url):
"""
Loads a web page in the current browser session.
"""
super().get(url)
time.sleep(0.5)
# 通过代码注入的方式进行修改 webdriver 的值。
self.execute_script('Object.defineProperties(navigator,{webdriver:{get:() => false}});')
self.execute_script('window.document.documentElement.getAttribute("webdriver");')

初始化 browser 打开登录页面

1
2
3
4
5
6
7
8
9
10
from selenium.webdriver.common.by import By
from selenium.webdriver.support import wait
from selenium.webdriver.support import expected_conditions as ec
browser = Browser()
# 浏览器最大化,也可以不设置
browser.maximize_window()
# 现在使用这个url地址
browser.get('https://kyfw.12306.cn/otn/resources/login.html')
# 等待用户密码登录按钮可以点击,切换到用户密码登录 Tab
wait.WebDriverWait(browser, 5).until(ec.element_to_be_clickable((By.CLASS_NAME, 'login-hd-account'))).click()

获取验证码图片

可以在 img 标签右键复制 xpath,使用 xpath 获取元素,也可以使用 id J-loginImg 获取元素

1
2
3
4
5
import time
time.sleep(1)
code_img_ele = browser.find_element_by_id('J-loginImg')
base64_img = code_img_ele.get_attribute('src')
code_img = base64_img[len('data:image/jpg;base64,'):]

调用打码服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from selenium.webdriver import ActionChains
import requests
# 这里也可以使用自建打码服务器
code_url = "https://12306.yinaoxiong.cn/verify/base64/"
data = {"imageFile": code_img}
resp = requests.post(code_url, data=data)
if resp.status_code is 200:
resp_json = resp.json()
if resp_json and resp_json.get("code") is 0:
points = codexy(resp_json.get("data")).split(',')
time.sleep(1)
# 遍历列表,使用动作链对每一个列表元素对应的x,y指定的位置进行点击操作
for i in range(len(points) // 2):
ActionChains(browser).move_to_element_with_offset(
code_img_ele,
float(points[i * 2]),
float(points[i * 2 + 1])
).click().perform()
time.sleep(1)
browser.find_element_by_id('J-userName').send_keys("用户名")
time.sleep(1)

codexy 代码获取点真实的坐标

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
def codexy(select):
post = []
offsetsX = 0 # 选择的答案的left值,通过浏览器点击8个小图的中点得到的,这样基本没问题
offsetsY = 0 # 选择的答案的top值
for ofset in select:
if ofset == '1':
offsetsY = 77
offsetsX = 40
elif ofset == '2':
offsetsY = 77
offsetsX = 112
elif ofset == '3':
offsetsY = 77
offsetsX = 184
elif ofset == '4':
offsetsY = 77
offsetsX = 256
elif ofset == '5':
offsetsY = 149
offsetsX = 40
elif ofset == '6':
offsetsY = 149
offsetsX = 112
elif ofset == '7':
offsetsY = 149
offsetsX = 184
elif ofset == '8':
offsetsY = 149
offsetsX = 256
else:
pass
post.append(offsetsX)
post.append(offsetsY)
randCode = str(post).replace(']', '').replace('[', '').replace("'", '').replace(' ', '')
print(u"验证码识别坐标为{0}".format(randCode))
return randCode

输入用户名密码登录

注意将用户名,密码替换成自己的。

1
2
3
4
5
browser.find_element_by_id('J-userName').send_keys("用户名")
time.sleep(1)
browser.find_element_by_id('J-password').send_keys("密码")
browser.find_element_by_id('J-login').click()
time.sleep(1)

滑块验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from selenium.common.exceptions import WebDriverException
# 获取滑块验证码元素
div = browser.find_element_by_id('nc_1_n1z')
# 创建动作链
action = ActionChains(browser)
# 点击长按指定的标签
action.click_and_hold(div)
# 处理滑动模块
for i in range(5):
# perform()立即执行动作链操作
# move_by_offset(x,y):x水平方向 y竖直方向
try:
action.move_by_offset(40, 0).perform() # 速度为30mm
except WebDriverException:
time.sleep(1)
time.sleep(1)
action.release()

完整文件:

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
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import wait
from selenium.webdriver.support import expected_conditions as ec

# ChromeDriver 路径 - ChromeDriver 配置版本只要和 Chrome 的大版本匹配就行
CHROME_PATH = 'D:\\chromedriver.exe'
options = webdriver.ChromeOptions()
options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36')


class Browser(webdriver.Chrome):
def __init__(self):
super().__init__(chrome_options=options, executable_path=CHROME_PATH)

def get(self, url):
"""
Loads a web page in the current browser session.
"""
super().get(url)
time.sleep(0.5)
# 通过代码注入的方式进行修改 webdriver 的值。
self.execute_script('Object.defineProperties(navigator,{webdriver:{get:() => false}});')
self.execute_script('window.document.documentElement.getAttribute("webdriver");')


browser = Browser()
# 浏览器最大化,也可以不设置
browser.maximize_window()
# 现在使用这个url地址
browser.get('https://kyfw.12306.cn/otn/resources/login.html')
# 等待用户密码登录按钮可以点击,切换到用户密码登录 Tab
wait.WebDriverWait(browser, 5).until(ec.element_to_be_clickable((By.CLASS_NAME, 'login-hd-account'))).click()

time.sleep(1)
code_img_ele = browser.find_element_by_id('J-loginImg')
base64_img = code_img_ele.get_attribute('src')
code_img = base64_img[len('data:image/jpg;base64,'):]


def codexy(select):
post = []
offsetsX = 0 # 选择的答案的left值,通过浏览器点击8个小图的中点得到的,这样基本没问题
offsetsY = 0 # 选择的答案的top值
for ofset in select:
if ofset == '1':
offsetsY = 77
offsetsX = 40
elif ofset == '2':
offsetsY = 77
offsetsX = 112
elif ofset == '3':
offsetsY = 77
offsetsX = 184
elif ofset == '4':
offsetsY = 77
offsetsX = 256
elif ofset == '5':
offsetsY = 149
offsetsX = 40
elif ofset == '6':
offsetsY = 149
offsetsX = 112
elif ofset == '7':
offsetsY = 149
offsetsX = 184
elif ofset == '8':
offsetsY = 149
offsetsX = 256
else:
pass
post.append(offsetsX)
post.append(offsetsY)
randCode = str(post).replace(']', '').replace('[', '').replace("'", '').replace(' ', '')
print(u"验证码识别坐标为{0}".format(randCode))
return randCode


from selenium.webdriver import ActionChains
import requests

# 这里也可以使用自建打码服务器
code_url = "https://12306.yinaoxiong.cn/verify/base64/"
data = {"imageFile": code_img}
resp = requests.post(code_url, data=data)
if resp.status_code is 200:
resp_json = resp.json()
if resp_json and resp_json.get("code") is 0:
points = codexy(resp_json.get("data")).split(',')
time.sleep(1)
# 遍历列表,使用动作链对每一个列表元素对应的x,y指定的位置进行点击操作
for i in range(len(points) // 2):
ActionChains(browser).move_to_element_with_offset(
code_img_ele,
float(points[i * 2]),
float(points[i * 2 + 1])
).click().perform()
time.sleep(1)
browser.find_element_by_id('J-userName').send_keys("用户名")
time.sleep(1)
browser.find_element_by_id('J-password').send_keys("密码")
browser.find_element_by_id('J-login').click()
time.sleep(1)
from selenium.common.exceptions import WebDriverException

# 获取滑块验证码元素
div = browser.find_element_by_id('nc_1_n1z')
# 创建动作链
action = ActionChains(browser)
# 点击长按指定的标签
action.click_and_hold(div)
# 处理滑动模块
for i in range(5):
# perform()立即执行动作链操作
# move_by_offset(x,y):x水平方向 y竖直方向
try:
action.move_by_offset(40, 0).perform() # 速度为30mm
except WebDriverException:
time.sleep(1)
time.sleep(1)
action.release()
time.sleep(99)


运行效果