基于TypeScript从0到1搭建一款爬虫工具

时间:2021-07-19 16:18:57

基于TypeScript从0到1搭建一款爬虫工具

前言

今天,我们将使用TS这门语言搭建一款爬虫工具。目标网址是什么呢?我们去上网一搜,经过几番排查之后,我们选定了这一个网站。

https://www.hanju.run/

一个视频网站,我们的目的主要是爬取这个网站上视频的播放链接。下面,我们就开始进行第一步。

第一步

俗话说,万事开头难。不过对于这个项目而言,恰恰相反。你需要做以下几个事情:

1.我们需要创建一个项目文件夹

2.键入命令,初始化项目

  1. npm init -y 

3.局部安装typescript

  1. npm install typescript -D 

4.接着键入命令,生成ts配置文件

  1. tsc --init 

5.局部安装ts-node,用于命令行输出命令

  1. npm install -D ts-node 

6.在项目文件夹中创建一个src文件夹

然后我们在src文件夹中创建一个crawler.ts文件。

7.在package.json文件中修改快捷启动命令

  1. "scripts": { 
  2.     "dev-t""ts-node ./src/crawler.ts" 
  3.   } 

第二步

接下来,我们将进行实战操作,也就是上文中crawler.ts文件是我们的主战场。

我们首先需要引用的这几个依赖,分别是

  1. import superagent from "superagent"
  2. import cheerio from "cheerio"
  3. import fs from "fs"
  4. import path from "path"

所以,我们会这样安装依赖:

superagent作用是获取远程网址html的内容。

  1. npm install superagent 

cheerio作用是可以通过jQ语法获取页面节点的内容。

  1. npm install cheerio 

剩余两个依赖fs,path。它们是node内置依赖,直接引入即可。

我们完成了安装依赖,但是会发现你安装的依赖上会有红色报错。原因是这样的,superagent和cheerio内部都是用JS写的,并不是TS写的,而我们现在的环境是TS。所以我们需要翻译一下,我们将这种翻译文件又称类型定义文件(以.d.ts为后缀)。我们可以使用以下命令安装类型定义文件。

  1. npm install -D @types/superagent 

  1. npm install -D @types/cheerio 

接下来,我们就认认真真看源码了。

