使用 node.js 爬取页面数据

使用到的技术

node.js 这个不用过多介绍
cheerio 抓取页面模块,为服务器特别定制的,快速、灵活、实施的jQuery核心实现。适合各种Web爬虫程序 (摘自百度)
fs node内置的文件模块 可以进行创建以及读取文件
request 用来发送请求的模块 (也可以使用 axios 以及其他)
iconv-lite 把纯 javascript 转化字符编码 (看别人也是这样用的 实际具体效果我也不知道)

1. 什么是爬虫

又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

2. 爬取直接的请求的数据 (代码写的可能过于啰嗦 请别介意)

const request = require('request') // 引入 request 模块
const iconv = require('iconv-lite') // 引入 iconv-lite 模块
const fs = require('fs') // 引入 fs 模块

const host = 'https://www.medtiku.com/api' // 请求的地址
const url = '/cat?id=10' // 请求后续的参数
const headers = {
	// 这里我们需要模拟浏览器 向这个请求发送数据
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36'
}
let i = 1
// 基于 requset 进行请求的封装
const requestPromise = (url) => {
    return new Promise((resolve, reject) => {
    // strictSSL 此值默认为 true 但是有些请求会报 unable to verify the first certificate 所以设置为 false 来阻止报错
    // [出处](https://www.136.la/javascript/show-54138.html)
        request(url, { encoding: null, strictSSL: false, headers }, (error, res, body) => {
            if (res.statusCode === 200) {
                const html = iconv.decode(body, 'utf8') // 转化字符集 指定为 utf-8
                console.log('完成第' + i++ + '次请求');
                resolve(html)
            } else {
                reject(error)
            }
        })
    })
}
let content = ''
requestPromise(host + url).then(async (res) => {
    // 请求出来的 string 转换一下
    // 这里因为这个请求的数据就是这样的 我下面代码进行那样的处理 不同的网站爬取的数据结构 以及其他可能不同 根据自己请求的数据进行处理
    content = JSON.parse(res).data
    if (content.subject.length > 0) {
        for (let i in content.subject) {
            let item = content.subject
            item[i].children = []

            let res1 = await requestPromise(`${host}/subject?id=${item[i].id}`)

            item[i].children = JSON.parse(res1).data.chapter
            if (item[i].children.length > 0) {
                for (let k in item[i].children) {
                    let item1 = item[i].children
                    item1[k].children = []

                    let res2 = await requestPromise(`${host}/q?cid=${item1[k].c_id}&sid=${item1[k].c_subject}`)
					// 这个数据是进行的 base64 编码 然后进行 base64 解码 Buffer.from(quiz, 'base64').toString('utf-8')
                    let quiz = JSON.parse(res2).data.quiz
                    item1[k].children = JSON.parse(Buffer.from(quiz, 'base64').toString('utf-8'))

                    // 存储json
                    fs.writeFileSync(`${content.cat['0'].title}.json`, JSON.stringify(content));
                }
            }
        }
    }
}).catch(e => {
    console.log('错误信息', e)
})

这个是针对于一些请求能够直接爬取的 还有的需要爬取页面的数据 进行处理 然后再进行请求 然后就是后续的这种

3. 爬取页面 再根据页面处理的数据进行请求 (爬取的是需要登录才能获取的数据)

首先这个数据是需要登录才能够进行获取的我们需要先进行登录

1. 引入各种需要的模块

其他的模块都不需要介绍了 上面已经使用过了
crypto-js 用来进行解密的模块
因为这个网址的部分数据进行了 AES 加密 然后我依赖于这个模块进行了解密

const request = require('request')
const cheerio = require('cheerio')
const cryptoJS = require('crypto-js') // 
const fs = require('fs')
2. 配置请求的网址

这个网站的爬取分为获取网站的信息 进行解析 然后才会发送请求获取数据

// 网站的网址
const host = 'https://www.xs507.com'
// 发送请求 获取课程的网址
const httpUrl = 'https://www.xs507.com/Tiku/NewKnows/get_ajax_items.html'
// 需要获取的页面的地址
const getUrl = 'https://www.xs507.com/Tiku/index/index/pid/1/id/58.html'
3. 配置请求头

因为这个网站需要登录才能爬取 需要设置请求头 模拟我们自己操作
每个人的请求头可能不一样 然后配置也可能不一样 *仅供参考

