关于网易云爬虫的一点总结
这几天因为工作需要爬取网易云音乐各个榜单的歌单。之前使用过python写爬虫,这一次使用的是node,因为工作需要。
说简单简单,加上注释也就200多行,不加注释100行的样子吧;说不简单也不简单,踩了三四天的坑。希望我的这篇文章能让你少踩一些坑。
1. 确定网站的反爬虫策略
所谓知己知彼,方能百战不殆。我在这个地方吃了很大亏。之前写的python爬虫都是爬豆瓣。豆瓣比较好爬,只需要注意访问频率不被封IP即可,即使被封了,再一次登录即可解封。而这次的网易云爬虫有点不一样,直接下载网页是得不到你所需要的歌单信息的。
首先贴一个关于反爬虫策略的链接 https://blog.csdn.net/miner_zhu/article/details/81093616
网易云反爬虫属于第四类,之前我也就知道ajax动态渲染,后面发现榜单网页的XHR文件里面包含的数据只有评论的,并没有歌单,所以我们需要采取的对策是selenium,导致在这里浪费太多时间。
2. 确定我们需要使用的方法
上文以及提及到了,selenium 模块。这个模块无论是python还是node都是支持的。
node下: npm install selenium-webdriver
3. selenium遇到的一点问题
selenium 是需要驱动的,而且需要驱动暴露在环境路径下。这里的坑主要是
1) firefox 的驱动
2)Chrome的驱动
提示是
按照提示的信息是说,这个版本只支持 76 的版本,而我使用的一直是 76 的版本,而 76 的版本总共就两个,我都尝试了,但是这个error 报了一下午,而Firefox的驱动也用不了,在这里浪费了大量时间;后面心想是不是只有76的版本不支持,然后下载了75 的驱动,然后这个error消失了,WTF! 真的,我那时很高兴又很不高兴,高兴的是error消失了,不高兴的是这个提示信息误导我太久,我也曾一度怀疑是不是我英语理解错误,然后找了外语系的同事问了一下,得到答复后我只想锤写这个错误信息的人一顿。
selenium 能用了之后,还有就是怎么得到完整的网页的问题。
是上图这样的网页,而不是下面这样的
最开始的想到的是需要找到JS,然后driver运行js。后面发现只需要switchTo().frame('frame')就可以了。
4. 分析得到的完整的网页
cheerio 加正则表达式
5. 爬取所有的榜单网页
6. 格式化保存
很晚了,不写了:)但是最后贴上代码以及git-hub地址,代码注释比较详细
SongScrapy.js
``` node.js
const chrome = require('selenium-webdriver/chrome');
const firefox = require('selenium-webdriver/firefox');
const {Builder, By, Key, until} = require('selenium-webdriver');
const fs = require('fs')
const cheerio = require('cheerio')
const Entities = require('html-entities').XmlEntities
const entities =new Entities();
const width =1080;
const height =1920;
module.exports = SongScrapy;
/**
* 用于爬取特定歌单页
* @param href
* @param name
* @constructor
*/
function SongScrapy(href, name) {
this.url = href;
this.name = name;
this.self =this;
// 使用Chromedriver, 只支持 75 及以下的版本
// 运行如果报错说缺少驱动,按着他提供的链接下载即可
// 不过driver需要暴露在系统路径下
this.driver =new Builder()
.forBrowser('chrome')
.setChromeOptions(
new chrome.Options().headless().windowSize({width, height}))
.setFirefoxOptions(
new firefox.Options().headless().windowSize({width, height}))
.build();
}
/**
* 将前面获得榜单名字以及 href
* 继续抓取,得到mei每个歌单的网页
* @param href
* @param name
*/
SongScrapy.prototype.getSource =function(){
this.driver.get(this.url)
.then(_ => {
//switchTo().frame 是必须的操作
//如果没有这一步,很可能得到的是没有渲染的网页
this.driver.switchTo().frame('g_iframe');
this.driver.wait( () => {
this.driver.getPageSource().then( (source) => {
this.getTopList(source, this.name);
});
return true;
},5000);
})
}
SongScrapy.prototype.getTopList =function (html, name) {
let $ = cheerio.load(html);
let topList = $('#song-list-pre-cache');
/**
* 直接查找的话,转为text中间会
* 夹杂一些无关字符
*/
// topList.find('tr').each(function (songItem) {
// let songName = $(this).find('.txt').find('')
// console.log('txt', songName.text())
// })
//使用正则表达式来查找歌名
/**
* 得到的item类似这样
*
* data-res-action="share" data-res-name="山上雪"
* data-res-author="万象凡音/黄诗扶"
* data-res-pic="http://p2.music.126.net/8C0LCrwm-BG3iNtWNFHqFw==/109951164212877338.jpg"
* class="icn icn-share" title="分享">分享
*/
// entities 的作用是将中文乱码重新编码
// 出现中文乱码的原因不是网页编码集不是'utf-8'
// 而是我们直接对topList 直接使用了 ToString 方法
let listString = entities.decode(topList.toString());
//正则提取
let songs = listString.match(/<span data-res-id="\d{0,40}" data-res-type=".{0,20}" data-res-action=".{0,20}" data-res-name=".{0,100}" data-res-author=".{0,100}".{0,300}" class=".{0,70}" title.{0,100}<\/span>/g)
//对于每首歌提取出我们需要的信息
console.log('length', songs.length)
// 得到真正的文件名
let filename =`${this.name}-${songs.length}.json`;
// 如果文件已经存在,则删除文件
if(fs.existsSync(filename)){
fs.unlink(filename, (err) => {
if(err){
return console.log(err);
}else{
}
})
}
for(let songof songs){
let songItem = $(song);
//将我们需要的信息提取出来
let songName = songItem.attr('data-res-name')
let songAuthor = songItem.attr('data-res-author')
let songId =songItem.attr('data-res-id');
let songObj = {
name: songName,
author: songAuthor,
id: songId,
}
//转为json
let jsonStr = JSON.stringify(songObj) +',';
//console.log(jsonStr)
fs.appendFileSync(filename, jsonStr);
}
}
//
// function test() {
// let songScrapy = new SongScrapy('https://music.163.com/#/discover/toplist?id=19723756', './netEaseLists/云音乐飙升榜');
// songScrapy.getSource();
// }
//
// test();
```
ListScrapy.js
```node.js
const chrome = require('selenium-webdriver/chrome');
const firefox = require('selenium-webdriver/firefox');
const {Builder, By, Key, until} = require('selenium-webdriver');
const fs = require('fs')
const cheerio = require('cheerio')
const Entities = require('html-entities').XmlEntities
const entities =new Entities();
let SongScrapy = require('./SongScrapy')
//windows size 参数
const width =1080;
const height =1920;
module.exporpts = ListScrapy;
/**
* baseUrl 是获取榜单的入口
* baseDir 是存取文件的base地址
* refer 是 netEase 的网址
* 因为这个爬虫只能爬取网易云
* 所以就写死了
* songLists 保存获取的榜单名以及 href
* @param baseUrl
* @param baseDir
* @constructor
*/
function ListScrapy(baseUrl, baseDir) {
this.self =this;
this.baseUrl = baseUrl;
this.baseDir = baseDir;
this.refer ='https://music.163.com/';
this.songLists = [];
// 使用Chromedriver, 只支持 75 及以下的版本
// 运行如果报错说缺少驱动,按着他提供的链接下载即可
// 不过driver需要暴露在系统路径下
this.driver =new Builder()
.forBrowser('chrome')
.setChromeOptions(
new chrome.Options().headless().windowSize({width, height}))
.setFirefoxOptions(
new firefox.Options().headless().windowSize({width, height}))
.build();
}
/**
* 将得到的网页 source 交给 getLists 处理
* 得到榜单的名字以及 href
*/
ListScrapy.prototype.run =function () {
this.driver.get(this.baseUrl)
.then(_ => {
//switchTo().frame 是必须的操作
//如果没有这一步,很可能得到的是没有渲染的网页
this.driver.switchTo().frame('g_iframe');
this.driver.wait( () => {
this.driver.getPageSource().then( (source) => {
this.getLists(source);
});
return true;
},5000);
})
}
/**
*
* @param html
* 将传过来的网页解析
* 得到每一个榜单的名字以及地址
*/
ListScrapy.prototype.getLists =function (html) {
let $ = cheerio.load(html);
//所有榜单都在 class="name" 下
//lists 不能遍历,所以采用的是正则分割
let lists = $('.name')
// entities 的作用是将中文乱码重新编码
// 出现中文乱码的原因不是网页编码集不是'utf-8'
// 而是我们直接对topList 直接使用了 ToString 方法
let listsStr = entities.decode(lists.toString())
let listArr = listsStr.match(/<p class="name"><a .{0,80}p>/g);
//对每一个 class="name" 的p标签进行遍历
//提取出 href 和 name
for(listof listArr){
let listObj = {}
let elementA = $(list);
listObj.href = elementA.find('a').attr('href');
listObj.name = elementA.text();
this.songLists.push(listObj);
console.log(listObj)
let songScrapy =new SongScrapy(this.refer + listObj.href, this.baseDir + listObj.name);
songScrapy.getSource();
}
}
function test(){
let netEaseScrapy =new ListScrapy('https://music.163.com/#/discover/toplist', './netEaseLists/');
netEaseScrapy.run()
}
test()
```