1.背景
最近接手了爬虫的项目, 爬取对象是天猫和京东的价格相关的数据, 其中对于天猫的优惠券的爬取需要有已登录的cookie才能成功爬到数据。
之前对于这块的cookie都是我们手动用自己淘宝账号登录淘宝m站,并获取cookie存到服务端。并且cookie的最长有效时间是24小时,也就是说我们每天都要有人去手动操作一下。
问题:
- 每天都需要手动操作,费时费力,而且容易忘记容易出错。
- 作为一个程序员,每天都干同样的事又无法改变这种事是忍不了的。
- 经常半夜惊醒, 想起来cookie忘了设置, 搞得神经衰弱..
思路很简单: 用程序去模拟一个真实的登录操作
2.曾经尝试过的方案
核心是使用无头浏览器去操作
- selenium(Java API) + headless chrome
- puppeteer(Node API) + headless chrome
PS:无头浏览器最开始在voc中的声音专题项目中有用过,用的是phantomjs,不过这个东西的API太难用了,刚开始就没考虑。
以上方案最终都是以失败而告终。原因是最终淘宝都会弹出一个验证的组件让你去操作
比如下面这样的:
刚开始以为,这种验证码不就是"点一下"的事吗,找到这个dom元素,去点就好了。
结果发现找是找到了,点也点中了,却是怎么点都“验证失败”。
怀疑是JavaScript去做click跟用户真实点击可能不太一样,所以尝试手动去点了一下,结果还是“验证失败”。
然后就怀疑淘宝对于这种headless chrome是不是做了什么识别,并且是限制了这种浏览器的行为。
因为换成正常安装的chrome,都是可以正常登录,连验证组件都不会跳出来。
3.为什么用chrome插件
既然headless chrome都被淘宝风控了,那么之后的思路就变成了“能不能控制现有安装的chrome浏览器来做一些自动化操作”
这里我和小伙伴有几次都想到了“按键精灵”,但是后来放弃了。一方面是因为对按键精灵不熟悉,学习成本比较高,第二是按键精灵太依赖于呈现在眼前的画面,个人理解很容易被一些意外情况打断。
使用chrome插件的灵感其实是来源于我的小伙伴(这里感谢@俊杰)。
不过真正用了才发现好处还是比较多的,列了以下4点:
- 可以依附在现有的浏览器中,只跟浏览器有关系,平台无关
- 主要使用JavaScript来开发,学习成本低,并且有成熟的调试方案
- 通过js来控制页面,不需要页面必须是呈现在你面前(比如最小化浏览器也ok),运行更可靠
- chrome插件内置了很多底层的库,可以模拟真实的用户点击操作 (这个是后来才知道的,具体下面会说)
4.chrome插件入门及基本结构
4.1 chrome插件开关入门
入门参考链接(第1个链接是入门demo, 第2个是详细教程。)
注意
入门教程中说加载自己开发的插件的时候,要先打包,再安装。实际上我打包安装好后,这个插件是不让启用的(应该是chrome的安全机制), 我没找到可以让它启用的地方, 后面用的是另一个方案:只需要加载"已解压的扩展程序", 选择你的插件所在的文件夹即可启用你的插件。具体操作见下图。
在开发的过程中,如果更新代码,重新启用插件即可生效,非常方便。
4.2 chrome插件基本结构
具体可以参考上面的第2个参考链接。实际上,我最后只用到了3个:
- manifest.json
- content-script
- background
所以说一下我个人对于这3个东西的理解:
可以看到content-script是针对具体的某个页面生效的,而background是浏览器这个层面的,即对所有的页面都生效。manifest.json只是一个配置文件
实际上background是包含background.html
和background.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个注意点:
- 需要调用的模块必须在permissions中声明, 有点像导包的感觉
- 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
做了一下对比
注: 为了方便对比, 我把cookie中的分号都换成了回车
可以看到左边Request Headers
中拿到的cookie要多出6个key-value对。
突然反应过来, 标识了httpOnly的cookie是通过js拿不到的, F12打开了浏览器的调试界面, 验证了下, 果然如此。
5.3 background脚本编码
接下来的问题,就变成了怎么(能不能)通过chrome插件拿到httpOnly的cookie。
5.3.1 cookie API
最后找到了chrome extension cookie api
这里有2个注意点:
- cookie api 必须在manifest中有声明
- 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之间是否可以通信。基本的思路是这样的:
通信机制可以参考链接:
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个注意点:
- 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
写了一个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上的方案, 要不然我用百度估计搜不到结果。
这个参考方案还很人性化的给了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个注意点:
- 用到debugger模块, 必须在manifest中声明, 并且只能在background中使用
- 鼠标的一次点击必须是sendCommand两次(即一次下压,一次释放), 之前我也是没注意, 导致点击都不生效
- 鼠标的点击只能根据坐标来定位, 但是不会伴随鼠标的移动, 所以到底准不准只有点完之后才知道的(推荐先用右键点击来模拟, 会弹出右键菜单, 所以可以测试坐标的准确性)
5.3.5 优化
以上, 问题基本都已经解决, 稳定运行1天(定时1小时刷新cookie), 没有再弹出验证组件了。
接下来还做了3个优化的点:
- 随机8-20小时刷新一次cookie
- 增加了智能验证块的点击逻辑(因为发现偶尔还是会弹出来, 但是用上面模拟点击的方案也能通过)
- 增加了控制台的输出日志, 方便观察
6.总结
写了很多, 但是最关键的其实就2个点:
- 如何获取httpOnly的cookie
- 如果模拟用户的真实点击
写这篇文章的目的, 并不是让大家能掌握什么技能, 而是想把这次解决问题的思路给大家分享一下, 可能里面有很多不严谨的地方, 也有很多代码不规范的地方, 但是还是那句话:
思路比结论重要 -- 58沈老师经常讲的一句话
目前这个插件稳定运行2天。不管后面是否能持续稳定运行下去,光这次技术方案的调研就让我学到了很多:chrome插件的开发流程,httpOnly对于cookie的安全性考虑,模拟点击和真实点击的关键点...
如果这个插件可以一直用下去, 也会后面其他网站(目前只支持天猫爬虫)的爬虫打下来基础。
目前爬虫的架构图如下:
"尖兵一号"的流程图如下:
6.1 "尖兵一号"的未来展望
为什么叫“尖兵一号”?我当时取名字想到了《最强狂兵》中的苏锐,emmm,好像也没什么太大关系...
实际上目前这个插件还是有一些缺陷以及可以优化的点, 比如刷新的时间可以控制在白天, 比如需要依赖浏览器(目前是用公司电脑7*24小时开机刷...)
不过我觉得从0到1的问题我都解决了, 后面的这些问题无非就是从1到n的问题了
6.2 对于chrome插件的理解及思考
chrome插件的优势就在于充分的利用了浏览器资源, 可以在现有网页上进行扩展。因为严格来说,应该叫chrome扩展程序。劣势也很明显,只能单体安装,多用户升级困难。
暂时没想到业务上什么场景能用chrome插件来解决,但是个人工作上还是能作为工具的开发,比如:
- ELK没有日志的导出(用于日志线下分析), 用chrome插件扩展一下
- DBPlus查询不方便(只能查前35条), 用chrome插件扩展一下
最后希望看完这篇文章的读者们都有收获, 如有不对的地方, 欢迎拍砖~