const headers = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Cache-Control': 'max-age=0',
    'Connection': 'keep-alive',
    'Host': 'www.xs507.com',
    'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="99", "Google Chrome";v="99"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': "Windows",
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate',
    'Sec-Fetch-Site': 'none',
    'Sec-Fetch-User': '?1',
    'Upgrade-Insecure-Requests': '1',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36',
    // 输入自己登录的 Cookie
    'Cookie': 'PHPSESSID=kv4jvjqmi5tqqkekrnm2qv3tb0; UM_distinctid=180259640aa7a7-054dec18d7a3b3-977173c-1fa400-180259640abc5b; CNZZDATA1278706964=1905803780-1649893603-%7C1649914353; acw_tc=2f61f27416499189816934234e7a3aaaeafb5c01b29633b64674ae02e2dc5e; remember_me=b737750f274be9fe6b8d47f9a1b13b03',
}
4. 封装解密函数

上面也说过 这个网站的部分数据进行的 AES 加密 然后这里封装了一个解密函数 方便后续使用

// 解密
let decryptAes = (str) => {
    let KEY = "ahsw3408814abcde";
    let IV = "7412369850741852";
    //密钥16位
    let key = cryptoJS.enc.Utf8.parse(KEY);
    //加密向量16位
    let iv = cryptoJS.enc.Utf8.parse(IV);
    let decrypted = cryptoJS.AES.decrypt(str, key, {
        iv: iv,
        mode: cryptoJS.mode.CBC,
        padding: cryptoJS.pad.ZeroPadding
    });
    let textStr = decrypted.toString(cryptoJS.enc.Utf8);
    // 不知道为啥 解密成功之后 有些数据有一些 unicode 码 然后下面进行处理
    let urlCode = encodeURI(textStr);
    let newStr = urlCode.replace(/\%((0[1-9A-F])|10)/gi, "");
    return decodeURI(newStr);
}
5. 封装删除 html 标签的方法

因为我这边需要标题什么的带 html 标签 所以我封装了一个这样的方法

let deleteHtmlTag = (content) => {
    return content.replace(/<.*?>/ig, '')
}
6. 封装请求接口

因为这个爬取的不只是有请求 还有 html 页面 所以这里封装了两次

// 获取数据的接口
const requestPromise = (url) => {
    return new Promise((resolve, reject) => {
        request(url, { strictSSL: false, headers }, (error, res, body) => {
            if (res.statusCode === 200) {
                let data = JSON.parse(res.body)
                resolve(data.data.itemsList)
            } else {
                console.log('请求失败的结果', error)
                reject(error)
            }
        })
    })
}
// 获取 html 页面的接口
const htmlRequestPromise = (url) => {
    return new Promise((resolve, reject) => {
        request(url, { strictSSL: false, headers }, (error, res, body) => {
            if (res.statusCode === 200) {
                resolve(res.body)
            } else {
                console.log('请求失败的结果', error)
                reject(error)
            }
        })
    })
}
7. 封装获取数据的接口

这个只是使用于这个网站的逻辑 假如想要获取别的网站 请进行处理

let getData = async (content = {}) => {
    if (content.chapter.length > 0) {
        for (let ci in content.chapter) {
            (function (t) {   // 注意这里是形参
                setTimeout( async () => {
                    let section = content.chapter[ci].section
                    for (let si in section) {
                        (function (t) {   // 注意这里是形参
                            setTimeout( async () => {
                                let res = await requestPromise(httpUrl + `?product_id=${content.product_id}&subject_id=${content.subject_id}&know_id=${section[si].id}`)
                                for (let i in res) {
                                    for (let k in res[i]) {
                                        let item = res[i][k].options
                                        let arr = []
                                        // 处理答案
                                        let answer = res[i][k].answer.split(',')
                                        let answerMap = {}
                                        for (let an in answer) {
                                            answerMap[answer[an]] = answer[an]
                                        }
                                        for (let j in item) {
                                            arr.push([
                                                item[j].content,
                                                answerMap[item[j].id] ? 1 : 0
                                            ])
                                        }
                                        let title = ''
                                        if (res[i][k].mtype === 1) { // 加密单选
                                            title = decryptAes(res[i][k].content)
                                        } else if (res[i][k].mtype === 2) { // 加密多选
                                            title = decryptAes(res[i][k].content)
                                        } else if (res[i][k].mtype === 6) { // 共享题干 (拼接名称)
                                            title = decryptAes(res[i][k].title) + res[i][k].content
                                        } else { // 其他
                                            title = res[i][k].content
                                        }
                                        // 添加卡片
                                        content.chapter[ci].section[si].card.push({
                                            // 删除 html 标签
                                            title: deleteHtmlTag(title),
                                            body: JSON.stringify(arr),
                                            description: res[i][k].analysis,
                                            // type === 1的是单选 type === 2 多选 剩余默认为图文
                                            type: res[i][k].type === 1 ? 3 : res[i][k].type === 2 ? 4 : 1
                                        })
                                    }
                                }
                                // 存储json
                                fs.writeFileSync(`${content.title}.json`, JSON.stringify(content));
                            }, 1000 * t)	// 还是每秒执行一次,不是累加的
                        })(si)   // 注意这里是实参,这里把要用的参数传进去
                    }
                }, 1000 * t)	// 还是每秒执行一次,不是累加的
            })(ci)   // 注意这里是实参,这里把要用的参数传进去
        }
    }
}
8. 请求数据

