使用 nodejs 可以非常方便的开发命令行工具,来解决我们遇到的一些问题。
现在就让我们看看如何使用 nodejs 开发一个把 .srt 格式的字幕文件翻译成中文和外语的双语字幕,然后在把它发布到 npm 仓库中。
准备
在安装好 nodejs 环境后,进入到项目目录后使用
代码语言:javascript复制npm init -y
来,创建 package.json 文件,然后我选择把主文件放入 src 下。
代码语言:javascript复制├── package.json└── src
└── fysrt.js
然后我们需要安装如下依赖
commander.js
commander.js 可以帮助我们解析命令行参数和注册子命令,显示帮助信息,版本号。。。
代码语言:javascript复制var program = require('commander');
program
.version('0.1.0')
.option('-f, --foo', 'enable some foo')
.option('-b, --bar', 'enable some bar')
.option('-B, --baz', 'enable some baz');
program.on('--help', function(){ console.log('') console.log('Examples:'); console.log(' $ custom-help --help'); console.log(' $ custom-help -h');
});
program.parse(process.argv);
代码语言:javascript复制Usage: custom-help [options]
Options:
-h, --help output usage information
-V, --version output the version number
-f, --foo enable some foo
-b, --bar enable some bar
-B, --baz enable some bazExamples:
$ custom-help --help
$ custom-help -h
Inquirer.js
Inquirer.js 可以让命令行与用户进行交互。
signale
signale 可以用来打印信息到屏幕
fs-extra 和 klaw
fs-extra 是对 fs
的包装,它提供了 promise 支持,还有一些有用的功能。
klaw 原本属于 fs-extra
的一个功能,但是现在它被抽离出来,它可以用来遍历目录。
translate-google-cn
translate-google-cn 是我把 google-translate-api 稍微改了一下。
- 把
google.com
变成google.cn
- 修改了获取
token
的正则(原来的不起作用了)。 - 添加了 cookie ,这样更不容易被 google 封 ip
更多
想要了解更多的命令行工具可以参考 这里。
执行脚本
现在我们可以使用 node src/fysrt.js 来执行这个文件,但是这很麻烦,我们想使用 fysrt 来直接执行这个文件。
首先我们在文件开头加入
代码语言:javascript复制#!/usr/bin/env node
不加的话我们的脚本文件,就不会使用 node 执行它。
bin
然后我们在 package.json
中加入 bin
字段
使用 bin
字段可以将命令名和文件名映射,在安装时 npm 会将我们的可执行文件符号链接到 {prefix}/bin
(全局安装)或 ./node_modules/.bin/
本地安装,这样我们就不用输入路径来执行文件了。
{
"name": "fysrt",
"main": "src/fysrt.js",
"bin": "src/fysrt.js"}
当 bin
是一个字符串时,代表命令名与包名同名。
它还可以安装多个命令。
代码语言:javascript复制{
"bin": {
"c1": "bin/c1.js",
"c2": "bin/c2.js"
}}
这样安装就有 c1 与 c2 两个命令。
npm link
我们想让上面设置的 bin
起作用,可以发布和安装包,npm 才会帮我们做符号链接,但是这样太麻烦,我们还可以使用 npm link
命令。
它可以简写为 npm ln
,我们直接去项目目录执行 npm link
就可以了。
它会根据 package.json
的配置,在 {prefix}/lib/node_modules/<package>
中创建一个符号链接,它还会将包中的任何 bin
文件链接到 {prefix}/bin/{name}
。
如果我们想把它当作一个普通的包使用,我们可以去要用到它的项目文件夹,执行 npm link fysrt
,它会在该项目文件夹下的 node_modules
中链接到全局的 fysrt
。我们对 fysrt 的修改都可以直接映射到该项目的 fysrt。
当我们想取消链接时可以执行 npm unlink fysrt
。
srt 字幕文件
srt 字幕文件中的一句字幕,分为三部分。
代码语言:javascript复制65000:45:07,650 --> 00:45:09,110Fifteen minutes.65100:45:10,650 --> 00:45:20,110Fifteen minutes.Fifteen minutes.Fifteen minutes.
索引编号,时间,和字幕。字幕前面可能会有一些特效代码,如 {an6}
等等命令,或者还有 html
形式的。
每句字幕使用两个换行符分隔。
代码编写
我们使用 commander.js 来处理命令行参数。
代码语言:javascript复制commander
.version(version)
.option('-d, --delete', '删除原文件')
.option('-s, --single', '单语字幕,而不是双语字幕')
.option('-f, --from <lang>', '原始语言,默认 auto')
.option('-t, --to <lang>', '翻译成什么语言,默认 zh-cn')
.option( '-T, --time <time>', '每个字幕文件的翻译时间间隔 毫秒,默认 3000 毫秒'
)
.option( '-S, --size <size>', '一次给 google api 翻译的文本量,默认一次 50 行字幕'
)
.on('--help', () => { console.log(); console.log('Examples:'); console.log(' $ fysrt ./subtitles'); console.log(' $ fysrt -d a.srt'); console.log(' $ fysrt -f en a.srt');
})
.parse(process.argv);
然后我们可以使用 commander.args[0]
获取到输入的 目录或者字幕文件。
当没有目录或文件时,我们可以提示是否翻译当前目录下的所有字幕文件。
代码语言:javascript复制const ans = await inquirer.prompt([
{ type: 'confirm', name: 'dir', message: '翻译当前文件夹下的所有字幕文件?', default: false
}
]);if (!ans.dir) return;
如果是文件夹的话,我们使用 klaw
遍历目录,找到所有 srt
文件。
const files = [];walk(target)
.on('data', ({ path: p }) => p && p.endsWith('.srt') && files.push(p))
.on('end', async () => { const len = files.length; if (len === 0) {
signale.error(`目录下没有 .srt 文件 -> ${target}`);
process.exit(1)
}
// ...
});
然后我们读取字幕文件然后解析它,由于有些 srt 字幕文件不严格符合规范, 所以需要一行一行的判断这一行是时间还是字幕。
代码语言:javascript复制const lines = rawData.trim().split(/(?:rn|n|r)/); // 获取所有行const data = [];for (let i = 0, len = lines.length; i < len; i ) { let l = lines[i].trim(); // eslint-disable-next-line eqeqeq
if (!l || ~~l !== 0 || l == 0) continue; // 如果是空行或者是编号行则跳过
if (/^(?:d :){2}d ,d s-->s(?:d :){2}d [,.]d $/.test(l)) {
data.push([l]); // 处理时间行
} else if (/^d :d .d s-->sd :d .d $/.test(l)) {
data.push([ // 处理 vtt 文件格式的时间行
l
.replace(/./g, ',')
.split(' --> ')
.map(s => '00:' s)
.join(' --> ')
]);
} else { // 处理字幕行
l = l.replace(/^(?:{\w.*}) /, ''); // 去除特效代码
let last = data[data.length - 1]; if (last.length === 1) {
last.push(l);
} else {
last[1] = last[1] 'n' l;
}
}
}
然后我们就使用谷歌翻译
代码语言:javascript复制const requests = [];for (let i = 0, len = textArr.length; i <= len; i = size) {// textArr 就是上面 data 的 data.map(d => d[1]),size 是上面命令行传入的参数,默认 50 行// 因为翻译是 get 请求,一次性太多文字,谷歌服务器会报 413 错
requests.push( translate(textArr.slice(i, i size).join('nn'), { from,
to
})
);
}const res = await Promise.all(requests); // 并发的去翻译
最后把得到的翻译组合起来,然后写入到文件中就可以了。
代码语言:javascript复制const translate = res
.map(r => r.text.split('nn'))
.reduce((acc, val) => {
acc.push(...val); return acc;
}, []);
data
.map( (d, i) =>
`${i 1}n${d[0]}n${translate[i]}${keep ? 'n' d[1] : ''}`
)
.join('nn') 'nn'
源码
上面的代码只是这个小工具的核心部分,
完整的代码可以参考 github 仓库。
发布 npm 包
npm 包分为 unscoped 和 scoped,unscoped 就是我们常见的 npm 包,scoped 就是包前面有一个 @
符号的包比如 @vue/cli
。
scoped 包可以分为团体和个人。
scoped 的包默认是私有的,但需要付费。可修改 package.json 文件让它是公开的。
要发布包到 npm 我们首先要注册一个 npm 帐号。
然后登入账户
代码语言:javascript复制npm login
再发布包
代码语言:javascript复制npm publish
这样就可以了。但是有可能报错,比如仓库中已经有这个包名了,这时只有换一个名字,或者发布 scoped 包。
我们可以修改 package.json
{
"name": "@npm账户名称/包名"}
账户名可以通过
代码语言:javascript复制npm whoami
查询。
然后我们在发布公共包
代码语言:javascript复制npm publish --access public
迭代包
我们可以使用 npm version
命令递增版本号。
npm 版本号是 major.minor.patch
主版本.次版本.补丁版本。
npm version patch
我们去查看 package.json 就会发现 version 字段改变了。
然后再发布包
代码语言:javascript复制npm publish
废弃 删除 包
我们可以废弃一个包的版本或者整个包。
代码语言:javascript复制npm deprecate <pkg>[@<version>] <message>
npm 不建议删除包,因为包可能被别人引用。所以 npm 做了限制
- 删除的版本 24 小时后方可重发
- 包发布 72 小时之内才可删除
npm unpublish pkg --force