使用TypeScript制作爬虫小案例

1,330 阅读5分钟

前言

最近接触到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();