爬虫天坑系列-百度指数爬虫

最近有很多朋友跟我说,“爬虫这东西很简单啊,好像还没学就已经会了,没啥深奥的东西哦。看了你之前的教程,不就是一个队列加一些Http请求吗,不就是写写XPath和正则吗,你们还做个神箭手云爬虫出来?我自己上个厕所就写完了啊。”


看来是时候拿出我们压箱底多年的老干妈了,哦不,老干货了。不吓屎你们这群小学生我就不在6年级混了。

废话不多说,所谓爬虫天坑,敢对得起这个名字的一定不能是等闲之辈。起码得是过完年老板给你扔这个任务,你儿童节还在头大的级别。今天第一课,咱们就先找个最难的热热身吧:爬取百度指数的关键词搜索指数


先贴一个logo让大家跪拜一下


好了,大家平身吧,咱们马上就正式开始了,想上厕所的赶紧去,不然看完这篇文章估计你就忘了怎么上厕所了。

正式开始之前,先插个广告:如果土豪朋友不想写代码或者中途看不下去的,我们将以下代码已经打包成一个完整的应用,大家进入神箭手的云市场搜索百度指数(http://www.shenjianshou.cn/index.php?r=market/product&product_id=500036)就可以看到应用,直接调用既可。


——————————–前方高能预警看也看不完上厕所赶紧去分割线————————————-
咱们正式开始:所谓知己知彼百战不殆,我们要先了解一下我们的对手。咱们打开百度指数
http://index.baidu.com,映入眼帘的是一个简单的输入框。好开心啊,好像不用登录啊,输入一个关键字试一下吧,输入神箭手,回车:


果然百度老司机不会让我们那么开心的。没事没事,不就是登录吗,也不是没做过登录,抓包研究下请求应该不难。我们先找一个账号登录看下。登录之后继续输入神箭手:


出来了。哈哈,不难嘛,这不就直接显示了。然后就按照以前的爬虫的教程,用XPATH获取一下数字就可以了,哈哈哈…哈哈..哈……..

慢着,怎么感觉这个数字看着怪怪。吓得我赶紧掀开被子看看这货到底是啥:


什么?这是图!!!!什么?这还是拼图!!!!什么?这货居然是异步的拼图!!!!


怎么样,感受到天坑的深度没有?

那咱们就一起来看看怎么见招拆招,用神箭手把百度指数搞定的吧。

开始具体的代码之前,我们先在神箭手后台新建三个应用,分别是百度指数API,百度登录爬虫,百度指数图片识别AI。


第一章 登录应用


第一节:咱先搞定登录
模拟登录一直是爬虫的一个老大难问题,虽然我们神箭手提供了智能登录接口login函数,但是遇上复杂一些的登录依然无能为力。当然你可以登录后复制本机Cookie直接用,但这种雕虫小技百度想封你真得比捏死一只蚂蚁还简单。咱们要有不怕苦,迎难而上的精神,死磕登录!算了~还是先去搜一下有没有别人写过。不搜不知道,一搜吓一跳啊。咱就随便找个源码借鉴借鉴。乔布斯老人家说过嘛,greate artist steal。

这个不错,逻辑清晰,代码干净,万能的github果然不辜负我的重望。我们steal到神箭手平台上来。


首先我们理清这个流程,根据这个代码我们知道百度的登录流程是这样的:
1.通过请求百度首页或者任意一个百度url获得百度的基础cookie。
3.通过 https://passport.baidu.com/v2/getpublickey 获得密码加密的key
4.通过 https://passport.baidu.com/v2/api/?login 将之前获得到的token,生成的gid,生成的时间戳,用key加密密码,来提交登录。
5.如果返回有验证码,获取codestring并请求 https://passport.baidu.com/cgi-bin/genimage 获得验证码图片并识别。
6.通过 https://passport.baidu.com/v2/?checkvcode 来验证是否识别成功
7.如果不成功通过 https://passport.baidu.com/v2/?reggetcodestr 来切换验证码,在重复前两步。
8.再次提交 https://passport.baidu.com/v2/api/?login 看是否登录成功。

好了,这中间很麻烦的两个地方是


1. 验证码识别 这个神箭手提供了验证码识别的函数,调用方式如下:

var codeUrl = "https://passport.baidu.com/cgi-bin/genimage?" + codeString;
var codeReg = getCaptcha(71, codeUrl);
var imgCaptchaData = JSON.parse(codeReg);
if (imgCaptchaData && imgCaptchaData.ret > 0) {
    var result = imgCaptchaData.result;
    verifycode = encodeURI(result,"UTF-8");
    tt = (new Date()).getTime();
    var codeCheck = site.requestUrl("https://passport.baidu.com/v2/?checkvcode&token=" + token + "&tpl=mn&apiver=v3&tt=" + tt + "&verifycode=" + verifycode + "&codestring=" + codeString + "&callback=");
    var checkInfo = JSON.parse(codeCheck);
    if (checkInfo.errInfo.no != "0") {
        console.log("验证码识别错误");    
    }
    continue;
} else {
    console.log("验证码识别失败");
    continue;
}


2. RSA加密

当我们获取到key之后需要对密码进行RSA加密,百度是采用的JS的开源RSA加密库,神箭手也提供了RSAEncode的方法,具体代码如下:

var pubkeyJson = site.requestUrl("https://passport.baidu.com/v2/getpublickey?token=" + token + "&tpl=mn&apiver=v3&tt=" + tt + "&gid=" + gid + "&callback=");
var pubkeyInfo = JSON.parse(pubkeyJson.replace(/'/g, "\""));
var pubkey = pubkeyInfo.pubkey;
var rsakey = pubkeyInfo.key;
var crypttype = "";
var rsaPassword="";
if (rsakey != "") {
    crypttype = "12";
    //加密密码
    pubkey = pubkey.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").trim();
    rsaPassword = (RSAEncode(password, pubkey));
}

其他的都是一些基础的请求,大家可以参考github中的代码进行编写。


第二节:疯狂登录

完成了第一节的工作之后,你以为登录就没问题了吗?你以为你可以用一个帐号爬到天荒地老吗?有人说限制爬取频率,这当然是一个方法,但却不是最好的解决方案。毕竟缩手缩脚,感觉很受限。最好的方案当然是登录一堆帐号,获取一堆的Cookie,然后从这堆Cookie中每次随机取一个Cookie,再通过这个Cookie去访问。那我们就需要一个新的东西:Cookie池。
我们看下神箭手如何调用Cookie池,首先我们需要新建一个爬虫应用专门用于登录:

var configs = {
    shareUserWithKey:"__bindex__",
};
configs.onUserAdded = function (use, psw, site) {
   var loginResult = login(use, psw, site);
    if( loginResult !="success") {   
      return false;
    }
    return true;
}

这里的login方法就是刚刚我们写的百度登录,然后我们再在beforeCrawl的回调函数中反复调用以下方法:

site.addUser(user, password);

当然这里还有一个问题,如果我们一直使用一个IP来登录,也很容易被百度封掉,所以我们最好打开企业代理IP接入。


通过这种形式我们就可以建立一个可共享的Cookie池。然后我们在百度指数API应用(下一章会详细介绍)里通过设置以下代码来共享这个Cookie池:

var configs = {
    shareUserWithKey:"__bindex__",
    multiUser: true
}

这样在这个应用中会在每次访问一个Url的周期中随机从Cookie池取一个Cookie并请求Url。通过这种形式我们还可以把登录和请求代码解耦合。将来还可以复用登录代码。


第三节:问题来了,帐号从哪来呢?

除了把七大姑八大姨的手机都来注册一遍以外,没什么好办法,除非…(此处省略1000个字)。


第二章:获取指数图片API


第一节:异步请求数据
终于完成了登录,感觉怎么样,是不是有点天坑的意思?哈哈,万里长征咱才走了第一步。下面我们才真正来揭开天坑的核心:数字图片。
然后我们继续掀被子看看这个标签是怎么来的:


貌似不难找,不过看这个URL看着就头大,感觉已经被百度登录伤害过一次之后真的无力再一个一个参数分析,我们直接使用神箭手提供的js渲染页面的接口,直接把页面渲染出来把:

var configs = {
    domains: ["index.baidu.com"],
    scanUrls: ["http://index.baidu.com/?tpl=trend&word=" + encodeURI(keyword, "GBK")],
    enableJS:true
}

这样我们在afterDownloadPage中拿到的就直接是渲染好的页面了,我们再通过正则和XPath取出数字图片的容器标签代码和Css代码(Css代码就是把图片设置成背景的style标签),之所以要拿Style标签是因为两个数字图片共享了一个Style,而这个Style在第一个数字图片的标签中,所以我们必须抽取出这个Style标签,在分别设置给两个不同的数字的容器标签代码。这段代码咱们再下一节中给出。


第二节:渲染数据成图片

我们拿到了数字图片的容器标签代码有什么用呢,当然是要渲染出对应的图片了。那为什么我们要这么大的弯去得到这张图呢。这一点正是百度指数能当选天坑的原因了,我们看一下这个图片是如何拼出来的,我们看下这段HTML代码。

<span class="imgval" style="width:6px;">
<div class="imgtxt" style="margin-left:-8px;"></div>
</span>
<span class="imgval" style="width:18px;">
<div class="imgtxt" style="margin-left:-62px;"></div>
</span>

我们可以看到这里有两个imgval标签,imgval是用来当蒙版的,可以从一张背景图中抠出需要的部分,而imgtxt则是显示图片的,这里又有一个margin-left用来具体调整整张图要显示的位置。

而最最最变态的是,两个标签并不是两个数字,而是三个数字!这就说明我们不可能一个一个数字去识别,必须作为一个整体图片来识别了。

我们知道PhantomJS这类Headless的浏览器都有渲染Html代码成图片的功能,神箭手渲染JS基于PhantomJS当然也支持这个功能,而且我们的调用接口更简单只需要调用site.renderImage方法既可实现将代码渲染成图片的功能,

下面是结合第一节的完整代码如下:

var sevenReg = /class="mtable profWagv">(.+?)<\/table>/;
var sevenMatch = sevenReg.exec(indexInfo); 
if(sevenMatch) {
    var sevenInfo = extractList(sevenMatch[1],"//*[@class='ftlwhf enc2imgVal']");
    var styleReg = /(<style>.+?<\/style>)/;
    var styleMatch = styleReg.exec(sevenMatch[1]);
    var sevenHtml = '<html><head><base href="http://index.baidu.com/" /></head><body style="background:#fff;"><style>.imgval .imgtxt {display: block;width: 19200px;height: 25px;margin-top: -2px;} .imgval {display: inline-block;height: 12px;margin-bottom: -2px;overflow: hidden;}</style><div class="profWagv">' 
    if(styleMatch) {
        var html = sevenHtml + sevenInfo[0].substring(0,sevenInfo[0].length-7) +styleMatch[1] + "</div></body></html>";
        var base64 = site.renderImage(html, 120, 25);
                                  index.week_all_index = base64ToImg(base64,site);
        var html2 = sevenHtml + sevenInfo[1].substring(0,sevenInfo[1].length-7) +styleMatch[1] + "</div></body></html>";
        var base641 = site.renderImage(html2, 120, 25);
        index.week_mobile_index = base64ToImg(base641,site);
    }
} else {
   system.exit("关键字不存在");
}

这里只展示一下对于7天指数的渲染,30天的几乎一样。可以看到代码中添加了一些定义的Css这是为了渲染这段代码时能够补上网页中本来相关的Css,而不至于显示不正常,我们看一下渲染出来的图片的base64。

iVBORw0KGgoAAAANSUhEUgAAAHgAAAAaCAYAAAB8WJiDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAxklEQVRoge3aWwqDMBBG4Vq6vcxis8HxqVAtmYxtNPhzvjevFA5JVLq4uz8g6zn7B+BcBBZHYHEEFkdgcQQWR2BxBBb3yp5oZpvtWmvznOhYdD1O4AmllO6+z+3oWLQP4w2Zos1sMyJrrV8jFnOkAjOd3ld6DX6L1tkWRvQ8hwLvp+Ij9tcR/BrpNfifuJgnFZi4N5Z51M680kSvSb/cD2Ms7v1/dLTWy9a62vvQwWxwnVRg3BffosURWByBxRFYHIHFEVjcCuhz388wTpcGAAAAAElFTkSuQmCC

把base64转换成图片之后如下图,没毛病。


第三章:识别指数图片


第一节:如何识别?

显然这张渲染出来的图片并不比验证码更复杂,那么干脆我们用验证码识别吧。不过我要提醒同学们,神箭手的验证码识别是要收费的哦。当然啦,土豪无所谓。

对于非土豪的同学继续往下看,这张图片看着并不复杂,甚至简单到我们可以切割后直接对比文件数据的方式来去判断图片,当然这太low了,而且代码写起来其实也不太轻松。有什么其他更有逼格的手段呢?

神箭手作为一个重视逼格胜过重视功能的平台,当然不会只给你这个方案。现在是祭出神箭手最新黑科技-TensorFlow的时候了。不过话说回来了,用TensorFlow来识别这张图片,有点大炮打蚊子的意思。不过本着只选难的,不选烦的工程师本性。

我宁愿写TensorFlow,也不想写像素点对比。当然最重要的是因为我们自己在研发的验证码识别的代码可以直接拿来改改用,那今天就拿出来跟大家共享一下。


第二节:编写机器学习代码

怎么样,感受到天坑牛逼闪闪的光辉了吗?是不是觉得开始看这课的时候是小学生,现在马上就要高中毕业了?

我们打开百度指数图片识别AI这个应用,之前接触过神箭手的同学们肯定对神箭手云爬虫应用不陌生,但是对于这个神箭手AI应用(也就是神箭手封装的TensorFlow)肯定是一头雾水。

首先与神箭手爬虫代码不一样,TensorFlow是Python的(来,咱们再学一个新语言)。但类似于神箭手爬虫,我们依然可以通过自定义输入项来定义输入参数(请求的时候,需要定义一个输入项来接受图片的base64编码)。

后面我们会特别开神箭手TensorFlow的课程(你们猜对了,老师还在学),这一次咱们先直接上代码:

content="" #@input(content,请求数据)
from PIL import Image
import tensorflow as tf
import numpy as np
import random
import shenjian as sj
import base64
from io import BytesIO
import re
import json
import urllib.request as client

number = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
            't', 'u', 'v', 'w', 'x', 'y', 'z']
ALPHABET = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
            'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
PADING = [' ']


def convert2gray(img):
    if len(img.shape) > 2:
        gray = np.mean(img, -1)
        return gray
    else:
        return img


def load_data(file_name):
    file = open(file_name)
    content = file.read()
    content = content.replace("\n","")
    imgs = re.findall(r'content.*?\}', content)
    result = []
    for img in imgs:
        img_x = re.findall(r'content:.*?result:', img)
        img_y = re.findall(r'result:.*?\}', img)
        img_x = img_x[0].replace("content:", "").replace("result:", "")
        img_y = json.loads(img_y[0].replace("result:", ""))
        if 'result' in img_y:
            if len(img_y['result']) == 4 and re.search(r'[0-9|a-z|A-Z]+$', img_y['result']):
                result.append((img_x, img_y['result']))
    return result

url = 'http://demo.shenjianshou.cn/tensor/baidu/'


def request():
    content = client.urlopen(url=url).read()
    content = content.decode('utf-8')
    data = json.loads(content)
    data['value'] = str(data['value'])
    size = len(data['value'])
    for i in range(size, 10):
        data['value'] += ' '
    return data['content'], data['value']


class Model(object):

    def __init__(self, text_set=number+PADING, captcha_size=10, width=120, height=26):
        self.text_set = text_set
        self.captcha_size = captcha_size
        self.width = width
        self.height = height
        self.captcha_len = len(text_set)
        self.X = tf.placeholder(tf.float32, [None, self.width*self.height])
        self.Y = tf.placeholder(tf.float32, [None, self.captcha_size*self.captcha_len])
        self.keep_prob = tf.placeholder(tf.float32)
        self.x = tf.reshape(self.X, shape=[-1, self.height, self.width, 1])

        self.w_alpha = 0.01
        self.b_alpha = 0.1
        #定义三层卷积层

        self.w_c1 = tf.Variable(self.w_alpha * tf.random_normal([3, 3, 1, 32]))
        self.b_c1 = tf.Variable(self.b_alpha * tf.random_normal([32]))
        self.conv1_a = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(self.x, self.w_c1, strides=[1, 1, 1, 1], padding='SAME'), self.b_c1))
        self.conv1_b = tf.nn.max_pool(self.conv1_a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
        self.conv1 = tf.nn.dropout(self.conv1_b, self.keep_prob)

        self.w_c2 = tf.Variable(self.w_alpha*tf.random_normal([3, 3, 32, 64]))
        self.b_c2 = tf.Variable(self.b_alpha*tf.random_normal([64]))
        self.conv2_a = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(self.conv1, self.w_c2, strides=[1, 1, 1, 1], padding='SAME'), self.b_c2))
        self.conv2_b = tf.nn.max_pool(self.conv2_a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
        self.conv2 = tf.nn.dropout(self.conv2_b, self.keep_prob)

        self.w_c3 = tf.Variable(self.w_alpha*tf.random_normal([3, 3, 64, 64]))
        self.b_c3 = tf.Variable(self.b_alpha*tf.random_normal([64]))
        self.conv3_a = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(self.conv2, self.w_c3, strides=[1, 1, 1, 1], padding='SAME'), self.b_c3))
        self.conv3_b = tf.nn.max_pool(self.conv3_a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
        self.conv3 = tf.nn.dropout(self.conv3_b, self.keep_prob)

        #全连接层

        self.w_d = tf.Variable(self.w_alpha*tf.random_normal([15*4*64, 1024]))
        self.b_d = tf.Variable(self.b_alpha*tf.random_normal([1024]))
        self.dense = tf.reshape(self.conv3, [-1, self.w_d.get_shape().as_list()[0]])
        self.dense = tf.nn.relu(tf.add(tf.matmul(self.dense, self.w_d), self.b_d))
        self.dense = tf.nn.dropout(self.dense, self.keep_prob)

        self.w_out = tf.Variable(self.w_alpha*tf.random_normal([1024, self.captcha_size*self.captcha_len]))
        self.b_out = tf.Variable(self.b_alpha*tf.random_normal([self.captcha_size*self.captcha_len]))
        self.out = tf.add(tf.matmul(self.dense, self.w_out), self.b_out)

        self.loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=self.Y, logits=self.out))
        self.optimizer = tf.train.AdamOptimizer(learning_rate=0.001).minimize(self.loss)
        self.predict = tf.reshape(self.out, [-1, self.captcha_size, self.captcha_len])
        self.max_idx_p = tf.argmax(self.predict, 2)
        self.max_idx_l = tf.argmax(tf.reshape(self.Y, [-1, self.captcha_size, self.captcha_len]), 2)
        self.correct_pred = tf.equal(self.max_idx_p, self.max_idx_l)
        self.accuracy = tf.reduce_mean(tf.cast(self.correct_pred, tf.float32))
        #self.data = load_data("data")
        #self.test = load_data("test")
        self.data_index = 0
        self.test_index = 0
        self.session = sj.Session(auto_init=True)


    def random_captcha_text(self):
        captcha_text = []
        for i in range(self.captcha_size):
            c = random.choice(self.text_set)
            captcha_text.append(c)
        return captcha_text

    def gen_captcha_text_and_image(self):
        img_x, img_y = request()
        imgdata = base64.b64decode(img_x)
        img_x = Image.open(BytesIO(imgdata))
        if img_x.width != self.width or img_x.height != self.height:
            img_x = img_x.resize((self.width, self.height), Image.ANTIALIAS)
        img_x = img_x.point(lambda x: 255 if x > 125 else 0).convert('RGB')
        img_x = np.array(img_x)
        self.data_index += 1
        return img_y, img_x

    def gen_captcha_text_and_image_test(self):
        img_x, img_y = request()
        imgdata = base64.b64decode(img_x)
        img_x = Image.open(BytesIO(imgdata))
        if img_x.width != self.width or img_x.height != self.height:
            img_x = img_x.resize((self.width, self.height), Image.ANTIALIAS)
        img_x = img_x.point(lambda x: 255 if x > 125 else 0).convert('RGB')
        img_x = np.array(img_x)
        self.test_index += 1
        return img_y, img_x

    def text2vec(self, text):
        text_len = len(text)
        if text_len != self.captcha_size:
            raise ValueError("验证码长度不匹配")
        vector = np.zeros(self.captcha_len * self.captcha_size)

        def char2pos(c):
            if c == ' ':
                k = 10
                return k
            k = ord(c)-48
            if k > 9:
                k = ord(c)-55
                if k > 35:
                    k = ord(c) - 61
                    if k > 61:
                        raise ValueError('No Map '+c)
            return k
        for i, c in enumerate(text):
            idx = i*self.captcha_len+char2pos(c)
            vector[idx] = 1
        return vector

    def vec2text(self, vec):
        char_pos = vec.nonzero()[0]
        text = []
        for i, c in enumerate(char_pos):
            char_idx = c % self.captcha_len
            if char_idx < 10:
                char_code = char_idx + ord('0')
            elif char_idx == 10:
                char_code = ord(' ')
            elif char_idx < 36:
                char_code = char_idx - 10 + ord('A')
            elif char_idx < 62:
                char_code = char_idx - 36 + ord('a')
            elif char_idx == 62:
                char_code = ord('_')
            else:
                raise ValueError('error')
            text.append(chr(char_code))
        return "".join(text)

    def get_next_batch(self, batch_size=128):
        batch_x = np.zeros([batch_size, self.width*self.height])
        batch_y = np.zeros([batch_size, self.captcha_len * self.captcha_size])
        for i in range(batch_size):
            text, image = self.gen_captcha_text_and_image()
            image = convert2gray(image)
            batch_x[i, :] = image.flatten() / 255
            batch_y[i, :] = self.text2vec(text)
        return batch_x, batch_y

    def get_next_batch_test(self, batch_size=128):
        batch_x = np.zeros([batch_size, self.width*self.height])
        batch_y = np.zeros([batch_size, self.captcha_len * self.captcha_size])
        for i in range(batch_size):
            text, image = self.gen_captcha_text_and_image_test()
            image = convert2gray(image)
            batch_x[i, :] = image.flatten() / 255
            batch_y[i, :] = self.text2vec(text)
        return batch_x, batch_y

    def train(self):
        step = 0
        for i in range(10000):
            batch_x, batch_y = self.get_next_batch_test(64)
            _, loss_ = self.session.run([self.optimizer, self.loss], feed_dict={self.X: batch_x, self.Y: batch_y, self.keep_prob: 0.75})
            step += 1
            print(step, loss_)
            if step % 10 == 0:
                batch_x_test, batch_y_test = self.get_next_batch(100)
                acc = self.session.run(self.accuracy, feed_dict={self.X: batch_x_test, self.Y: batch_y_test, self.keep_prob: 1.0})
                print(acc)

    def serve(self):
        img_data = base64.b64decode(content)
        img_x = Image.open(BytesIO(img_data))
        if img_x.width != self.width or img_x.height != self.height:
            img_x = img_x.resize((self.width, self.height), Image.ANTIALIAS)
        img_x = img_x.point(lambda x: 255 if x > 125 else 0).convert('RGB')
        img_x = np.array(img_x)
        img_x = convert2gray(img_x)
        img_x = img_x.flatten() / 255
        predict = tf.argmax(tf.reshape(self.out, [-1, self.captcha_size, self.captcha_len]), 2)
        text_list = self.session.run(predict, feed_dict={self.X: [img_x], self.keep_prob: 1})
        text = text_list[0].tolist()
        vector = np.zeros(self.captcha_size*self.captcha_len)
        print(text)
        i = 0
        for n in text:
            vector[i*self.captcha_len + n] = 1
            i += 1
        result = self.vec2text(vector)
        print(result)
        return str(result)
