前言
因为版权原因,在线听歌越来越麻烦,于是开始自建一个小乐库,纯属自用。 记录一下爬取经历。
突破首页防卫
用在线工具测试好了首页xpath提取表达式,UserAgent什么的都设置了,但是在scrapy执行一直报错。 于是curl一下首页,不开心了:
<html>
<head>
<script language="javascript">setTimeout("try{setCookie();}catch(error){};location.replace(location.href.split(\"#\")[0])",2000);</script>
<script type="text/javascript" src="http://********:80/usershare/flash.js"></script>
<script type="text/javascript">var ret=getIPs(function(ip){rtcsetcookie(ip)});checkflash(ret)</script>
</head>
<body>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0" width="0" height="0" id="m" align="center">
<param name="allowScriptAccess" value="always"/><param name="movie" value="http://********:80/usershare/1.swf"/><param name="quality" value="high" />
<embed src="http://********:80/usershare/1.swf" quality="high" width="0" height="0" name="m" align="center" allowScriptAccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.co
m/go/getflashplayer"/></object>
</body>
</html>
计算cookie,然后loadpage得到加载页面。 js文件没有混淆,可以模拟计算,或者用JavaScript插件执行。 不过在浏览器看了请求参数,发现
'Cookie': '__cfduid=dcbd0294352d18643417ae3aa8bbdd8fe1576674941; blk=0; jploop=false; jpvolume=0.923'
__cfduid,那是用了cloudflare防护。想起来了,之前偶尔打开也遇到5秒钟防护盾的页面。
The _cfduid cookie collects and anonymizes End User IP addresses using a one-way hash of certain values so they cannot be personally identified. The cookie is a session cookie that expires after 30 days.
于是找到cloudflare-scrape模块
A simple Python module to bypass Cloudflare’s anti-bot page (also known as “I’m Under Attack Mode”, or IUAM), implemented with Requests.
看能不能绕过去,得到真实的首页:
import cfscrape
def __init__(self):
# returns a CloudflareScraper instance
self.scraper = cfscrape.create_scraper(delay=5)
def start_requests(self):
url = 'xxxx'
text = self.scraper.get(url).text
可是报错了
File "c:\users\xxx\pycharmprojects\scrapy_learn\venv\lib\site-packages\cfscrape\__init__.py", line 351, in get_tokens
'Unable to find Cloudflare cookies. Does the site actually have Cloudflare IUAM ("I\'m Under Attack Mode") enabled?'
ValueError: Unable to find Cloudflare cookies. Does the site actually have Cloudflare IUAM ("I'm Under Attack Mode") enabled?
试了一下,先手动打开访问一次网页,再用CloudflareScraper解析,就可以正常访问首页数据,具体原因还没想明白,先用着
def start_requests(self):
url = 'xxxx'
r = requests.get(url)
r.close()
text = self.scraper.get(url).text
多级爬取
爬取思路:
- 首页解析音乐类别列表(genre列表)
- 分页爬取每个音乐类别的歌曲列表(play list列表)
- 爬取每个歌曲播放页面的元数据(play music列表)
整个爬取流程经历:
首页->音乐分类->歌曲列表->歌曲页面
最终要解析元数据的页面是歌曲页面。但是要经历前面3个页面,才能遍历全量的歌曲页面地址。
scrapy的start_requests方法用于初始化爬取网页url。以前写简单的页面爬取,直接在parse方法做最终页面的解析即可。难道要在parse方法塞几个页面的解析逻辑?
Off course not!
scrapy的Request支持callback。不同页面的设置不同回调函数解析即可。
yield Request(play_music_page_url, headers=self.headers, callback=self.parse_music, meta=meta)
分页爬取上下文
分页爬取,直接读取接口
http://xxxx/genre/musicList/{genre}?pageIndex={pageIdx}&pageSize=20&order=hot
有个小问题,pageIdx要用来计算下一次的页码。 可以用正则提取。但是scrapy Request提供了meta对象,用来保留多次爬取的上下文,非常方便。 发送meta
yield Request(str.format(self.GENRE_DETAIL_TEMPLATE, genre_idx, page_idx), headers=self.headers, meta={
"genre_idx": genre_idx,
"page_idx": page_idx,
"genre": tag
})
读取meta
next_page_idx = response.meta['page_idx'] + 1
genre_idx = response.meta['genre_idx']
xpath解析
解析音乐文件路径,发现url不是写在某个属性,而是在script里面拼接。
<script type="text/javascript">
var format = "mp3";
var PageData = {};
PageData.Host="xxxxx";
PageData.DownHost="xxxxx";
PageData.down="down7";
PageData.musicId = "33";
PageData.likeCount = "1221";
PageData.commentCount = 176;
PageData.day=20;
PageData.code=12;
PageData.format="mp3";
var ip="xxxxx";
var fileHost="xxxxx";
var musicHost="xxxxx";
var isCMobile=isMobileIp(ip) && window.isCMIp;
setItem("cm",isCMobile);
var mp3="33/mp3/12";
mp3=fileHost+mp3;
var bdText = "清晨(New Morning) - 班得瑞";
var bdText2 = "分享一首好听的轻音乐:" + bdText;
var imgUrl="xxxxx";
</script>
首先要定位这个script,发现xpath表达式在页面工具和scrapy解析有兼容性问题,单纯计算script index定位的结果不一致😂。 那就用文本匹配。涉及2个xpath函数:
- contains():匹配一个属性值中包含的字符串
- text():显示文本信息
定位script标签,再转为文本做正则匹配即可。
script = str(response.xpath('//script[contains(text(), "var mp3=")]').extract()[0])
match = re.search('mp3=".+";', script)
同样方式得到fileHost等属性。
另外,提取标题的时候,发现元数据出现多个空格,正则解决
re.sub(' +', ' ', title)