登陆时间:2019-10-21 实现难度:★★★☆☆☆ 请求链接:https://passport.bilibili.com/login 实现目标:模拟登陆哔哩哔哩,攻克滑动验证码 涉及知识:滑动验证码的攻克、自动化测试工具 Selenium 的使用 完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/bilibili-login 其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice 爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
【1x00】思维导图
利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证
分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片
对比原始的图片和带缺口的图片的像素,像素不同的地方就是缺口位置
计算出滑块缺口的位置,得到所需要滑动的距离
拖拽时要模仿人的行为,由于有个对准过程,所以要构造先快后慢的运动轨迹
最后利用 Selenium 进行对滑块的拖拽
【2x00】登陆模块 【2x01】初始化函数 1 2 3 4 5 6 7 8 9 10 11 12 13 def init () : global url, browser, username, password, wait url = 'https://passport.bilibili.com/login' path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe' chrome_options = Options() chrome_options.add_argument('--start-maximized' ) browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options) username = '155********' password = '***********' wait = WebDriverWait(browser, 20 )
global
关键字定义了发起请求的url、用户名、密码等全局变量,随后是登录页面url、谷歌浏览器驱动的目录path、实例化 Chrome 浏览器、设置浏览器分辨率最大化、用户名、密码、WebDriverWait()
方法设置等待超时
【2x02】登陆函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def login () : browser.get(url) user = wait.until(EC.presence_of_element_located((By.ID, 'login-username' ))) passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd' ))) user.send_keys(username) passwd.send_keys(password) login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login' ))) time.sleep(random.random() * 3 ) login_btn.click()
等待用户名输入框和密码输入框对应的 ID 节点加载出来
获取这两个节点,用户名输入框 id="login-username"
,密码输入框 id="login-passwd"
调用 send_keys()
方法输入用户名和密码
获取登录按钮 class="btn btn-login"
随机产生一个数并将其扩大三倍作为暂停时间
最后调用 click()
方法实现登录按钮的点击
【3x00】验证码处理模块 【3x01】验证码元素查找函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def find_element () : c_background = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute' ))) c_slice = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute' ))) c_full_bg = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute' ))) hide_element(c_slice) save_screenshot(c_background, 'back' ) show_element(c_slice) save_screenshot(c_slice, 'slice' ) show_element(c_full_bg) save_screenshot(c_full_bg, 'full' )
获取验证码的三张图片,分别是完整的图片、带有缺口的图片和需要滑动的图片
分析页面代码,三张图片是由 3 个 canvas 组成,3 个 canvas 元素包含 CSS display
属性,display:block
为可见,display:none
为不可见,在分别获取三张图片时要将其他两张图片设置为 display:none
,这样做才能单独提取到每张图片
定位三张图片的 class 分别为:带有缺口的图片(c_background):geetest_canvas_bg geetest_absolute
、需要滑动的图片(c_slice):geetest_canvas_slice geetest_absolute
、完整图片(c_full_bg):geetest_canvas_fullbg geetest_fade geetest_absolute
最后传值给 save_screenshot()
函数,进一步对验证码进行处理
【3x02】元素可见性设置函数 1 2 3 4 5 6 7 8 def hide_element (element) : browser.execute_script("arguments[0].style=arguments[1]" , element, "display: none;" ) def show_element (element) : browser.execute_script("arguments[0].style=arguments[1]" , element, "display: block;" )
【3x03】验证码截图函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def save_screenshot (obj, name) : try : pic_url = browser.save_screenshot('.\\bilibili.png' ) print("%s:截图成功!" % pic_url) left = obj.location['x' ] top = obj.location['y' ] right = left + obj.size['width' ] bottom = top + obj.size['height' ] print('图:' + name) print('Left %s' % left) print('Top %s' % top) print('Right %s' % right) print('Bottom %s' % bottom) print('' ) im = Image.open('.\\bilibili.png' ) im = im.crop((left, top, right, bottom)) file_name = 'bili_' + name + '.png' im.save(file_name) except BaseException as msg: print("%s:截图失败!" % msg)
location
属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x轴向右递增,y轴向下递增
size
属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息
首先调用 save_screenshot()
属性对整个页面截图并保存
然后向 crop()
方法传入验证码的位置信息,由位置信息再对验证码进行剪裁并保存
【4x00】验证码滑动模块 【4x01】滑动主函数 1 2 3 4 5 6 def slide () : distance = get_distance(Image.open('.\\bili_back.png' ), Image.open('.\\bili_full.png' )) print('计算偏移量为:%s Px' % distance) trace = get_trace(distance - 5 ) move_to_gap(trace) time.sleep(3 )
向 get_distance()
函数传入完整的图片和缺口图片,计算滑块需要滑动的距离,再把距离信息传入 get_trace()
函数,构造滑块的移动轨迹,最后根据轨迹信息调用 move_to_gap()
函数移动滑块完成验证
【4x02】缺口位置寻找函数 1 2 3 4 5 6 7 8 9 10 11 12 def is_pixel_equal (bg_image, fullbg_image, x, y) : bg_pixel = bg_image.load()[x, y] fullbg_pixel = fullbg_image.load()[x, y] threshold = 60 if (abs(bg_pixel[0 ] - fullbg_pixel[0 ] < threshold) and abs(bg_pixel[1 ] - fullbg_pixel[1 ] < threshold) and abs( bg_pixel[2 ] - fullbg_pixel[2 ] < threshold)): return True else : return False
将完整图片和缺口图片两个对象分别赋值给变量 bg_image
和 fullbg_image
,接下来对比图片获取缺口。遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,abs()
用于取绝对值,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置
【4x03】计算滑块移动距离函数 1 2 3 4 5 6 7 8 9 def get_distance (bg_image, fullbg_image) : distance = 60 for i in range(distance, fullbg_image.size[0 ]): for j in range(fullbg_image.size[1 ]): if not is_pixel_equal(fullbg_image, bg_image, i, j): return i
get_distance()
方法即获取缺口位置的方法,此方法的参数是两张图片,一张为完整的图片,另一张为带缺口的图片,distance
为滑块的初始位置,遍历两张图片的每个像素,利用 is_pixel_equal()
缺口位置寻找函数判断两张图片同一位置的像素是否相同,若不相同则返回该点的值
【4x04】构造移动轨迹函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def get_trace (distance) : trace = [] faster_distance = distance * (4 / 5 ) start, v0, t = 0 , 0 , 0.1 while start < distance: if start < faster_distance: a = 10 else : a = -10 move = v0 * t + 1 / 2 * a * t * t v = v0 + a * t v0 = v start += move trace.append(round(move)) return trace
get_trace()
方法传入的参数为移动的总距离,返回的是运动轨迹,运动轨迹用 trace 表示,它是一个列表,列表的每个元素代表每次移动多少距离,利用 Selenium 进行对滑块的拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功,因此要设置一个加速和减速的距离,这里设置加速距离 faster_distance
是总距离 distance
的4/5倍,滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 move 表示,所需时间用 t 表示,它们之间满足以下关系:
1 2 move = v0 * t + 0.5 * a * t * t v = v0 + a * t
设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为10和-10,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了
【4x05】模拟拖动函数 1 2 3 4 5 6 7 8 9 10 11 12 def move_to_gap (trace) : slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button' ))) ActionChains(browser).click_and_hold(slider).perform() for x in trace: ActionChains(browser).move_by_offset(xoffset=x, yoffset=0 ).perform() time.sleep(0.5 ) ActionChains(browser).release().perform()
传入的参数为运动轨迹,首先查找到滑动按钮,然后调用 ActionChains 的 click_and_hold()
方法按住拖动底部滑块,perform()
方法用于执行,遍历运动轨迹获取每小段位移距离,调用 move_by_offset()
方法移动此位移,最后调用 release()
方法松开鼠标即可
【5x00】完整代码 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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 from selenium import webdriverfrom selenium.webdriver.chrome.options import Optionsfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.common.by import Byfrom selenium.webdriver import ActionChainsimport timeimport randomfrom PIL import Imagedef init () : global url, browser, username, password, wait url = 'https://passport.bilibili.com/login' path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe' chrome_options = Options() chrome_options.add_argument('--start-maximized' ) browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options) username = '155********' password = '***********' wait = WebDriverWait(browser, 20 ) def login () : browser.get(url) user = wait.until(EC.presence_of_element_located((By.ID, 'login-username' ))) passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd' ))) user.send_keys(username) passwd.send_keys(password) login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login' ))) time.sleep(random.random() * 3 ) login_btn.click() def find_element () : c_background = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute' ))) c_slice = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute' ))) c_full_bg = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute' ))) hide_element(c_slice) save_screenshot(c_background, 'back' ) show_element(c_slice) save_screenshot(c_slice, 'slice' ) show_element(c_full_bg) save_screenshot(c_full_bg, 'full' ) def hide_element (element) : browser.execute_script("arguments[0].style=arguments[1]" , element, "display: none;" ) def show_element (element) : browser.execute_script("arguments[0].style=arguments[1]" , element, "display: block;" ) def save_screenshot (obj, name) : try : pic_url = browser.save_screenshot('.\\bilibili.png' ) print("%s:截图成功!" % pic_url) left = obj.location['x' ] top = obj.location['y' ] right = left + obj.size['width' ] bottom = top + obj.size['height' ] print('图:' + name) print('Left %s' % left) print('Top %s' % top) print('Right %s' % right) print('Bottom %s' % bottom) print('' ) im = Image.open('.\\bilibili.png' ) im = im.crop((left, top, right, bottom)) file_name = 'bili_' + name + '.png' im.save(file_name) except BaseException as msg: print("%s:截图失败!" % msg) def slide () : distance = get_distance(Image.open('.\\bili_back.png' ), Image.open('.\\bili_full.png' )) print('计算偏移量为:%s Px' % distance) trace = get_trace(distance - 5 ) move_to_gap(trace) time.sleep(3 ) def get_distance (bg_image, fullbg_image) : distance = 60 for i in range(distance, fullbg_image.size[0 ]): for j in range(fullbg_image.size[1 ]): if not is_pixel_equal(fullbg_image, bg_image, i, j): return i def is_pixel_equal (bg_image, fullbg_image, x, y) : bg_pixel = bg_image.load()[x, y] fullbg_pixel = fullbg_image.load()[x, y] threshold = 60 if (abs(bg_pixel[0 ] - fullbg_pixel[0 ] < threshold) and abs(bg_pixel[1 ] - fullbg_pixel[1 ] < threshold) and abs( bg_pixel[2 ] - fullbg_pixel[2 ] < threshold)): return True else : return False def get_trace (distance) : trace = [] faster_distance = distance * (4 / 5 ) start, v0, t = 0 , 0 , 0.1 while start < distance: if start < faster_distance: a = 10 else : a = -10 move = v0 * t + 1 / 2 * a * t * t v = v0 + a * t v0 = v start += move trace.append(round(move)) return trace def move_to_gap (trace) : slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button' ))) ActionChains(browser).click_and_hold(slider).perform() for x in trace: ActionChains(browser).move_by_offset(xoffset=x, yoffset=0 ).perform() time.sleep(0.5 ) ActionChains(browser).release().perform() if __name__ == '__main__' : init() login() find_element() slide()
【6x00】效果实现动图 最终实现效果图:(关键信息已经过打码处理)