使用nodejs进行了简单的文件分卷工具
去骨鸡腿排 人气:0关键词:node fs readline generator
(在这之前需要声明的是这篇博客的应用范围应该算是相当狭隘,写出来主要也就是给自己记录一下临时兴起写的一个小工具,仅从功能需求上来说我相信是不适用于大多数读者的,欢迎有兴趣看的朋友给我做一次review)
最近沉迷漫画,收集了一堆野生资源,偶尔会遇到一些四格漫画,观看体验不是很好,因为每话篇幅比较短,就独立成了一个目录,譬如这样:
然后我就下意识的想写个脚本把这些目录合并成一个目录,解决这个问题,需要操作本地文件,而这是用很多途径都可以实现的,由于入门编程的时候学的是py,我第一时间是想用py的os模块去实现功能,一想上一次用py是快三年前的事情了,很多东西忘得差不多,包括前段时间换了新电脑,也没装有py环境,所以还是选择了语法上更熟悉也不需要额外配环境的node。
在明确了功能是在不改变源目录中文件顺序的情况下将多个目录按文件名顺序合并到新目录之后,由于涉及到操作本地文件(复制文件、创建目录等),所以确定了核心功能需要通过fs模块来完成;如果是自己用的话,一些比如输入输出目录这种配置参数,大可直接写死在代码里,需要用的时候再改就是了,但为了提高灵活性吧,所以还需要做一个微型的CLI,通过用户在命令行的输入来决定配置,这需要用到readline模块来提示和获取用户在命令行中的输入(在了解readline之后发现这里会比py麻烦不少,py用一个input方法就可以获取用户在命令行里的输入了);为了实现单向的流程,以及在提高代码在注释之外的的易理解和可读性,使用了平时比较少用的generator来实现了一个状态机并串联各个独立操作。
一边看node文档一边写代码,在忙活小半天之后,出来一点成果:
const fs = require('fs'); const readline = require('readline'); /** * @description 获取questtion的返回 * @param {String} question 用户提示 * @param {Function} handler 验证用户输入 * @returns {Promise} rl.question方法本身是通过回调来处理用户输入的,所以选择了返回promise来做阻塞,有序地抛出question并接收answer */ function getQuestionResult(question,handler){ return new Promise((res)=>{ // 创建一个可读流,用来读取在cmd中的输入 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); /** * 考虑到即使用户输入异常,question方法都会在监听到换行之后结束,所以把handleResult的结构设计成一个对象, * 由一个状态值success来表示是否通过handler的校验,success为false时应再次执行并且获取用户输入 */ rl.question(question, async (ans)=>{ const handleResult = handler(ans) if (handleResult.success){ rl.close() res(handleResult.value) } else { //handleResult.success为false时,handleResult.value是handler中设置的错误提示 rl.close() const rejecthandle = await getQuestionResult(handleResult.value,handler) res(rejecthandle) } }) }) } // 这里创建一个generator实例,我觉得generator的yield单向有序的特性很适合我这个需求 function*gen(){ function getChangeSettingsFlag(ans){ return ans==="Y"?{success:true,value:true}:{success:true,value:false} } const getPath = function (ans) { /**
* @description 验证路径(是否是目录)
* @param {String} path
*/ const validatePath = function (path) { try { // node10.x及以下版本不支持readdirSync,需要你需要这种写法,在运行之前需要切换node版本 const dir = fs.readdirSync(path)
if (dir) { return {success:true,value:path} } } catch(err){ return {success:false,value:"提供的地址不合理,请重新输入:"} } } return validatePath(ans) } // yield并不会返回值,这里声明的changeSettingFlag的值实际上是接收的next方法的参数 const changeSettingFlag = yield getChangeSettingsFlag const settings = { volumeSize: 10, dirName: "新建分卷" } // 改变预设 if (changeSettingFlag){ const volumeSize = yield function(volumeSize){ return Number(volumeSize)>0&&Number(volumeSize)!==Infinity?{success:true,value:volumeSize}:{success:false,value:"输入的数字不合理,请重新输入:"} } settings.volumeSize = Number(volumeSize)||settings.volumeSize const dirName = yield function(dirName){ return dirName.trim()?{success:true,value:dirName}:{success:false,value:"输入的目录名不合理,请重新输入:"} } settings.dirName = dirName.trim()||settings.dirName } // 接收路径 const pathInfo = {
input: "",
output: ""
} const inputPath = yield getPath pathInfo.input = inputPath; const outputPath = yield getPath pathInfo.output = outputPath; const conf = {
pathInfo,
settings
} console.log("conf",conf) yield conf } // run it async function workflow(generator){ const func0 = generator.next().value const changeSettingFlag = await getQuestionResult("当前预设置如下:\n\t输出的分卷名:“新建分卷”;\n\t容量:10话/卷;\n希望调整预设吗?(Y/n) ",func0) let getvolumeSize,getdirName if (changeSettingFlag){ const func1 = generator.next(changeSettingFlag).value getvolumeSize = await getQuestionResult("期望的卷容量(话/卷)是: ",func1) const func2 = generator.next(getvolumeSize).value getdirName = await getQuestionResult("期望的分卷名是:",func2) } const func3 = generator.next(getdirName).value const inputPath = await getQuestionResult("选择的源路径是:",func3) const func4 = generator.next(inputPath).value const outputPath = await getQuestionResult("期望的输出路径是:",func4) const conf = generator.next(outputPath).value // 当前计数,通过在文件名中添加count来保持排序 let currentCount = 0; // 当前分卷 let currentVolume = 0; /** * @param {String} path * @param {String} newFolderName */ async function letsdance(path,newFolderName="") { const childs = fs.opendirSync(path) let chunkNum = 0 let newFolderPath = newFolderName for await (const dirent of childs) { if (dirent.isDirectory()) { // 填充满一个目录之后创建一个新目录 if (currentVolume%conf.settings.volumeSize===0) { chunkNum+=1; newFolderPath = conf.pathInfo.output+"/"+conf.settings.dirName+"_"+chunkNum fs.mkdirSync(newFolderPath) // 如果你不希望命名后缀一直递增,也可以在新建目录之后把currentCount重新置为0 } currentVolume+=1 const nextLevelPath = path+"/"+dirent.name letsdance(nextLevelPath,newFolderPath) } else if (dirent.isFile()){ currentCount += 1 const extName = dirent.name.split(".").reverse()[0] const targetFilePath = path+"/"+dirent.name const newFileName = newFolderPath+"/"+currentCount+"."+extName fs.copyFileSync(targetFilePath,newFileName) } } } letsdance(conf.pathInfo.input) } const g = gen() workflow(g)
运行这个脚本,如果我有两个目录,需要将他们之中的文件复制到一个新目录来实现合并的效果的话,在讲第一个目录的文件按原顺序命名为1-n之后。第二个目录的文件则会从n+1开始命名,效果如下:
加载全部内容