TypeScript Node實(shí)現(xiàn)下載簡書文章圖片工具

寫在前面

經(jīng)常的寫作的人都有備份的好習(xí)慣,為了防止自己的文章丟失,簡書提供了下載所有文章功能嘲恍,可以讓作者將文章下載到本地保存,或者上傳到自己的站點(diǎn)例驹。


但是簡書的圖片是放在專門的圖片服務(wù)器上的,下載所有文章并不包含文章中的所有圖片职抡。所以我們現(xiàn)在寫個(gè)小工具,通過命令行的方式將文章中的所有圖片下載本地保存误甚。

需求實(shí)現(xiàn)步驟

  • 下載簡書文章缚甩,解壓到 A 目錄;
  • 建一個(gè) TypeScript + Node 項(xiàng)目窑邦,讀取 A 目錄中的所有 .md 文件擅威;
  • 提取文件內(nèi)容中的圖片鏈接,下載下來冈钦;
  • 把下載的圖片放到 B 目錄/當(dāng)前文章/ 中郊丛,用來分類;
  • 重構(gòu)優(yōu)化代碼瞧筛;

下面按照這幾個(gè)步驟一步步完成簡書下載圖片工具厉熟。

下載簡書文章

進(jìn)入我的簡書 ->賬號(hào)管理 打包下載全部的簡書文章即可,我是下載到了這個(gè)目錄 D:\jianshu_article\user-5541401-1565071963较幌,這個(gè)目錄下的所有文件都是文集/文章的格式揍瑟。接下來開始搭建項(xiàng)目結(jié)構(gòu)。

TypeScript Node 搭建項(xiàng)目

先在 github 上新建一個(gè)倉庫乍炉,然后 clone 下來月培。開發(fā)工作一直在 master 分支上嘁字,然后每完成一步需求,新建一個(gè)分支用來保留記錄杉畜,以后看的時(shí)候更清晰。

新建一個(gè)倉庫然后 clone 下來:
git clone git@github.com:mxcz213/download-jianshu-images.git

開始項(xiàng)目搭建:
  • 生成 package.json 文件衷恭;
npm init -f
  • 下載項(xiàng)目依賴 :typescript node 的ts 版本此叠,download下載文件包,runscript 用來執(zhí)行 shell 命令随珠,ts-node 用來開發(fā)調(diào)試灭袁;
npm install @types/node download runscript ts-node typescript --save-dev
  • 配置 tsconfig 文件,用來按照這個(gè)規(guī)則編譯 ts 文件為 js 文件窗看。執(zhí)行命令 tsc --init茸歧,自動(dòng)生成 tsconfig.json 文件;
//tsconfig.json
{
  "compilerOptions": {
    "target": "es5",  
    "module": "commonjs",
    "outDir": "./dist/", 
    "strict": true,
    "esModuleInterop": true                  
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
  • 配置 package.json 文件的 scripts 字段显沈,啟動(dòng)項(xiàng)目和編譯命令
{
  "name": "download-jianshu-images",
  "version": "1.0.0",
  "description": "Node + typescript 實(shí)現(xiàn)下載簡書文章中所有的圖片鏈接",
  "main": "dist/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mxcz213/download-jianshu-images.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/mxcz213/download-jianshu-images/issues"
  },
  "homepage": "https://github.com/mxcz213/download-jianshu-images#readme",
  "devDependencies": {
    "@types/node": "^12.6.9",
    "download": "^7.1.0",
    "runscript": "^1.4.0",
    "ts-node": "^8.3.0",
    "typescript": "^3.5.3"
  }
}
  • 配置 vscode 的調(diào)試腳本 launch.json
//.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [

        {
            "name": "Current TS File",
            "type": "node",
            "request": "launch",
            "program": "${workspaceRoot}/node_modules/ts-node/dist/bin.js",
            // "program": "${workspaceRoot}/test.js",
            "args": [
                "${relativeFile}"
            ],
            "cwd": "${workspaceRoot}",
            "protocol": "inspector"
        }
    ]
}
  • 添加 .gitignore 文件配置忽略提交的目錄
//.gitignore
/node_modules
  • 新建 dist 目錄用來放編譯之后的 js 文件
  • 新建 src 源代碼文件目錄

具體代碼實(shí)現(xiàn)软瞎,新建 src/index.ts 文件

//src index.ts
const fs = require('fs')
const path = require('path')
const runScript = require('runscript')
const download = require('download')