m = Model()
sj.run(m.train, m.serve)

简单说一下这段代码,这段代码是我们从验证码识别的代码中临时改过来的,有很多的不必要的代码,比如大小写字符这里是不需要的,我们暂且忽略。

思路是一致,咱们通过一个三层神经网络,输入一张图和一个One-hot Encoding之后的识别结果,因为我们只需要识别数字(直接忽略逗号),因此这段代码相对比较简单。像是百度登录的验证码会出现中文的情况就会复杂不少。

这里值得注意的是和标准TensorFlow代码不一样的部分只有一个

sj.run(m.train, m.serve)

这个方法两个参数分别对应训练函数和服务函数,训练完成之后,神箭手会自动生成一个服务接口来调用这个模型。我们将这段代码贴进我们在神箭手后台新建的百度指数AI应用中,点击保存。


第三节:生成训练样本

有没有听过这样一句话:人工智能就是有多少人工就有多少智能。特别是在深度学习中,虽然我们的训练代码可以逐步做到通用,但我们需要大量的数据来训练才能让训练模型达到更高准确度。

那问题又来了,数据怎么来呢? 难不成我们自己去截图标注吗,那岂不是要累死自己的节奏吗?作为善于偷懒的程序员,我们当然不能这么干,因为百度指数的数字很标准,并没有什么变化,我们把10个数字+逗号截图出来,动态生成图片以及对应的真实数字既可,我们看一下生成样本的PHP代码:

