node.js 这个不用过多介绍
cheerio 抓取页面模块,为服务器特别定制的,快速、灵活、实施的jQuery核心实现。适合各种Web爬虫程序 (摘自百度)
fs node内置的文件模块 可以进行创建以及读取文件
request 用来发送请求的模块 (也可以使用 axios 以及其他)
iconv-lite 把纯 javascript 转化字符编码 (看别人也是这样用的 实际具体效果我也不知道)
又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
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)
})
这个是针对于一些请求能够直接爬取的 还有的需要爬取页面的数据 进行处理 然后再进行请求 然后就是后续的这种
首先这个数据是需要登录才能够进行获取的我们需要先进行登录
其他的模块都不需要介绍了 上面已经使用过了
crypto-js 用来进行解密的模块
因为这个网址的部分数据进行了 AES 加密 然后我依赖于这个模块进行了解密
const request = require('request')
const cheerio = require('cheerio')
const cryptoJS = require('crypto-js') //
const fs = require('fs')
这个网站的爬取分为获取网站的信息 进行解析 然后才会发送请求获取数据
// 网站的网址
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'
因为这个网站需要登录才能爬取 需要设置请求头 模拟我们自己操作
每个人的请求头可能不一样 然后配置也可能不一样 *仅供参考
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',
}
上面也说过 这个网站的部分数据进行的 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);
}
因为我这边需要标题什么的带 html 标签 所以我封装了一个这样的方法
let deleteHtmlTag = (content) => {
return content.replace(/<.*?>/ig, '')
}
因为这个爬取的不只是有请求 还有 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)
}
})
})
}
这个只是使用于这个网站的逻辑 假如想要获取别的网站 请进行处理
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) // 注意这里是实参,这里把要用的参数传进去
}
}
}
和上述一样
// 解析 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)
})