nodejs实现简单的自动约车

最近约车真是越来越难了,网上约车经常车位刚放出来便已空空如也。突然回想起之前学车时教练反复提到的约车软件,去淘宝上一查:我去,卖出去一千多份了!还能约到车那就是有鬼了……此刻我深深怀疑这个软件是他们自家开发的,贵圈水真深。然而作为一名程序猿的尊严是不允许我去买这软件的……于是花了一天捣鼓出来一个极其简陋的约车系统,虽然因为官方网站对这方面的限制很多,效果并不是很好,不过试用了一下淘宝的爆款约车软件基本确定原理相同,那么就满足了吧……(挽尊可矣)

软件使用nodejs实现,理由一个字,简单,方便。在此记录下一些思路。

验证码识别

首先,要实现自动约车,验证码是第一个需要突破的关卡。这里我用了google著名的图像识别库tesseract-ocr,基本可以对一些简单的纯字母组成的验证码完成识别。对一些有不同颜色的验证码,再辅以graphicsmagick库做一些图像灰度化处理之后再识别,能提高一些准确率。可用npm下载nodejs的tesseract和graphicsmagick库,前提是计算机内得预先安装这两款软件。安装过程请参照:

tesseract: https://www.npmjs.com/package/node-tesseract

graphicsmagick: https://www.npmjs.com/package/gm

第一步,我们需要访问主页面得到验证码。一般网站验证码会存于session之中,因此我们需要通过response中的set-cookie字段来获取该次请求的session id,并存入之后每次请求request头携带的Cookie中,这样服务器才能将脚本发出的多个请求归入同一次会话,保存登录状态。代码如下:

// Refresh the validcode and then download it.
function downloadValidCode() {
    var deferred = Q.defer();
    if (!VALIDIMG_SOURCE) {
        deferred.reject('null resource');
    }
    var file = fs.createWriteStream(DOWNLOAD_DIR + FILE_NAME); 
    var options = {
        hostname: HOST,
        port: 80,
        path: '/' + VALIDIMG_SOURCE,
        headers: {
            'Cookie': COOKIE
        }
    };

    http.get(options, function(res) {  
        res.on('data', function(data) { 
             file.write(data);  
        })
        .on('end', function() {
            file.end();  

            if (res.headers['set-cookie']) {
                COOKIE = res.headers['set-cookie'] + ';';
            }
            deferred.resolve(); 
        })
        .on('error', function(e) {
            deferred.reject(e);
        });  

    });  

    return deferred.promise;
}    

fs获取文件流并将验证码存入下载目录供之后图像处理函数调用。这里还使用了nodejs中的q实现了一个promise的API,方便之后程序主循环实现调用链,将在下文提到。

function recognizeValidCode() {
    var deferred = Q.defer();
    log('recognizing');
    gm(DOWNLOAD_DIR + FILE_NAME).colorspace('GRAY').write(PROCESS_DIR + FILE_NAME, function(err) {
        if (err) {
            deferred.reject(err);
        }
        // Recognize text of any language in any format
        tesseract.process(PROCESS_DIR + FILE_NAME,function(err, text) {
            if (err) {
                deferred.reject(err);
            } else {
                log('validcode is: ' + text);
                code = text.trim();
                deferred.resolve();
            }
        });
    });

    return deferred.promise;
}

以上代码是对验证码的图像处理,看起来就很明了了——首先利用gm对图像作灰度化处理,然后调用tesseract识别出文字,最后去掉前后可能会产生的空白符。这里存在一些问题:约车官网的验证码中有一些噪点,时常会干扰识别的准确性。解决这个问题的方法并不是没有,需要动用一些图像去噪的算法,考虑到无视之的情况下正确率也有约25%,因此出于成本和效率的综合考量,决定直接采用暴力破解(其实是懒)。

得到验证码之后,将用户名、密码、验证码和Cookie以表单形式发送到login接口,大功告成。当然,以上方式仅限于简单验证码,对于12306之类上升到图灵测试层次的验证码目前还并没有思考解决方案……

预约

登录搞定之后,预约就是体力活了。通过抓包和对它的js代码分析出预约操作的各个参数,然后模拟格式无限发请求。需要注意的是,官网对发请求的频率有严格限制,因此一般设个几分钟的间隔,不然就成DDoS了……

调用链

nodejs的“回调地狱”应该是它的一个比较著名的现象了,这是由于它事件驱动以及异步编程的特性所致。大致说来,就是下图这种效果:

step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});