和上述一样

// 解析 html 最后获取到需要的数据的格式
htmlRequestPromise(getUrl).then((res) => {
    const $ = cheerio.load(res) // 这里就是类似于转化成功  然后能够使用 jquery 的一些 api 了
    $('.page-list-question>li>.title').each((i, item) => {
        // url /Tiku/Product/index/product_id/485.html 以 '/' 截取 获取最后一个 然后以 '.' 截取 获取第一个 获取到id (转化类型)
        let courseUrl = $(item).attr('href')
        let urlArr = $(item).attr('href').split('/')
        // 添加一个新的课程
        course.push({
            title: $(item).text(),
            product_id: parseInt(urlArr[urlArr.length - 1].split('.')[0]),
            chapter: []
        })
        // 发送请求 获取这个关于这个课程的页面
        htmlRequestPromise(host + courseUrl).then((chapterHtml) => {
            const $1 = cheerio.load(chapterHtml)
            $1(".question-conten-list>.big").each((a, item) => {
                let chapterArr = $1(item).find('.title').text().split('\n')
                 // 有些课程章节下面就是卡片 针对于这种章节做一下处理
                if ($1(item).find('.btns>.Btn>.a1').attr('href')) {
                    // 添加一个章节
                    course[i].chapter.push({
                        title: chapterArr[0].trim(),
                        section: []
                    })
                    // 获取到这个节下面的数据
                    let aloneHref = $1(item).find('.btns>.Btn>.a1').attr('href').split('&')
                    aloneHref.forEach((itemAlone) => {
                        // 获取章节 ID
                        if (itemAlone.indexOf('know_id') !== -1) {
                            course[i].chapter[a].section.push({
                                title: chapterArr[0].trim(),
                                id: parseInt(itemAlone.split('=')[1]),
                                card: []
                            })
                        }
                        // 找到课程的 subject_id
                        if (itemAlone.indexOf('subject_id') !== -1) {
                            course[i].subject_id = parseInt(itemAlone.split('=')[1])
                        }
                    })
                } else { // 正常的章节
                    // 添加一个章
                    course[i].chapter.push({
                        title: chapterArr[1].trim(),
                        section: []
                    })
                    let secArr = []
                    $1(item).next().find('.title').each((b, itemb) => {
                        secArr.push($1(itemb).text().trim())
                    })
                    secArr.forEach((itemSec) => {
                        if (itemSec.indexOf('免费') !== -1) {
                            let secTitle = itemSec.split('\n')[0].trim()
                            let secHref = $1(item).next().find('.a3').attr('href').split('&')
                            secHref.forEach((itemHref) => {
                                // 获取章节 ID
                                if (itemHref.indexOf('know_id') !== -1) {
                                    course[i].chapter[a].section.push({
                                        title: secTitle,
                                        id: parseInt(itemHref.split('=')[1]),
                                        card: []
                                    })
                                }
                                // 找到课程的 subject_id
                                if (itemHref.indexOf('subject_id') !== -1) {
                                    course[i].subject_id = parseInt(itemHref.split('=')[1])
                                }
                            })
                        }
                    })
                }
            })
            // 删除空的章节
            for (let c = 0; c < course[i].chapter.length; c++) {
                if (course[i].chapter[c].section.length === 0) {
                    course[i].chapter.splice(c, 1)
                    c--
                }
            }
            // 发送请求 获取课程数据
            getData(course[i]).then()
        })
    })
}).catch(err => {
    console.log('请求失败的数据', err)
})

你可能感兴趣的:(node.js,javascript,前端)