我媽是廣場舞大媽,常常讓我下載廣場舞視頻,而且要裁剪出背面演示部分或者分解動做部分,那麼問題就來了。html
🤔我想,既然我要常常作這件事,可否寫個 node 來簡化一下呢?node
我媽平時主要是用 51廣場舞 和 糖豆廣場舞 這兩個網站,並且糖豆廣場舞還有微信小程序。git
因此我這個工具必須可以下載這兩個網站的視頻,其次還要實現裁剪功能,設想下這個使用場景github
好,需求明確了,開始分析正則表達式
隨便打開一個 51廣場舞 的視頻,發現頁面就有提供下載按鈕,咱們用開發者工具看一下,能夠看到按鈕的 href 就是視頻請求 URL,這就好辦了,這裏要注意下,這個 URL 不是咱們在命令行輸入的那個,咱們輸入的是頂部連接欄的 URL ,這個是咱們爬取的目標!npm
隨便打開一個 糖豆廣場舞 的視頻,打開開發者工具,發現這裏用的是 HTML5 的 video,這個屬性的 src 就是視頻的請求URL小程序
新建一個文件夾,而後 npm 或 yarn 初始化一下微信小程序
mkdir dance-video-downloader
cd dance-video-downloader
yarn init
複製代碼
新建 index.js,咱們就在這個文件寫代碼api
touch index.js
複製代碼
把用到的模塊安裝一下,具體文章下面有解說bash
yarn add superagent cheerio ora inquirer fluent-ffmpeg
複製代碼
這一步咱們使用到兩個爬蟲的神器,superagent 和 cheerio ,和一個命令行 loading 工具 ora
superagent 實際上是一個 http 工具,可用於請求頁面內容,cheerio 至關於 node 中的 jq
廣場舞地址咱們選擇在命令行輸入,好比 node index http://www.51gcw.com/v/26271.html
,這樣咱們能夠經過 process.argv[2]
獲取到這個 URL
咱們先請求到網頁內容,而後經過 cheerio 操做 dom 的方式獲取到視頻的 URL,具體實現代碼以下
const superagent = require('superagent')
const cheerio = require('cheerio')
const ora = require('ora')
function run() {
const scraping = ora('正在抓取網頁...\n').start()
superagent
.get(process.argv[2])
.end((err, res) => {
if (err) {
return console.log(err)
}
scraping.succeed('已成功抓取到網頁\n')
const downloadLink = getDownloadLink(res.text)
console.log(downloadLink)
})
},
function is51Gcw(url) {
return url.indexOf('51gcw') > -1
},
function isTangDou(url) {
return url.indexOf('tangdou') > -1
},
function getDownloadLink(html) {
const $ = cheerio.load(html)
let downloadLink
if (this.is51Gcw(process.argv[2])) {
downloadLink = $('.play_xz_mp4 a').eq(1).attr('href')
} else if (process.argv[2]) {
downloadLink = $('video').attr('src')
}
return downloadLink
},
複製代碼
測試一下,首先是 51廣場舞 的
node index http://www.51gcw.com/v/26271.html
能夠看到視頻的 URL 打印出來了
再試一下 糖豆廣場舞 的,結果卻打印出了 undefined
superagent
.get(process.argv[2])
.end((err, res) => {
if (err) {
return console.log(err)
}
scraping.succeed('已成功抓取到網頁\n')
// const downloadLink = getDownloadLink(res.text)
console.log(res.text)
})
},
複製代碼
結果發現,糖豆廣場舞的視頻是使用插件作的,也即,一開始時,頁面並無 video 這個標籤,因此 $('video').attr('src')
一定是獲取不到的。
仔細看看這段 HTML內容,發現這個地址就藏在某個對象裏,而這段內容其實也就是字符串,因此我決定使用正則表達式來取到這個 URL
改寫下獲取 URL 的方法
function getDownloadLink(html) {
const $ = cheerio.load(html)
let downloadLink
if (this.is51Gcw(this.url)) {
downloadLink = $('.play_xz_mp4 a').eq(1).attr('href')
} else if (this.isTangDou(this.url)) {
const match = /video:\s?'(https?\:\/\/\S+)'/.exec(html)
downloadLink = match && match[1]
}
return downloadLink
},
複製代碼
ok,如今能夠取到 URL 了
superagent 其實就是一個 http 工具,因此直接用它下載便可
咱們在取到 URL 後,傳到 downloadVideo 進行下載,代碼以下
const fs = require('fs')
const DOWNLOAD_PATH = 'gcw.mp4'
function downloadVideo(downloadLink) {
console.log(`${downloadLink}\n`)
if (!downloadLink) {
console.log('獲取下載連接失敗')
return
}
const downloading = ora('正在下載視頻...\n').start()
const file = fs.createWriteStream(DOWNLOAD_PATH)
file.on('close', () => {
downloading.succeed('已成功下載視頻\n')
// this.cutVideo()
})
superagent
.get(downloadLink)
.pipe(file)
}
複製代碼
測試一下,成功下載到視頻
視頻下載完成後,咱們要實現裁剪視頻,這裏用到兩個工具,
一個是 Inquirer 用於命令行交互,提問開始時間和結束時間
而裁剪視頻我使用的是 node-fluent-ffmpeg,這個實際上是用 node 調用 ffmpeg,因此電腦要安裝有 ffmpeg,這也是我平時經常使用的,安利下,功能很是強大,可使用命令進行視頻轉換格式,圖片轉視頻,切割視頻等等,程序猿就應該用這種😎
查閱下 node-fluent-ffmpeg 文檔,發現它只提供了 setStartTime(),沒有 setEndTime(),只能用 setDuration() 傳秒數來設置你要裁剪的時長(秒)
可是我總不能輸入開始時間,而後再計算出到結束時間的秒數,再輸入這個秒數吧,因此這裏我仍是讓用戶輸入結束時間,我在代碼用 ffprobe 獲取到視頻總長度,計算出開始到結束的秒數,這裏用到了兩個時間轉換的工具方法
咱們在下載完成後,調用 cutVideo 方法,以下
const ffmpeg = require('fluent-ffmpeg')
const inquirer = require('inquirer');
/**
* HH:mm:ss 轉換成秒數
* @param {string} hms 時間,格式爲HH:mm:ss
*/
function hmsToSeconds(hms) {
const hmsArr = hms.split(':')
return (+hmsArr[0]) * 60 * 60 + (+hmsArr[1]) * 60 + (+hmsArr[2])
},
/**
* 秒數轉換成 HH:mm:ss
* @param {number}} seconds 秒數
*/
function secondsToHms(seconds) {
const date = new Date(null)
date.setSeconds(seconds)
return date.toISOString().substr(11, 8)
}
const CUT_RESULT_PATH = 'cut_gcw.mp4'
function cutVideo() {
inquirer.prompt([
{
type: 'confirm',
name: 'needCut',
message: '是否須要裁剪?',
default: true
},
{
type: 'input',
name: 'startTime',
message: '請輸入開始時間, 默認爲 00:00:00 (HH:mm:ss)',
default: '00:00:00',
when: ({ needCut }) => needCut
},
{
type: 'input',
name: 'endTime',
message: '請輸入結束時間, 默認爲視頻結束時間 (HH:mm:ss)',
when: ({ needCut }) => needCut
}
]).then(({ needCut, startTime, endTime }) => {
if (!needCut) {
process.exit()
}
ffmpeg
.ffprobe(DOWNLOAD_PATH, (err, metadata) => {
const videoDuration = metadata.format.duration
endTime = endTime || utils.secondsToHms(videoDuration) // 設置默認時間爲視頻結束時間
const startSecond = utils.hmsToSeconds(startTime)
const endSecond = utils.hmsToSeconds(endTime)
const cutDuration = (videoDuration - startSecond) - (videoDuration - endSecond)
console.log(`\n開始時間:${startTime}`)
console.log(`結束時間:${endTime}`)
console.log(`開始時間(s):${startSecond}`)
console.log(`結束時間(s):${endSecond}`)
console.log(`裁剪後時長(s):${cutDuration}\n`)
const cutting = ora('正在裁剪視頻...\n').start()
ffmpeg(DOWNLOAD_PATH)
.setStartTime(startTime)
.setDuration(cutDuration)
.saveToFile(CUT_RESULT_PATH)
.on('end', function () {
cutting.succeed(`已成功裁剪視頻,輸出爲 ${CUT_RESULT_PATH} `)
})
})
})
}
複製代碼
至此,開發完成了,咱們能夠用單體模式封裝一下,使得代碼優雅一點😂,完整的代碼以下,也可在我 github 上查看
const fs = require('fs')
const superagent = require('superagent')
const cheerio = require('cheerio')
const ora = require('ora')
const inquirer = require('inquirer');
const ffmpeg = require('fluent-ffmpeg')
const utils = {
/**
* HH:mm:ss 轉換成秒數
* @param {string} hms 時間,格式爲HH:mm:ss
*/
hmsToSeconds(hms) {
const hmsArr = hms.split(':')
return (+hmsArr[0]) * 60 * 60 + (+hmsArr[1]) * 60 + (+hmsArr[2])
},
/**
* 秒數轉換成 HH:mm:ss
* @param {number}} seconds 秒數
*/
secondsToHms(seconds) {
const date = new Date(null)
date.setSeconds(seconds)
return date.toISOString().substr(11, 8)
}
}
const downloader = {
url: process.argv[2],
VIDEO_URL_REG: /video:\s?'(https?\:\/\/\S+)'/,
DOWNLOAD_PATH: 'gcw.mp4',
CUT_RESULT_PATH: 'gcw_cut.mp4',
run() {
if (!this.url) {
console.log('請輸入 51廣場舞 或 糖豆廣場舞 地址')
return
}
const scraping = ora('正在抓取網頁...\n').start()
superagent
.get(this.url)
.end((err, res) => {
if (err) {
return console.log(err)
}
scraping.succeed('已成功抓取到網頁\n')
const downloadLink = this.getDownloadLink(res.text)
this.downloadVideo(downloadLink)
})
},
is51Gcw(url) {
return url.indexOf('51gcw') > -1
},
isTangDou(url) {
return url.indexOf('tangdou') > -1
},
getDownloadLink(html) {
const $ = cheerio.load(html)
let downloadLink
if (this.is51Gcw(this.url)) {
downloadLink = $('.play_xz_mp4 a').eq(1).attr('href')
} else if (this.isTangDou(this.url)) {
const match = this.VIDEO_URL_REG.exec(html)
downloadLink = match && match[1]
}
return downloadLink
},
downloadVideo(downloadLink) {
console.log(`${downloadLink}\n`)
if (!downloadLink) {
console.log('獲取下載連接失敗')
return
}
const downloading = ora('正在下載視頻...\n').start()
const file = fs.createWriteStream(this.DOWNLOAD_PATH)
file.on('close', () => {
downloading.succeed('已成功下載視頻\n')
this.cutVideo()
})
superagent
.get(downloadLink)
.pipe(file)
},
cutVideo() {
inquirer.prompt([
{
type: 'confirm',
name: 'needCut',
message: '是否須要裁剪?',
default: true
},
{
type: 'input',
name: 'startTime',
message: '請輸入開始時間, 默認爲 00:00:00 (HH:mm:ss)',
default: '00:00:00',
when: ({ needCut }) => needCut
},
{
type: 'input',
name: 'endTime',
message: '請輸入結束時間, 默認爲視頻結束時間 (HH:mm:ss)',
when: ({ needCut }) => needCut
}
]).then(({ needCut, startTime, endTime }) => {
if (!needCut) {
process.exit()
}
ffmpeg
.ffprobe(this.DOWNLOAD_PATH, (err, metadata) => {
const videoDuration = metadata.format.duration
endTime = endTime || utils.secondsToHms(videoDuration)
const startSecond = utils.hmsToSeconds(startTime)
const endSecond = utils.hmsToSeconds(endTime)
const cutDuration = (videoDuration - startSecond) - (videoDuration - endSecond)
console.log(`\n開始時間:${startTime}`)
console.log(`結束時間:${endTime}`)
console.log(`開始時間(s):${startSecond}`)
console.log(`結束時間(s):${endSecond}`)
console.log(`裁剪後時長(s):${cutDuration}\n`)
const cutting = ora('正在裁剪視頻...\n').start()
ffmpeg(this.DOWNLOAD_PATH)
.setStartTime(startTime)
.setDuration(cutDuration)
.saveToFile(this.CUT_RESULT_PATH)
.on('end', () => {
cutting.succeed(`已成功裁剪視頻,輸出爲 ${this.CUT_RESULT_PATH} `)
})
})
})
}
}
downloader.run()
複製代碼
收到任務
楊麗萍廣場舞 醉人的花香
只要背面演示部分
安排
找到這個廣場舞:www.51gcw.com/v/35697.htm… 而後輸入命令
在等待下載的過程當中,去看看背面演示的開始時間和結束時間,下完後輸入
而後等待剪切完成!
比起之前效率提高了很多!🎉