编写日期:2019-01-28 09:01:15
最近我司有个需求,1: 根据单词列表下载百度翻译上对应单词的英文mp3到本地,2: 根据单词列表爬取英文翻译的双例语句中最短的一个例句,并且把对应的英文push到表格内指定的位置,下载这个例句对应的音频到指定文件夹
爬取单词mp3
1. 解析xlsx
大致思路,通过fs.readFile读取指定的文件,转换成Array,接着循环该Array,根据单词去百度下载指定的mp3
// 第一步
// 编码格式设置为utf-8,如果不设置data为二进制的流
fs.readFile(filePath, {encoding:'utf-8'}, function (err, data) {})
// 第二步
// 把readFile获取到的data传入到ConvertToTable方法内,转换成一个数组
// 因为在字符串内表格内每一行中间通过'\r\n',做换行分割的
// ConvertToTable接收一个函数作为callBack
function ConvertToTable(data, callBack) {
data = data.toString();
var table = new Array();
var rows = new Array();
rows = data.split("\r\n");
for (var i = 0; i < rows.length; i++) {
table.push(rows[i].split(","));
}
callBack(table);
}
download 音频mp3
fs.readFile(filePath, {encoding:'utf-8'}, function (err, data) {
var table = new Array();
if (err) {
console.log(err.stack);
return;
}
ConvertToTable(data, function (table) {
table.forEach((element, idx) => {
if (idx === 0) return false
if (!obj[`${element[2]}`]) {
obj[`${element[2]}`] = {}
}
// 创建文件夹
shell.mkdir('-p', `/Users/sinker/workspace/wordlist/mp3/${element[2]}`)
// 下载mp3到刚才创建的文件夹内
setTimeout(() => {
download(`https://sp0.baidu.com/-rM1hT4a2gU2pMbgoY3K/gettts?lan=en&text=${element[0]}&spd=2&source=alading`).then((data) => {
fs.writeFileSync(`/Users/sinker/workspace/wordlist/mp3/${element[2]}/${element[0]}.mp3`, data);
}).catch((err) => {
console.log(err, idx, `${element[0]}`)
})
}, 10 * idx)
});
})
});
- 下载的时候需要根据level分成不同的文件夹,把对应的音频放到指定的level文件夹内
- 利用download这个库,去百度对应的页面下载音频
- 下载完,通过fs.writeFileSync写入到指定文件夹
- download方法为什么要加延迟,因为如果不加的话会并发出去好几百条请求,nodejs底层的dns解析模块会直接报错cancel流程,所以加一个延迟
爬取完的目录结构
├── 5
│ └── active.mp3
└── 6
└── Africa.mp3
升级 -> 下载双例语句
要求
- 爬取对应单词双例语句内最短的一个英文语句和出处合并到指定表格内该单词这一行的最后一个表格内
- 下载该英文语句的mp3到指定文件夹
分析页面
打开百度翻译分析页面结构

