美文网首页
TypeScript Node实现下载简书文章图片工具

TypeScript Node实现下载简书文章图片工具

作者: 梦想成真213 | 来源:发表于2019-08-28 16:01 被阅读0次

写在前面

经常的写作的人都有备份的好习惯,为了防止自己的文章丢失,简书提供了下载所有文章功能,可以让作者将文章下载到本地保存,或者上传到自己的站点。


但是简书的图片是放在专门的图片服务器上的,下载所有文章并不包含文章中的所有图片。所以我们现在写个小工具,通过命令行的方式将文章中的所有图片下载本地保存。

需求实现步骤

  • 下载简书文章,解压到 A 目录;
  • 建一个 TypeScript + Node 项目,读取 A 目录中的所有 .md 文件;
  • 提取文件内容中的图片链接,下载下来;
  • 把下载的图片放到 B 目录/当前文章/ 中,用来分类;
  • 重构优化代码;

下面按照这几个步骤一步步完成简书下载图片工具。

下载简书文章

进入我的简书 ->账号管理 打包下载全部的简书文章即可,我是下载到了这个目录 D:\jianshu_article\user-5541401-1565071963,这个目录下的所有文件都是文集/文章的格式。接下来开始搭建项目结构。

TypeScript Node 搭建项目

先在 github 上新建一个仓库,然后 clone 下来。开发工作一直在 master 分支上,然后每完成一步需求,新建一个分支用来保留记录,以后看的时候更清晰。

新建一个仓库然后 clone 下来:
git clone git@github.com:mxcz213/download-jianshu-images.git

开始项目搭建:
  • 生成 package.json 文件;
npm init -f
  • 下载项目依赖 :typescript node 的ts 版本,download下载文件包,runscript 用来执行 shell 命令,ts-node 用来开发调试;
npm install @types/node download runscript ts-node typescript --save-dev
  • 配置 tsconfig 文件,用来按照这个规则编译 ts 文件为 js 文件。执行命令 tsc --init,自动生成 tsconfig.json 文件;
