nodejs抓取动态网页(使用nodejs写爬虫过程中常用模块和一些必须掌握的js语法)

优采云 发布时间: 2022-03-25 19:11

  nodejs抓取动态网页(使用nodejs写爬虫过程中常用模块和一些必须掌握的js语法)

  本文是使用nodejs编写爬虫系列教程的第一篇。介绍了使用nodejs编写爬虫过程中常用的模块以及一些必须掌握的js语法

  常用模块

  常用模块如下:

  fs-extrasuperagentcheeriolog4jssequelizechalkpuppeteerfs-extra

  使用async/await的前提是接口必须封装成promise,看一个简单的例子:

  const sleep = (milliseconds) => {

return new Promise((resolve, reject) => {

setTimeout(() => resolve(), milliseconds)

})

}

const main = async () => {

await sleep(5000);

console.log('5秒后...');

}

main();

  在 async 函数中使用 await + promise 来组织异步代码就像同步代码一样,非常自然,有助于我们分析代码的执行流程。

  在 node 中,fs 模块是一个非常常见的用于操作文件的原生模块。fs(文件系统)模块提供了一些与文件系统相关的同步和异步API。有时需要使用同步 API。当一个自写的模块在访问文件后导出一些接口时,这个时候使用同步api是很实用的。看一个例子:

  const path = require('path');

const fs = require('fs-extra');

const { log4js } = require('../../config/log4jsConfig');

const log = log4js.getLogger('qupingce');

const createModels = () => {

const models = {};

const fileNames = fs.readdirSync(path.resolve(__dirname, '.'));

fileNames

.filter(fileName => fileName !== 'index.js')

.map(fileName => fileName.slice(0, -3))

.forEach(modelName => {

log.info(`Sequelize define model ${modelName}!`);

models[modelName] = require(path.resolve(__dirname, `./${modelName}.js`));

})

return models;

}

module.exports = createModels();

  该模块访问当前目录中的所有模型模块并导出模型。如果使用异步接口,即fs.readdir,则无法通过在其他模块中导入该模块来获取模型。原因是 require 是一个同步操作。虽然接口是异步的,但同步代码不能立即获得异步操作的结果。

  为了充分发挥节点异步的优势,我们应该尽量使用异步接口。

  我们可以使用 fs-extra 模块代替 fs 模块,类似的模块是 mz。fs-extra 收录了 fs 模块的所有接口,也为每个异步接口提供了 Promise 支持。更好的是 fs-extra 还提供了一些其他有用的文件操作功能,比如删除和移动文件的操作。更详细的介绍请查看官方仓库 fs-extra。

  超级代理

  superagent是一个节点的http客户端,可以类比java中的httpclient和okhttp,python中的requests。允许我们模拟 http 请求。superagent 库有很多有用的特性。

  superagent会根据response的content-type自动序列化,序列化后的返回内容可以通过response.body获取。这个库会自动缓存和发送cookies,所以我们不需要手动管理cookies然后它的api是链式调用样式,调用起来很爽,但是在使用的时候要注意调用顺序。它的异步 api 都返回承诺。

  有木头很方便。官方文档是一个很长的页面,目录清晰,很容易搜索到自己需要的内容。最后,superagent 还支持插件集成。例如,如果您需要在超时后自动重新发送,您可以使用 superagent-retry。更多插件可以去npm官网搜索关键词superagent-。更多详细信息,请参见官方文档 superagent

  // 官方文档的一个调用示例

request

.post('/api/pet')

.send({ name: 'Manny', species: 'cat' })

.set('X-API-Key', 'foobar')

.set('Accept', 'application/json')

.then(res => {

alert('yay got ' + JSON.stringify(res.body));

});

  切里奥

  写过爬虫的人都知道,我们经常有解析html的需求,从网页的源代码中爬取信息应该是最基本的爬取方式。python中有beautifulsoup,java中有jsoup,node中有cheerio。

  Cheerio 专为服务器端设计,为您提供近乎完整的 jquery 体验。使用cheerio解析html获取元素,调用方式与jquery操作dom元素的用法一模一样。而且它还提供了一些方便的接口,比如获取html,看一个例子:

  const cheerio = require('cheerio')

const $ = cheerio.load('Hello world')

$('h2.title').text('Hello there!')