<?php

class Verify
{
    protected $config = array(
        'expire'   => 1800, // 验证码过期时间(s)
        'useImgBg' => false, // 使用背景图片
        'fontSize' => 25, // 验证码字体大小(px)
        'imageH'   => 160, // 验证码图片高度
        'imageW'   => 60, // 验证码图片宽度
        'length'   => 5, // 验证码位数
        'fontttf'  => '', // 验证码字体,不设置随机获取
        'bg'       => array(255, 255, 255), // 背景颜色
        'reset'    => true, // 验证成功后是否重置
    );
    private $imageW = 120;
    private $imageH = 26;
    private $_image = null; // 验证码图片实例

    /**
     * 架构方法 设置参数
     * @access public
     * @param  array $config 配置参数
     */
    public function __construct($config = array())
    {
        $this->config = array_merge($this->config, $config);
    }

    /**
     * 使用 $this->name 获取配置
     * @access public
     * @param  string $name 配置名称
     * @return multitype    配置值
     */
    public function __get($name)
    {
        return $this->config[$name];
    }

    /**
     * 设置验证码配置
     * @access public
     * @param  string $name 配置名称
     * @param  string $value 配置值
     * @return void
     */
    public function __set($name, $value)
    {
        if (isset($this->config[$name])) {
            $this->config[$name] = $value;
        }
    }