//tsconfig.json
{
  "compilerOptions": {
    "target": "es5",  
    "module": "commonjs",
    "outDir": "./dist/", 
    "strict": true,
    "esModuleInterop": true                  
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
  • 配置 package.json 文件的 scripts 字段,启动项目和编译命令
{
  "name": "download-jianshu-images",
  "version": "1.0.0",
  "description": "Node + typescript 实现下载简书文章中所有的图片链接",
  "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 的调试脚本 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 源代码文件目录

具体代码实现,新建 src/index.ts 文件

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

//windows中用户复制的目录
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 简书解压目录 目标存储图片目录
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('获取命令参数错误', 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()
  • 执行命令 npm run build 编译 ts 文件
  • 执行命令node . D:\jianshu_article\user-5541401-1565071963 D:\jianshu_article\article_img,下载图片
    node . 命令会到 package.json 文件中找到 main 字段执行入口文件。
    process.argv 会获取到命令行参数。

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

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

然后根据 master 新建一个分支,用来保存这次的提交历史:

git checkout -b node_tool
git pull origin master
git push

实现工具命令,如 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 实现下载图片。

通过const [, , sourceDir, targetDir] = process.argv;来获取命令行参数。

提交代码之后,这一步同样新建 node_cli 分支用来保存历史:

git checkout -b node_cli
git pull origin master
git pull

代码重构优化

上面的代码只是实现的简单的功能,流程并不清晰,现在来重构代码,使主流程变的清晰。

代码重构的原则:主流程要清晰

每个函数只做一件事,有两个以上的函数,有内部函数式,就要考虑把这每个函数放到单独的文件里,然后用模块导入的方式。

以上代码展现的问题:
    1. handleDir getArticleContent 重复判断平台和路径,没有把判断平台提出来
    1. getMarkdownImageUrls getRealImageUrl 重复使用相似的正则,没使用exec和正则的捕获组
    1. 小函数嵌套太严重,一个函数能搞定的
    1. 没有异常判断,没有log
    1. 逻辑层次不清晰,分了好多层
    1. 关键注释缺失,例如files这个是相对路径的列表,不注明的话,以后肯定不知道

所以接下来就要重构这些代码,主要根据以下分类原则来实现模块的拆分:

分类原则

哪些是项目独有的逻辑(业务逻辑),
哪些是通用逻辑(可复用的),
哪些是模板代码(没啥用但是要写的)

根据以上原则,拆分出来工具函数 log,文件操作;核心函数 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 查询二级目录下的所有.md后缀的文件
    //stdio: pipe 在父进程和子进程之间建立管道
    const { stdout } = await runScript('ls **/*.md', {
        cwd: sourceDir,
        stdio: 'pipe'
    });
    const files: string[] = stdout.toString().split('\n');

    //去掉ls命令产生的尾部空行
    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;
}

主入口函数:

//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';

//入口函数
const main = async () => {
    //平台判断
    const { platform } = process;
    const isWindows: boolean = platform === 'win32';

    //获取命令行参数
    const [, , sourceDir, targetDir] = process.argv;

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

    //下载文件列表中每个文章的图片
    for(const file of files){
        // file 是相对路径 例如:"2017-2018/前端模块化总结.md"

        // 兼容 windows 系统路径规则
        let platFile: string = isWindows ? `${file.split('.md')[0].split('/').join('\\')}.md` : file;
        const filepath: string = platFile.split('.md')[0];

        //读取文件内容
        const filecontent = read(path.join(sourceDir, platFile), { encoding: 'utf8'});
        
        //根据 md 文件名,创建目标文件夹,如果目标文件夹存在,则删除重建
        const newTargetDir: string = path.join(targetDir, filepath);
        if(isExistDir(newTargetDir)){
            await deleteDir(newTargetDir);
        }
        await createDir(newTargetDir);

        //找出图片,下载图片到目标目录
        const urlList: string[] = getMarkdownImageUrls(filecontent);
        for(const url of urlList){
            await downloadFile(url, newTargetDir);
        }
    }

    log('所有文章中的图片已下载成功!');
}
main();

提交代码到 master 分支,然后新建 node-cli-refactory 分支用来保存重构历史。

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

最后这个下载图片的小工具就做好。

总结:在写代码的过程中,一定要分析什么是通用工具类,什么是独有的业务逻辑类,该模块化的模块化,目的只有一个就是:主流程要清晰

项目地址: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/

相关文章

  • TypeScript Node实现下载简书文章图片工具

    写在前面 经常的写作的人都有备份的好习惯,为了防止自己的文章丢失,简书提供了下载所有文章功能,可以让作者将文章下载...

  • typescript运行步骤

    1、下载typescript并全局安装(安装node的情况下) npm install -g typescript...

  • 简书个人文章备份,图片批量导出小工具

    此小工具弥补简书的 “打包下载文章” 功能上的不足,它能批量的将简书发布的个人文章上用到的所有图片批量爬取并导...

  • 搭建vue开发环境

    1:vue的运行是要依赖于node的npm的管理工具来实现,因此要下载node。下载地址:https://node...

  • 安装环境

    概述 Egret基于TypeScript开发的,而TypeScript编译工具tsc是基于Node.js开发的。所...

  • js2x:简书 to Hexo 格式转换器

    下载「简书」文章内容及图片,并转换为 Hexo 博客可以直接解析的 Markdown 格式。使「简书」文章快捷同步...

  • 简书文章打包下载(图片本地化)

    背景:备份简书所有的文章目前简书提供了文章打包下载功能。但文章中的图片是以链接的形式存在的,并未下载到本地。因此用...

  • 新的旅程。

    图片发自简书App 忽然想写一些东西,下载简书,才发觉以前已经下载过了啊,还写了几篇文章。然而看到那几篇文章的标题...

  • ts开发node应用

    1,安装typescript有两种主要的方式来获取TypeScript工具: 通过npm(Node.js包管理器)...

  • webstorm配置typescript自动编译 + 语法检查

    1、安装node、typescript、tslint阿里仓库下载node安装包,我使用的是8.11.0网址:htt...

网友评论

      本文标题:TypeScript Node实现下载简书文章图片工具

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