$('h2').addClass('welcome')

$.html()

//=> Hello there!

  官方仓库:cheerio

  log4js

  log4j 是为 node 设计的日志模块。在简单的场景中,考虑使用调试模块。log4js 更符合我对日志库的需求。其实他们的定位是不一样的。debug 模块是为调试而设计的,而 log4js 是一个日志库。它必须提供文件输出和分类等常规功能。

  log4js模块的名字有点符合java中著名的日志库log4j的节奏。log4j 具有以下特点:

  您可以自定义 appender(输出目标),lo4js 甚至提供了输出到目标(例如邮件)的 appender。通过组合不同的appender,可以达到不同目的的记录器(loggers)提供日志分类功能。官方FAQ中提到如果要实现appender的级别过滤,可以使用logLevelFilter提供滚动日志和自定义输出格式

  让我们通过我最新的爬虫项目的配置文件来感受一下这个库的以下特点:

  const log4js = require('log4js');

const path = require('path');

const fs = require('fs-extra');

const infoFilePath = path.resolve(__dirname, '../out/log/info.log');

const errorFilePath = path.resolve(__dirname, '../out/log/error.log');

log4js.configure({

appenders: {

dateFile: {

type: 'dateFile',

filename: infoFilePath,

pattern: 'yyyy-MM-dd',

compress: false

},

errorDateFile: {

type: 'dateFile',

filename: errorFilePath,

pattern: 'yyyy-MM-dd',

compress: false,

},

justErrorsToFile: {

type: 'logLevelFilter',

appender: 'errorDateFile',

level: 'error'

},

out: {

type: 'console'

}

},

categories: {

default: {

appenders: ['out'],

level: 'trace'

},

qupingce: {

appenders: ['out', 'dateFile', 'justErrorsToFile'],

level: 'trace'

}

}

});

const clear = async () => {

const files = await fs.readdir(path.resolve(__dirname, '../out/log'));

for (const fileName of files) {

fs.remove(path.resolve(__dirname, `../out/log/${fileName}`));

}

}

module.exports = {

log4js,

clear

}

  续集

  在编写项目时,我们经常有持久的需求。在简单的场景中,可以使用 JSON 来保存数据。如果数据量比较大,又容易管理,那么就要考虑使用数据库了。如果是操作mysql和sqllite,推荐使用sequelize。如果是mongodb,我推荐使用专门为mongodb设计的mongoose

  sequelize中还有一些我觉得还不是很好的点,比如默认生成的id(主键)、createdAt和updatedAt。

  除了一些自己造成的故障外,sequelize 设计得很好。内置的操作符、钩子和验证器很有趣。sequelize 还提供了 Promise 和 typescript 支持。如果你使用 typescript 开发项目,还有一个不错的 orm 选择:typeorm。更多信息参见官方文档:sequelize

  粉笔

  chalk 的中文意思是粉笔。该模块是node的一个非常有特色和实用的模块。它可以为您的输出内容添加颜色、下划线、背景颜色和其他装饰。我们在写项目的时候,经常需要记录一些步骤和事件,比如打开数据库链接、发起http请求等等。我们可以适当地使用粉笔来突出显示某些内容,例如在请求的 url 下划线。

  const logRequest = (response, isDetailed = false) => {

const URL = chalk.underline.yellow(response.request.url);

const basicInfo = `${response.request.method} Status: ${response.status} Content-Type: ${response.type} URL=${URL}`;

if (!isDetailed) {

logger.info(basicInfo);

} else {

const detailInfo = `${basicInfo}\ntext: ${response.text}`;

logger.info(detailInfo);

}

};

  调用上面的 logRequest 效果:

  

  有关更多信息,请参阅官方存储库粉笔

  傀儡师

  如果您还没有听说过这个库,那么您可能听说过 selenium。puppeteer 是 Google Chrome 团队开源的一个节点模块,用于通过 devtools 协议操作 chrome 或 Chromium。由 Google 制造,质量有保证。该模块提供了一些高级 API。默认情况下,这个库操作的浏览器的用户是看不到界面的,也就是所谓的无头浏览器。当然也可以通过配置一些参数来启动interfaced模式。在chrome中也有一些记录puppeteer操作的扩展,比如Puppeteer Recorder。使用这个库,我们可以用来获取一些由 js 呈现的信息,而不是直接呈现在页面源代码中。比如spa页面,页面的内容是js渲染的。这时候puppeteer就为我们解决了这个问题。我们可以调用 puppeteer 来获取页面上某个标签出现时渲染出来的 html。事实上,很多高难度爬虫解决的终极法宝就是操纵浏览器。

  前置 js 语法 async/await

  首先要提的是async/await,因为node在很早的时候就已经支持async/await了(node 8 LTS),现在没有理由写没有async/await的后端项目。使用 async/await 可以将我们从回调炼狱中解放出来。这里主要提一下使用async/await时可能遇到的问题

  如何同时使用异步/等待?

  看一段测试代码:

  const sleep = (milliseconds) => {

return new Promise((resolve, reject) => {

setTimeout(() => resolve(), milliseconds)

})

}

