chrome插件(尖兵一号)实现自动刷新淘宝m站的cookie

1.背景

最近接手了爬虫的项目, 爬取对象是天猫和京东的价格相关的数据, 其中对于天猫的优惠券的爬取需要有已登录的cookie才能成功爬到数据。

之前对于这块的cookie都是我们手动用自己淘宝账号登录淘宝m站,并获取cookie存到服务端。并且cookie的最长有效时间是24小时,也就是说我们每天都要有人去手动操作一下。

问题:

  1. 每天都需要手动操作,费时费力,而且容易忘记容易出错。
  2. 作为一个程序员,每天都干同样的事又无法改变这种事是忍不了的。
  3. 经常半夜惊醒, 想起来cookie忘了设置, 搞得神经衰弱..

思路很简单: 用程序去模拟一个真实的登录操作

2.曾经尝试过的方案

核心是使用无头浏览器去操作

  1. selenium(Java API) + headless chrome
  2. puppeteer(Node API) + headless chrome

PS:无头浏览器最开始在voc中的声音专题项目中有用过,用的是phantomjs,不过这个东西的API太难用了,刚开始就没考虑。

以上方案最终都是以失败而告终。原因是最终淘宝都会弹出一个验证的组件让你去操作

比如下面这样的:

image

刚开始以为,这种验证码不就是"点一下"的事吗,找到这个dom元素,去点就好了。

结果发现找是找到了,点也点中了,却是怎么点都“验证失败”。

怀疑是JavaScript去做click跟用户真实点击可能不太一样,所以尝试手动去点了一下,结果还是“验证失败”。

然后就怀疑淘宝对于这种headless chrome是不是做了什么识别,并且是限制了这种浏览器的行为。

因为换成正常安装的chrome,都是可以正常登录,连验证组件都不会跳出来。

3.为什么用chrome插件

既然headless chrome都被淘宝风控了,那么之后的思路就变成了“能不能控制现有安装的chrome浏览器来做一些自动化操作”

这里我和小伙伴有几次都想到了“按键精灵”,但是后来放弃了。一方面是因为对按键精灵不熟悉,学习成本比较高,第二是按键精灵太依赖于呈现在眼前的画面,个人理解很容易被一些意外情况打断。

使用chrome插件的灵感其实是来源于我的小伙伴(这里感谢@俊杰)。

不过真正用了才发现好处还是比较多的,列了以下4点:

  1. 可以依附在现有的浏览器中,只跟浏览器有关系,平台无关
  2. 主要使用JavaScript来开发,学习成本低,并且有成熟的调试方案
  3. 通过js来控制页面,不需要页面必须是呈现在你面前(比如最小化浏览器也ok),运行更可靠
  4. chrome插件内置了很多底层的库,可以模拟真实的用户点击操作 (这个是后来才知道的,具体下面会说)

4.chrome插件入门及基本结构

4.1 chrome插件开关入门

入门参考链接(第1个链接是入门demo, 第2个是详细教程。)

  1. https://www.jianshu.com/p/e8c21f194e34
  2. https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html

注意

入门教程中说加载自己开发的插件的时候,要先打包,再安装。实际上我打包安装好后,这个插件是不让启用的(应该是chrome的安全机制), 我没找到可以让它启用的地方, 后面用的是另一个方案:只需要加载"已解压的扩展程序", 选择你的插件所在的文件夹即可启用你的插件。具体操作见下图。

在开发的过程中,如果更新代码,重新启用插件即可生效,非常方便。

4.2 chrome插件基本结构

image

具体可以参考上面的第2个参考链接。实际上,我最后只用到了3个:

  1. manifest.json
  2. content-script
  3. background

所以说一下我个人对于这3个东西的理解:


image

可以看到content-script是针对具体的某个页面生效的,而background是浏览器这个层面的,即对所有的页面都生效。manifest.json只是一个配置文件

实际上background是包含background.htmlbackground.js,但是我这次的插件完全不需要页面交互,所以只涉及到了js的部分。

5."尖兵一号"实现过程及踩坑实录

5.1 manifest.json配置

首先创建基本的配置文件, 配置代码如下:

{
      "name" : "尖兵一号",
      "version" : "0.8",
      "description" : "淘宝m站自动刷新cookie插件",
      "permissions": [ "cookies", "debugger", "http://*/*", "https://*/*" ],
      "browser_action": {
        "default_icon": "icon.png"
      },
      "background": {
        "scripts": ["background.js"]
      },
      "content_scripts": [ {
        "matches": ["https://login.m.taobao.com/login.htm*","https://h5.m.taobao.com/mlapp/mytaobao.html*"],
        "js": [ "jquery.min.js","jquery.cookie.min.js","contentJs.js" ],
        "run_at": "document_end"
       } ],
      "manifest_version": 2
}

