使用Puppeteer抓取动态网页的完整指南

当遇到React/Vue等现代前端框架构建的SPA(单页应用)时,传统爬虫无法获取JavaScript动态渲染的内容。本文将教你使用Puppeteer破解这个难题,实现真正的动态网页抓取。

使用Puppeteer抓取动态网页的完整指南_第1张图片

我们开始准备环境

1. 安装Node.js 版本至少要在14以上才行哦

2. 初始化开发项目的命令在这里

mkdir puppeteer-crawler && cd puppeteer-crawler
npm init -y

3. 在项目里边安装我们的依赖吧

npm install puppeteer

环境准备好了以后,我们开始规划我们的核心代码

1. 引入库并进行虚拟浏览器页面的加载

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

    // 你需要进行数据处理的代码...
    await browser.close();
}

2. 开始加载我们需要爬取数据的页面,这里要设置我们的网络等待方式和时间

await page.goto('https://你要设置的域名', {
    /** 等待网络空闲 */
    waitUntil: 'networkidle2',
    /** 30秒超时 */
    timeout: 30000
})

3. 等待页面加载完成或则是等某个指定元素加载完成

await page.waitForSelector('.list-box', {
    visible: true,
    timeout: 5000
});

4. 对得到的页面进行数据提取

let result = await page.$$eval('.list-box ul li', el => (
    el.map(ele => {
        let spans = ele.querySelectorAll('.ext-info span')
        let tags = []
        ele.querySelectorAll('.ktags').forEach(node => {
            tags.push(node.innerText)
        })
        return {
            title: ele.querySelector('.title a')?.innerText,
            tags,
            date: spans && spans[0].innerText,
            city: spans && spans[1].innerText
        }
    })
))

5. 设置浏览器指纹

await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...');
await page.setViewport({ width: 1366, height: 768 });

6. 我们可以对页面的请求进行拦截,不必要的内容直接不加载,提升加载速度

await page.setRequestInterception(true);
page.on('request', (req) => {
    if (['image', 'stylesheet', 'font', 'video'].includes(req.resourceType())) {
        req.abort();
    } else {
        req.continue();
    }
})

7. 如果有弹出框和验证框的话,我们也可以直接给它关闭了,当然要考虑你的具体情况而行哦

page.on('dialog', async dialog => {
    await dialog.dismiss();
});

8. 绕过反爬机制

await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'webdriver', { get: () => false });
});

我们来看看我们的完整代码

const puppeteer = require('puppeteer')
const fs = require('fs')

/** 设置请求头 */
function setHeaders(page) {
    return Promise.all([
        /** 设置cookie数据,在这里一定要注意cookie的格式是{name: '', value: '', url: ''} */
        page.setCookie(...[
            {
                "name":"Du4_vi-ds",
                "value":"a06bc51efc49f2706c5d2434e10e9ec0",
                "url":"https://你要设置的域名"
            }
        ]),
        /** 设置UserAgent数据,模拟一个真实的用户环境 */
        page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'),
        page.setViewport({ width: 1366, height: 768 })
    ])
}

async function main() {
    /** 启动一个虚拟浏览器 */
    const browser = await puppeteer.launch({
        headless: false
    })
    /** 新建一个空白页面 */
    const page = await browser.newPage()
    /** 设置不必要加载内容拦截 */
    await page.setRequestInterception(true);
    page.on('request', (req) => {
        if (['image', 'stylesheet', 'font'].includes(req.resourceType())) {
            req.abort();
        } else {
            req.continue();
        }
    })
    /** 弹出框关闭,这里一定要符合你自己的需求而定的 */
    page.on('dialog', async dialog => {
        await dialog.dismiss();
    });
    /** 禁用WebDriver特征 */
    await page.evaluateOnNewDocument(() => {
        Object.defineProperty(navigator, 'webdriver', { get: () => false });
    });
    /** 设置请求头 */
    await setHeaders(page)
    try {
        /** 跳转到指定页面 */
        await page.goto('https://你要设置的域名', {
            /** 等待网络空闲 */
            waitUntil: 'networkidle2',
            /** 30秒超时 */
            timeout: 30000
        })
        /** 开始获取当前页的数据内容 */
        await handleHtml(page, 'page1.json')

        /** 处理分页的数据,这里可以采用递归的方式来实现 */
        await handlePaging(page, 2)
    } catch (e) {
        console.log('------------------------------ 错误')
        console.log(e)
    } finally {
        /** 关闭应用 */
        await browser.close()
    }

}

