美文网首页爬虫专题
百度指数抓取 selenium 💗 Keras

百度指数抓取 selenium 💗 Keras

作者: 未不明不知不觉 | 来源:发表于2018-10-16 20:51 被阅读18次

    更新于2018-10-17 18:57

    前言

    前两天产品让抓取百度指数,首先打开百度网站看了下,发现指数数字是异步图片拼出来的,百度了下解决方案,发现了这篇文章爬虫天坑系列-百度指数爬虫和这篇文章百度指数爬虫, 非模拟浏览器操作参考这两篇文章,综合两者的优点写了下自己的解决方案,代码片段可能和源代码里的不太一样

    登录

    纯代码登陆实在懒得写,直接调用selenium进行登陆获取cookie,这里我选择的是百度指数的首页,我测试期间没有弹出验证码,挺好的,上代码和注释

    # 打开浏览器
    from selenium import webdriver
    import time
    import random
    import json
    from selenium.webdriver.common.action_chains import ActionChains
    
    def get_cookie(browser, account):
        url = "http://index.baidu.com/#/"
        browser.get(url)
        browser.find_element_by_class_name("username-text").click()
        time.sleep(3)#等等登陆框加载
        browser.find_element_by_id("TANGRAM__PSP_4__userName").clear()    
        browser.find_element_by_id("TANGRAM__PSP_4__password").clear()
        # 输入账号密码
        # 输入账号密码
        for ele in list(account[0]):
            browser.find_element_by_id(
                "TANGRAM__PSP_4__userName").send_keys(ele)
            time.sleep(random.random())
        for ele in list(account[1]):
            browser.find_element_by_id(
                "TANGRAM__PSP_4__password").send_keys(ele)
            time.sleep(random.random())
        #逐词输入,发现直接输入有可能会出问题
        browser.find_element_by_id("TANGRAM__PSP_4__submit").click()
        cookie = browser.get_cookies()
        return cookie
    
    browser = webdriver.Chrome()
    cookie = get_cookie(browser, ['username', 'password'])
    #这里建议保存cookie为文件,方便测试,cookie 是一个字典
    

    代码很简单,如果是自己动手测试建议保存cookie为文件,下次直接导入就行了

    获取指数方案一(鼠标滑动)

    假设我们把cookie 保存为了cookie 为cookie.json,我们导入cookie,进入目标页面让selenium对页面进行渲染,上代码

    def get_brower():
        browser = Chrome()
        browser.maximize_window()
        browser.get('http://index.baidu.com/?tpl=trend&word=btc')
        # 这个get不能省略
        f = open('baidu.json', 'r')
        cookies = json.load(f)
        for ele in cookies:
            browser.add_cookie(ele)
        return browser, cookies
    

    通过上面的代码我们拿到渲染完毕页面的浏览器句柄,和cookie(后面有用),选然后的页面差不多是这个样子


    BTC 指数

    我的目标是抓取7天的,其他的类似,需要点击相应的按钮,这里我们模拟点击7天的按钮,出现下面的页面(代码我就不上了)


    BTC 7 天指数
    模拟鼠标移动道对应日期会出现相应的指数
    v.png

    通过一顿操作,我们应该能看出指数的数字是由class为imgval的span拼成的,注意每个span都有自己的长度,那么span怎么能显示出图片呢?密码就在下面:


    span.png
    这里span内部的div有一个背景图,且限定了显示开始位置(margin-left),外层的span控制了背景图的显示宽度,最后把所有的小块组合起来就是我们看到的数字

    获取指数方案一(模拟js请求)

    上面的方案有很大的确点,每次切换日期需要对页面进行点击,且移动鼠标获取弹出图有时候不太灵光,导致获取的数据错误。参考上面的第二篇文章我们使用了js模拟请求的方法,具体方法流程(我抄的)

    1. 通过requests向搜索页发送请求,获取搜索页的html
    2. 解析第1步中的html,可以直接得到res1的值,并且通过一些小技巧拿到res2的js加密代码
    3. 使用selenium,加载下载到本地的Raphael.js(res2加密必须的js,而这个代码必须在浏览器环境下运行),并运行第2步得到 的res2加密代码,得到res2
    4. 使用res1、res2、开始日期、结束日期构造获取res3的url,并发送请求
      解析第4步得到的html,获取res3列表
    5. 使用res1、res2、res3构造url获取百度指数的html代码(含有上面viewbox下移动鼠标生成的内容)
      这里感谢下LongYu作者的js解析😁

    图片获取组装

    通过上面我们知道想要获取指数,首先要下载背景图,获取背景图的链接很重要,它在哪里呢,复制一下,然后搜索我们发现就在下面的style里面


    sty.png

    这个地方获取style的内容要使用get_attribute('innerHTML'),直接使用text会得到空字符
    好了既然知道了地址,我们使用前面获取的cookie ,配合requests下载图片道本地,然后把它转换为numpy数组

    res = requests.get(url, headers=headers)
    with open('tmp.jpg', 'wb') as f:
                f.write(res.content)
    img = Image.open('tmp.jpg')
    arr = np.array(img)
    

    循环对数组进行操作获取各小块图片所代表的数组,然后合并就是我们要组装的图片

    tmp = []
    for ix, ele in enumerate(values):
         width = ele.get_attribute('style')
         margin = ele.find_element_by_class_name(
                    'imgtxt').get_attribute('style')
         width = re.findall(r'\d+', width)[0]
         margin = re.findall(r'\d+', margin)[0]
         margin, width = int(margin), int(width)
         roi = arr[:, margin:margin + width] * 255
         roi = Image.fromarray(roi)
         tmp.append(roi)
    merge = np.hstack(tmp)
    

    结果示例:


    数字识别

    数字识别我首先偷懒的使用了tesseract,上面提到的参考解决方案有使用tesseract,精确度只有87%,不能忍受,果断弃之(其实可以通过训练tesseract进行提示准确度),参考之前阅读的文章使用神经网络进行识别,但是Tensorflow我又不会,抄来的代码又长看起来令人头大,幸好之前遇到过Keras,这是一个很人性化很易读的高级的超级牛逼的机器学习工具(我是小白我都会用你懂的)。官方文档 https://keras-cn.readthedocs.io/en/latest/,直接粘贴链接,不要在意这些细节。安装什么的自己去百度(PS: 开始我使用的Theano作后端速度很慢,可能是不会配置,换了Tensorflow做后端速度飙升)
    在序贯模型这一段我找到了VGG模型,代码如下:

    import numpy as np
    import keras
    from keras.models import Sequential
    from keras.layers import Dense, Dropout, Flatten
    from keras.layers import Conv2D, MaxPooling2D
    from keras.optimizers import SGD
    
    # Generate dummy data
    x_train = np.random.random((100, 100, 100, 3))
    y_train = keras.utils.to_categorical(np.random.randint(10, size=(100, 1)), num_classes=10)
    x_test = np.random.random((20, 100, 100, 3))
    y_test = keras.utils.to_categorical(np.random.randint(10, size=(20, 1)), num_classes=10)
    
    model = Sequential()
    # input: 100x100 images with 3 channels -> (100, 100, 3) tensors.
    # this applies 32 convolution filters of size 3x3 each.
    model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(100, 100, 3)))
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    
    model.add(Conv2D(64, (3, 3), activation='relu'))
    model.add(Conv2D(64, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    
    model.add(Flatten())
    model.add(Dense(256, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(10, activation='softmax'))
    
    sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
    model.compile(loss='categorical_crossentropy', optimizer=sgd)
    
    model.fit(x_train, y_train, batch_size=32, epochs=10)
    score = model.evaluate(x_test, y_test, batch_size=32)
    

    是不是看起来很简洁?Amazing😃,这里我偷懒的复制粘贴下,修修改改得到下面的代码:

    import numpy as np
    from keras.layers import (Conv2D, Dense, Dropout, Flatten,
                              MaxPooling2D)
    from keras.models import Sequential
    from keras.optimizers import Adadelta
    from keras.utils import to_categorical
    from sklearn.utils import shuffle
    from load_data import load_data
    
    #加载数据
    x_train, y_train = load_data("train")
    x_test, y_test = load_data("test")
    x_train = np.array(x_train)
    x_test = np.array(x_test)
    x_train, y_train = shuffle(x_train, y_train)
    y_train = to_categorical(y_train, num_classes=11)
    x_test, y_test = shuffle(x_test, y_test)
    y_test = to_categorical(y_test, num_classes=11)
    
    #模型定义
    model = Sequential()
    model.add(Conv2D(32, (2, 2), input_shape=(14, 10, 1), activation='relu'))
    model.add(Conv2D(32, (2, 2), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(32, (2, 2), activation='relu'))
    model.add(Conv2D(32, (2, 2), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dropout(0.15))
    model.add(Dense(128, activation='relu'))
    model.add(Dense(11, activation='softmax'))
    model.compile(
        loss='categorical_crossentropy',
        optimizer=Adadelta(lr=1, decay=0.06),
        metrics=['accuracy'])
    model.fit(x_train, y_train, epochs=20, batch_size=300)
    score = model.evaluate(x_test, y_test, batch_size=200)
    
    #model.save('model.h5')
    

    阅读完代码你可能心里不舒服,是不是缺了什么东西?训练数据,没有数据模型写再好也不能运行,所以这里我们要获取测试数据,这里说下我生成数据的策略

    生成数据

    在读参考文章的时候我发现作者是用php生产的数据,鄙人不才不会世界上最好的语言,只能另寻思路,我发现在上面组装图片的时候有的时候一张图恰好是一个数字,这张的图片一般长度为7左右(其实就是8),初始策略是利用宽度限制获取单个可识别的字符,然后进行标记再进行训练。我自己分类标记了93个。在后来我发现如果是4位数它的长度固定是40,因为4位数长度是5(其中含有一个,),按每个字符长度为8恰好可以把各个字符进行分开(同时也方便了后面进行分割识别😭)。所以新的策略是先获取一批图片然后对每张图片按宽度8进行分割即可得到样本,然后自己就可以进行愉快的标注了,有人说我不想标注那么多,累死人了要,没关系,你只需要每样标注5到10个即可,那么最终的标注样本数为50,有人要说了数据那么少训练个毛线模型! 别急,接下来看

    数据扩充

    假设上面我们得到了50个样本,样本书太少了,巧妇难为无米之炊,米太少也不行啊!百度一下我找到了这篇文章 数据增强利器Augmentor,使用方法很简单,首先安装该包

    pip install Augmentor
    

    假设我们的标记样本在source目录下

    #我复制粘贴了代码
    import Augmentor
    # 1. 指定图片所在目录
    p = Augmentor.Pipeline("./source")
    # 2. 增强操作
    # 旋转 概率0.7,向左最大旋转角度10,向右最大旋转角度10
    p.rotate(probability=0.7,max_left_rotation=10, max_right_rotation=10)
    # 放大 概率0.3,最小为1.1倍,最大为1.6倍;1不做变换
    p.zoom(probability=0.3, min_factor=1.1, max_factor=1.6)
    # resize 同一尺寸 200 x 200
    #p.resize(probability=1,height=200,width=200) 这段我没用
    # 3. 指定增强后图片数目总量
    p.sample(10000)
    

    经过上面的一顿复制粘贴我们生成了10000个样本()进行训练,可以自己随意生成数据进行测试,这里不再提了

    增强(扩充)后的图片会保存在指定增强图片所在目录下的output目录里

    数据加载

    接下来我们要把数据加载为测试集,直接上代码

    def load_data(folder, target_index=2):
        datas = []
        targets = []
        for image_file in pathlib.Path(folder).iterdir():
            split = image_file.stem.split('_')
            target = split[target_index]
            if target == ',':
                target = 10#这里我偷懒用10代表','#
            else:
                target = int(target)
            image = Image.open(str(image_file.absolute()))
            image = ImageOps.invert(image)# 原来的图片是黑色背景需要反转
            image_arr = np.array(image)
            image_arr[image_arr < 200] = 0 # 极值化处理,去除噪音点
            image_arr[image_arr > 200] = 255
            try:
                datas.append(image_arr.reshape(14, 10, 1))
            except Exception:
                zeros = np.zeros((14, 10))
                zeros[:, :image_arr.shape[1]] += image_arr
                datas.append(zeros.reshape(14, 10, 1))
            targets.append(target)
        return datas, np.array(targets)
    

    有人可能会为你用那个try catch是什么鬼,是因为本人开始自做聪明生成样本大小不一样的遗留问题,不需在意,这里这么写是为了解释上面模型中的input_shape为(14,10,1)怎么来的,如果采用上面说过的第二张方式生成基本样本,这里就不需要try catch,直接写作(14,8,1)即可。
    这里我解释下target_index的含义,我标注样本使用的方法是如果一个样本应该识别为1那么它的文件名为1_0,依此又1_1,1_2等。我们通过制定target_index指定样本图片的实际目标(target),至于为什么默认为2而不是0,是一个为使用扩容工具生成的数据样子是这个名字是这个样子的

    'source_original_,_1.jpg_bc1707b3-d1fa-4acb-b887-3e8822c510bf '
    #target是 ‘,’
    

    训练

    接下来就是训练了,这里不再提了直接运行就可以了,自己根据实际情况进行参数调节,运行完保存下模型下面识别要用到,通过上的方法我训练的模型准确度达到97.5%基本够用了

    识别

    训练完当然要进行应用了,假设我们有拼接的完整图片,首先我们要把图片按步长8进行切割

    def load(filename):
        x = []
        image = Image.open(filename)
        image = ImageOps.invert(image)
        arr = np.array(image)
        datas = np.hsplit(arr, 5)
        for ele in datas:
            zeros = np.zeros((14, 10))
            zeros[:, :ele.shape[1]] += ele
            x.append(zeros.reshape(14, 10, 1))
        return x
    

    然后再进行预测就可以了,代码如下:

    y = model.predict(x)
    rst = y.argsort()[:, -1]
    string = []
    for ele in rst:
        if ele == 10:#处理前面挖的坑,用10代表‘,’
            string.append(',')
        else:
            string.append(str(ele))
    print("{} rst {}".format(name, ''.join(string)))
    

    总结

    文章写完了,基本思路是这个样子,代码在我的github上BaiduTrend,写的很急,有很多不足之处和错别字,多多留言指正! 谢谢 😁,源代码可能与文章不太一致,但是思路相同。

    相关文章

      网友评论

        本文标题:百度指数抓取 selenium 💗 Keras

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