    /**
     * 检查配置
     * @access public
     * @param  string $name 配置名称
     * @return bool
     */
    public function __isset($name)
    {
        return isset($this->config[$name]);
    }


    /**
     * 输出验证码并把验证码的值保存的session中
     * 验证码保存到session的格式为: array('verify_code' => '验证码值', 'verify_time' => '验证码创建时间');
     * @access public
     * @return string
     */
    public function entry()
    {
        // 建立一幅 $this->imageW x $this->imageH 的图像
        $this->_image = imagecreatetruecolor($this->imageW, $this->imageH);
        // 设置背景
        //imagecolorallocate($this->_image, $this->bg[0], $this->bg[1], $this->bg[2]);
        $background_color=imagecolorallocate($this->_image,255,255,255);
        imagefill($this->_image,0,0,$background_color);
        $digits_number = rand(1,9);
        $max = pow(10, $digits_number);
        $min = $max/10;
        $max = $max-1;
        $number = rand($min, $max);
        //$number = 10415;
        $code = number_format($number); // 验证码
        //$code = "$";
        $len = strlen($code);
        for($i=0;$i<$len;$i++){
                $char = $code[$i];
                if($char == ","){
                        $char = "comma";
                }else if($char == "$"){
                        $image = imagecreatefrompng("template.png");
                        imagecopy($this->_image,$image,0,0,0,0,120,26);
                        continue;
                }
                $image = imagecreatefrompng($char.".png");
                imagecopy($this->_image,$image,$i*8+8,8,0,0,8,12);
        }
        imagepng($this->_image,'./code.png');
        $img = array(
            'content'  => base64_encode(file_get_contents('./code.png')),
            'value' => $number
        );
        unlink('./code.png');
        echo json_encode($img);exit;
    }
}

