创建TS项目

  1. npm init -y // 初始化npm 项目
  2. tsc --init // 在目录下生成 tsconfig.json 文件 在这里我们可以对ts的默认配置做修改
  3. npm install ts-node -D
  4. npm install typescript -D

这里需要注意一下,我们如果在全局安装了 ts-node或者 ts,建议卸载掉,在项目中安装,以避免一些问题。

这是部分的package.json中的内容,

  1. {
  2. "scripts": {
  3. "dev": "ts-node ./src/crawler.ts"
  4. },
  5. "devDependencies": {
  6. "ts-node": "^10.4.0",
  7. "typescript": "^4.5.2"
  8. }
  9. }

声明文件 .d.ts

我们可以通过 npm install superagent -s 来安装superagent,这是一个是一个轻量级、灵活的、易读的、低学习曲线的客户端请求代理模块,使用在NodeJS环境中。

我们在代码中引入 import superagent from 'superagent'; 这时候会有报错提示:
image.png
并且我们在ts文件中调用 superagent.xxx 的时候没有任何的语法提示。这是因为superagent 是用js语法开发的,并不是ts语法。
在ts文件中,直接引入js文件的时候,ts并不知道我们所引入的类库中的方法是什么,方法中的参数是什么。
所以 ts 直接引入 js会飘红,所以,我们可以写一个 .d.ts 为后缀的类型定义文件。

ts -> .d.ts 翻译文件 -> js

所以,直接在ts中引入js,ts不知道怎么使用,我们可以提供这个翻译文件,我们只要把翻译文件安装上了,则ts会先去读取翻译文件,翻译文件会帮助ts去理解js中的内容,这样就可以用这个js文件了。

我们要先安装翻译文件:npm install @types/superagent -D, 我们安装了这个插件后其实已经没有报错了。

实际上,我们的JS文件中是没有这些类型声明的。翻译文件,相当于把js文件中的类型声明补全了。这2个结合起来,就可以让TS去识别js文件了。

翻译文件,一般都在 @types 这样的命名空间中。

爬虫内容准备

爬虫需要对页面上的数据做一些提取和分析,
我们可以通过安装 cheerio 这样的库,通过类似jQuery这样的语法,来提取页面上数据。

npm install cheerio -s
如果飘红提示没有翻译文件,我们可以:npm install @types/cheerio -D

1- 在这里我希望可以把我拿到的数据存储到data目录中去,所以我们在根目录中创建data目录
2- 我们需要引入node中的 fs 和 path 模块

  1. import fs from 'fs';
  2. import path from 'path';
  3. import superagent from 'superagent';
  4. import cheerio from 'cheerio';
  5. interface Book {
  6. img: string | undefined;
  7. title: string;
  8. rating: number;
  9. }
  10. interface BookResult {
  11. time: number;
  12. data: Book[];
  13. }
  14. interface Content {
  15. [propName: number]: Book[];
  16. }
  17. class Crawler {
  18. private url = 'https://book.douban.com/top250';
  19. private filePath = path.resolve(__dirname, '../data/crawler.json');
  20. getJSONInfo(html: string) {
  21. const $ = cheerio.load(html);
  22. const bookItems = $('.indent table>tbody');
  23. const bookInfos: Book[] = [];
  24. bookItems.map((index, element) => {
  25. const book = $(element).find('td');
  26. const img = book.eq(0).find('img').attr('src');
  27. const title = book.eq(1).children('p.pl').text();
  28. const rating = parseFloat(book.eq(1).find('.rating_nums').text());
  29. bookInfos.push({ img, title, rating });
  30. });
  31. return {
  32. time: new Date().getTime(),
  33. data: bookInfos,
  34. };
  35. }
  36. async getRawHtml() {
  37. const result = await superagent.get(this.url);
  38. return result.text;
  39. }
  40. generateJsonContent(bookResult: BookResult) {
  41. let fileContent: Content = {};
  42. if (fs.existsSync(this.filePath)) {
  43. fileContent = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
  44. }
  45. fileContent[bookResult.time] = bookResult.data;
  46. return fileContent;
  47. }
  48. writeFile(content: string) {
  49. fs.writeFileSync(this.filePath, content);
  50. }
  51. async initSpiderProcess() {
  52. const html = await this.getRawHtml();
  53. const result = this.getJSONInfo(html);
  54. const fileContent = this.generateJsonContent(result);
  55. this.writeFile(JSON.stringify(fileContent));
  56. }
  57. constructor() {
  58. this.initSpiderProcess();
  59. }
  60. }
  61. const crawler = new Crawler();

因为代码的定制性太强了,导致我们只能针对指定的页面做爬取分析,如果换了网页就无法爬取了,我们可以对代码进行一些改造。
把现在的代码拆分成2个部分,一部分是爬虫工具,一部分是爬取策略。

