美文网首页
入坑TypeScript(一):使用TypeScript写nod

入坑TypeScript(一):使用TypeScript写nod

作者: Martain | 来源:发表于2020-04-17 10:46 被阅读0次

入坑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:点击进入
  • 使用到的第三方库:axiosfs
  • 环境: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的单线程以及网络请求异步的原因,如果直接调用方法使用网络请求去爬取数据的话很容易就会出问题,这里使用到了asyncawait,达到同步请求的功能。(这里折腾了很久,因为对asyncawait 不是很熟悉,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 里面的httpfs 这类的包的话,需要执行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的同步异步原理,学会使用asyncawait
  • 学习Promise.all啥的

也许我永远也追不上他们的脚步,但是我可以超越昨天的自己。

相关文章

网友评论

      本文标题:入坑TypeScript(一):使用TypeScript写nod

      本文链接:https://www.haomeiwen.com/subject/yncevhtx.html