async function handlePaging(page) {
    /** 计算页面的最大分页值,然后进行逐页递归 */
    let pagings = await page.$$eval(
        '.pages a',
        elements => elements.map(el => (el.textContent.trim()))
    )
    let max = Math.max(...(pagings.filter(it => !isNaN(it)).map(it => it * 1)))
    return new Promise(async resolve => {
        try {
            /** 递归每个页面的数据,并进行数据处理 */
            async function loop(nowPage) {
                /** 获取所有的分页按钮 */
                let btns = await page.$$('.pages a')
                for (let i = 0;i < btns.length;i ++) {
                    let num = btns[i].evaluate(e => e.textContent.trim())
                    if (!isNaN(num) && num == 2) {
                        btns[i].click()
                        break;
                    }
                }
                /** 等待页面加载完成 */
                await page.waitForNavigation();
                /** 处理页面加载的数据 */
                await handleHtml(page, 'page' + nowPage + '.json')
                nowPage += 1;
                if (nowPage > max) {
                    resolve()
                } else {
                    setTimeout(() => {
                        loop(nowPage)
                    }, 1000)
                }
            }
            setTimeout(() => {
                loop(2)
            }, 1000)
        } catch {
            resolve()
        }
    })
}

function handleHtml(page, filename) {
    /** 使用cheerio加载html并进行页面数据解析,把解析后的数据存储起来 */
    return new Promise(async resolve => {
        try {
            let result = await page.$$eval('.list-box ul li', el => (
                el.map(ele => {
                    let spans = ele.querySelectorAll('.ext-info span')
                    let tags = []
                    ele.querySelectorAll('.ktags').forEach(node => {
                        tags.push(node.innerText)
                    })
                    return {
                        title: ele.querySelector('.title a')?.innerText,
                        tags,
                        date: spans && spans[0].innerText,
                        city: spans && spans[1].innerText
                    }
                })
            ))
            fs.writeFileSync(filename, JSON.stringify(result), {encoding: 'utf-8'})
            resolve()
        } catch (e) {
            console.log(e)
            resolve()
        }
    })
}

/** 调用方法 */
main()

性能优化技巧

  1. 无头模式:headless: 'new'(新版更高效)
  2. 复用浏览器实例:连接已存在的浏览器
  3. 并行处理:使用Promise.all管理多个page实例
  4. 缓存管理:定期清除缓存和cookies

重点注意 ⚠️

  1. 避免高频访问(建议≥3秒/次)
  2. 注意内存泄漏(监控page.metrics())

总结

  1. 通过Puppeteer,我们不仅可以抓取动态渲染的内容,还能实现:
  2. 模拟真实用户操作
  3. 处理复杂交互场景
  4. 获取完整渲染后的DOM生成页面快照和报告

相比传统爬虫,Puppeteer虽然资源消耗较大,但能完美解决现代Web应用的爬取难题。建议将Puppeteer与Cheerio结合使用,在需要执行交互操作时启动浏览器,静态内容解析时使用轻量级方案。

如果需要 HTTPS 支持或更多高级功能,关注我!!
 源码已上传Gitee(虚拟链接:simple-crawler-demo: 三分钟打造一个简单的Node.js爬虫)

你可能感兴趣的:(使用Puppeteer抓取动态网页的完整指南)