这里有2个注意点:

  1. 需要调用的模块必须在permissions中声明, 有点像导包的感觉
  2. content_scripts中的matches必须配置正确, 否则可能不生效或者打开所有页面都生效(刚开始我配的是"matches": ["<all_urls>"], 即对所有页面生效, 后面脚本跑疯了...)

5.2 content-script脚本编码

5.2.1 jQuery的click()和原生dom的click()

刚开始我还不认为需要用到background.js, 因为浏览器已经是"正规军"了, 就想说是不是直接用JavaScript去触发click登录就可以了。我对jQuery比较熟, 就用jQuery去触发了登录按钮的click事件。

结果还是会跳出来验证组件让我去点击,虽然这次人去点还是能验证通过的,但是还是没解决问题的。

查了一下jQuery的click()和真实点击的区别, 结果发现有一个答案是:

jQuery 的 .click() 只是 jQuery 的,并不是大家的,触发点击事件的话还是用原生 .click() 的好。

参考链接: https://segmentfault.com/q/1010000002491025

结论我没有去深究,因为本身我对JavaScript还没到这种阶段,所以我只是去试了一把原生的click()。

原代码:

$("#btn-submit").click()

修改为:

document.getElementById("btn-submit").click()

结果很喜人,就是一把过,直接登录成功。(单纯的我以为到这里已经找到真相了...)

5.2.2 获取cookie

已经能够顺利登录了(至少当时看起来是的),那么接下来的问题就剩下拿到cookie并上传了。

拿cookie很简单,在content-script中调用document.cookie一把拿到当前域下的所有cookie, 首先我只是先用console.log打印cookie到控制台, 然后在本地测试了一下这个cookie的可用性。

这一试, 果然不能用, 心都凉了半截。

因为之前是从Request Headers中拿到cookie再手动上传服务器的, 所以把Request Headers中的cookie和document.cookie做了一下对比

image

注: 为了方便对比, 我把cookie中的分号都换成了回车

可以看到左边Request Headers中拿到的cookie要多出6个key-value对。

突然反应过来, 标识了httpOnly的cookie是通过js拿不到的, F12打开了浏览器的调试界面, 验证了下, 果然如此。


image

5.3 background脚本编码

接下来的问题,就变成了怎么(能不能)通过chrome插件拿到httpOnly的cookie。

5.3.1 cookie API

最后找到了chrome extension cookie api

https://developer.chrome.com/extensions/cookies

这里有2个注意点:

  1. cookie api 必须在manifest中有声明
  2. cookie api 只能在background中使用, 而不能在content-script中使用

到了这里, 我们才真正第一次使用上了background.js的功能。

关键代码如下:

chrome.cookies.getAll({'domain':'.taobao.com'},function(cookie){ 
    var needKeys = ['enc','cookie2','unb','skt','cookie1','uc3','cookie17'];
    var needCookie = '';
    
    for(i=0;i<cookie.length;i++){
        if(needKeys.includes(cookie[i].name)){
            //do something..
        }
    }
});

至此, 顺利拿到所有cookie

5.3.2 content-script和background的通信

cookie是拿到了, 接下来怎么上传cookie呢, 而且是什么时候上传呢?

刚开始想到一种方案是background去定时刷cookie(比如1个小时拿一次), 一旦发现所有需要的cookie都能拿到, 就调用服务端接口上传。

这个方案也不是不可行,但是感觉太low了点,程序员都是很执着的,总想着用优雅的方式去解决问题。实际上只有在重新登录之后,我才需要再去重新上传一遍cookie。于是想到了,content-script和background之间是否可以通信。基本的思路是这样的:

image

通信机制可以参考链接:

https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html#%E6%B6%88%E6%81%AF%E9%80%9A%E4%BF%A1

关键代码:

//content-script
chrome.runtime.sendMessage(request, function(response,r2,r3) {
    console.log('收到来自background的回复:' + response);
    //do something
});

//background
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
    console.log('收到来自content-script的消息:');
    console.log(request, sender, sendResponse);
    //do something    
    sendResponse(response);
    return true;
});

这里有1个注意点:

  1. background中最后的return true是必须的, 否则content-script将收不到background给它的response.

参考链接: https://blog.csdn.net/anjingshen/article/details/75579521

5.3.3 上传cookie(服务端接口)

服务端接口是对我来说是最简单的一步了, 用im-api网关暴露一个爬虫系统(im-spider)存储cookie的接口出去, 配置一下就行了。

到此整个流程都完整了。

5.3.4 原来还有这种事..

我设置了1小时刷新一次cookie(即1小时就重新跳转到login页面进行重新登录并重新上传cookie)。

1小时后,等待我的确实login页又弹出了验证组件。难道是我的1小时太短了,把时间设置成了10个小时,结果第二天发现还是有验证组件,又陷入了绝望中...

