起因

前些天帮朋友写了一点自动登录的脚本,需求简单来说就是,有一个网站需要每天登录,因为是国企,经常会有这种操作, 也不为什么,反正不登录可能会扣钱。没敢直接答应,因为模拟登录这种需求,主要看人机识别做得怎么样,之前工作中 遇到过一些爬虫和反爬、刷子和防刷的问题,实际上最后可以上升到攻防甚至是风控的层面,如果复杂起来可以非常复杂。 所以只说网站先发过来看一下。简单看了一下网页代码和验证码,发现验证码是比较简单的这种。

验证码

所以预计难点顶多就是识别一下验证码,因为有一点 Python 和机器学习的经验,所以应该不在话下。虽然最后脚本也就 一百来行,比较好的实现了。这篇文章主要记录一下整个开发的心路历程。

选择

第一个问题是,开发语言选 python 还是 nodejs,之前看过别人写的一些用 puppeteer 来模拟登录,效果很赞。但问题是我对 node 不是很熟,而且还想到后面可能会遇到验证码识别的问题,毕竟 python 在这方面还是有优势的, 而且 python 的 requests 库也很适合做模拟请求,于是这个问题的选择也是 python 了。

另外问题就是发布安装包做单机安装还是直接部署到我的服务器上,因为中间可能会涉及到拿别人的密码这种可能以后会 比较狗血的问题。(后来发现)网站的网页源码有了这样一段:

var pubKey = $("#pubKey").val();
var encrypt = new JSEncrypt();
encrypt.setPublicKey(pubKey);
password = encrypt.encrypt("thisispassword");

虽然他们的链接是 https 的,但是前端的同学还是做了一些加密处理,那这个问题就简单了,我也可以不接触他们的密码, 可以让他们做同样的 RSA 非对称加密的之后发过来。所以最后这个问题的答案就是部署到自己的服务器。

requests 模拟 ajax 请求

这属于最轻松愉快的部分了,因为就是很简单的模拟登录。遇到的问题就是刚开始的时候在 postman 里试的时候以为是简单的 post 或者 get 请求,没成功。看了一下网页源码如下(省略了一些):

LoginB.pwdLogin = function(){
    var login = $("#login").val();
    var password = $("#password").val();
    // ...加密
    password = encrypt.encrypt(password);
    $.ajax({
        url: "/auth/user/loginByLoginAndPassword",
        type: "post",
        dataType: "json",
        data: {
            "login": login,
            "password": password,
            "checkCode": checkCode1
        },
        success: function(data) {
            $.cookie("loginToken",data.loginToken,{expires:1, path:'/'});
            window.location.href=data.url;
        },
    });
};

需要发送的是 ajax 请求,所以用 Session 来解决,暂时先不考虑验证码识别的问题,人肉识别验证码输入就行了。

from requests import Session
session = Session()
pic_response = session.get(url=config.captcha_url)
im = Image.open(BytesIO(pic_response.content))
im.show()
code = input('Enter code: ')
response = session.post(
    url=config.login_url,
    data={
        "login": user_name,
        "password": password,
        "checkCode": code
    },
)
print(response.text)

还有遇到问题就是,密码怎么都不对,如第一部分所述,看了一下才知道是加了密发送的,首先用 python 的加密库试了一下,发现 还是会密码错误。所以索性直接在 Chrome 里跑了一下前面的 js,发送加密之后的密码。那这个问题也解决了。

返回了一个 loginToken 和 一个 URL,然后本来想直接设置 Session 的 Cookies,重新发一个带 Cookies 的请求就可以解决了。但是试了很久发现不行,也不知道是哪里出了问题,但基本上 so far so good。

自动登录实现

接上一部分的问题,当时觉得应该只是哪里出了点小问题,问题就出在 window.location.href=data.url 客户端跳转上, 因为如果是浏览器的话就自动跳转了,但是用 python 新开的 Session发带 Cookies 的请求就是404。在这里卡了很久。

