这是一件单纯因为有趣,因为好玩做的事情~
对技术实现不感兴趣的同学可以直接跳转到文末看精彩评论(谁能告诉我如何实现页面内跳转...)。
一、背景
干程序猿这行已经几年了,一直以来都忙于工作,业余时间学一些东西也都是面试相关的,比如说volatile,HashMap之类的源码,总觉得学点其他的东西面试用不到,是浪费时间。
阿里月饼事件以后,一直很羡慕那几个哥们,随手就可以写出一个程序做点有意思的事情,自己却没有这样的能力。
后来在知乎上看到一个帖子,程序猿可以做哪些有趣的事,看到很多关于爬虫的东西,了解爬虫以后发现这个东西真的很有意思,确实可以做很多有意思的事情。
关于网易云音乐的爬虫,无论是抓取评论还是抓取歌曲都已经有很多人做了。网上也有很多资料,但别人能抓不代表自己能抓,况且我就想知道点赞数最多的评论是哪条,这个网上还真没有资料。于是在一个无聊的周末终于决定自己开搞了。
二、从零开始写爬虫
网上搜一下怎么写爬虫,大把资料,各种语言都可以搞,因为之前听同事说过一般都用python写爬虫,搜了一下,果然有现成的爬虫框架scrapy,那就不墨迹了,直接开始,我是不懂python的,更别说scrapy了,但由于写了几年的Java程序,所以虽然不会python但也能看懂,就直接跳过学习python的阶段,直接看scrapy的资料。
这里必须要给scrapy文档点个赞了,不论是中文的还是英文的都写的很棒(后来的开发中发现中文文档更新会慢一点),盯着文档看了几个小时,发现零基础上来就看文档实在是太过于乏味了,而且看了就忘,根本不能理解。于是去B站上面搜了一下爬虫,果然有很多教学视频,挑了一个看了下,直接按照视频抄了一个Demo下来,一运行真的跑起来了,原来真的这么简单。
这里我就不再继续看视频研究了,打算边写边学,直接开始解析网易云音乐的页面。最开始肯定是选定一首歌把它点赞最多的评论爬下来,这里其实通过看网易云音乐的布局就知道点赞最多的就是热门评论中的第一条。
一开始我还是很单纯的以为页面展示的内容都是链接https://music.163.com/#/song?id=26584453返回的,但打开chrome调试工具从返回值中搜索评论里面的关键字却怎么都找不到,在网上搜了一下才发现是由其他链接查询的(这里我很惭愧,自己写了这么多年代码,给前端提供了无数接口居然在实际应用中连这点东西都反应不过来,汗。。。)。
看了网上说的自己排查所有请求的资料后,入了门这就简单多了,毕竟工作几年了,基础的东西还是知道的。
当你在找哪个请求返回评论的时候,其实没什么特别好的办法,只能一个一个查,这里有个小技巧就是在请求类别里面,在页面上就是Doc,通过Ajax发请求的就是XHR(至少我就遇到这两个),这样可以加快排查速度,上个页面截图:
WX20171126-233042@2x.png如上图,经过排查,我们发现这个R_SO_4请求的Response返回值中包含了评论信息,仔细观察一下就可以从请求中找到三个信息:
- R_SO_4_后面跟的数字很容易就能发现是歌曲ID。
- 该请求是POST。
- 这个请求的两个参数是params跟encSecKey,从它们的内容看肯定是经过加密处理的。
前两条都好办,关键就是我们的参数要怎么获取?这里知乎上一篇帖子的回答里有个大神详细解释了如何推测参数的加密过程,感兴趣的可以去查一下。庆幸的是因为我只要查点赞最多的评论并且这些评论都是在热门评论里,测试以后会发现不管哪首歌曲,把params跟encSecKey参数传一样的值都是可以获取到热门评论的(如果你需要翻页获取所有评论,那就要研究如何加密参数了)。
有了这个结论以后很快就可以写出爬取指定歌曲页的热门评论:
发送请求的代码片段(可以看到我的参数是写死的):
def start_requests(self):
url = 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_' + self.song_id + '?csrf_token=37bdc4f8c949263c8003ab9cb140cc94'
# FormRequest 是Scrapy发送POST请求的方法
yield scrapy.FormRequest(
url,
formdata={"params": "EwqXu+gKzBxeOxs0ipIsqrj3FrBx+9+0rVNMetFypm+wyolPofTK3OunU6ublmvwlKd/DOQBXXuQsG7plOY1Ld3M07otT0/zkMbRChueAwaw/vWt2preqSAjzL90fjcHZC5Fpu+2/G9phSJ2uNdzoL+CL+7p596lJ1+IreZ/EQ9YrGld5cf34wr8vnix2bWeswbFKU3mZhT7joxZCZb3VZteJfAo8ZaGRnBbHRwrvr0=", "encSecKey": "5179db23b4a2292422de58534d147edac80387513fa066f958f9d849af7a4718a4362e028884f6985eb109c0ef1fa9e7b42b9b8135fd169c273a275a90efbc635d829722e5308e5c05c77fb6cca3b4b62bbd5cd28058e1db0fc8c7d6f9026db0ae1f7596bf8c27fc405325fd0bb106c97d61801a096c5891b8731a70ed58b791"}
)
解析返回结果的代码片段:
def parse_song_page(response):
jsonstr = json.loads(response.body_as_unicode())
try:
if len(jsonstr["hotComments"]) > 0:
for comm in jsonstr["hotComments"]:
item = CommentItem()
item['content'] = comm["content"]
item['likeNum'] = comm["likedCount"]
yield item
except Exception, e:
logging.info("解析热门评论异常,输出返回结果:" + response.body + ",异常:" + str(e))
找到评论以后由于一条评论包含的信息很多,而我只想要评论内容跟点赞数,因此根据文档引入了Item对象并加入了pipeline把Item输出到指定文件。
这一步成功获取到评论以后,信心大增。按照这种方式依次可以写出爬取指定歌单页的所有歌曲的热门评论,这样一来输出的评论就比较多了,于是想到先排序再输出,办法是创建一个大小为10的链表,当评论数量小于10时就直接加入链表,当大于10时就对链表中的Item以点赞数排序,然后让新来的Item跟链表中点赞数最小的Item比较,如果小于就忽略,大于就替换掉原来最小的,以此过滤所有的Item(这其实是一个最小堆的数据结构)。
有了想法以后自然就是用python去实现,这里我是从网上现查python的list结构,然后怎么排序,如果元素是对象怎么按照元素的某个字段排序,反正找到例子,然后写个简单的python文件进行反复调试,最终解决。
调试不同页面时,我是使用scrapy自带的shell功能反复进行尝试的,上个截图:
image.png
进行到这一步,我们已经完成了爬取指定歌单下面的所有的歌曲中点赞数最多的前10条评论并且输出到指定文件。下面就是遍历网易云音乐的所有歌单,然后反复执行上面的操作。
image.png在网易云音乐歌单页中可以看到有很多分类,排序也分热门跟最新两种。由于歌单类型中无论你选择全部,还是华语,欧美,日语歌单页数都差不多37页左右,汗。。。所以为了安全起见,我打算按照语种顺序依次查询华语,欧美,日语,韩语,粤语,小语种下面所有歌单中的所有歌曲,有了前面的经验这里就只剩下反复调试页面结构,然后写代码实现了,不难但要有耐心。
好了,终于大功告成了,貌似可以达到目标了。赶紧启动爬虫任务,在我调试完工程中的bug以后任务终于成功跑起来了,看着不断输出的爬虫信息,心里真是充满了满足感。
就在我以为可以喝杯咖啡坐等结果出来时,突然发现程序运行一段时间以后大量报出异常,提示信息为找不到热门评论的字段,难道是网易云音乐改版了?赶紧加上打印返回结果的逻辑再启动任务,发现返回结果中没有评论信息,相反很简单并且里面有cheating字样,看来是短时间内一个IP请求了太多次,被封了。上一张截图:
image.png
这也就意味着必须要进行反封知识的学习了。网上很多防止被封的策略都写的很好,我按照他们的思路一一进行了尝试。首先是加入头信息UserAgent,并且找了各个浏览器的UserAgent集合每次请求从集合里面随便选一个,这里通过各种资料以及文档引入了scrapy的下载器中间件进行这个功能的实现。然后又加入下载延迟时间,然后不断调试修复BUG,最终任务炮一段时间发现还是会被封。那就只能引入代理IP了。
关于代理方面的资料网上很多,在scrapy的请求中引入代理很简单,找一个免费的代理网站然后把上面的IP爬下来也很简单,一开始我手动试了几个代理IP,发现很多都不行,然后就打算写个自动化流程,思路就是每次发请求时去代理池里面获取一个IP,如果池子为空,则实时去代理网站爬一些代理下来,然后在用一个普通的网站去验证一下代理是否有效,有效则加入我们的代理池,然后让开始的请求从代理池用随机选一个,无效则继续从代理网站上爬。在处理请求返回的内容前首先判断一下IP是否被封(这点从内容就可以判断出来)。如果被封就把IP删除,然后进行重试。
这个功能也是使用下载中间件特性来做,先来个发送请求前的处理代码片段如下:
if "music.163.com" in request.url:
# 需要更换代理
if "change_proxy" in request.meta.keys():
logging.info("检测到需要更换代理IP的请求,change_proxy=" + str(request.meta['change_proxy']) + ",proxy=" + request.meta['proxy'])
del request.meta['change_proxy']
# 删除无用代理
invalid_proxy = request.meta['proxy']
if invalid_proxy in http_proxies:
http_proxies.remove(invalid_proxy)
# 没有可用代理,需要重新从代理网站获取
while len(http_proxies) == 0:
logging.info("没有可用代理,开始重新获取...")
get_new_proxies()
logging.info("本次获取到有效代理IP:" + str(http_proxies))
proxy = random.choice(http_proxies)
request.meta['proxy'] = proxy
get_new_proxies()方法中做的事情就是解析代理网站上面的IP,并对有效性进行验证。
再来一个收到请求结果以后的代码片段:
def process_response(self, request, response, spider):
if "music.163.com" in request.url:
try:
jsonstr = json.loads(response.body_as_unicode())
if "msg" in jsonstr.keys():
if jsonstr['msg'] == 'Cheating':
# 请求被封则换代理重试
logging.info("IP被封,开始重新获取..." + request.meta['proxy'])
request.meta['change_proxy'] = True
request.dont_filter = True
return request
except Exception:
pass
return response
这里是根据返回结果进行判断是否被封,如果被封就标记该请求需要换代理IP,然后进行重试。
随着爬虫项目功能越来越完善,我发现整个爬虫环节变得很长,调试起来很费劲,而且由于爬整个网易云音乐的时间比较长,按照我的实际测试一个类型的所有歌单需要爬一个多小时,所以必须引入日志系统,在各个环节打印相关的日志,另外输出解析的过程,这样我也可以明确的看出系统是在正常运行还是已经报错了。把这一套逻辑下来,调试没有BUG之后,整个爬虫就可以顺利跑起来了。
上几张我的日志截图,任务刚开始时获取代理IP:
image.png
任务执行一段时间以后,不断爬取歌曲的日志(满满的幸福感~):
image.png
最后任务执行完成以后的汇总日志:
image.png
还记得那天是11.25号晚上11点半左右,当把网易云音乐华语类别下面的所有歌单成功跑完时我真的兴奋的不得了(在此之前经历了各种被封以及无数错误需要修复)。第二天早上起来,我又跑了剩下的几个栏目,确认数据是正确的,总算大功告成。
由于获取代理,获取网易云音乐评论都是动态实时进行的,除非这两个网站页面进行改版,不然随时随地我的爬虫都可以运行。
三、总结
这个爬虫项目前后持续了两三周,每天下班回家就研究怎么写,周末两天也要进行大量调试,时间虽然持续的不就,但由于不懂python,经常每加入一个新的功能都会引入各种bug,两三次已经烦的想放弃了,庆幸自己坚持了下来,在最后成功跑出数据的时候还是很有成就感的。
总体来说这个爬虫是很简单的,用来入门很不错。网易云音乐也很良心,获取评论不需要登录,而且获取热门评论的话也不需要研究参数加密这一块东西,唯一的难点应该就是把代理搞好,防止被封,关键是要有耐心。
程序员真的可以做很多有趣的事情,努力不让生活的压力扼杀自己的想象力。
网易云音乐点赞最多评论top10:
排名 | 所在歌曲 | 点赞数 | 评论内容 |
---|---|---|---|
1 | 童话镇 | 58万 | 对了~平时每晚八点半,在斗鱼67373直播间可以看到我。不一定在唱歌,也可能在讲段子[] |
2 | 成都 | 57万 | 你真的红了 很开心 内场票也可以卖到1000+ 虽然以前50就能听你的livehouse 越来越听不起你的演唱会 不过没关系 我会努力变得跟你一样好 民谣不应该穷 以前你吃过的苦都是值得的 难过的日子都是你陪我一起过的 你说你是个普通人 想要买房结婚 我知道 愿你有酒有肉有姑娘 #赵雷 |
3 | 晴天 | 52万 | 高一听的,那时候遇到了孩儿他妈,然后就这么幸福下来了 |
4 | See You Again (feat. Charlie Puth) | 50万 | 就在保罗沃克去世前13天,我高中最好的一个兄弟也因为意外去世了。曾经一起在寝室看了速度1,之后一起看了速度5速度6。曾经也同样挚爱速度与激情。今天去看了第七部,保罗沃克不在了。我兄弟也不在了。当听到这首see you again真的眼眶湿润了。献给保罗沃克,献给我的兄弟! |
5 | 告白气球 | 49万 | 刚刚我把这首歌分享给我喜欢的女生向她告白成功了!!! |
6 | Fade | 47万 | 在Alan walker还不怎么火的时候,我机智的注册了这个id,因为我知道,这叼毛绝对会火的🙈 |
7 | 带你去旅行 | 46万 | 帮你算了一下。你这次旅游估计要花20多万 |
8 | 悟空 | 44万 | 今天去看了大圣归来。 我旁边有个小孩儿问他妈妈 “这个不是动画片么?为什么有这么多大人来看?” 他妈妈回答: “因为他们一直在等大圣归来啊,等啊等啊,就长大了." ------泪目~ |
9 | 小半 | 44万 | 我的心借了你的光是明是暗 |
10 | 说散就散 | 44万 | 路过你楼下,车停下来了。 你给我发了短信:我结婚了,不要再来骚扰我! 我没回你短信,因为我用自行车接送你三年,而他用奥迪送了你三回,于是你们结婚了。去你楼下,只是三年的习惯。我很庆幸,这么早的看透你。 昨天你打我电话:他有外遇了,我怎么办? 我说:你有外遇的时候,想到我怎么办吗? |
网友评论
感兴趣,求链接~