chenxiaochun / blog

🖋️ChenXiaoChun's blog
181 stars 15 forks source link

如何使用nodejs抓取具有懒加载机制的页面链接 #35

Open chenxiaochun opened 7 years ago

chenxiaochun commented 7 years ago

最近在开发一个网页链接抓取工具,需要将页面中所有的链接都抓取下来进行分析。

对于一般网页来说很好处理,直接去抓取页面的源代码,然后将其中的链接分析出来即可。但是京东商城的大部分页面都使用了懒加载的机制,只有用鼠标真正滚动页面之后,才会加载楼层数据。

所以,我得用 js 实现一个自动滚动页面的功能,当页面滚动结束之后,再去触发抓取链接机制。这里面有一个难点是怎么才能感知到页面已经滚动到最底部了,可以进行后续操作了。因为 js 中并没有什么原生方法能告诉你滚动条的滚动状态,所以我想到可以使用 async + await + setInterval 来模拟一下这个操作。

先来看一个简单的示例,我定义了一个sleep函数,参数ms的默认值为 3000 毫秒。运行代码,立刻输出的是 1,后面的 2 并没有立刻输出,而是等待 3 秒之后才会看到。

它的实现原理就是,sleep函数返回的是一个Promise对象,在async函数中调用sleep函数时添加了await关键字。代码执行到此处就会暂停,然后等待sleep函数状态变为resolve()之后,才会继续执行。

function sleep(ms=3000){
    return new Promise(resolve => setTimeout(resolve, ms))
}

(async () => {
    console.log(1)
    await sleep()
    console.log(2)
})()

有了上面的功能做基础,接下来就是如何实现页面滚动的功能。

定义变量totalHeight用来存储已经滚动的总高度,distance用来存储每次滚动的高度。页面每滚动一次,totalHeight就累加一次,然后将它与页面的scrollHeight进行比较,直到大于等于它为止。这时候说明页面确实已经滚动到了最底部。

(async () => {
    function scroll(ms=200){
        return new Promise((resolve, reject) => {
            var totalHeight = 0
            var distance = 600
            var timer = setInterval(() => {
                window.scrollBy(0, distance)
                totalHeight += distance
                if(totalHeight >= document.body.scrollHeight){
                    clearInterval(timer)
                    resolve()
                }
            }, ms)
        })
    }

    console.log(1)

    await scroll()

    console.log(2)
})()

抓取页面链接我用的是 GoogleChrome 团队出品的 Puppeteer

Puppeteer 是一个基于chrome开发工具协议提供了一系列的高级 api 用于控制 chrome 无头浏览器的 Nodejs 库。

所谓无头浏览器,可以理解为没有界面的浏览器。也就是说,可以使用这个库在不需要打开浏览器界面的情况下去模拟用户的行为操作。

puppeteer.launch()方法创建一个浏览器实例,传入headless: false参数是为了打开浏览器的操作界面,这样我们才能看到后面模拟的滚动操作。

browser.newPage()方法是创建一个标签页,并设置它的可视区域大小为 1200*800。

page.goto()方法就是在当前创建的标签页中打开我们指定的链接。

const puppeteer = require('puppeteer')
const browser = await puppeteer.launch({
    headless: false
})
const page = await browser.newPage();

page.setViewport({
    width: 1200,
    height: 800
})

await page.goto('http://www.jd.com', {
    waitUntil: 'load'
})

现在把滚动页面和抓取页面链接的功能添加进来。

var scrollTimer = page.evaluate(() => {
    return new Promise((resolve, reject) => {
        var totalHeight = 0
        var distance = 600
        var timer = setInterval(() => {
            window.scrollBy(0, distance)
            totalHeight += distance

            if(totalHeight >= document.body.scrollHeight){
                clearInterval(timer)
                resolve()
            }
        }, 200)
    })

})

var crawler = scrollTimer.then(async () => {
    var urls = await page.evaluate(() => {
        var links = [...document.querySelectorAll('a')]
        return links.map(el => {
            return {href: el.href.trim(), text: el.innerText}
        })
    })

    await page.close()
    return Promise.resolve(urls)
}).catch((e) => {
    console.log(e)
})

crawler.then(urls => {
    console.log(urls)
})

上面的scrollTimer就是用来滚动页面,返回的是一个Promise对象。下面的crawler在等待页面滚动完毕之后,就开始抓取页面中所有的链接[...document.querySelectorAll('a')],分析完之后将数据返回。

无意中看到另外一种页面滚动的实现方式:https://github.com/GoogleChrome/puppeteer/issues/844

liangtongzhuo commented 6 years ago

感谢,提供了思路

chenxiaochun commented 6 years ago

客气,互相交流哈。

tiu5 commented 6 years ago

感谢

felix021 commented 5 years ago

友情提示一下,这种情况被无限加载的页面(例如微博feed流之类的)坑到,建议参考这个方案:

https://www.screenshotbin.com/blog/handling-lazy-loaded-webpages-puppeteer

chenxiaochun commented 5 years ago

@felix021 ,我的方案在最初确实没有考虑无限加载的情况。其实对于无限加载的页面,可以设置一个最大截取长度。

nqzhang commented 5 years ago

mark 等下来看

MarcBalaban commented 5 years ago

This was helpful, thanks.