甚至一度想放弃 requests,直接用 node 的 puppeteer。又看了一些资料发现有人写了一个叫 pyppeteer 的库,参考 puppeteer 的原理,直接调用 Chrome 的 dev-tools。就又用这个库试验了一下,本质上也是在执行 js,但确实 js 没怎么 写过,折腾了一番之后完全用 pyppeteer 的方案也作罢了。

然后又一想,其实需要的只是 Cookies 而已,只要有了 Cookies打开浏览器就可以直接登录上去,根本不用复杂的 js, 而之前用 requests 刚好是到这一步,可以两个方案组合一下,这样既能走通整个流程,又不用写太多不熟悉的 js。 在 Chrome 里观察了一下,发现只要把里面的 Cookies 拷贝出来,放到 pyppeteer里面去请求,就可以登录上去了。

这里还有一个小插曲就是关于 urlencode 的,返回的 loginToken 解析出来是字符串,直接放进去还是会报错,仔细观察了一下才 发现区别是做了编码。真是一个小问题也会导致无法往下。

组合起来,代码就如下了:

def get_login_token(user_name, password):
    session = Session()
    pic_response = session.get(url=config.captcha_url)
    im.show()
    code = input('Enter code: ')

    response = session.post(
        url=config.login_url,
        data={
            "login": user_name,
            "password": password,
            "checkCode": code
        },
    )

    print(response.text)
    json_data = json.loads(response.text)
    loginToken = json_data["loginToken"]
    return json_data

async def open_home(user_name, password):
    json_data = get_login_token(user_name, password)
    browser = await launch(headless=True, args=['--no-sandbox', '--disable-setuid-sandbox'])
    # context = await browser.createIncognitoBrowserContext()
    # Create a new page in a pristine context.
    # page = await context.newPage()
    page = await browser.newPage()
    await page.setCookie(
        {
            "name": "loginToken",
            "value": urllib.parse.quote(json_data["loginToken"]),
            "url": config.base_url,
            "path": "/",
        }
    )

    await page.goto(config.home_url)
    # await page.screenshot({"path": "home.png"})
    await browser.close()

验证码识别

最后一步就是加上验证码自动识别了,这一步我本来以为是最难的,会涉及到图像处理啊,以及一些机器学习的部分。然而当我发现 pytesseract,也就是 Google 家的 ocr 工具 tesseract-ocr 之后,感到的是真的太牛了,就是预计一项工作本来要花 很长的时间的时候,发现有现成的工具可以非常快的帮助到自己,这种感动真的很强烈。

谷人希后,发现图片进去,啥都识别不出来,尴尬了。

又查了一下资料,原来这个工具需要质量比较好的图片,一般的验证码都会做一些干扰之类的,所以接下来,就是去掉干扰,做一下二值化。 重新输入,可以识别了,虽然识别率不高。解决识别率的问题,无外乎对图片做近一步的处理,或者缩小结果的范围,但机器的好处在于还有第三种, 重复的成本低,多试几次,总有比较简单的,可以识别的验证码。

这里就不贴代码了,很多前人做了很多工作。

收尾和总结

最后收尾就是 nohup 后台,再加入一些定时和延时之类的。不再赘述了。总结和想法主要有以下这些吧:

  1. 从0到1的工作其实是最复杂的,tesseract 真的牛,虽然我只用了很小的一部分。
  2. 整个东西其实挺水的,核心的部分就是验证码识别,不过这大概就是所谓的“前人挖坑,后人灌水”吧。
  3. 因为想的是最快速解决问题,所以很多东西没有深入研究。
  4. 结合两种方法,这大概就是所谓的“深度不够,广度来凑”吧。
  5. 不过真的 Google + Stack Overflow + Ctrl C + Ctrl V 可以解决太多问题了。
  6. 但还是要做一些 hard work。

参考链接(未整理)