一个使用nodejs爬取http://findsounds.com 上的动物音频脚本
简单介绍下使用的库,如图所示:

fs: 保存一些爬取的数据信息到本地
readline: 一行一行读取动物名文件(需要爬取的动物都保存在 animal.txt上)
superagent-charset:处理中文乱码的问题(但是爬取的网站都是英文,感觉用不上这库,以防万一吧)
async:强大的async库,但是这里主要使用async.mapLimit()控制并发请求数量和async.series()按顺序依次执行方法
分析需要爬取的网页结构

同图中可以看出,在首页点击搜索是发送的post请求,附带关键字
在分析下请求回来的源码,如图所示:

上面都是对首次点击搜索分析得到的结论,下面再看看点击第二页,网站是如何渲染数据的。
如图所示:


通过请求第二页数据可以发现,第二页数据是使用的是 GET,音频URL一样都是报错在 <script>标签中
到此就分析完了该网站,开始思考爬取的思路了:
爬取思路
爬取的方法以及方法间的联系如图所示:
readName():使用readline一行一行读取animal.txt的数据。
getUrl(): 根据readName()方法读取出来的数据拼接成搜索动物音频的URL,一个动物能拼接10条搜索音频的URL。
getAudioUrl():根据拼接好的搜索动物音频URL获取动物所有音频文件URL。
getAudio(): 根据获取到的动物音频文件URL发送请求获取音频,并音频保存到本地audio文件夹上。
animal.txt文件
animal.txt文件格式如下 单词+空格+中文:

readName() 读取animal.txt文件的动物单词
// -----------------读取 animal.txt 获取动物名--------------------
async function readName (callback) {
await new Promise(function (resolve,reject){
fs.readFile("./animal.txt", "utf-8", function(error, data) {
for (let i = 0; i<data.split('\r\n').length; i++) {
lines.push(data.split('\r\n')[i])
}
lines = lines.rmduplicate();//工具类 动物名去重并保存到 lines中
resolve();
})
})
console.log('总共 '+ lines.length +' 个动物');
}
getUrl()方法 根据动物单词拼接 GET 请求的URL
String.prototype.getUrl = async function (callback) {
await new Promise((resolve, reject)=>{
lines.forEach((item,index)=>{
let animalName = item.split(' ')[0];//获取一个动物单词
let animalNameChina = item.split(' ')[1];//获取动物中文
//每一个动物单词拼接20条GET请求url
for (let i = 1; i < 200 ;i+=10) {
//拼接GET请求url
let url = `http://www.findsounds.com/ISAPI/search.dll?start=${i}&keywords=${animalName}&seed=43-----${item}`
//保存所有拼接的url
urlPJ.push(url)
//保存到本地
fs.appendFile('拼接的getUrl.txt', url+'\r\n', function (err) {
if (err) {
console.log(err)
}else{
console.log('保存拼接的getUrl成功')
GET_URLS = 10
}
})
}
})
// 防抖函数 利用promise阻塞代码
let interID = setInterval(()=>{
if (GET_URLS < 0) {
resolve();
clearInterval(interID);
GET_URLS === null;
}
GET_URLS--;
},1000)
})
}
```JavaScript
### getAudioUrl POST请求获取音频url
// --------------------------获取 音频文件 audio------------------------------------
String.prototype.getAudioUrl = async function (callback) {
let type = ['.wav','.mp3','.au','.aif'];
await new Promise((resolve, reject)=>{
// 防抖 利用promise阻塞代码
let intervalID = setInterval(()=>{
if (GET_AUDIO_URLS < 0) {
resolve();
clearInterval(intervalID);
GET_AUDIO_URLS = null;
}
GET_AUDIO_URLS--;
},2000)
async.mapLimit(urlPJ, 20, async (url, callback)=>{
let animalName = url.split('-----')[1].split(' ')[0];
let animalNameChina = url.split('-----')[1].split(' ')[1];
await request
.get(url.split('-----')[0])
// .send({keywords: animalName, Search: 'search'})
// 必须附带请求头 否则无法获取数据
.set('Accept','text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3')
.set('Accept-Encoding','gzip, deflate')
.set('Accept-Language','zh-CN,zh;q=0.9')
.set('Content-Type','application/x-www-form-urlencoded')
.set('Host','www.findsounds.com')
.set('Origin','http://www.findsounds.com')
.set('Referer','http://www.findsounds.com/ISAPI/search.dll')
.set('Upgrade-Insecure-Requests',1)
.set('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36')
.set('buffer','true')
.then((res)=>{
//阻塞标识
GET_AUDIO_URLS = 10;
//从请求到的页面解析出音频url
let str1 = res.text.substring(res.text.indexOf('<script type="text/javascript">hit'))
str1 = str1.substring(0,str1.indexOf('</script>'))
let audioUrls = str1.replace('<script type="text/javascript">','').replace(/"/g,'').split(',').filter((item)=>{
return item.length > 30 && (item.indexOf(type[0]) > 0 || item.indexOf(type[1]) > 0 || item.indexOf(type[2]) > 0 || item.indexOf(type[3]) > 0)
});
//当没有数据时,进行POST请求
if (audioUrls.length === 0) {
url.postAudioUrl();
return false;
}
//由于有些URL字符串不带协议或者不带www的为它补全
audioUrls = audioUrls.map((item)=>{
if (item == '') return false;
let itemA = item;
if (itemA.indexOf('w') == 0) {
itemA = 'http://'+itemA;
myAppendFile('能获取的音频url.txt',`${itemA}-----${animalName} ${animalNameChina},`,'保存音频url '+filterUrlArr.length+'个成功');
return `${itemA}-----${animalName}-----${animalNameChina}`;
} else if (itemA.indexOf('http://') === 0 && itemA.indexOf('www')<0) {
itemA = itemA.replace('http://','http://www.');
myAppendFile('能获取的音频url.txt',`${itemA}-----${animalName} ${animalNameChina},`,'保存音频url '+filterUrlArr.length+'个成功');
return `${itemA}-----${animalName}-----${animalNameChina}`
} else if (itemA.indexOf('http://www') < 0) {
itemA = 'http://www.'+itemA;
myAppendFile('能获取的音频url.txt',`${itemA}-----${animalName} ${animalNameChina},`,'保存音频url '+filterUrlArr.length+'个成功');
return `${itemA}-----${animalName}-----${animalNameChina}`;
} else {
myAppendFile('能获取的音频url.txt',`${itemA}-----${animalName} ${animalNameChina},`,'保存音频url '+filterUrlArr.length+'个成功');
return `${itemA}-----${animalName}-----${animalNameChina}`;
}
}).rmduplicateAudios();
//保存去重 扁平后的url
filterUrlArr.push(...audioUrls);
filterUrlArr = filterUrlArr.myflat(2).rmduplicate();
}).catch(error => console.log('caught', error))
}, function(err, result){
if(err){
console.log(err);
}else{
console.log('resultgetAudioURL',result)
}
})
})
}
POST请求获取音频url数据
POST请求与GET请求获取音频url类似 这里就不写出来了
downloadAudio()函数 下载音频文件
let downloadAudio = function (callback) {
console.log('需要请求的数量',filterUrlArr.length)
//使用 setInterval 控制访问速度
var intervalIDS = []
//遍历所有音频URL
filterUrlArr.forEach((item,index)=>{
var id = setTimeout(()=>{
let urlItem = item.split('-----')[0];
let animalName = item.split('-----')[1];
request.get(urlItem).charset('binary').buffer(true).end(function (err, res) {
if (err) {
console.log('urlItem',urlItem)
console.log(res)
}
if (!res) {return false}
if (res.headers['content-type'].indexOf('audio')<0) {return false}
fs.writeFile(`.//audio//${animalName}-${Math.ceil(Math.random()*200)}.wav`,res.text,'binary',(err)=>{
if (err) {
console.log('有错误了==========',err);
return false;
}
console.log(animalName+'---下载成功')
})
})
clearInterval(intervalIDS.splice(index,1,0)[0])
},index*500)
intervalIDS.push(id)
})
}
函数主入口
// ----------------------程序主入口-----------------
//例如async的series()函数,readName、''.getUrl、''.getAudioUrl、downloadAudio依次执行
async.series([readName,''.getUrl,''.getAudioUrl,downloadAudio],function (err,result) {
if (err || err === 'err') {
console.log('err---',err);
} else {
console.log('result',result);
}
})
爬取结果
笔者找了249个动物单词,爬取了3400左右的音频文件,有些动物没有在网站中不存在音频....例如 鲫鱼。。。

源码链接 如果对你有帮助,能否点个小星星...
https://github.com/734776328/nodejs-
结束语
笔者还是大四生(遗憾的错过了秋招 求职中...),因此技术有限,欢迎各路大佬帮忙指出不足之处...万分感谢...
笔者也会陆陆续续把曾经的学习笔记搬到简书来,说不低对你会有点小帮主,欢迎订阅哈