$code = new Verify();
$code->entry();
?>

你们猜对了,这又是一个临时改过来的代码,有大量的无用代码(还有一些测试代码..)。

不过没关系,咱们就是要训练一个能用的模型,不需要在意这些细节。中间那几张图片是我事先截图的,不过因为这个Url我们线上直接公布了,所以其实大家不用写了。


第四节:训练样本生成服务API

好了,可以看到在第二节贴的训练的代码中我们已经使用了生成样本的URL,我们点击右上角的训练按钮。

通过不断训练新生成的样本,再加上百度指数图片本身的识别难度实在太低,我们很快就可以得到我们想要的准确率。训练完成之后,神箭手会自动存储这个模型并生成一个服务的接口来供我们调用这个模型:



最终章:完成百度指数API

我们返回第二章创建的百度指数API中,因为renderImage方法返回的是图片的base64字符串,而TensorFlow中我们也是读取的base64字符串,

因此我们直接调用我们TensorFlow的应用生成的服务接口,实现最终指数图片的识别:

var base64ToImg = function (base64,site) {
    console.log("base64:"+base64)
   var tryTimes = 3;
    var result;
    var data;
    while(tryTimes-- > 0) {
      data = site.requestUrl("http://ai.shenjianshou.cn/?appid=<你自己的TensorFlow应用id>&content="+ encodeURI(base64),{noCookies:true,noUser:true});
       if(data) {
          data = JSON.parse(data);
           if(data && data.error_code == 0) {
               return data.data;
           }
       }         
    }
    system.exit("调用失败");
}

注意这段代码不能直接用的,因为中间我把appid改成了占位符,大家复制自己的应用id贴进去既可。

好了,小同志快醒醒,延安就要到了!


我们最后测试一下我们的天坑解决方案:


结语

感谢大家耐心看完天坑系列教程第一课,能看到这里的绝对都是真爱。所以就在啰嗦两句:首先由于本人水平所限,教程中难免可能有一些粗陋的地方,如有bug,欢迎指正。

填这个坑我们用到了验证码识别,RSA加密,Cookie池,JS渲染,HTML图片渲染,Php生成图片,TensorFlow训练。

需要用到神箭手三种不同类型的应用相互协同。教程中的语言包括Js,Python,Php,Html,Css。大家如果中间哪里有不懂的地方,欢迎自行百度~