通过查看页面结构可以看到,通过获取class为sample-source、sample-target、sample-resource的三个标签的内容即可满足该需求
解析csv表格
需求方给到的表格格式是xlsx,由于换了解析库,所以手动转换格式成了csv(不能直接改后缀,需要另存为)
表格实例
| Id | word_name | word_name_cn | grade_id | point_id | part_id |
|---|---|---|---|---|---|
| 1 | ant | 蚂蚁 | 1 | 1 | 1 |
// 网上找的解析表格的例子,自己根据需求改造了一下
var parse = require('csv-parse');
var async = require('async');
var parser = parse({delimiter: ','}, function (err, data) {
// data的最大长度,主要为了退出循环用
maxLen = data.length
// 每次从表格内取出5条数据,主要为了并发用,否则一条条循环特别慢
async.mapLimit(data, 5, function (line, callback) {
doSomething(line).then(function(keyword) {
callback();
}, () => {}).catch((err) => {
// 把出错的line收集起来,方便后期继续循环
errorArr.push([line[1]])
console.log('async.mapLimit出错了')
});
})
});
fs.createReadStream(inputFile).pipe(parser);
-
csv-parse 解析csv类型表格的库
-
async主要为了控制流程,比如并发等
-
doSomething是自定义的方法,接受一个参数line,该字段代表你解析出来的每一条数据
// 解析出来数据实例
[1 , ant, '蚂蚁', 1, 1, 1]
// doSomething
// 解析每一行csv的数据
function doSomething (line) {
return new Promise(async function (resolve, reject) {
// 如果每一行的数据的第二个字段的值为word_name,那么直接pass,因为这个是表格的title
// 如果数组内索引为7的数据为空字符串,那么代表没有爬取成功
if (line[1] !== 'word_name' && line[7] =='') {
// 爬虫的具体实现方法
await spinder(line[1], function insetXlsx (info = {}){
// info为爬取的数据,根据爬取的数据重新组装表格或者下载音频,下面会讲
}
}
}
}
spinder具体实现
第一次爬取的时候用的是cheerio,但是这个库有个弊端只能爬取静态html,也就是说你的网页是通过js生成的,那么这个库只能爬取到源码
那么如果爬取模版生成的页面应该怎么做呢?经过Google发现一个神奇的框架puppeteer
puppeteer 翻译是操纵木偶的人,利用这个工具,我们能做一个操纵页面的人。puppeteer是一个Nodejs的库,支持调用Chrome的API来操纵Web,相比较Selenium或是PhantomJs,它最大的特点就是它的操作Dom可以完全在内存中进行模拟既在V8引擎中处理而不打开浏览器,而且关键是这个是Chrome团队在维护,会拥有更好的兼容性和前景
spinder方法具体实现
// 爬取百度翻译对应的页面
async function spinder (keyword, cb) {
var doubleLang = []
console.log('keyword', keyword)
// 启动Chrome浏览器以headless的方式,not gui
const browser = await puppeteer.launch({
headless: true
});
// 启动一个tab
const page = await browser.newPage();
// 该tab跳转到指定的百度翻译的页面
await page.goto(`https://fanyi.baidu.com/translate?aldtype=16047&query=${keyword}&keyfrom=baidu&smartresult=dict&lang=auto2zh#en/zh/${keyword}`);
// 等待2.5s,如果没有的话,可能页面还没渲染完成,抓取到的都是空数据
await page.waitFor(2500);
// 分析页面得到双例语句的父级有一个class为double-sample
const DOUBLESAMPLE = '.double-sample';
// 解析.double-sample内的html
doubleLang = await page.evaluate(sel => {
// 像jquery一样操作页面
const ulList = Array.from($(sel).find('ol li > div'));
var newinfo = ulList.map((v, idx) => {
// 获取英文语句
const enText = $(v).find('.sample-source').text()
// 获取下载该语句对应mp3地址的参数
const mp3Text = $(v).find('.sample-source .op-sound').attr('data-sound-text')
// 出处
const source = $(v).find('.sample-resource').text()
return {
enText,
source,
mp3Text,
len: enText.length,
idx
}
});
return newinfo
}, DOUBLESAMPLE);
// 按照enText的字符串长度,从小到大排序,取出长度最下的一个
doubleLang.sort(function (cur, next) {
return cur.len - next.len
})
// 传出来字数最小的那个英文文案
// doubleLang数组如果有多个,第一个为空,所以直接取第二个,
// 如果只有1个,取第一个
cb && cb(doubleLang.length > 1 ? doubleLang[1]: doubleLang[0])
// 关闭浏览器,一个流程结束
await browser.close();
}
OK,在这一步获取到了我们想要每个单词对应的百度翻译的内容,那么我们下面继续处理这些数据
组装数据和下载音频
function doSomething (line) {
return new Promise(async function (resolve, reject) {
// 如果每一行的数据的第二个字段的值为word_name,那么直接pass,因为这个是表格的title
if (line[1] !== 'word_name' && line[7] =='') {
await spinder(line[1], function insetXlsx (info = {}) {
if (!info.enText || info.enText == '') {
errorArr.push(line)
} else {
line[7] = info.enText
line[8] = info.source
}
// 创建文件夹
// shell.mkdir('-p', `/Users/sinker/workspace/wordlist/doubleLangMp3/${line[1]}`)
download(`https://fanyi.baidu.com/gettts?lan=en&text=${info.mp3Text}&spd=3&source=web`).then((data) => {
fs.writeFileSync(`/Users/sinker/workspace/wordlist/doubleLangMp3/${line[1]}.mp3`, data);
}).catch((err) => {
errorArr.push(line)
console.error(err, `${line[1]}`)
})
}).catch(() => {
errorArr.push(line)
console.log('spinder出错了')
})
xlsxArr.push(line)
} else {
if (line[1] !== 'word_name') {
xlsxArr.push(line)
}
}
if (maxLen === xlsxArr.length) {
console.log('开始往本地注入数据')
// 3秒以后再往表格里面注入
setTimeout(() => {
writeXls(xlsxArr, '/Users/sinker/workspace/wordlist/new_word_list_all.xlsx')
writeXls(errorArr, '/Users/sinker/workspace/wordlist/error_word_list_all.xlsx')
}, 3000)
}
resolve(xlsxArr.length)
})
}
这里需要注意的是,由于数据相对比较多不到1000条,所以爬取的过程比较久,所以要做好一些异常处理,否则很容易出现下载到500条的时候出错,直接crash正个流程,目前处理方式是通过catch捕捉到出错的行数,然后手动再次执行该脚本。其实可以做成自动的,只需要在循环完成的的时候判断errorArr,如果不为空再次自动执行一下即可