更新于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(后面有用),选然后的页面差不多是这个样子

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

模拟鼠标移动道对应日期会出现相应的指数

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

这里span内部的div有一个背景图,且限定了显示开始位置(margin-left),外层的span控制了背景图的显示宽度,最后把所有的小块组合起来就是我们看到的数字
获取指数方案一(模拟js请求)
上面的方案有很大的确点,每次切换日期需要对页面进行点击,且移动鼠标获取弹出图有时候不太灵光,导致获取的数据错误。参考上面的第二篇文章我们使用了js模拟请求的方法,具体方法流程(我抄的)
- 通过requests向搜索页发送请求,获取搜索页的html
- 解析第1步中的html,可以直接得到res1的值,并且通过一些小技巧拿到res2的js加密代码
- 使用selenium,加载下载到本地的Raphael.js(res2加密必须的js,而这个代码必须在浏览器环境下运行),并运行第2步得到 的res2加密代码,得到res2
- 使用res1、res2、开始日期、结束日期构造获取res3的url,并发送请求
解析第4步得到的html,获取res3列表 - 使用res1、res2、res3构造url获取百度指数的html代码(含有上面viewbox下移动鼠标生成的内容)
这里感谢下LongYu作者的js解析😁
图片获取组装
通过上面我们知道想要获取指数,首先要下载背景图,获取背景图的链接很重要,它在哪里呢,复制一下,然后搜索我们发现就在下面的style里面

这个地方获取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,写的很急,有很多不足之处和错别字,多多留言指正! 谢谢 😁,源代码可能与文章不太一致,但是思路相同。
网友评论