前言
最近接触到typescript,学习了一些基础的知识,刚好又是周末有时间,想着能不能用ts来做点什么东西。看到别的小伙伴有用它来写爬虫的,刚好自己也对这块比较感兴趣,就也试着写了一个简单的爬虫,今天跟大家分享下。
前期准备
写爬虫之前我们需要做一些准备工作,下面一起来看一下:
目标
下图是我们本次要爬取的内容,要求获取到每门课程的信息并最终写入到一个json文件中; 目标网址。
环境搭建
我们先创建一个新文件夹依次执行以下命令 初始化项目
npm init -y
生成typescript配置文件tsconfig.json
tsc --init
安装typescript
npm install typescript -D
下面再安装一些项目中所需要的模块
- ts-node(省去编译ts这一步骤,方便调试)
- superagent(用来发送请求)
- cheerio(用来解析获取到的html结构)
相应的,要安装一下对应的类型声明模块,有的小伙伴可能还不清楚是什么意思,举个例子来说,就是superagent是用js来写的,但在我们的typescript中并不能识别它,需要一个翻译文件,这个翻译文件在这就叫做与它对应的类型声明模块
我们安装下这两个类型声明模块:
- @types/superagent
- @types/cheerio
上面列出的这写依赖安装完成后,我们的环境搭建基本就算完成了
项目启动测试
我们修改下package.json的scripts配置:
"scripts": {
"dev": "ts-node ./src/app.ts"
}
然后新建一个src目录,在src目录下创建一个app.ts文件,并在里面写入:
console.log('我想写爬虫');
然后运行npm run dev,能够看到输出了我想写爬虫就表示没有问题了
分析网页并抓取数据
分析网页标签结构
开始写爬虫之前我们需要对我们的目标网页进行结构分析,观察我们所需要的信息存储在那些标签之中,可能让我们更准确的就行爬取;拿我们的这个例子来说,我们随便选择一门课程,右击检查,如下图所示:我们可以看到每一门课程都是放在一个li标签之中
- 课程的名字就存放在对应li标签的
data-name属性中 - 课程报名人数信息则存储在
class=one的p标签下 - 课程的价格信息则存储在
class=two的p标签下的class=price的span标签中
抓取数据
经过上面的分析之后,我们就可以开始写我们的代码了 我们先引入我们所需要的模块
import superagent from 'superagent';
import cheerio from 'cheerio';
const fs = require('fs');//后面写入文件会用到
- 我们先定义一个类,定义一个获取目标网页的方法。这里我们使用superagent来发起请求,同时使用了es6的
async await来处理异步操作;拿到数据之后我们打印结果会发现html结构存储在返回对象的text属性中,所以我们直接把text存储的值return出去。
class Grabcourse {
//存储目标网页地址
private url: string = 'https://coding.imooc.com/?c=fe&sort=0&unlearn=0&page=1';
//获取要爬取页面的html结构
async getHtml() {
const courseHtml = await superagent.get(this.url);
return courseHtml.text;
}
constructor() {
this.getHtml();
}
}
new Grabcourse();
- 然后我们还需要对拿到的html结构进行解析,使用
cheerio模块可以让我们很方便的拿到html中你想要的标签,因为它支持jquery的语法,熟悉的朋友可以非常快速的上手。
我们同样在类上定义一个方法用来解析获取到的html,它接受一个参数值必须是string类型,最终将解析到的结果return出去。
async loadhtml(html: string) {
return cheerio.load(html);
}
- 到了这一步,我们现在需要获取到每一门课程的信息,我们同样在类上定义一个获取课程信息的方法,结合我们上面的分析,用
'.course-list li'选择器可以获取的到所有的课程,然后我们对它进行遍历,拿到其他需要的值并存入到数组中;这里我将函数的参数类型定义为any了不然ts会抛出警告,同时我们还需要定义一个数组来存储课程信息,由于我们知道我们这个数组里的每一项都只有课程名称,课程报名人数信息和价格,所以这里我们可以定义一个接口
interface Course {
courseName: string;
courseType: string;
coursePrice: string;
}
然后在类上加上下面这行代码来规定这个数组里的每一项必须的Course类型
private courseItems:Course[] = [];
获取所有课程信息的相关代码
//获取课程信息
async getCourseInfo($element: any) {
$element('.course-list li').each((idx: any, ele: any) => {
const courseName = $element(ele).attr('data-name');
//发现有空格用replace去除
const courseType = $element(ele).find('.one').text().replace(/\s/g, '');
const coursePrice = $element(ele).find('.two .price').text();
this.courseItems.push({
courseName,
courseType,
coursePrice,
});
});
return this.courseItems;
}
接下来我们需要写一个保存所获取到的课程信息的方法,我们依然在类上定义一个方法,
//保存所获取到的课程
async saveCourseItems(result: Course[]) {
const data = {
course: result
}
//保存逻辑
fs.writeFile('./course.json', JSON.stringify(data), (err: any) => {
if (err) {
console.error(err)
return
}
console.log('文件写入成功。')
})
}
秉着高内聚低耦合的原则,我们前面都是定义的一个个单独的方法,现在我们需要把这些方法放在一个方法内,然后在constructor中来执行这一个单独的方法就可以了,当我们new Grabcourse时,这些爬取的逻辑就会一一的执行,并且阅读起来也更容易
async initSpride() {
const html = await this.getHtml();
const $element = await this.loadhtml(html);
const courseItems = await this.getCourseInfo($element);
this.saveCourseItems(courseItems);
}
constructor() {
this.initSpride();
}
最后运行一下命令npm run dev,就可以看到生成了course.json文件,打开文件就能看到下图所示的数据,就表示我们的爬虫已经成功了
完整代码
import superagent from 'superagent';
import cheerio from 'cheerio';
const fs = require('fs');
interface Course {
courseName: string;
courseType: string;
coursePrice: string;
}
class Grabcourse {
private url: string = 'https://coding.imooc.com/?c=fe&sort=0&unlearn=0&page=1';
private courseItems:Course[] = [];
//获取要爬取页面的html结构
async getHtml() {
const courseHtml = await superagent.get(this.url);
return courseHtml.text;
}
//解析html
async loadhtml(html: string) {
return cheerio.load(html);
}
//获取课程信息
async getCourseInfo($element: any) {
$element('.course-list li').each((idx: any, ele: any) => {
const courseName = $element(ele).attr('data-name');
const courseType = $element(ele).find('.one').text().replace(/\s/g, '');
const coursePrice = $element(ele).find('.two .price').text();
this.courseItems.push({
courseName,
courseType,
coursePrice,
});
});
return this.courseItems;
}
async saveCourseItems(result: Course[]) {
const data = {
course: result
}
//保存逻辑
fs.writeFile('./course.json', JSON.stringify(data), (err: any) => {
if (err) {
console.error(err)
return
}
console.log('文件写入成功。')
})
}
async initSpride() {
const html = await this.getHtml();
const $element = await this.loadhtml(html);
const courseItems = await this.getCourseInfo($element);
this.saveCourseItems(courseItems);
}
constructor() {
this.initSpride();
}
}
new Grabcourse();