//windows中用戶復(fù)制的目錄
let originDir: string = 'D:\\jianshu_article\\user-5541401-1565071963\\'
let targetDir: string = 'E:\\workCode\\download-jianshu-images\\jianshu_article\\'

const readmeUrlReg: RegExp = /\s!\[\]\(https:\/\/\upload-images.jianshu.io\/upload_images\/[a-zA-Z0-9-_?%./]+\)\s/g
const imageUrlReg: RegExp = /https:\/\/\upload-images.jianshu.io\/upload_images\/[a-zA-Z0-9-_?%./]+/g

//用戶通過命令行工具輸入命令比如:node dist/index.js 簡書解壓目錄 目標(biāo)存儲(chǔ)圖片目錄
process.argv.forEach((val, index) => {
    console.log(`${index}: ${val}`)
});

try {
    originDir = process.argv[2] ? process.argv[2] : originDir
    targetDir = process.argv[3] ? process.argv[3] : targetDir
} catch(e) {
    console.log('獲取命令參數(shù)錯(cuò)誤', e)
}

const downloadImages = (imgurl: string[], path: string) => {   
    let newUrlArr: any[] = []
    imgurl.forEach((item: any) => {
        if(item.match(imageUrlReg)){
            newUrlArr.push(item.match(imageUrlReg)[0])
        }
    })
    console.log(newUrlArr)
    Promise.all(newUrlArr.map((url: string) => {
        download(url, path)
    })).then(() => {
       console.log('all files downloaded')
    })
}

const runFunction = async () => {
    //shell ls拿到所有的.md文章
    const { stdout } = await runScript('ls **/*.md', {
        cwd: originDir,
        stdio: 'pipe'
    })
    let files: string[] = stdout.toString().split('\n')
    let num: number = 0
    try {
        files.forEach((fileitem: any, index: number) => {
            if(fileitem){
                let filepath: string = fileitem.split('.md')[0].split('/').join('\\')
                let dirStr: string = `${targetDir}\\${filepath}`                
                runScript(`mkdir ${dirStr}`, { stdio: 'pipe' })
                .then((stdio: any) => {
                    let fileContent = fs.readFileSync(path.join(originDir, fileitem.split('/').join('\\')), { encoding: 'utf8'})
                    let urlList: any = fileContent.match(readmeUrlReg)
                    if(urlList && urlList.length > 0){
                        downloadImages(urlList, dirStr)
                    }
                })
            }
        })
    } catch(e) {
        console.log(e)
    }
}
runFunction()
  • 執(zhí)行命令 npm run build 編譯 ts 文件
  • 執(zhí)行命令node . D:\jianshu_article\user-5541401-1565071963 D:\jianshu_article\article_img,下載圖片
    node . 命令會(huì)到 package.json 文件中找到 main 字段執(zhí)行入口文件拉讯。
    process.argv 會(huì)獲取到命令行參數(shù)涤浇。

接下來提交文件到 master 分支:

git add .
git commit -m "download jianshu images"
git push

然后根據(jù) master 新建一個(gè)分支,用來保存這次的提交歷史:

git checkout -b node_tool
git pull origin master
git push

實(shí)現(xiàn)工具命令魔慷,如 jianshu ...

配置命令行只锭,通過 package.json 文件的 bin 字段,然后新建 bin 目錄院尔,在 bin 目錄下新建 jianshu 文件蜻展;

//package.json
{
  ...
  "bin": {
    "jianshu": "bin/jianshu"
  }
  ...
}
//bin/jianshu
#!/usr/bin/env node

require('../dist/index');

配置完就可以通過命令 jianshu D:\jianshu_article\user-5541401-1565071963 D:\jianshu_article\article_img 實(shí)現(xiàn)下載圖片。

通過const [, , sourceDir, targetDir] = process.argv;來獲取命令行參數(shù)邀摆。

提交代碼之后纵顾,這一步同樣新建 node_cli 分支用來保存歷史:

git checkout -b node_cli
git pull origin master
git pull

代碼重構(gòu)優(yōu)化

上面的代碼只是實(shí)現(xiàn)的簡單的功能,流程并不清晰隧熙,現(xiàn)在來重構(gòu)代碼片挂,使主流程變的清晰。

代碼重構(gòu)的原則:主流程要清晰

