入坑TypeScript(一):使用TypeScript写node脚本爬取小说
一、为何入坑TypeScript?
虽然我不是专业的前端工程师,但是对于TypeScript早有所耳闻,但是对于javascript众多的框架以及第三方库来说,我并没有特别在意(或许这就是专业和业余的差距吧~),我的前端技术栈目前仅仅是 vue.js
以及会点 nodejs
而已,最近的遭遇让我不得不去接触下TypeScript,当然,都跑来写博客了,那要不是非常痛恨ts,要不是就是对ts喜出望外,显然,我就是后者。我如此坚定的入坑原因以下两点:
-
“听说”
在群里听到一些前端大佬说牛逼一定的前端或者js(或node)工程师ts是必须会的(也许你们不一定认同,只是我的圈子里面我认为他们说的有道理而已~),甚至有大佬在讨论说TypeScript解决了许多JavaScript的坑, 虽然我平时不是经常写前端,但是前端多少也会一点,于是乎,TypeScript便在我的心里埋下了一颗种子。
-
“企业用”
近来在网上无聊逛了几个公司的前端产品,发现了很多公司提供的web版的SDK基本都是以TypeScript写的,或者就是也提供JavaScript版本的但是推荐你用TypeScript版本的;甚至我去下载他们的Demo都是用React+TypeScript写的,让我阅读起来异常难受(React不会也是一部分原因吧 o(╥﹏╥)o),我一直以为TypeScript还只是没有发展、还不够成熟的一套东西而已(估计是我孤弱寡闻吧...)
于是乎,我认真的逛了逛官网~
二、我对TypeScript的第一印象
从简单的阅读了官网以及找朋友讨论了下之后,我对TypeScript是满怀期待的,吸引我的是它的严谨(变量要显示地指定类型)、支持Class、支持枚举、支持接口。开始我是喜欢Javascript这个动态类型的编程的,觉得没有那么繁杂的定义,不用掌握很多东西都能出一点成果,写了一段时间之后也开始厌恶它比较难维护,相对于现在的我来说,我更注重的是代码的质量,设计的质量,而不是仅仅是为了完成某个功能而已。
三、开始学吧
官网文档简单的看了下,对于现在的我来说,着实是没有耐心慢慢地、细细的学,还是喜欢项目驱动地来边做边学,敲好一个朋友在做他的毕业设计,是一个阅读的app,之前有听他提到过用的是某网站的接口来获取书本资源,于是乎,我觉得来写个小爬虫(虽然我知道可能不足以称之为爬虫~)把书本资源保存到本地吧。
3.1 前期准备
- 接口提供地址::link:点击进入
- 使用到的第三方库:
axios
、fs
- 环境:
node
3.2 分析接口
github上那位大佬提供了三个接口:
-
获取书本列表:
/books
-
获取书本详情:
/book/{id}
-
获取章节详情:
/book/{bookId}/{cid}
根据这几个接口很容易就能想出如何使用这三个接口爬取数据:
- 获取书本列表,拿到books
- 遍历books,拿到bookId去拿书本详情bookDetail
- 书本详情里面肯定会有章节列表chepters,遍历chepters,拿到chepterId去获取章节详情ChepterDetail
顺着这个思路就能获取到书本的数据了
3.3 定义实体
根据接口的分析,我们可以抽象出4个实体类,他们是书本实体Book
、书本详情BookDetail
、章节Chepter
、章节详情ChepterDetail,这里就很好的用上了TypeScript的class
的知识点
-
Book Entity
class Book { _id: string; title: string; author: string; shortIntro: string; cover: string; cat: string; followerCount: string; zt: string; updated: string; lastchapter: string; }
-
BookDetail Entity
class BookDetail { _id: string; title: string; author: string; shortIntro: string; cover: string; cat: string; followerCount: string; zt: string; updated: string; lastchapter: string; wordCount: string; retentionRatio: string; chapters: Chapter[] }
-
Chapter Entity
class Chapter { cid: string; wordCount: string; title: string; }
-
ChapterDetail Entity
class ChapterDetail{ title:string; content:string; bookDetail:BookDetail; }
3.4 封装方法
因为js的单线程以及网络请求异步的原因,如果直接调用方法使用网络请求去爬取数据的话很容易就会出问题,这里使用到了
async
、await
,达到同步请求的功能。(这里折腾了很久,因为对async
和await
不是很熟悉,o(╥﹏╥)o) 网络请求这里使用的是
axios
,因为api比较简单,所以这里将所有的api封装成了一个枚举值,也是TypeScript的一个特性enum
:import axios from 'axios' const request = axios.create({ timeout: 1000 * 60 }) enum api { books = "https://novel.juhe.im/books", bookDetail = "https://novel.juhe.im/book/", chapterDetail = "https://novel.juhe.im/book/" }
这里我们封装了三个方法,获取书本列表、获取书本详情、获取章节详情。调用关系就像分析接口的步骤那样,这里我们只是加入了对请求数据的保存功能。
-
getChapterDetail 获取章节详情
async function getChapterDetail(bookTitle: string, bookId: string, chapterId: string) { try { let res = await request.get(api.chapterDetail + bookId + "/" + chapterId) let chapterDetail:ChapterDetail = res.data let csavePath = fileSavePath + bookTitle + "/" + chapterDetail.title + chapterId csavePath = csavePath.replace(' ', '') let _mkResult = fs.mkdirSync(csavePath, 0o777) console.log("\t\t创建目录:" + csavePath + "==>" + _mkResult); let fileName = csavePath + "/" + chapterDetail.title + "_" + chapterId + ".json"; fileName = fileName.replace(' ', '') fs.writeFileSync(fileName, JSON.stringify(chapterDetail)) console.log("\t\t\t保存文件:" + fileName); return res } catch (error) { console.log("getChapterDetail error", bookTitle, chapterId, error); } }
-
getBookDetail 获取书本详情
async function getBookDetail(book: Book) { try { let res = await request.get(api.bookDetail + book._id) let bookDetail: BookDetail = res.data let bookId = bookDetail._id let title = bookDetail.title let chapters = bookDetail.chapters let savePath = fileSavePath + title let mkResult = fs.mkdirSync(savePath, 0o777) console.log("\t创建目录:" + savePath + "==>" + mkResult); fs.writeFileSync(savePath + "/" + title + ".json", JSON.stringify(bookDetail)) for (const chapter of chapters) { await getChapterDetail(title, bookId, chapter.cid) } return res } catch (error) { console.log("getBooDetail errpr", book, error); } }
-
getBooks 获取书本列表
async function getBooks() { try { let res = await request.get(api.books, {}) let books: Book[] = res.data.books for (const book of books) { await getBookDetail(book) } return res } catch (error) { console.log(error) } }
这里只是对文件做了简单的保存,获取到一个book的话, 通过book的title创建一个以title为名的文件夹,并将bookDetail保存在这个文件夹中同名json文件中。在获取章节详情的时候,会在这个book的文件夹中创建章节名的文件夹,并把章节详情保存在文件夹中的同步json文件中。
如图:
文件夹结构一 文件夹结构二这里有个问题就是爬起来还是比较慢的,而且接口文件存在部分问题,比如title为空的情况,以及网络原因导致无法获取的情况,不过用来学习也够了。
四、完整代码
import axios from 'axios'
import * as fs from 'fs'
const request = axios.create({
timeout: 1000 * 60
})
const fileSavePath = "book/"
fs.mkdir(fileSavePath, err => {
console.log(err);
})
enum api {
books = "https://novel.juhe.im/books",
bookDetail = "https://novel.juhe.im/book/",
chapterDetail = "https://novel.juhe.im/book/"
}
class Book {
_id: string;
title: string;
author: string;
shortIntro: string;
cover: string;
cat: string;
followerCount: string;
zt: string;
updated: string;
lastchapter: string;
}
class BookDetail {
_id: string;
title: string;
author: string;
shortIntro: string;
cover: string;
cat: string;
followerCount: string;
zt: string;
updated: string;
lastchapter: string;
wordCount: string;
retentionRatio: string;
chapters: Chapter[]
}
class Chapter {
cid: string;
wordCount: string;
title: string;
}
class ChapterDetail{
title:string;
content:string;
bookDetail:BookDetail;
}
async function getBookDetail(book: Book) {
try {
let res = await request.get(api.bookDetail + book._id)
let bookDetail: BookDetail = res.data
let bookId = bookDetail._id
let title = bookDetail.title
let chapters = bookDetail.chapters
let savePath = fileSavePath + title
let mkResult = fs.mkdirSync(savePath, 0o777)
console.log("\t创建目录:" + savePath + "==>" + mkResult);
fs.writeFileSync(savePath + "/" + title + ".json", JSON.stringify(bookDetail))
// chapters.forEach(chapter=>{
// getChapterDetail(title,bookId,chapter.cid)
// })
for (const chapter of chapters) {
await getChapterDetail(title, bookId, chapter.cid)
}
return res
} catch (error) {
console.log("getBooDetail errpr", book, error);
}
}
async function getChapterDetail(bookTitle: string, bookId: string, chapterId: string) {
try {
let res = await request.get(api.chapterDetail + bookId + "/" + chapterId)
let chapterDetail:ChapterDetail = res.data
let csavePath = fileSavePath + bookTitle + "/" + chapterDetail.title + chapterId
csavePath = csavePath.replace(' ', '')
let _mkResult = fs.mkdirSync(csavePath, 0o777)
console.log("\t\t创建目录:" + csavePath + "==>" + _mkResult);
let fileName = csavePath + "/" + chapterDetail.title + "_" + chapterId + ".json";
fileName = fileName.replace(' ', '')
fs.writeFileSync(fileName, JSON.stringify(chapterDetail))
console.log("\t\t\t保存文件:" + fileName);
return res
} catch (error) {
console.log("getChapterDetail error", bookTitle, chapterId, error);
}
}
async function getBooks() {
try {
let res = await request.get(api.books, {})
let books: Book[] = res.data.books
for (const book of books) {
await getBookDetail(book)
}
return res
} catch (error) {
}
}
let res = getBooks()
console.log(res);
五、总结
5.1 学了如何使用TypeScript编写node脚本
因为之前用过nodejs+js来写脚本,所以使用ts来代替js其实也就是照搬照套,区别除了语法之外,就是执行流程上需要使用tsc xxx.ts 编译成xxx.js 之后,再执行node xxx.js,才能正常运行。关于如何使用第三方库的问题,我也是直接使用npm去安装第三方库的。如果要使用node 里面的http
、fs
这类的包的话,需要执行npm install @types/node -D
安装相关的库,至于axios的话就直接执行npm install axios
了。
5.2 async与await的基本用法
这两个关键字一般都是配合一起使用的,因为我了解甚少,这里就不给出详细解释以免误人子弟 o(╥﹏╥)o,后面有空了会对这个专门写个文章学习下把,可以先参考阮一峰的教程文章
5.3 forEach是同步的,但是forEach里面加await是无法同步执行的
这个问题也是我在使用await和async的时候遇到的,因为当时对这个不是很了解,我想要的效果是获取书本列表之后,一本一本的去爬,就是爬第一本的时候,要等这本书的所有章节都爬完了再开始爬第二本书,也是这里才遇到这些问题,也是靠大神指点的,后来将forEach
改成了for of
。
5.4 下一步计划
- 学习js的同步异步原理,学会使用
async
和await
- 学习Promise.all啥的
也许我永远也追不上他们的脚步,但是我可以超越昨天的自己。
网友评论