解决该问题的方案是使用promise。实现了promise的函数会将回调结果传入链条中下一个方法中处理。我在约车软件的主循环中需要这样一个逻辑:刷新验证码 --> 下载验证码并识别 --> 登录 --> 预约循环 --> 若session过期,重新刷新验证码登录过程。

在这条调用链中存在很多条件判断和异常处理,要是每个方法都做一次显然会令代码显得十分臃肿且不便调试。nodejs中,使用q来实现promise。实现过程如下:

在一个函数中,首先声明defer:

var deferred = Q.defer();

然后,若结果成功,则调用resolve方法,链条中下一个节点的第一个回调函数便会处理成功回调;反之,若失败则调用reject方法,下一节点的第二个回调函数将会处理失败回调。

if (content.indexOf('true') !== -1) {
    deferred.resolve();
} else if (content.indexOf('验证码不符合!') != -1) {
    deferred.reject('wrong validcode');
} else {
    deferred.reject('unknown error');
}

最后,函数返回一个promise的API,以便能够实现链式调用。

return deferred.promise;

一个登录、预约循环便如下所示:

function login(startTime, endTime, date) {
    downloadValidCode()
        .then(recognizeValidCode)
        .then(requestLogin)
        .then(function() {
            log('Login successfully!');
            reserve(startTime, endTime, date);
        }, function(e) {
            log(e);
            // If login failed, then login again.
            setTimeout(function() {
                login(startTime, endTime, date);
            }, parseInt(INTERVAL) * 1000);
        });
}

存在问题

上述的一些方案能解决基本的需求,但还存在很多问题。比如官方网站对访问频率有很严格的限制,若在尝试登录时脸黑六次以上都没有识别出正确的验证码,那么极有可能ip会被屏蔽两小时。我还不太清楚nodejs的http客户端如何像C#的http client一样自由设置代理ip,因此目前暂时以手动切换ip来解决这个问题……如果以后有了新的思路再来解决这个问题好了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Albert陈凯

理解zookeeper选举机制

zookeeper集群 配置多个实例共同构成一个集群对外提供服务以达到水平扩展的目的,每个服务器上的数据是相同的,每一个服务器均可以对外提供读和写的服务,这点...

96350
来自专栏测试开发架构之路

Android软件测试Monkey测试工具

前言: 最近开始研究Android自动化测试方法,对其中的一些工具、方法和框架做了一些简单的整理,其中包括android测试框架、CTS、Monkey、Monk...

1.3K110
来自专栏信安之路

我是如何通关信安之路巅峰挑战赛的

我滴个天啊。。。。。弱密码随意登陆。。。。。不好意思,这题我真是非预期解法,具体的预期解法,之前信安之路文章已发,就不仔细讲述啦!

26460
来自专栏小樱的经验随笔

深入理解USB流量数据包的抓取与分析

在一次演练中,我们通过wireshark抓取了一个如下的数据包,我们如何对其进行分析?

48220
来自专栏coding

听说,撸代码,ide与vim更配哦vim折腾记vim常用命令

在选择编辑器上面,我是一个纠结的人,曾经年少的我执着地追求一款万能的编辑器,可以支持所有编辑语言,灵活可定制,可纯粹用键盘操作。符合这种条件的编辑器,非vim莫...

11920
来自专栏吴伟祥

生词篇-Shiro官网 的笔记_0114

xml  [ˌeks em ˈel ] abbr. Extensible Markup Language 可扩展标记语言

8010
来自专栏中国Android研究院

番外篇-Flutter初识三问

在Android中,您可以通过直接对view进行改变来更新视图。然而,在Flutter中Widget是不可变的,不会直接更新,而必须使用Widget的状态。

14130
来自专栏大数据架构师专家

运维安全之Tripwire

Tripwire是最为著名的unix下文件系统完整性检查的软件工具,这一软件采用的技术核心就是对每个要监控的文件产生一个数字签名,保留下来。

13510
来自专栏木子昭的博客

往"某度文库"上传资源之前,请先做好这些...想到一句好玩的话:知道是你干的,只是懒得抓你!如果你喜欢python,喜欢故事,请点赞或关注我!您的支持是对作者最大的鼓励!

先讲个相关的故事:匿名黑客的"复仇行动" 2010年12月10日,黑客组织匿名者发布了一条消息,解释了他们发起最近一次代号为”复仇行动”的攻击的大致动机(Pr...

402130
来自专栏美团技术团队

Android OOM案例分析

在Android(Java)开发中,基本都会遇到java.lang.OutOfMemoryError(本文简称OOM),这种错误解决起来相对于一般的Except...

53240

扫码关注云+社区

领取腾讯云代金券