const test1 = async () => {

for (let i = 0, max = 3; i {

Array.from({length: 3}).forEach(async () => {

await sleep(1000);

});

}

const main = async () => {

console.time('测试 for 循环使用 await');

await test1();

console.timeEnd('测试 for 循环使用 await');

console.time('测试 forEach 调用 async 函数')

await test2();

console.timeEnd('测试 forEach 调用 async 函数')

}

main();

  运行的结果是:

  测试 for 循环使用 await: 3003.905ms

测试 forEach 调用 async 函数: 0.372ms

  我想有些人可能会认为测试 forEach 的结果会是 1 秒左右,其实测试 2 等价于如下代码:

  const test2 = async () => {

// Array.from({length: 3}).forEach(async () => {

// await sleep(1000);

// });

Array.from({length: 3}).forEach(() => {

sleep(1000);

});

}

  从上面的运行结果也可以看出,直接在for循环中使用await+promise相当于同步调用,所以耗时3秒左右。如果要并发,应该直接调用promise,因为forEach不会帮你await,所以相当于上面的代码,三个任务直接异步并发。

  处理多个异步任务

  上面的代码还有一个问题,就是在测试2中,并没有等到三个任务都执行完就直接结束了。有时我们需要等待多个并发任务结束后再执行后续任务。其实很简单,使用 Promise 提供的几个工具功能就可以了。

  const sleep = (milliseconds, id='') => {

return new Promise((resolve, reject) => {

setTimeout(() => {

console.log(`任务${id}执行结束`)

resolve(id);

}, milliseconds)

})

}

const test2 = async () => {

const tasks = Array.from({length: 3}).map((ele, index) => sleep(1000, index));

const resultArray = await Promise.all(tasks);

console.log({ resultArray} )

console.log('所有任务执行结束');

}

const main = async () => {

console.time('使用 Promise.all 处理多个并发任务')

await test2();

console.timeEnd('使用 Promise.all 处理多个并发任务')

}

main()

  运行结果:

  任务0执行结束

任务1执行结束

任务2执行结束

{ resultArray: [ 0, 1, 2 ] }

所有任务执行结束

使用 Promise.all 处理多个并发任务: 1018.628ms

  除了 Promise.all,Promise 还有race等接口,但最常用的就是all和race。

  正则表达式

  正则表达式是处理字符串的强大工具。核心是匹配,从中衍生出抽取、查找、替换等操作。

  有时当我们通过cheerio获取标签中的文本时,我们需要提取一些信息,这时正则表达式就应该发挥作用了。正则表达式的相关语法在此不做详述。初学者,推荐看廖雪峰的正则表达式教程。让我们看一个例子:

  // 服务器返回的 img url 是: /GetFile/getUploadImg?fileName=9b1cc22c74bc44c8af78b46e0ca4c352.png

// 现在我只想提取文件名,后缀名也不要

const imgUrl = '/GetFile/getUploadImg?fileName=9b1cc22c74bc44c8af78b46e0ca4c352.png';

const imgReg = /\/GetFile\/getUploadImg\?fileName=(.+)\..+/;

const imgName = imgUrl.match(imgReg)[1];

console.log(imgName); // => 9b1cc22c74bc44c8af78b46e0ca4c352

  暂时就介绍到这里,以后会增加更多的内容。

  本文为原创的内容,首发于我的个人博客。转载请注明出处。如有任何问题,请发邮件骚扰。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线