每個(gè)函數(shù)只做一件事贞盯,有兩個(gè)以上的函數(shù)音念,有內(nèi)部函數(shù)式,就要考慮把這每個(gè)函數(shù)放到單獨(dú)的文件里躏敢,然后用模塊導(dǎo)入的方式闷愤。

以上代碼展現(xiàn)的問題:
    1. handleDir getArticleContent 重復(fù)判斷平臺(tái)和路徑,沒有把判斷平臺(tái)提出來
    1. getMarkdownImageUrls getRealImageUrl 重復(fù)使用相似的正則件余,沒使用exec和正則的捕獲組
    1. 小函數(shù)嵌套太嚴(yán)重讥脐,一個(gè)函數(shù)能搞定的
    1. 沒有異常判斷遭居,沒有l(wèi)og
    1. 邏輯層次不清晰,分了好多層
    1. 關(guān)鍵注釋缺失旬渠,例如files這個(gè)是相對(duì)路徑的列表俱萍,不注明的話,以后肯定不知道

所以接下來就要重構(gòu)這些代碼告丢,主要根據(jù)以下分類原則來實(shí)現(xiàn)模塊的拆分:

分類原則

哪些是項(xiàng)目獨(dú)有的邏輯(業(yè)務(wù)邏輯)枪蘑,
哪些是通用邏輯(可復(fù)用的),
哪些是模板代碼(沒啥用但是要寫的)

根據(jù)以上原則岖免,拆分出來工具函數(shù) log岳颇,文件操作;核心函數(shù) libs颅湘。

//src/utils/log.ts
const log = (str: string) => {
    console.log(str);
}
 const error = (str: string) => {
    console.error(str);
}
const warn = (str: string) => {
    console.warn(str);
}
export {
    log,
    error,
    warn
}
//src/utils/fs.ts
const fs = require('fs');
const runScript = require('runscript');
const download = require('download');

const read = (path: string, options?: {}) => {
    let fileContent = fs.readFileSync(path, options);
    return fileContent;
}
const createDir = async (targetDir: string) => {
    await runScript(`mkdir ${targetDir}`);
}
const deleteDir = async (targetDir: string) => {
    await runScript(`rd /s/q ${targetDir}`);
}
const isExistDir = (targetDir: string): boolean => {
    return fs.existsSync(targetDir);
}
const downloadFile = async(url: string, targetDir: string) => {
    await download(url, targetDir);
}

export {
    read,
    createDir,
    deleteDir,
    isExistDir,
    downloadFile
}
//src/libs/lib.ts
const runScript = require('runscript');
import { log } from '../utils/log';

//sourceDir:簡書文章目錄
export const getAllMarkdownFiles = async (sourceDir: string) => {
    //ls **/*.md 查詢二級(jí)目錄下的所有.md后綴的文件
    //stdio: pipe 在父進(jìn)程和子進(jìn)程之間建立管道
    const { stdout } = await runScript('ls **/*.md', {
        cwd: sourceDir,
        stdio: 'pipe'
    });
    const files: string[] = stdout.toString().split('\n');

    //去掉ls命令產(chǎn)生的尾部空行
    files.pop();
    log('獲取所有的簡書文章列表话侧;');
    return files;
}

//獲取圖片url的markdown寫法![](https://....)
export const getMarkdownImageUrls = (fileContent: string) => {
    const urlRegExp = /\!\[.*\]\((https?:\/\/.+?)\)/g;

    const imageUrls: string[] = [];
    while(true) {
        const match = urlRegExp.exec(fileContent);
        if(match === null) {
            break;
        }
        
        const [, url] = match;
        imageUrls.push(url);
    }
    return imageUrls;
}

主入口函數(shù):

//src/index.ts
import path from 'path';
import { log } from './utils/log';
import { read, createDir, deleteDir, isExistDir, downloadFile } from './utils/fs';
import { getAllMarkdownFiles, getMarkdownImageUrls } from './libs/lib';

