nodejs抓取动态网页( 使用puppeteer爬取链家的房价数据,详解了puppeteer的相关用法及注意事项)
优采云 发布时间: 2021-12-25 14:03nodejs抓取动态网页(
使用puppeteer爬取链家的房价数据,详解了puppeteer的相关用法及注意事项)
使用puppeteer抓取链家的价格数据,详细讲解puppeteer的相关使用方法和注意事项,并进行地图的直观展示。
使用puppeteer爬取链家房价信息
内容
本文记录了使用puppeteer库进行动态网站爬取的过程。页面结构
地址
链家的历史交易记录页面在这里。是后台渲染模式,无法通过监控模拟xhr请求快速获取。您只能找到一种方法来分析其页面结构并提取元素。
页面是分页管理的,比如第二页链接是,遍历分页没问题。
问题是通过首页可以看到它有5万多条历史信息,一个页面有30条,但是它的首页只显示100页,没有办法遍历分页获取所有数据.
幸运的是,链家提供了过滤器。经测试,使用街道级区域过滤可以满足寻呼限制。
那么爬取的思路就是遍历区级按钮,遍历每个区级按钮下的街道按钮,遍历每个街道按钮下的每一页。
爬虫库
在nodejs爬虫库领域,常用cheerio和puppeteer。其中cheerio一般用于抓取静态网页,puppeteer常用于抓取动态网页。
链家网页虽然是后台静态生成的,但考虑到需要对页面进行操作(点击其区域选择器),还是首选puppeteer库。
木偶图书馆
ppeteer 库是在 2017 年谷歌 Chrome 开发自己的 Chrome Headless 功能的同时推出的。本质上,它是一个没有界面的浏览器,有点像电脑终端,所有的操作都是通过代码来完成的。
这样我们就可以在检索网站之前操作指定元素滚动到底部触发更多信息。或者当需要翻页时,操作码点击翻页按钮,然后在翻页后对页面进行相关处理。
完成
这是它的git地址,这是它的中文教程。
打开要抓取的页面
// 1. 引包
const puppeteer = require('puppeteer');
// 2. 在异步环境中执行(pupeteer 所有操作都是异步实现的)
(async ()=>{
// 创建浏览器窗口
const browser = await puppeteer.launch({
headless: false, // 有界面模式,可以查看执行详情
});
// 创建标签页
const page = await browser.newPage();
// 进入待爬页面
await page.goto('https://wh.lianjia.com/chengjiao/');
// 遍历页面
})()
这样,链家网站就在Pupeteer成功开通了。
仅仅打开它是不够的。我们期望的是操作网页上的过滤按钮来获取每条街道的页面,以便我们可以遍历其分页进行查询。
遍历区级页面
我们首先需要找到区按钮并点击它。
标准思维
(async ()=>{
// ......
// 使用选择器
/* page.$$() 会在页面执行 document.querySelectorAll,并返回 ElementHandle 对象的数组
page.$() 执行 document.querySelector,返回 ElementHandle 对象
*/
let districts = await page.$$('div[data-role=ershoufang]>div>a')
for(let district of districts){
await district.click() // 模拟点击页面对象
// 遍历街道
}
})
第一个想法大概是这样写,通过选择器获取所有的按钮,然后一个一个的点击。
恭喜,我收到一个错误。
错误:执行上下文被破坏,很可能是因为导航。
假设您的执行上下文被杀死,可能是因为页面导航。
为了澄清这个问题,我们需要先看一下 Execution 上下文是什么。
这是puppeteer的内部组织结构。一页下有多个框架,一个框架下有一个执行上下文。
单击第二个按钮时触发了我们的错误。
这很清楚。点击第一个导航成功,页面变了,你的二区还是依赖上一个页面。结果找不到执行上下文,然后报错。
如何解决?
有两个想法。
方法一
缓存区域级按钮的链接,使其在遍历跳转时不依赖原页面。
(async ()=>{
// ......
// 使用选择器
/* page.$$eval('选择器', callback(eles)) 会在page页面内部执行 Array.from(document.querySelectorAll(selector)),然后把数组参数传给 callback
*/
let districts = await page.$$eval('div[data-role=ershoufang]>div>a',links=>{
// 对传进来的元素处理
let arr = []
for(let link of links){
arr.push(link.href)
}
return arr
})
for(let district of districts){
await page.goto(district) // 使用 page.goto() 替代点击
// 遍历街道
}
})
这里需要特别说明的是,点击按钮、导航链接等页面操作都是在node.js中完成的。页面中的操作,比如读取元素的某个属性,都是在浏览器的引擎中处理的,类似于html文件中script标签中的script。
对于puppeteer,它的脚本文件一般都是用*.*eval()包裹的,比如page.evaluate(pageFunction[, ...args]), page.$eval(selector,pageFunction, ...args), ElementHandle。 $eval(selector, pageFunction, ...args)。
在这种脚本中,除非在以下参数中传递参数,否则无法访问节点环境中的全局变量:
let name = 'bug'
page.$eval('id',(ele/* 这个参数是该方法自身返回的所选择元素 */, nodeParam)=>{
console.log(nodeParam) // 'bug'
},name)
方法二
另一种方式是在链接跳转时不直接跳转到原页面,而是打开一个新的page2页面。这样你就不能使用点击,而是得到它的链接。
(async ()=>{
// 新建一个标签页用来做跳转缓存
const page2 = await browser.newPage();
// ......
// 仍使用原方法获取元素
let districts = await page.$$('div[data-role=ershoufang]>div>a')
for(let district of districts){
let link = (await district.getProperty('href'))._remoteObject.value // 获取属性
await page2.goto(link) // 在新页面跳转,原 page 不变
// 遍历街道
}
})
这两种方法都是可行的,但是第一种方法似乎更简单一些。每个按钮的链接缓存后,似乎不需要保留原来的页面了。
总之,我们现在可以遍历所有区级页面了!
遍历街道页面
下面的操作写在遍历区级页的for循环中。
操作类似于遍历区级页面,先找到街道按钮,然后循环跳转。这里的跳转逻辑也和上面类似,要么选择缓存它的链接,要么打开一个新的page3进行分页循环。
我喜欢缓存,毕竟新页面也是消耗内存的吧?
(async ()=>{
let streets = await page.$$eval(
'div[data-role=ershoufang] div:last-child a', (links => {
// 对传进来的元素处理
let arr = []
for(let link of links){
arr.push(link.href)
}
return arr
})
)
for(let street of streets){
await page.goto(street) // 使用 page.goto() 替代点击
// 遍历页码
}
})
遍历分页
因为分页的链接处理比较简单,增量就够了。
有一个小问题,我们如何确定循环的结束。
有几个想法,
首先,街道首页会显示该地区有多少套房。每页收录
30 个套件,只需除以一。
其次,我们可以获取分页按钮的最后一个值,但不幸的是,大多数情况下最后一个值是下一页。有鉴于此,我们或许可以做一个while循环,当分页的最后一个按钮不是下一页时,就表示遍历结束。但是对于房间比较少的区域,可能只有两三页,而且没有下一页按钮,所以直接跳过漏爬。
第三,看一下页面结构。以上是从渲染出来的页面看到的信息,页面结构上可能会有totalPage等字段。我仔细查看了分页组件,果然,标签属性里有总页数。
在上述想法中,第二个可能是第二个。然而,这是我使用的方法......在我改变它之前犯了很多低级错误。其实第二个只要做简单的优化就可以用了,比如获取分页按钮的最后一个,如果是下一页就获取它之前的兄弟元素,或者你可以很容易的获取总的数量页。
简而言之,让我们使用最简单的一个:
// 遍历页码
let totalPage = await page.$eval('div.house-lst-page-box',el => {
return JSON.parse(el.getAttribute('page-data')).totalPage
})
for (let i = 1; i 1) await page.goto(`${street}pg${i}`) // 跳转拼接的分页链接
// 业务代码
}
商业信息
这样我们就实现了每一页数据的遍历,可以愉快的写业务逻辑了。
基本上,您可以根据自己的兴趣获取所有可以看到的数据。