搜索了一下:

js模拟点击和真实点击有什么区别

然后知道了, 点击之后产生的event对象原来还有一个叫isTrusted的属性, true表示用户真实点击, false表示是用js触发的click

参考链接: https://gaianote.github.io/2017/04/23/%E5%A6%82%E4%BD%95%E5%8C%BA%E5%88%86%E7%9C%9F%E5%AE%9E%E7%82%B9%E5%87%BB%E4%B8%8Ejs%E7%9A%84click/

写了一个demo测了一把:

<!DOCTYPE html>
<html>
<head>
    <title>demo</title>
    <script type="text/javascript">
        function bodyonclick(e) {
            alert(e.isTrusted);
        }
    </script>
</head>
<body style="width: 1000px;height: 800px;background-color: black;" onclick="bodyonclick(event);document.body.click()">

</body>
</html>

我用鼠标点击body之后, 第1次弹出true, 第2次弹出false。先不管这个代码写的有没有问题,结果确实验证了通过event对象可以区分出是js模拟点击还是真实点击

此时的问题又变成了怎么样(能不能)让chrome插件模拟用户真实的点击?

这里感谢公司开放了Google,最终让我搜索到了一个在stackoverflow上的方案, 要不然我用百度估计搜不到结果。

参考方案: https://stackoverflow.com/questions/34853588/how-to-trigger-an-istrusted-true-click-event-using-javascript

这个参考方案还很人性化的给了chrome插件对应的官方API文档URL。

关键代码:

chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
    type:'mousePressed',
    x:position.x,
    y:position.y,
    button:'left',
    clickCount:1
}, function(s){
    
});
chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
    type:'mouseReleased',
    x:position.x,
    y:position.y,
    button:'left',
    clickCount:1
}, function(s){
    
});

用这个方法测试了一下, 果然event对象的isTrusted就变成了true

这里有3个注意点:

  1. 用到debugger模块, 必须在manifest中声明, 并且只能在background中使用
  2. 鼠标的一次点击必须是sendCommand两次(即一次下压,一次释放), 之前我也是没注意, 导致点击都不生效
  3. 鼠标的点击只能根据坐标来定位, 但是不会伴随鼠标的移动, 所以到底准不准只有点完之后才知道的(推荐先用右键点击来模拟, 会弹出右键菜单, 所以可以测试坐标的准确性)

5.3.5 优化

以上, 问题基本都已经解决, 稳定运行1天(定时1小时刷新cookie), 没有再弹出验证组件了。

接下来还做了3个优化的点:

  1. 随机8-20小时刷新一次cookie
  2. 增加了智能验证块的点击逻辑(因为发现偶尔还是会弹出来, 但是用上面模拟点击的方案也能通过)
  3. 增加了控制台的输出日志, 方便观察

6.总结

写了很多, 但是最关键的其实就2个点:

  1. 如何获取httpOnly的cookie
  2. 如果模拟用户的真实点击

写这篇文章的目的, 并不是让大家能掌握什么技能, 而是想把这次解决问题的思路给大家分享一下, 可能里面有很多不严谨的地方, 也有很多代码不规范的地方, 但是还是那句话:

思路比结论重要 -- 58沈老师经常讲的一句话

目前这个插件稳定运行2天。不管后面是否能持续稳定运行下去,光这次技术方案的调研就让我学到了很多:chrome插件的开发流程,httpOnly对于cookie的安全性考虑,模拟点击和真实点击的关键点...

如果这个插件可以一直用下去, 也会后面其他网站(目前只支持天猫爬虫)的爬虫打下来基础。

目前爬虫的架构图如下:

image

"尖兵一号"的流程图如下:

image

6.1 "尖兵一号"的未来展望

为什么叫“尖兵一号”?我当时取名字想到了《最强狂兵》中的苏锐,emmm,好像也没什么太大关系...

实际上目前这个插件还是有一些缺陷以及可以优化的点, 比如刷新的时间可以控制在白天, 比如需要依赖浏览器(目前是用公司电脑7*24小时开机刷...)

不过我觉得从0到1的问题我都解决了, 后面的这些问题无非就是从1到n的问题了

6.2 对于chrome插件的理解及思考

chrome插件的优势就在于充分的利用了浏览器资源, 可以在现有网页上进行扩展。因为严格来说,应该叫chrome扩展程序。劣势也很明显,只能单体安装,多用户升级困难。

暂时没想到业务上什么场景能用chrome插件来解决,但是个人工作上还是能作为工具的开发,比如:

  1. ELK没有日志的导出(用于日志线下分析), 用chrome插件扩展一下
  2. DBPlus查询不方便(只能查前35条), 用chrome插件扩展一下

最后希望看完这篇文章的读者们都有收获, 如有不对的地方, 欢迎拍砖~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容