# Node爬虫

# 依赖

分享两个比较不错的Node爬虫库

cheerio: 用于爬取静态的资源,功能面较小,但速度快

puppeteer: 用于爬取静态/动态/异步资源,功能强大,但速度满

# cheerio

可以对cheerio库进行二次封装

可以写一个通用函数用于专门爬取静态的HTML

// methods/getHTML.js

const https = require('https')
/**
 * 获取爬虫html
 * @param {*} url 爬虫地址
 * @returns {string} html
 */
async function getHTML(url) {
    return new Promise((resolve) => {
        https.get(url, (res) => {
            let html = ''

            res.on('data', (chunk) => {
                html += chunk
            })

            res.on('end', () => {
                resolve(html)
            })
        })
    })
}

module.exports = getHTML

接下来需要借助cheerio库进行dom的操作,cheerio能够让我们像使用jquery一样使用cheerio

// utils/cheerio.js

const cheerio = require('cheerio')
/**
 * 爬取href属性
 * @param {*} html html字符串
 * @param {*} classTag 类名
 * @returns hrefs
 */
function getHrefsByTagClass(html, classTag) {
    console.log(`开始爬取${classTag}中...`)
    const $ = cheerio.load(html)
    const resHrefs = []
    $(classTag).each(function() {
        const href = $(this).attr('href')
        if (href) {
            resHrefs.push(href)
        }
    })
    console.log(`结束爬取${classTag}...`)
    return resHrefs
}

/**
 * 爬取文本内容
 * @param {*} html html字符串
 * @param {*} classTag 类名
 * @returns 
 */
function getContentByTagClass(html, classTag) {
    console.log(`开始爬取${classTag}中...`)
    const $ = cheerio.load(html)
    const resContent = []
    $(classTag).each(function() {
        const text = $(this).text()
        if (text) {
            resContent.push(text)
        }
    })
    console.log(`结束爬取${classTag}...`)
    return resContent
}


module.exports = {
    getHrefsByTagClass,
    getContentByTagClass
}

# puppeteer

puppeteer能够让我们像使用浏览器一样,比如滚动行为、点击行为等等

我们同样可以对puppeteer进行二次封装

// utils/puppeteer.js

const puppeteer = require('puppeteer');
const browserPool = require('./browser-pool');
/**
 * 滚动到底部
 * @param {} page 
 */
async function autoScroll(page) {
    console.log('正在滚动页面,时间较长请稍等...')
    await page.evaluate(async () => {
        await new Promise((resolve) => {
            let totalHeight = 0;
            const distance = 100;
            const scrollInterval = setInterval(() => {
                const scrollHeight = document.body.scrollHeight;
                window.scrollBy(0, distance);
                totalHeight += distance;
                if (totalHeight >= scrollHeight) {
                    clearInterval(scrollInterval);
                    console.log('滚动页面结束...')
                    resolve();
                }
            }, 100);
        });
    });
}


/**
 * 优化
 * 使用链接池
 */
async function runPuppeteerScriptForTexts(urls, classTag) {
    // console.log(`开始爬取${url}的Text`)
    return new Promise(async (resolve) => {
        const browser = await browserPool.acquire(); // 从连接池获取浏览器实例

        const page = await browser.newPage();
        await page.setDefaultNavigationTimeout(60000);

        const dynamicContents = []

        for (const urlItem in urls) {
            console.log(`开始爬取${urls[urlItem]}的Text`)
            await page.goto(urls[urlItem]);
            await page.waitForSelector(classTag);
            const dynamicContent = await page.evaluate((classTag) => {
                const element = document.querySelector(classTag);
                return element ? element.textContent : null;
            }, classTag);
            dynamicContents.push(dynamicContent)
            console.log(`结束爬取${urls[urlItem]}的Text`)
        }

        // 关闭页面
        await page.close();
    
        // 将浏览器实例返回连接池
        browserPool.release(browser);

        resolve(
            dynamicContents
        )
    })
}


async function runPuppeteerScript(url, classTag, classTagText, classTagDate, classTagName) {
    console.log(`开始爬取${url}的Text和href`)
    return new Promise(async (resolve) => {
        const browser = await browserPool.acquire(); // 从连接池获取浏览器实例

        const page = await browser.newPage();
        await page.goto(url);
        await page.click('.btn-load.m-c-bg-color-white.m-c-color-gray.btn-middle');
        // 模拟滚动到底部
        await autoScroll(page);
        await page.waitForSelector(classTag);

        // 执行其他 Puppeteer 操作...
        const dynamicHrefs = await page.evaluate((classTag) => {
            const elements = document.querySelectorAll(classTag);
            const data = []
            elements.forEach(element => {
                data.push(element.getAttribute('href'))
            })
            return data
        }, classTag);

        const dynamicContents = await page.evaluate((classTagText) => {
            const elements = document.querySelectorAll(classTagText);
            const data = []
            elements.forEach(element => {
                data.push(element.textContent)
            })
            return data
        }, classTagText)

        const dynamicDates = await page.evaluate((classTagDate) => {
            const elements = document.querySelectorAll(classTagDate);
            console.log(elements)
            const data = []
            elements.forEach(element => {
                data.push(element.textContent)
            })
            return data
        }, classTagDate)

        const dynamicNames = await page.evaluate((classTagName) => {
            const elements = document.querySelectorAll(classTagName);
            console.log(elements)
            const data = []
            elements.forEach(element => {
                data.push(element.textContent)
            })
            return data
        }, classTagName)

        // 关闭页面
        await page.close();
        console.log(`结束爬取${url}的Text和Href`)

        // 将浏览器实例返回连接池
        browserPool.release(browser);

        resolve({
            dynamicHrefs,
            dynamicContents,
            dynamicDates,
            dynamicNames
        })
    })
}


module.exports = {
    runPuppeteerScript,
    runPuppeteerScriptForTexts
}

这里我们使用了generic-pool对性能进行优化,它能够开启一个连接池减少开关浏览器的行为。

// utils/browser-pool.js
const puppeteer = require('puppeteer');
const genericPool = require('generic-pool')
// 创建一个浏览器工厂
const browserFactory = {
  create: async () => {
    const browser = await puppeteer.launch();
    return browser;
  },
  destroy: (browser) => {
    browser.close();
  },
};

// 创建浏览器连接池
const browserPool = genericPool.createPool(browserFactory, { min: 1, max: 10 }); // 可根据需求调整最小和最大连接数

module.exports = browserPool;

# 定时爬虫

借助node-schedule我们能够实现定时进行爬虫,当然前提是在开启了服务器之后

// utils/schedule.js

const schedule = require('node-schedule')
const SETTING = require('../setting')
const reptile = require('../reptile')

function startReptileSchedule() {
    const rule = new schedule.RecurrenceRule()
    rule.hour = SETTING.repiteTime.hour
    rule.minute = SETTING.repiteTime.min

    const job = schedule.scheduleJob("reptile" ,rule, async () => {
        try {
            await reptile()
            console.log('爬取任务已完成')
        } catch (error) {
            console.error('爬取任务出错:', error)
        }
    })
}

module.exports = {
    startReptileSchedule
}

# 一个小案例

亚运会数据统计[Python/Nodejs]

统计亚运会关注人数最多的比赛

实验环境

  • 操作系统:MacOS
  • 编程语言:Nodejs 18.17.0

代码仓库

前往仓库