美文网首页程序员
爬虫笔记(10)插曲 挑战极限验证码

爬虫笔记(10)插曲 挑战极限验证码

作者: 无事扯淡 | 来源:发表于2017-01-14 17:00 被阅读0次

    1.前言

    既然有爬虫的存在那就有反爬虫技术的存在,验证码是常见手段,不过最近发现不少网站使用极限验证码。对于普通验证码如何识别的问题,在我倒腾了一晚上如何安装pytesseract-ocr之后,我终于还是放弃倒腾这玩意。在windows上编译python库就是个坑,而且还是个深坑。目前的想法是通过学习tensorflow来破解,目前阶段只学了一个入门教程。
    极限验证码采用拖动图片来进行验证,这个验证方式初看起来确实让机器识别确实很难,而且也能防止人工打码。据说还带有行为识别功能,反正具体怎么实现的,我也没看js代码,还有我讨厌括号,js里面到处是括号。网上也有一些文章谈到如何破解的问题,但是有些含糊其辞。既然别人能破,那我行不行,怎么滴也得试试。

    2.分析

    什么也不要谈先F12再说,很容易就能找到滑块。

    滑块html

    当鼠标放到滑块上时显示的是一张图片:

    鼠标停在滑块上

    当在滑块上按下鼠标显示另一张图片:

    按下滑块

    这两张图片有一些差异,那是不是可以根据这个差异来找到拼图的缺口?只要找到了缺口的边界,那就可以通过selenium模拟鼠标移动。

    • 要找到这两张图片:
    带缺口的图片 完整的图片

    上面两张图片显然有问题,被打乱了。

    • 重新编排图片
      从源代码中找到如下代码:
      Paste_Image.png
      注意background-position,这里面有几十个div背景图都是一张图片,只是位置不同。每个div宽度是10px,高度是58px,总数量是52个,最后拼成的图片是260X116px。查找另外一幅图也是这样的设置。根据```background-position``来重新绘制这两幅图片,然后进行比较找出缺口位置。这里还注意一个细节问题,打乱的原图宽度是312,拼接后的图片宽度只有260,也就是每个小切片丢失了2px。

    3.安装环境

    python的版本是3.5,Chrome版本是57

    • 安装selenium
    pip3 install selenium
    
    • 安装geckodriver-v0.13.0-win64
      配合firefox下载地址
      解压到某个目录之后,将该目录设置到系统路径PATH中。
    • 安装Chromedriver 3.27
      下载地址
      解压到某个目录之后,将该目录设置到系统路径PATH中。

    实际上我并未使用Firefox,原因就是moveto这个指令系统报错,具体是Firefox的问题还是geckodriver的问题没有搞明白。还有这个webdriver之间是有些差异的,而且这种差异还不小。选择selenium+chromedriver的组合花了一整晚的时间,也曾考虑过phantomjs。

    4.实现

    • 获取图片路径
      <div class="gt_cut_bg_slice" style="background-image: url("https://static.geetest.com/pictures/gt/9b872c380/bg/d15f30e0.webp"); background-position: -157px -58px;"></div>
    from selenium import webdriver
    import re
    url = 'https://user.geetest.com/login'
    driver = webdriver.Chrome()
    driver.get(url)
    bg_slice = driver.find_element_by_class_name('gt_cut_bg_slice')
    #background-image: url("https://static.geetest.com/pictures/gt/d0fe39770/bg/b03de89e.webp"); background-position: -157px -58px;
    pattern = re.compile(r'"(.+)"')
    bgurl = pattern.findall(bg_slice.get_attribute('style'))[0]
    #https://static.geetest.com/pictures/gt/d0fe39770/bg/b03de89e.webp
    
    • 下载图片
      图片的下载使用的是requests模块:
    def downloadimage( image_url):
        dir_path = 'D:/imgs'
        if not os.path.exists(dir_path):
            os.makedirs(dir_path)
        us = image_url[image_url.rfind('/'):]
        image_file_path = dir_path + us
        with open(image_file_path, 'wb') as handle:
            response = requests.get(image_url, stream=True)
            for block in response.iter_content(1024):
                if not block:
                    break
                handle.write(block)
            return image_file_path
    
    • 获取图片偏移坐标
      获取background-position与上面获取图片类似:
    es = driver.find_elements_by_class_name('gt_cut_bg_slice')
    #这个代码与上面的类似,但是elements这个加了复数,它会获取一组元素并且返回一个列表
    ss = [e.get_attribute('style') for e in es]
    pattern = re.compile(r'([-0-9]+)px')
    xys =[pattern.findall(s) for s in ss]
    # [['-157', '-58'], ['-145', '-58'], ['-265', '-58'], ['-277', '-58'], ['-181', '-58'], ['-169', '-58'], ['-241', '-58'],
    # ['-253', '-58'], ['-109', '-58'], ['-97', '-58'], ['-289', '-58'], ['-301', '-58'], ['-85', '-58'], ['-73', '-58'], 
    #['-25', '-58'], ['-37', '-58'], ['-13', '-58'], ['-1', '-58'], ['-121', '-58'], ['-133', '-58'], ['-61', '-58'], 
    #['-49', '-58'], ['-217', '-58'], ['-229', '-58'],['-205', '-58'], ['-193', '-58'], ['-145', '0'], ['-157', '0'],
    #['-277', '0'], ['-265', '0'], ['-169', '0'], ['-181', '0'],['-253', '0'],['-241', '0'],['-97', '0'], ['-109', '0'],
    #['-301', '0'], ['-289', '0'], ['-73', '0'], ['-85', '0'], ['-37', '0'], ['-25', '0'], ['-1', '0'], ['-13', '0'],
    #['-133', '0'], ['-121', '0'], ['-49', '0'], ['-61', '0'], ['-229', '0'], ['-217', '0'], ['-193', '0'], ['-205', '0']]
    #把上面的数据转化为整数就可以了
    

    经过对比两幅图片偏移是一样的,所以这里就省略对另一幅图的获取。

    • 图片重排列
    from PIL import Image
    def realign(imgpath):
        x1 = [157,145,265,277,181,169,241,253,109,97,289,301,85,73,25,37,13,1,121,133,61,49,217,229,205,193]
        x2 = [145,157,277,265,169,181,253,241,97,109,301,289,73,85,37,25,1,13,133,121,49,61,229,217,193,205]
        i = 0
        #这些数字实际上没有使用上面的方法获取,我直接从HTML中抄的
        im = Image.open(imgpath)
        nim = Image.new('RGB',(260,116))
        #新建一个图形文件
        for x in x1:
            box = (x,58,x+10,116)
            pastebox = (i,0,i+10,58)
            imx = im.crop(box)
       #先抠图,再粘贴到新图中
            nim.paste(imx,pastebox)
            i = i +10
        i = 0
       #图片的编排分上下两部分
        for x in x2:
            box = (x,0,x+10,58)
            pastebox = (i,58,i+10,116)
            imx = im.crop(box)
            nim.paste(imx,pastebox)
            i = i +10
        return nim
    
    • 图形对比查找缺口位置
    def iseq(p1,p2,diff = 70):
        p1 = sum(p1)
        p2 = sum(p2)
        if abs(p1-p2)<diff:
            return True
        return False
    def pos(img1,img2,diff = 70):
        pix1=img1.load()
        pix2=img2.load()
        for x in range(260):
            for y in range(116):
                p1 = pix1[x,y]
                p2 = pix2[x,y]
                if iseq(p1,p2,diff) == False:
                    return (x,y)
    

    上面的判断一个像素点是不是相等,采用非常简单的策略,RGB之和进行比较。为了调试这里特别设置了一个diff,通过调整这个参数来控制误判。查找到缺口之后,还要减去一个差值,滑块不在图片的最左端,经过测试7是个合适的数值。

    • 移动滑块
      操作步骤就是点击滑块不松,然后移动,释放左键:
    from selenium.webdriver.common.action_chains import ActionChains
    slider = driver.find_element_by_class_name('gt_slider_knob')
    (x,y) = getpos(driver,diff)
    actions = ActionChains(driver)
    actions.click_and_hold(slider).perform()#按住滑块
    end = x-7
    for i in range(end):
        actions.move_by_offset(1,0).perform()#移动
        time.sleep(0.1)
    actions.release().perform()#释放鼠标
    

    上面的代码确实按照步骤来实现的,可惜这代码有问题。这个问题出现在ActionChains,perform()执行之后并不会清空以前的命令,例如上一次移动了1px,如果下次再移动1px,实际上却执行的是2px。
    下面的代码只能部分正确,所谓部分正确就是能滑块能正确移动到缺口处,但是却不会判定为正确。

    def move(driver):
        (x,y) = f.getpos(driver,120)
        slider = driver.find_element_by_class_name('gt_slider_knob')
        actions = ActionChains(driver)
        actions.click_and_hold(slider).perform()
        end = x-7
        print(x)
        p = getint(end)
        print(p)
        for i in p:
            actions = ActionChains(driver)
            actions.move_by_offset(i,int(math.sin(i)*10)).perform()
            time.sleep(0.05+0.1*abs(math.sin(i)))
        actions = ActionChains(driver)
        actions.release().perform()
        time.sleep(0.1)
        actions = ActionChains(driver)
        actions.move_by_offset(0,200).perform()
    def getint(a):
        p = []
        while sum(p)<a:
            v = random.randint(1,5)
            p.append(v)
        p[-1] = p[-1]-3
        return p
    
    • 路径规划
      关于路径的问题一晚上都没解决,这个问题就是前面提到的行为判断。那如何安排路径才能让机器移动的路径表现得像人的行为一样,最好的办法就是直接记录自己拖动滑块的轨迹。
    轨迹记录软件

    鼠标放在滑块上,按F9,按下鼠标左键拖动滑块,直到滑块拖不动释放左键,按F9停止记录。

    轨迹记录截图

    上面的数据pos中的x,y代表坐标,tOfffset代表时间。这是个xml文件,需要用lxml来解析这三个数据。这里要提到一个事情就是,需要清理掉xmlns这些属性,还要把a:这个前缀去掉。使用sublime能批量替换掉这些不要的元素。因为有这些内容之后,lxml解析存在一些问题,比如用//x无法匹配到数据。

    整理之后的xml文件
    >>> doc = etree.parse('C:/6.xml')
    >>> xs = doc.xpath('//x/text()')
    >>> len(xs)
    205
    >>> ys = doc.xpath('//y/text()')
    >>> ts = doc.xpath('//tOffset/text()')
    >>> xs = [int(x) for x in xs]
    >>> ys = [int(y) for y in ys]
    >>> ts = [int(t) for t in ts]
    

    上面获取的坐标值是绝对值,这里需要的是相对值,也就是每一步走了多少。

    #计算差值
    def calcdelta(xx):
        ret = []
        for i in range(len(xx)-1):
            delta = xx[i+1]-xx[i]
            ret.append(delta)
        return ret
    

    时间是微秒,这么大得数应该是微秒:

    ts = [t/1000000 for t in ts]
    

    其实滑块移动只是水平方向,我已经获取到整个从左到右的路径。也就是不管缺口在哪,我只需要从xs中累积到缺口位置值,就可以获取路径。

    #x代表上面的xs,target是目标点
    def getpath(x,target):
        ret = []
        for i in x:
            if sum(ret)<target:
                ret.append(i)
            else:
                break
        return ret
    

    根据上面的分析和实现,整个移动过程实现如下:

    def stepto(driver,xs,ys,ts,diff=100):
        (x,y) = getpos(driver,diff)
        slider = driver.find_element_by_class_name('gt_slider_knob')
        actions = ActionChains(driver)
        actions.click_and_hold(slider).perform()
        end = x-7
        i = 0
        time.sleep(ts[i])
        print(x)
        path = getpath(xs,end)
        print(path)
        for j in range(len(path)):
            i = i+1
            actions = ActionChains(driver)
            actions.move_by_offset(path[j],ys[j]).perform()
            time.sleep(ts[i])
        i = i+1
        actions = ActionChains(driver)
        actions.release().perform()
        time.sleep(ts[i])
        actions = ActionChains(driver)
        actions.move_by_offset(200,200).perform()
    

    5.存在问题

    查找缺口点存在问题,需要提高抗干扰能力。还有如果缺口向左突出,这个查找可能有问题。

    6.相关资料

    selenium手册
    PIL手册

    相关文章

      网友评论

        本文标题:爬虫笔记(10)插曲 挑战极限验证码

        本文链接:https://www.haomeiwen.com/subject/bbjdbttx.html