//入口函數(shù)
const main = async () => {
    //平臺(tái)判斷
    const { platform } = process;
    const isWindows: boolean = platform === 'win32';

    //獲取命令行參數(shù)
    const [, , sourceDir, targetDir] = process.argv;

    //獲取markdown文件列表
    const files: string[] = await getAllMarkdownFiles(sourceDir);

    //下載文件列表中每個(gè)文章的圖片
    for(const file of files){
        // file 是相對(duì)路徑 例如:"2017-2018/前端模塊化總結(jié).md"

        // 兼容 windows 系統(tǒng)路徑規(guī)則
        let platFile: string = isWindows ? `${file.split('.md')[0].split('/').join('\\')}.md` : file;
        const filepath: string = platFile.split('.md')[0];

        //讀取文件內(nèi)容
        const filecontent = read(path.join(sourceDir, platFile), { encoding: 'utf8'});
        
        //根據(jù) md 文件名,創(chuàng)建目標(biāo)文件夾闯参,如果目標(biāo)文件夾存在瞻鹏,則刪除重建
        const newTargetDir: string = path.join(targetDir, filepath);
        if(isExistDir(newTargetDir)){
            await deleteDir(newTargetDir);
        }
        await createDir(newTargetDir);

        //找出圖片,下載圖片到目標(biāo)目錄
        const urlList: string[] = getMarkdownImageUrls(filecontent);
        for(const url of urlList){
            await downloadFile(url, newTargetDir);
        }
    }

    log('所有文章中的圖片已下載成功赢赊!');
}
main();

提交代碼到 master 分支乙漓,然后新建 node-cli-refactory 分支用來保存重構(gòu)歷史。

git checkout -b node-cli-refactory
git pull origin master
git push

最后這個(gè)下載圖片的小工具就做好释移。

總結(jié):在寫代碼的過程中叭披,一定要分析什么是通用工具類,什么是獨(dú)有的業(yè)務(wù)邏輯類玩讳,該模塊化的模塊化涩蜘,目的只有一個(gè)就是:主流程要清晰

項(xiàng)目地址:https://github.com/mxcz213/download-jianshu-images

參考:
https://www.npmjs.com/package/runscript
https://www.npmjs.com/package/download
https://www.npmjs.cn/files/package.json/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末熏纯,一起剝皮案震驚了整個(gè)濱河市同诫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌樟澜,老刑警劉巖误窖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秩贰,居然都是意外死亡霹俺,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門毒费,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丙唧,“玉大人,你說我怎么就攤上這事觅玻∠爰剩” “怎么了培漏?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胡本。 經(jīng)常有香客問我牌柄,道長,這世上最難降的妖魔是什么侧甫? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任友鼻,我火速辦了婚禮,結(jié)果婚禮上闺骚,老公的妹妹穿的比我還像新娘。我一直安慰自己妆档,他們只是感情好僻爽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贾惦,像睡著了一般胸梆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上须板,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天碰镜,我揣著相機(jī)與錄音,去河邊找鬼习瑰。 笑死绪颖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的甜奄。 我是一名探鬼主播柠横,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼课兄!你這毒婦竟也來了牍氛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤烟阐,失蹤者是張志新(化名)和其女友劉穎搬俊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜒茄,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唉擂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扩淀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片楔敌。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖驻谆,靈堂內(nèi)的尸體忽然破棺而出卵凑,到底是詐尸還是另有隱情庆聘,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布勺卢,位于F島的核電站伙判,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏黑忱。R本人自食惡果不足惜宴抚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甫煞。 院中可真熱鬧菇曲,春花似錦、人聲如沸抚吠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽楷力。三九已至喊式,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間萧朝,已是汗流浹背岔留。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留检柬,地道東北人献联。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像厕吉,于是被迫代替她去往敵國和親酱固。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 概要 64學(xué)時(shí) 3.5學(xué)分 章節(jié)安排 電子商務(wù)網(wǎng)站概況 HTML5+CSS3 JavaScript Node 電子...
    阿啊阿吖丁閱讀 9,180評(píng)論 0 3
  • 1 Node.js模塊的實(shí)現(xiàn) 之前在網(wǎng)上查閱了許多介紹Node.js的文章,可惜對(duì)于Node.js的模塊機(jī)制大都著...
    zlx_2017閱讀 1,241評(píng)論 0 1
  • NPM NPM 是隨同 Node 一起安裝的包管理工具头朱,能解決 Node 代碼部署上的很多問題运悲,常見的使用場景有以...
    heyi_let閱讀 2,580評(píng)論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,096評(píng)論 1 32
  • 張小姐年方二八,正青春项钮,一頭亂糟糟的長發(fā)班眯。一大早,鬧鐘一響烁巫,瞌睡魔把她的頭裹進(jìn)被子里署隘,死守著她的睡眠。直到手機(jī)里第...
    9dc50ca4b68f閱讀 350評(píng)論 0 2