1.安装完两个依赖后,我们需要创建一个Crawler类,并且将其实例化。

  1. import superagent from "superagent"
  2. import cheerio from "cheerio"
  3. import fs from "fs"
  4. import path from "path"
  5.  
  6. class Crawler { 
  7.   constructor() { 
  8.      
  9.   } 
  10.  
  11. const crawler = new Crawler(); 

2.我们确定下要爬取的网址,然后赋给一个私有变量。最后我们会封装一个getRawHtml方法来获取对应网址的内容。

getRawHtml方法中我们使用了async/await关键字,主要用于异步获取页面内容,然后返回值。

  1. import superagent from "superagent"
  2. import cheerio from "cheerio"
  3. import fs from "fs"
  4. import path from "path"
  5.  
  6. class Crawler { 
  7.   private url = "https://www.hanju.run/play/39221-4-0.html"
  8.  
  9.   async getRawHtml() { 
  10.     const result = await superagent.get(this.url); 
  11.     return result.text; 
  12.   } 
  13.  
  14.   async initSpiderProcess() { 
  15.     const html = await this.getRawHtml(); 
  16.   } 
  17.  
  18.   constructor() { 
  19.     this.initSpiderProcess(); 
  20.   } 
  21.  
  22. const crawler = new Crawler(); 

3.使用cheerio依赖内置的方法获取对应的节点内容。

我们通过getRawHtml方法异步获取网页的内容,然后我们传给getJsonInfo这个方法,注意是string类型。我们这里通过cheerio.load(html)这条语句处理,就可以通过jQ语法来获取对应的节点内容。我们获取到了网页中视频的标题以及链接,通过键值对的方式添加到一个对象中。注:我们在这里定义了一个接口,定义键值对的类型。

  1. import superagent from "superagent"
  2. import cheerio from "cheerio"
  3. import fs from "fs"
  4. import path from "path"
  5.  
  6. interface Info { 
  7.   name: string; 
  8.   url: string; 
  9.  
  10. class Crawler { 
  11.   private url = "https://www.hanju.run/play/39221-4-0.html"
  12.  
  13.   getJsonInfo(html: string) { 
  14.     const $ = cheerio.load(html); 
  15.     const info: Info[] = []; 
  16.     const scpt: string = String($(".play>script:nth-child(1)").html()); 
  17.     const url = unescape( 
  18.       scpt.split(";")[3].split("(")[1].split(")")[0].replace(/\"/g, "") 
  19.     ); 
  20.     const name: string = String($("title").html()); 
  21.     info.push({ 
  22.       name
  23.       url, 
  24.     }); 
  25.     const result = { 
  26.       time: new Date().getTime(), 
  27.       data: info, 
  28.     }; 
  29.     return result; 
  30.   } 
  31.  
  32.   async getRawHtml() { 
  33.     const result = await superagent.get(this.url); 
  34.     return result.text; 
  35.   } 
  36.  
  37.   async initSpiderProcess() { 
  38.     const html = await this.getRawHtml(); 
  39.     const info = this.getJsonInfo(html); 
  40.   } 
  41.  
  42.   constructor() { 
  43.     this.initSpiderProcess(); 
  44.   } 
  45.  
  46. const crawler = new Crawler(); 

4.我们首先要在项目根目录下创建一个data文件夹。然后我们将获取的内容我们存入文件夹内的url.json文件(文件自动生成)中。

我们将其封装成getJsonContent方法,在这里我们使用了path.resolve来获取文件的路径。fs.readFileSync来读取文件内容,fs.writeFileSync来将内容写入文件。注:我们分别定义了两个接口objJson与InfoResult。

  1. import superagent from "superagent"
  2. import cheerio from "cheerio"
  3. import fs from "fs"
  4. import path from "path"
  5.  
  6. interface objJson { 
  7.   [propName: number]: Info[]; 
  8.  
  9. interface Info { 
  10.   name: string; 
  11.   url: string; 
  12.  
  13. interface InfoResult { 
  14.   time: number; 
  15.   data: Info[]; 
  16.  
  17. class Crawler { 
  18.   private url = "https://www.hanju.run/play/39221-4-0.html"
  19.  
  20.   getJsonInfo(html: string) { 
  21.     const $ = cheerio.load(html); 
  22.     const info: Info[] = []; 
  23.     const scpt: string = String($(".play>script:nth-child(1)").html()); 
  24.     const url = unescape( 
  25.       scpt.split(";")[3].split("(")[1].split(")")[0].replace(/\"/g, "") 
  26.     ); 
  27.     const name: string = String($("title").html()); 
  28.     info.push({ 
  29.       name
  30.       url, 
  31.     }); 
  32.     const result = { 
  33.       time: new Date().getTime(), 
  34.       data: info, 
  35.     }; 
  36.     return result; 
  37.   } 
  38.  
  39.   async getRawHtml() { 
  40.     const result = await superagent.get(this.url); 
  41.     return result.text; 
  42.   } 
  43.  
  44.   getJsonContent(info: InfoResult) { 
  45.     const filePath = path.resolve(__dirname, "../data/url.json"); 
  46.     let fileContent: objJson = {}; 
  47.     if (fs.existsSync(filePath)) { 
  48.       fileContent = JSON.parse(fs.readFileSync(filePath, "utf-8")); 
  49.     } 
  50.     fileContent[info.time] = info.data; 
  51.     fs.writeFileSync(filePath, JSON.stringify(fileContent)); 
  52.   } 
  53.  
  54.   async initSpiderProcess() { 
  55.     const html = await this.getRawHtml(); 
  56.     const info = this.getJsonInfo(html); 
  57.     this.getJsonContent(info); 
  58.   } 
  59.  
  60.   constructor() { 
  61.     this.initSpiderProcess(); 
  62.   } 
  63.  
  64. const crawler = new Crawler(); 

5.运行命令

  1. npm run dev-t 

6.查看生成文件的效果

  1.   "1610738046569": [ 
  2.     { 
  3.       "name""《复仇者联盟4:终局之战》HD1080P中字m3u8在线观看-韩剧网"
  4.       "url""https://wuxian.xueyou-kuyun.com/20190728/16820_302c7858/index.m3u8" 
  5.     } 
  6.   ], 
  7.   "1610738872042": [ 
  8.     { 
  9.       "name""《钢铁侠2》HD高清m3u8在线观看-韩剧网"
  10.       "url""https://www.yxlmbbs.com:65/20190920/54uIR9hI/index.m3u8" 
  11.     } 
  12.   ], 
  13.   "1610739069969": [ 
  14.     { 
  15.       "name""《钢铁侠2》中英特效m3u8在线观看-韩剧网"
  16.       "url""https://tv.youkutv.cc/2019/11/12/mjkHyHycfh0LyS4r/playlist.m3u8" 
  17.     } 
  18.   ] 

准结语

到这里真的结束了吗?

不!不!不!

真的没有结束。

我们会看到上面一坨代码,真的很臭~

我们将分别使用组合模式与单例模式将其优化。

优化一:组合模式

组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。

简言之,就是可以像处理简单元素一样来处理复杂元素。

首先,我们在src文件夹下创建一个combination文件夹,然后在其文件夹下分别在创建两个文件crawler.ts和urlAnalyzer.ts。

crawler.ts

crawler.ts文件的作用主要是处理获取页面内容以及存入文件内。

  1. import superagent from "superagent"
  2. import fs from "fs"
  3. import path from "path"
  4. import UrlAnalyzer from "./urlAnalyzer.ts"
  5.  
  6. export interface Analyzer { 
  7.   analyze: (html: string, filePath: string) => string; 
  8.  
  9. class Crowller { 
  10.   private filePath = path.resolve(__dirname, "../../data/url.json"); 
  11.  
  12.   async getRawHtml() { 
  13.     const result = await superagent.get(this.url); 
  14.     return result.text; 
  15.   } 
  16.  
  17.   writeFile(content: string) { 
  18.     fs.writeFileSync(this.filePath, content); 
  19.   } 
  20.  
  21.   async initSpiderProcess() { 
  22.     const html = await this.getRawHtml(); 
  23.     const fileContent = this.analyzer.analyze(html, this.filePath); 
  24.     this.writeFile(fileContent); 
  25.   } 
  26.  
  27.   constructor(private analyzer: Analyzer, private url: string) { 
  28.     this.initSpiderProcess(); 
  29.   } 
  30. const url = "https://www.hanju.run/play/39257-1-1.html"
  31.  
  32. const analyzer = new UrlAnalyzer(); 
  33. new Crowller(analyzer, url); 

urlAnalyzer.ts

urlAnalyzer.ts文件的作用主要是处理获取页面节点内容的具体逻辑。

  1. import cheerio from "cheerio"
  2. import fs from "fs"
  3. import { Analyzer } from "./crawler.ts"
  4.  
  5. interface objJson { 
  6.   [propName: number]: Info[]; 
  7. interface InfoResult { 
  8.   time: number; 
  9.   data: Info[]; 
  10. interface Info { 
  11.   name: string; 
  12.   url: string; 
  13.  
  14. export default class UrlAnalyzer implements Analyzer { 
  15.   private getJsonInfo(html: string) { 
  16.     const $ = cheerio.load(html); 
  17.     const info: Info[] = []; 
  18.     const scpt: string = String($(".play>script:nth-child(1)").html()); 
  19.     const url = unescape( 
  20.       scpt.split(";")[3].split("(")[1].split(")")[0].replace(/\"/g, "") 
  21.     ); 
  22.     const name: string = String($("title").html()); 
  23.     info.push({ 
  24.       name
  25.       url, 
  26.     }); 
  27.     const result = { 
  28.       time: new Date().getTime(), 
  29.       data: info, 
  30.     }; 
  31.     return result; 
  32.   } 
  33.  
  34.   private getJsonContent(info: InfoResult, filePath: string) { 
  35.     let fileContent: objJson = {}; 
  36.     if (fs.existsSync(filePath)) { 
  37.       fileContent = JSON.parse(fs.readFileSync(filePath, "utf-8")); 
  38.     } 
  39.     fileContent[info.time] = info.data; 
  40.     return fileContent; 
  41.   } 
  42.  
  43.   public analyze(html: string, filePath: string) { 
  44.     const info = this.getJsonInfo(html); 
  45.     console.log(info); 
  46.     const fileContent = this.getJsonContent(info, filePath); 
  47.     return JSON.stringify(fileContent); 
  48.   } 

可以在package.json文件中定义快捷启动命令。

  1. "scripts": { 
  2.   "dev-c""ts-node ./src/combination/crawler.ts" 
  3. }, 

然后使用npm run dev-c启动即可。

优化二:单例模式

**单例模式(Singleton Pattern)**是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

应用实例:

  • 1、一个班级只有一个班主任。
  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

同样,我们在src文件夹下创建一个singleton文件夹,然后在其文件夹下分别在创建两个文件crawler1.ts和urlAnalyzer.ts。

这两个文件的作用与上文同样,只不过代码书写不一样。

crawler1.ts

  1. import superagent from "superagent"
  2. import fs from "fs"
  3. import path from "path"
  4. import UrlAnalyzer from "./urlAnalyzer.ts"
  5.  
  6. export interface Analyzer { 
  7.   analyze: (html: string, filePath: string) => string; 
  8.  
  9. class Crowller { 
  10.   private filePath = path.resolve(__dirname, "../../data/url.json"); 
  11.  
  12.   async getRawHtml() { 
  13.     const result = await superagent.get(this.url); 
  14.     return result.text; 
  15.   } 
  16.  
  17.   private writeFile(content: string) { 
  18.     fs.writeFileSync(this.filePath, content); 
  19.   } 
  20.  
  21.   private async initSpiderProcess() { 
  22.     const html = await this.getRawHtml(); 
  23.     const fileContent = this.analyzer.analyze(html, this.filePath); 
  24.     this.writeFile(JSON.stringify(fileContent)); 
  25.   } 
  26.  
  27.   constructor(private analyzer: Analyzer, private url: string) { 
  28.     this.initSpiderProcess(); 
  29.   } 
  30. const url = "https://www.hanju.run/play/39257-1-1.html"
  31.  
  32. const analyzer = UrlAnalyzer.getInstance(); 
  33. new Crowller(analyzer, url); 

urlAnalyzer.ts

  1. import cheerio from "cheerio"
  2. import fs from "fs"
  3. import { Analyzer } from "./crawler1.ts"
  4.  
  5. interface objJson { 
  6.   [propName: number]: Info[]; 
  7. interface InfoResult { 
  8.   time: number; 
  9.   data: Info[]; 
  10. interface Info { 
  11.   name: string; 
  12.   url: string; 
  13. export default class UrlAnalyzer implements Analyzer { 
  14.   static instance: UrlAnalyzer; 
  15.  
  16.   static getInstance() { 
  17.     if (!UrlAnalyzer.instance) { 
  18.       UrlAnalyzer.instance = new UrlAnalyzer(); 
  19.     } 
  20.     return UrlAnalyzer.instance; 
  21.   } 
  22.  
  23.   private getJsonInfo(html: string) { 
  24.     const $ = cheerio.load(html); 
  25.     const info: Info[] = []; 
  26.     const scpt: string = String($(".play>script:nth-child(1)").html()); 
  27.     const url = unescape( 
  28.       scpt.split(";")[3].split("(")[1].split(")")[0].replace(/\"/g, "") 
  29.     ); 
  30.     const name: string = String($("title").html()); 
  31.     info.push({ 
  32.       name
  33.       url, 
  34.     }); 
  35.     const result = { 
  36.       time: new Date().getTime(), 
  37.       data: info, 
  38.     }; 
  39.     return result; 
  40.   } 
  41.  
  42.   private getJsonContent(info: InfoResult, filePath: string) { 
  43.     let fileContent: objJson = {}; 
  44.     if (fs.existsSync(filePath)) { 
  45.       fileContent = JSON.parse(fs.readFileSync(filePath, "utf-8")); 
  46.     } 
  47.     fileContent[info.time] = info.data; 
  48.     return fileContent; 
  49.   } 
  50.  
  51.   public analyze(html: string, filePath: string) { 
  52.      const info = this.getJsonInfo(html); 
  53.      console.log(info); 
  54.     const fileContent = this.getJsonContent(info, filePath); 
  55.     return JSON.stringify(fileContent); 
  56.   } 
  57.  
  58.   private constructor() {} 

可以在package.json文件中定义快捷启动命令。

  1. "scripts": { 
  2.     "dev-s""ts-node ./src/singleton/crawler1.ts"
  3.  }, 

然后使用npm run dev-s启动即可。

结语

这下真的结束了,谢谢阅读。希望可以帮到你。

完整源码地址:

https://github.com/maomincoding/TsCrawler

原文地址:https://mp.weixin.qq.com/s/Q8AJsDqrQAy9NQdv6Ykg3w