开发环境:WIN7+Anaconda+py2.7+scrapy
数据库:MongoDB
文章的顺序:
一、先分析思路;
二、再分析scrapy框架每一个模块的做用;
三、最后写代码和分析API,以及评论javascript
方法一:遍历html
优势:有个别歌手有主页,可是没有申请音乐人,因此不存在歌单列表页,用第二种方法也获取不到。
缺点:很差测试它到底有多少,大概十一二万的样子,大多id是相隔不远的。有些id之间相隔了几位数,原本挺稳定的,想着往4位数遍历就行,却发现还有7位数的,这样遍历的跨度有些大,(id从1872开始),要作些处理,还有判断这个页面存在与否。java
方法二:从歌手分类爬取全部歌手的idpython
歌手分类页:http://music.163.com/#/discover/artist
这里要说一下,网易云的全部网址,要去掉中间那个#号才是真正的url,带#的查看源代码是获取不到真正的信息的。
因此实际上是:http://music.163.com/discover/artistgit
优势:方便,不须要考虑遍历的数量,不须要对页面是否存在作处理
缺点:可能会漏掉一些有主页但未注册的歌手。github
咱们主要以方法二入手,分析以下:mongodb
咱们看这个页面左侧栏:数据库
二、由于当时我写的时候,参考到前面提到的那篇GitHub上的代码,
这个group_ids里的就是左侧每一个项对应全部的页面了(不包括最上方的推荐歌手和入驻歌手,由于包含在其余里面了)json
三、咱们按F12或右键检查,如图,每一个对应的url是:http://music.163.com/discover/artist/cat?id=xxx,
这里的id就是上面group_ids里的数字了。api
四、而后咱们再点进去,如图四,url的id就是上面这个group_ids里的元素了,然后面的initial是首字母的意思,你看下面咱们选中的是A,而后它是65,是否是想到ASCII码?在ASCII码中A就是从65开始的,Z是90,后面以此类推,最后有个其余,代替的是0:
咱们将这两个分别存储为一个列表或元组:
# 左侧栏全部:男女、国家分类id group_ids = (1001, 1002, 1003, 2001, 2002, 2003, 6001, 6002, 6003, 7001, 7002, 7003, 4001, 4002, 4003) # 歌手姓名首字母id initials = [i for i in range(65,91)] + [0]
一、点进来以后咱们来到歌手页,http://music.163.com/#/artist?id=6452,一样,查看源代码的时候去掉url里的#。
二、咱们获取的这个歌手页的url对应的是热门50首,在对应网页里咱们会发现下面有好几个块:热门50首、专辑、MV、歌手介绍
三、由于受框架的限制,以上四个信息的内容不在一个传递链里,
如下两种顺序的特色都是后者传入的参数都是由前者返回的,而这四个之间属于相同的id,他们并不须要由前者返回,不构成一个传递链:
1)、歌手 ——>专辑列表——>歌曲列表——>歌曲信息——>第5步
2)、歌手 ——>热门50首的歌曲列表——>歌曲信息——>第4步
四、若是若是你只须要热门歌曲你能够获取它全部连接,这个代码被我分为两块:
1)、第一块是包含热门50首的url,也只有url,在id名为'song-list-pre-cache'的div标签里,div->ul->li->a->href
2)、而第二块textarea里是json,是这50首歌的比较完整的信息,只不过,这些信息经过lxml.etree或者BeautifulSoup用text的方式获取下来会是字符串,咱们须要用json将它格式化。
若是你只须要歌曲的话,选择第一条就行了,直接跳到第四篇讲API的),用歌曲的API便可。
五、咱们要获取全部歌手的歌曲,就得从歌手的专辑下手,获取专辑里全部的歌手才行。咱们在专辑页会发现,有些是有不少页的,我最开始用的是scrapy的xpath解析页面,后来搜的时候发现了API,因此接下来的东西,咱们就不经过页面的方式了,API我是经过这个网站发现的:http://moonlib.com/606.html(最近发现网站挂了,请看个人第四篇讲API的,有其余相似API的文章连接)。
咱们用到的是2到6(不包括5,没用到歌单),第7条接口是MV的,不过不幸没有发现像专辑同样的列表页信息,它只有单曲的MV的API。不过这里咱们用不上。后面第四篇会专门分析API。
六、接下来就是每一个专辑的全部歌曲还有专辑、歌手的一些信息,另外专辑下也有评论,且评论数的获取方式有些不一样,所以评论有两种处理。
七、最后从图八里的歌曲连接点进去的就是歌曲页了,如图九:
关于如何创建一个scrapy程序,能够参考这两篇文章:
一、http://cuiqingcai.com/3472.html(建立的时候推荐)
二、http://www.cnblogs.com/wuxl360/p/5567631.html
关于使用mongodb,能够参考:
scrapy startproject + 你的项目名
第一篇文章有提到两个比较特别且有用的地方:
一、
解释一下:execute里面的三个字符串连起来它其实就是最后执行scrapy程序的命令。这个文件的好处是,假若你在使用编辑器,好比sublime,是能够在配置后直接执行的,而不用打开DOS窗口而后执行,若是你在sublime里直接执行scrapy自己的任何一个文件,它都不会执行成功,而只能执行这个entrypoint.py,名字应该随意吧,无所谓。
另外一点请参考如下的第三部分
如今整个框架的结构是这样的:
固然,这个spiders文件夹下的WangYiYun.py并非自动生成的,这个须要咱们本身创建,这个文件就是主爬虫程序。
另外,这个脚本的名字建议不要取和项目名同名,不然后面可能会踩坑。如下简称WYY.py,免得出错,我由于已经生成了,改了别的地方又会出错,解决办法是在代码的最前面,编码注释的后面加上这么一句(参考连接找不到了,可是参考GitHub的连接代码里也有):
from __future__ import absolute_import
一、关于调试
上面的缘由和配置解释的很清楚,
二、关于spidername和robots.txt
BOT_NAME很重要,在WYY.py文件里写脚本的时候,继承自scrapy.Spider的这个类,它须要有一个name,而这二者必须同名。
最下面那行的ROBOTSTXT_OBEY,你们知道爬虫绕不开robots.txt这个文件,每一个网站都会有这个网站,是必须遵照的一个守则吧,就是有些不让你爬,有些又容许你爬。默认是True,若是失败了,能够尝试将其注释,而后复制一行,改成False。
settings.py文件里大多都是写好的,你只要将它复制,取消注释,而后修改便可,最好不要不复制直接在原文上改,万一改到了什么出了错,还能有个参照物。
三、关于headers
重要的通常就是Referer、User-Agent(这个必需要有)、Accept(可选,可是涉及到xhr,即json文件,就要修改了)。
这里将它注释,改为本身的,你也能够写在主爬虫WYY.py文件里另写,比较自由,写在这里算是一个基本配置吧。
四、关于ITEM_PIPELINES
这个是启用一个Item Pipeline组件,数字表明优先级,越小越优先,没有注释的那行是个人,而下面还有一行,是我以前在网上看过的一种写法,可是并不能成功,它应当是一个字典,列表不行
五、关于mongodb配置
随便写在哪,咱们就写在刚刚ITEM_PIPELINES的后面
这里顺便建议,常量都用大写。
HOST是本地,PORT是端口,DBNAME是数据库,WYY。
接下来四个是集合了,至关于table,这个顺序是倒序。
一、MONGODB_COL_ARTIST - > ArtistInfo -> 全部的歌手列表
二、MONGODB_COL_ALBUMLIST - >AlbumListInfo - > 每一个歌手的全部专辑列表
三、MONGODB_COL_ALBUM - >AlbumInfo - > 每张专辑内的全部歌曲列表
四、MONGODB_COL_SONG - > SongInfo -> 每首歌曲的信息
它就至关于SQL/MySQL里的字段,它没有什么特别的字段类型,反正全部都是scrapy.Field()就能够了,另外三个集合一样,每一个单独写个类,依照大家本身的需求定字段便可。
切记,要记得导入items里的那几个你定义的字段的类,我以前忘了导入,而后一切程序正常,就死活存不进去,也不报错,差点掉坑里走不出来
而后这个WangyiyunPipeline基本就两块,一个初始化init(),一个process_item(),前者是用来链接的,后者是用来存储的。
能够看到我init里有一些注释,这里说明一下,由于涉及到多个集合存储,一开始真不知道怎么弄,一开始我觉得把每一个都扔init就成了,而后经过self调用,后来发现不行,在init定义一个集合就能够了。process_item()仍是参考刚刚那个GitHub那个项目,才知道经过isinstance判断。
isinstance你们知道什么意思吧,而后每一个item对应的什么在注释我也写了。另外,我下面还有一些被注释掉的代码部分,这里就是我在最开头说的,想要跳过一些重复的地方,可是跳过以后不知道作什么处理。
在不用框架的时候,咱们存Mongodb。是先定义一个空字典,而后赋值,最后insert_many/insert_one,这里也是同样的,只不过,咱们是将传入的item给dict化。
然后面,在不使用默认的集合时,从新赋一个取代以前的artist便可。
接下来咱们开始正式写代码了。
前面有提到,spiders目录下的文件最好不要取和项目相同的名字,若是取了也不要紧,有办法,在导入模块的最前面加上这句:
from __future__ import absolute_import
由于参考的文章太多了,我也找不到出处的连接了抱歉。
仍然提醒,要记得导入items的那几个模块、
class WangYiYunCrawl(scrapy.Spider): name = 'WangYiYun' allowed_domains = ['music.163.com'] # start_urls = 'http://music.163.com/discover/artist/cat?id={gid}&initial={initial}' group_ids = (1001, 1002, 1003, 2001, 2002, 2003, 6001, 6002, 6003, 7001, 7002, 7003, 4001, 4002, 4003) initials = [i for i in range(65,91)] + [0] headers = { "Referer":"http://music.163.com", "User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3067.6 Safari/537.36", } def start_requests(self): pass def parse(self,response): pass
最前面的那一大段前面都有说过,就再也不提,这里的headers是本身写的,因此后面会调用到self.headers,只在settings.py文件里配置的这里能够省略,后面也不用用。
还剩allowed_domains。
首先讲一下我以前一直困惑的地方:start_urls 和start_requests()能够同时存在,也能够只要一个便可。
若是你写的是start_urls,那start_requests()这个函数能够省掉,直接在parse里对它进行处理,parse这个函数,就是爬虫的主程序,日常怎么写就怎么写。
而后这个response,咱们先来看代码:
start_requests()这个函数在返回的时候,(对了,这个scrapy里返回用的都不是return,而是yield,迭代的意思),使用Request,能够看到它大可能是和requests这个库很像,它的做用也是同样,返回是一个response,它特别的在于它最后一个参数,callback的值接的是回调函数,即你要把返回的response做为参数传递给哪一个函数,这个函数后面不须要括号,因此一开始我也没搞懂它是个什么。
另外,这里调用headers是由于我将headers定义在了这个class里,若是是定义在settings.py里,这里可省略。
以后的函数都是这样,若是你要将什么参数穿到下一个函数,均可以用这个,而在回调函数里必须传入这个response参数。
关于parse函数:
parse这个函数的名称无所谓,可是最好带上parse(许多scrapy类型的文章都这么用,一眼看上去就知道是什么),而且保证传递的回调函数参数和这个函数名称一致便可。
一、默认状况,scrapy推荐使用Xpath,由于response这个对象能够直接使用Xpath来解析数据,好比我代码中的,response对象下直接就能够用selector.xpath。
response.selector.xpath('//ul[@id="m-artist-box"]/li')
固然,除此以外,还有一种使用xpath的方法:
from scrapy.selector import Selector selector = Selector(response.body)
关于Selector的用法,能够参考:
http://blog.csdn.net/liuweiyuxiang/article/details/71065004
可是这种方法并非特别方便,因此直接使用response.selector.xpath
的方法就好。
二、关于xpath的格式,参考中文官方文档吧,http://scrapy-chs.readthedocs.io/zh_CN/1.0/intro/tutorial.html。它跟lxml大同小异,可是仍是有些区别,如图,这是四种基本的方法:
它返回的其实都是数组,xpath不用说,而后最经常使用的就是extract了,这个返回的列表里都是文本,而不是Selector对象
它获取的就是全部href的集合。
等价于BeautifulSoup这么用,只不过这个是获取单个的:
from bs4 import BeautifulSoup soup = BeautifulSoup(response.content,'lxml') href = soup.find('a')['href']
而后简单提两个xpath简单而经常使用用法:
@href:这种@后面加什么的,都是某个标签的某个属性,其余好比src也是这样用。
text():这个就是获取文本了。
三、item它就是那个对应某个爬虫所对应的数据库的字段,由于mongodb存储的格式相似json,在python里它就是个dict,当它是个dict就能够了。
item = WYYArtistItem()
四、使用scrapy.Request它能够传递的不仅是url,它也能够传递整个item,使用meta,例如
yield scrapy.Request(url=url,meta={'item': item}, headers=self.headers, method='GET', callback=self.parse)
而后在parse()函数调用的时候,
def parse(self,response): item = response.meta['item']
可是并不建议这么用,由于很浪费资源。
另外,传递url的时候,除了用url,若是得到的url这段直接存进了item里,也能够直接用item['url']:
yield scrapy.Request(url=item['album_url'], headers=self.headers, method='GET', callback=self.parse_album_list)
最最最最重要的一点是,若是要存到数据库里,好比最后一个不用再Request了,那么必定要加上
yield item
这样才能存进数据库里,以前一直存不进去,一个就是前面忘了导入items,一个就是这里。
后面基本都照这个模式来,由于个人顺序是:歌手--专辑页--专辑全部歌曲--歌曲,恰好每个爬下来的url均可以直接传递给下一个函数,经过callback的方式。
这里最大的好处就是,好比歌手页,不用爬下来存一个列表,而后到了下一个函数,再遍历一遍这个列表,它每抓一个url,直接就能到下一个函数运行。
我运行的时候最大的一个问题就是‘yield item’那里,四个部分,我最后一个步骤才放‘yield item’,因而它只存最后一个,即歌曲部分,搞得我一脸懵逼,后来想一想大概要执行完这个,而后再把前面的改为yield item,才能都存进去。这个是一个很严重的问题。
因此最好就是在parse就是第一个地方就存,yield item,存完再改为yield Request再执行下一个函数。我才知道以前参照的那个项目里为何会有注释掉的'yield item'。
由于这份代码被我弃用了,因此还有一些有瑕疵的地方没改过来,代码就不发出来了。
我以前贴的那个GitHub的项目就还能够,我就是参照那个改的,基本上我讲清楚了,弄懂了就能够看得懂,就能够上手了。
前面有提到,API的参考连接,另外再放上几个,
一、http://moonlib.com/606.html(我用的这个)
二、http://blog.csdn.net/qujunjie/article/details/34422379
三、https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=neteasecloudmusicapi(这个比较官方,我也不知道是否是官方,可是很全很全很全)
咱们爬取的顺序是:
一、歌手专辑
二、专辑信息(不包括评论)
三、歌曲信息(不包括评论)
四、歌词
五、专辑和歌曲评论(这个另起一章写)
咱们拿一个来说解,其余的相似:
好比,歌手专辑:
http://music.163.com/api/artist/albums/166009?id=166009&offset=0&total=true&limit=12
second_offset = limit*(first_offset+1)
个人那个get_req()函数就是对requests.get作了些处理,中途确定会遇到各类各样的状态码对吧,这个大家本身去思考。
这里我没有用response,由于不涉及到一个完整的传递链,它只是要存进数据库的某一个字段,如图,这个才是我要进行存储的的函数,其中调用了get_artist_album_info()这个函数,它只是做为一个字段存进了item。
而后回到get_artist_album_info()函数,这里的建议就是,将固定的不变,会变的用params这个参数,requests.get它后面能够传各类参数,包括params,以及前面的headers。
这个params里有四个参数:
# album_count是一个歌手全部专辑的总数 # 得到的方法能够先爬第一页的json数据,或者别的大家本身找 for offset in range(0,album_count,12): params = { 'id':singer_id, 'offset':offset, 'total':'true', 'limit':12 }
以此类推,其余到底都是这样了,这里把http://moonlib.com/606.html的API集中写一下,method都是GET:
一、歌手专辑:
# 歌手专辑: # 三种写法,随意,推荐第三种,后面都是 一、http://music.163.com/api/artist/albums/[artist_id]/ 二、http://music.163.com/api/artist/albums/166009/id=166009&offset=0&total=true&limit=5 三、url='http://music.163.com/api/artist/albums/166009' params = {....}
二、专辑里的歌曲列表
# 专辑里的歌曲列表 http://music.163.com/api/album/2457012?ext=true&id=2457012&offset=0&total=true&limit=10
三、歌曲信息
# 歌曲信息 # 这里说明一下,%5B和%5D就是一对中括号[],最好改为[],像歌手专辑里第一种写法那个同样,由于%5B那种写法还要处理,且麻烦。 http://music.163.com/api/song/detail/?id=28377211&ids=%5B28377211%5D
四、歌词信息
# 这个跟其余都不同,后面的lv、kv、tv是固定的,只要改id便可。 http://music.163.com/api/song/lyric?os=pc&id=93920&lv=-1&kv=-1&tv=-1
评论的API的参考连接:
一、https://github.com/darknessomi/musicbox/wiki/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%96%B0%E7%89%88WebAPI%E5%88%86%E6%9E%90%E3%80%82(这个是从歌单下手的,里面的评论能够参考)
二、http://www.imooc.com/article/17459?block_id=tuijian_wz
三、http://blog.csdn.net/u012104691/article/details/53766045
后面这几篇都讲的比较详细,当时查资料的时候,还查到另一种写法,就是里面有一堆命名是first_param什么的,看得头晕眼花,而后当时测试彷佛也没有成功,建议用如今的这种就行了。
基本模式就是这样:
由于专辑和歌曲都有评论,因此我专门将它写成了个类,后面直接调用就能够了。
# -*-coding:utf-8-*- import os import re import sys import json import base64 import binascii import hashlib import requests from Crypto.Cipher import AES class CommentCrawl(object): '''评论的API封装成一个类,直接传入评论的API,再调用函数get_song_comment()和get_album_comment()便可分别获取歌曲和专辑的评论信息 ''' def __init__(self,comment_url): self.comment_url = comment_url self.headers = { "Referer":"http://music.163.com", "User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3067.6 Safari/537.36", } def createSecretKey(self,size): '''生成长度为16的随机字符串做为密钥secKey ''' return (''.join(map(lambda xx: (hex(ord(xx))[2:]), os.urandom(size))))[0:16] def AES_encrypt(self,text, secKey): '''进行AES加密 ''' pad = 16 - len(text) % 16 text = text + pad * chr(pad) encryptor = AES.new(secKey, 2, '0102030405060708') encrypt_text = encryptor.encrypt(text.encode()) encrypt_text = base64.b64encode(encrypt_text) return encrypt_text def rsaEncrypt(self, text, pubKey, modulus): '''进行RSA加密 ''' text = text[::-1] rs = int(text.encode('hex'), 16) ** int(pubKey, 16) % int(modulus, 16) return format(rs, 'x').zfill(256) def encrypted_request(self, text): '''将明文text进行两次AES加密得到密文encText, 由于secKey是在客户端上生成的,因此还须要对其进行RSA加密再传给服务端。 ''' pubKey = '010001' modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' nonce = '0CoJUm6Qyw8W8jud' text = json.dumps(text) secKey = self.createSecretKey(16) encText = self.AES_encrypt(self.AES_encrypt(text, nonce), secKey) encSecKey = self.rsaEncrypt(secKey, pubKey, modulus) data = { 'params': encText, 'encSecKey': encSecKey } return data def get_post_req(self, url, data): try: req = requests.post(url, headers=self.headers, data=data) except Exception,e: # dosomething print url,e # return None return req.json() def get_offset(self, offset=0): '''偏移量 ''' if offset == 0: text = {'rid':'', 'offset':'0', 'total':'true', 'limit':'20', 'csrf_token':''} else: text = {'rid':'', 'offset':'%s' % offset, 'total':'false', 'limit':'20', 'csrf_token':''} return text def get_json_data(self,url,offset): '''json 格式的评论 ''' text = self.get_offset(offset) data = self.encrypted_request(text) json_text = self.get_post_req(url, data) return json_text def get_song_comment(self): '''某首歌下所有评论 ''' comment_info = [] data = self.get_json_data(self.comment_url,offset=0) comment_count = data['total'] if comment_count: comment_info.append(data) if comment_count > 20: for offset in range(20,int(comment_count),20): comment = self.get_json_data(self.comment_url,offset=offset) comment_info.append(comment) return comment_info def get_album_comment(self,comment_count): '''某专辑下所有评论 ''' album_comment_info = [] if comment_count: for offset in range(0,int(comment_count),20): comment = self.get_json_data(self.comment_url,offset=offset) album_comment_info.append(comment) return album_comment_info
重复的地方我就不赘述了,最后两个地方我之因此分开写,是由于专辑的评论数能够从专辑信息里获取,但歌曲评论数从专辑列表信息里获取不到,只能先爬取它第一页的json数据,它里面的total就是评论总数,而后再作后面的处理。
评论的API:
# 一、专辑评论API: comment_url = 'http://music.163.com/weapi/v1/resource/comments/R_AL_3_%s?csrf_token=' % album_id # 二、歌曲评论API: comment_url = 'http://music.163.com/weapi/v1/resource/comments/R_SO_4_%s?csrf_token=' % song_id
而后将comment_url 做为参数传入上面封装的那个类里便可,不一样的是专辑还需先获取专辑评论的数量。
全部的分析都结束了,接下来的代码本身写吧。