使用 nodejs 能夠很是方便的開發命令行工具,來解決咱們遇到的一些問題。javascript
如今就讓咱們看看如何使用 nodejs 開發一個把 .srt 格式的字幕文件翻譯成中文和外語的雙語字幕,而後在把它發佈到 npm 倉庫中。html
在安裝好 nodejs 環境後,進入到項目目錄後使用vue
npm init -y
複製代碼
來,建立 package.json 文件,而後我選擇把主文件放入 src 下。java
├── package.json
└── src
└── fysrt.js
複製代碼
而後咱們須要安裝以下依賴node
commander.js 能夠幫助咱們解析命令行參數和註冊子命令,顯示幫助信息,版本號。。。git
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);
複製代碼
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 baz
Examples:
$ custom-help --help
$ custom-help -h
複製代碼
Inquirer.js 可讓命令行與用戶進行交互。github
signale 能夠用來打印信息到屏幕npm
fs-extra 是對 fs
的包裝,它提供了 promise 支持,還有一些有用的功能。json
klaw 本來屬於 fs-extra
的一個功能,可是如今它被抽離出來,它能夠用來遍歷目錄。api
translate-google-cn 是我把 google-translate-api 稍微改了一下。
google.com
變成 google.cn
token
的正則(原來的不起做用了)。想要了解更多的命令行工具能夠參考 這裏。
如今咱們可使用 $ node src/fysrt.js
來執行這個文件,可是這很麻煩,咱們想使用 $ fysrt
來直接執行這個文件。
首先咱們在文件開頭加入
#!/usr/bin/env node
複製代碼
不加的話咱們的腳本文件,就不會使用 node 執行它。
而後咱們在 package.json
中加入 bin
字段
使用 bin
字段能夠將命令名和文件名映射,在安裝時 npm 會將咱們的可執行文件符號連接到 {prefix}/bin
(全局安裝)或 ./node_modules/.bin/
本地安裝,這樣咱們就不用輸入路徑來執行文件了。
{
"name": "fysrt",
"main": "src/fysrt.js",
"bin": "src/fysrt.js"
}
複製代碼
當 bin
是一個字符串時,表明命令名與包名同名。
它還能夠安裝多個命令。
{
"bin": {
"c1": "bin/c1.js",
"c2": "bin/c2.js"
}
}
複製代碼
這樣安裝就有 c1 與 c2 兩個命令。
咱們想讓上面設置的 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 字幕文件中的一句字幕,分爲三部分。
650
00:45:07,650 --> 00:45:09,110
Fifteen minutes.
651
00:45:10,650 --> 00:45:20,110
Fifteen minutes.Fifteen minutes.Fifteen minutes.
複製代碼
索引編號,時間,和字幕。字幕前面可能會有一些特效代碼,如 {\an6}
等等命令,或者還有 html
形式的。
每句字幕使用兩個換行符分隔。
咱們使用 commander.js 來處理命令行參數。
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]
獲取到輸入的 目錄或者字幕文件。
當沒有目錄或文件時,咱們能夠提示是否翻譯當前目錄下的全部字幕文件。
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 字幕文件不嚴格符合規範, 因此須要一行一行的判斷這一行是時間仍是字幕。
const lines = rawData.trim().split(/(?:\r\n|\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-->\s\d+:\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;
}
}
}
複製代碼
而後咱們就使用谷歌翻譯
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('\n\n'), {
from,
to
})
);
}
const res = await Promise.all(requests); // 併發的去翻譯
複製代碼
最後把獲得的翻譯組合起來,而後寫入到文件中就能夠了。
const translate = res
.map(r => r.text.split('\n\n'))
.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('\n\n') + '\n\n'
複製代碼
上面的代碼只是這個小工具的核心部分,
完整的代碼能夠參考 github 倉庫。
npm 包分爲 unscoped 和 scoped,unscoped 就是咱們常見的 npm 包,scoped 就是包前面有一個 @
符號的包好比 @vue/cli
。
scoped 包能夠分爲團體和我的。
scoped 的包默認是私有的,但須要付費。可修改 package.json 文件讓它是公開的。
要發佈包到 npm 咱們首先要註冊一個 npm 賬號。
而後登入帳戶
npm login
複製代碼
再發布包
npm publish
複製代碼
這樣就能夠了。可是有可能報錯,好比倉庫中已經有這個包名了,這時只有換一個名字,或者發佈 scoped 包。
咱們能夠修改 package.json
{
"name": "@npm帳戶名稱/包名"
}
複製代碼
帳戶名能夠經過
npm whoami
複製代碼
查詢。
而後咱們在發佈公共包
npm publish --access public
複製代碼
咱們可使用 npm version
命令遞增版本號。
npm 版本號是 major.minor.patch
主版本.次版本.補丁版本。
npm version patch
複製代碼
咱們去查看 package.json 就會發現 version 字段改變了。
而後再發布包
npm publish
複製代碼
咱們能夠廢棄一個包的版本或者整個包。
npm deprecate <pkg>[@<version>] <message>
複製代碼
npm 不建議刪除包,由於包可能被別人引用。因此 npm 作了限制
npm unpublish pkg --force
複製代碼