代码拆解:
1- 爬虫工具:

  1. // crawler.ts
  2. import fs from 'fs';
  3. import path from 'path';
  4. import superagent from 'superagent';
  5. import DoubanAnalyzer from './doubanAnalyzer';
  6. export interface Analyzer {
  7. analyzer: (html: string, filePath: string) => string;
  8. }
  9. class Crawler {
  10. private filePath = path.resolve(__dirname, '../data/crawler.json');
  11. async getRawHtml() {
  12. const result = await superagent.get(this.url);
  13. return result.text;
  14. }
  15. writeFile(content: string) {
  16. fs.writeFileSync(this.filePath, content);
  17. }
  18. async initSpiderProcess() {
  19. const html = await this.getRawHtml();
  20. const fileContent = this.analyzer.analyzer(html, this.filePath);
  21. this.writeFile(fileContent);
  22. }
  23. constructor(private url: string, private analyzer: Analyzer) {
  24. this.initSpiderProcess();
  25. }
  26. }
  27. const url = 'https://book.douban.com/top250';
  28. const analyzer = new DoubanAnalyzer();
  29. new Crawler(url, analyzer);
  30. /*
  31. 爬虫需要对页面上的数据做一些提取和分析,
  32. 我们可以通过安装 cheerio 这样的库,通过类似jQuery这样的语法,来提取页面上数据。
  33. npm install cheerio -s
  34. 如果飘红提示没有翻译文件,我们可以:npm install @types/cheerio -D
  35. 1- 在这里我希望可以把我拿到的数据存储到data目录中去,所以我们在根目录中创建data目录
  36. 2- 我们需要引入node中的fs 和 path 模块
  37. 因为代码的定制性太强了,导致我们只能针对指定的页面做爬取分析,如果换了网页就无法爬取了,我们可以对代码进行一些改造。
  38. 把现在的代码拆分成2个部分,一部分是爬虫工具,一部分是爬取策略。
  39. */

2- 爬取策略:

  1. // doubanAnalyzer.ts
  2. import fs from 'fs';
  3. import cheerio from 'cheerio';
  4. import { Analyzer } from './crawler';
  5. interface Book {
  6. img: string | undefined;
  7. title: string;
  8. rating: number;
  9. }
  10. interface BookResult {
  11. time: number;
  12. data: Book[];
  13. }
  14. interface Content {
  15. [propName: number]: Book[];
  16. }
  17. export default class DoubanAnalyzer implements Analyzer {
  18. private getJSONInfo(html: string) {
  19. const $ = cheerio.load(html);
  20. const bookItems = $('.indent table>tbody');
  21. const bookInfos: Book[] = [];
  22. bookItems.map((index, element) => {
  23. const book = $(element).find('td');
  24. const img = book.eq(0).find('img').attr('src');
  25. const title = book.eq(1).children('p.pl').text();
  26. const rating = parseFloat(book.eq(1).find('.rating_nums').text());
  27. bookInfos.push({ img, title, rating });
  28. });
  29. return {
  30. time: new Date().getTime(),
  31. data: bookInfos,
  32. };
  33. }
  34. generateJsonContent(bookResult: BookResult, filePath: string) {
  35. let fileContent: Content = {};
  36. if (fs.existsSync(filePath)) {
  37. fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
  38. }
  39. fileContent[bookResult.time] = bookResult.data;
  40. return fileContent;
  41. }
  42. public analyzer(html: string, filePath: string) {
  43. const result = this.getJSONInfo(html);
  44. const fileContent = this.generateJsonContent(result, filePath);
  45. return JSON.stringify(fileContent);
  46. }
  47. }

如果我们想要爬取另外的网页内容,我们只需要去新建一个分析类就行了,而不需要去创建 爬虫工具:

  1. // bookAnalyzer.ts
  2. import { Analyzer } from './crawler';
  3. export default class BookAnalyzer implements Analyzer {
  4. public analyzer(html: string, filePath: string) {
  5. return html;
  6. }
  7. }

这种 爬取策略和爬虫工具组合使用的模式,在设计模式中被叫做:组合模式。

我们如果希望把代码给别人用就需要将代码编译成JS文件:

  1. "scripts": {
  2. "dev": "ts-node ./src/crawler.ts",
  3. "build": "tsc"
  4. },

这里我们运行 npm run build ts会主动去找到对应的ts文件编译成为js。
这样的做法有个不好的地方就是编译的文件会和ts文件在同一个目录下,我们可以为编译的文件制定一个目录。
当我们运行 build 命令的时候,tsc命令会结合tsconfig.json 文件对代码的编译过程进行控制。
我们可以再改文件中进行一些修改:

  1. "outDir": "./build",

这样就可以把编译好的js文件放在build目录中了。

我们也可以通过修改 package.json 中的配置来监控ts文件的修改,事实构建:

  1. "scripts": {
  2. "build": "tsc -w"
  3. },

可以听过nodemon 来监测项目文件的变化:

  1. npm install nodemon -D

然后我们在package.json里面,

  1. "scripts": {
  2. "start": "nodemon node ./build/crawler.js"
  3. },