puppeteer下载携程酒店数据(反爬虫)

如何抓取到携程的每个酒店的装修时间和客房数量呢?本文以puppeteer来抓取。

官方demo就很容易上手,再加上awesome-puppeteer中的例子,很容易就可以实现自己的目标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const puppeteer = require('puppeteer');

(async () => {
const conf = {
// 还是携程上海五角场江湾地区的url
workUrl: 'http://hotels.ctrip.com/hotel/shanghai2/zone368#ctm_ref=hod_hp_sb_lst',
// 设置ua,不然ua中包含headless,会被识别出来,拒绝提供服务
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
viewport: {
width: 1920,
height: 1080,
},
};
const browserSetting = {
// 默认是headless的模式打开的,改为false可以打开实际的chrome,方便我们查看
// 但是设置为true会快很多
headless: false,
// 或者直接打开指定path的chrome,最好还是使用默认提供的chromium
executablePath: 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
// 设置后可以操作慢点方便调试
// slowMo: 250,
// 打开F12
devtools: true,
};
const browser = await puppeteer.launch(browserSetting);
const page = await browser.newPage();
// 页面设置
await page.setViewport(conf.viewport);
await page.setUserAgent(conf.ua);
// await page.emulate(conf.device);
// 页面跳转
await page.goto(conf.workUrl);
// page.$$(sel);= document.querySelectorAll(sel)
// page.$(sel);= document.querySelector(sel)
// page.$eval(sel);
// page.$$eval(sel);
// const els = await page.$$eval('p', els => els);// 奇怪的是这样得到的els里的元素都是{}
// 改为
// const elsHtml = await page.$$eval('p', els => els.map(el => el.innerHTML));
// console.log(elsHtml);
// 但个人觉得可以直接js实现的就不必用 puppeteer api,记一堆api不如用好js
const hotels = await page.evaluate(async () => {
// 这里可以直接执行js代码了
const resArr = [];
let timer = null;
// 注意此处的异步操作
async function getRes() {
return new Promise((resolve) => {
function getData() {
// return new Promise((resolve, reject) => {
// 因为ctrip本来就有jQuery,所以可以直接使用
const num = $('.hotel_item').length;
for (var i = 0; i < num; i++) {
const item = $('.hotel_item:eq(' + i + ')');
const hotel = {
name: `${item.find('.hotel_name a').attr('title')}`,
address: `${item.find('.hotel_item_htladdress').text().replace(/地图|街景/g, '')}`,
url: `${item.find('.hotel_name a').attr('href').replace(/\?.*/g, '')}`,
rate: `${item.find('.hotel_value').text()}`,
price: `${item.find('.J_price_lowList').text()}`,
};
resArr.push(hotel);
}
let $nextBtn = $('.c_down');
if ($nextBtn.length) {
$nextBtn.click();
timer = setTimeout(getData, 1000);
$nextBtn = null;
} else {
clearTimeout(timer);
resolve(resArr);
}
}
getData();
});
}
// 加debugger可以在打开的chrome里调试js
// debugger;
return getRes();
});
})();

以上功能都可以直接按上一篇在chrome snippet中实现,但是如果需要自动获取detail信息,就需要puppeteer来帮我们操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 开始获取detail
async function getDetail(h) {
const nh = h;
await page.goto(`http://hotels.ctrip.com${h.url}`);
nh.info = await page.evaluate(() => $('#htlDes>p')[0].childNodes[0].data);
return nh;
}
// 数量太多测试时间太长,先测试4个试下
hotels.length = 4;
// 此处await不能使用forEach,await不能放在循环中,使用promise.all
// 参见http://es6.ruanyifeng.com/#docs/async
const promises = hotels.map(h => getDetail(h));
await Promise.all(promises);
console.log(hotels);

node直接写入csv文件

1
2
3
4
5
6
7
// 将得到的结果写入csv文件
fs.writeFile('hotels.csv', hotels, function(err) {
if (err) {
return console.error(err);
}
// 得到csv文件会有乱码问题,可以找框架来直接转为csv文件,此处不赘述
});

携程会自动监测是不是用了selenium、puppeteer这类工具,在测试的过程中发现,使用正常的Chrome浏览器可以直接打开酒店详情页。但是如果使用火狐浏览器或者puppeteer操控Chrome的话,输入酒店详情链接,会直接从详情页跳转到登陆提示页,打开浏览器调试页面,切换到console栏,输入navigator,发现正常的Chrome和puppeteer打开的Chrome中,navigator主要是webdriver、plugins、mimetypes这几个属性不一样,携程就是根据这几个属性来判断是否要跳转到登录页。

修改webdriver

webdriver标记是反爬一定在检测的属性

目前资料都是

1
ignoreDefaultArgs: ['--enable-automation']

1
2
3
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
})

实测这两种方法已经无效,webdriver还在,只是值为undefined,通过 webdriver in navigator 或者 navigator.hasOwnProperty("webdriver") 都是为true

最终解决方案:

1
2
3
4
5
await page.evaluateOnNewDocument(() => {
const newProto = navigator.__proto__;
delete newProto.webdriver;
navigator.__proto__ = newProto;
});

改完以后,headless: false的情况下浏览器不会在自动跳转了,但是改成无头浏览器的话,还是会自动跳转,因为还有其他几个属性不一样。
有两个解决方法:
1.参考本博客中的文章【navigator plugins与mimetyps的模拟实现分析】手动修改。
2.集成扩展组件puppeteer-extra-plugin-stealth,它里面已经把所有属性都改了,可以防止被检测。
不过这个项目半年没更新了,如果报错【browser.setMaxListeners is not a function】,处理方法:
找到puppeteer-extra/packages/puppeteer-extra-plugin-stealth/index.js第155行:
browser.setMaxListeners(30),注释掉此行即可。

扩展阅读:反爬虫——使用chrome headless时一些需要注意的细节