1. 初衷
作为云音乐的死忠用户,在大量版权丢失的情况下,依然坚守阵地,除了对UI的喜欢,还有就是歌曲下面精彩的评论信息了。
翻过很多热评后,就想把这些数据抓取下来。有了数据,就可以做数据分析。
2.探索抓取方案
我感兴趣的数据包括歌曲信息、用户信息和评论信息。
在web端搜索一首歌曲,例如女友喜欢的《umbrella》:http://music.163.com/song?id=21563094
打开开发者选项,抓一下这个接口的返回:
这个静态页面包含了我们需要的歌曲信息:歌曲名称,歌手名称,简单描述,封面等。
过滤一下comment
关键字,可以抓到评论接口:
http://music.163.com/weapi/v1/resource/comments/R_SO_4_21563094?csrf_token=
观察下这个请求url,发现是
http://music.163.com/weapi/v1/resource/comments/R_SO_4_{songId}?csrf_token=
的规则。
但是直接发POST请求拿不到数据,说明这个接口做了权限校验。
继续分析这个接口的请求参数,发现Form Data中带了两个参数:
我们也加上两个表单参数:
返回结果结构如下:
这个接口就包含我感兴趣的热门评论信息和用户信息。
对于简单抓取,这两个接口已经满足我的需求。如果想抓取全部评论,包含分页数据,那么需要分析云音乐的加密算法,其实在混淆后的JS中都有,有专门的文章分析,这里不做过多解释。
3. 代理IP池
有了接口,就可以写代码发送http请求遍历songId进行数据抓取了。
但是云音乐做了防刷处理,抓取几万条数据后,开始接口报错,被封了IP。
于是想要维护一个代理IP池,每次抓取时从池子中拿一个代理IP进行接口调用,防止IP封禁。
3.1 代理IP
来源:可以从淘宝上面买,可以从免费网站抓取。
分类:根据匿名程度,可以分为高匿,普通和透明。高匿的代理ip可以屏蔽掉真实IP,目标服务器完全不能感觉到代理IP的存在;普通代理ip能隐藏真实IP,但是目标服务器能感受到代理IP的存在;透明代理IP不能隐藏真实IP。
3.2 免费代理IP池
搜索一下代理IP,能找到大量网站提供一些免费代理IP,质量参差不齐,站越大,用的人越多,质量越差。
于是搭建免费代理IP池包含两个步骤:
(1)从免费代理IP网站抓取代理IP列表
(2)验证代理IP可用性
不同的代理IP网站抓取和解析的过程也不尽相同,但是不难。
验证过程就是使用抓取来的代理IP访问常用网站或者目标网站,如果可用则保留,不可用则丢弃。
4. 实现
4.1 配置文件读取
项目中使用三个自定义配置参数:请求代理IP列表超时时间,代理IP验证超时时间和真正请求歌曲信息接口超时时间。application.yml
中配置如下:
这里使用ConfigurationProperties
注解读取
为了能够在yml配置文件中提示自定义参数,需要引入spring-boot-configuration-processor
:
注意每次修改HttpConfig
都需要重新maven编译,yml里面才能有提示。
4.2 使用普通http请求获取动态IP
网络请求层使用OKHttp + Retrofit2框架。
Retrofit2本身是对OKHttp网络请求工具的封装实现,可以方便进行各类HTTP方法请求,对请求参数和返回值进行类型转换。
首先引入maven依赖:
retrofit
是核心包,converter-jackson
和converter-scalars
是两个返回值类型转换器,分别对json和普通类型(如string)的返回值进行转换。
获取动态IP的接口为:https://raw.githubusercontent.com/stamparm/aux/master/fetch-some-list.txt
返回结果为:
对于代理IP,统一封装成一个 Proxy
实体类:
首先我们封装一个请求client接口,专门负责发起https://raw.githubusercontent.com/
域名下的请求,当然,这里我们只有一个普通的GET请求。
之后,需要想Spring容器注入bean:
HttpClientFactory
先注册一个OkHttpClient
bean,设置了超时时间和拦截器,这个请求并不需要设置代理IP。
其中HttpHeaderInterceptor
很简单,只是添加一些公共头信息,包括一个随机UA:
之后注册一个GitProxyClient
bean,定制了Jackson反序列化时使用的ObjectMapper
,设置了该client的基础域名。
接下来就可以使用gitProxyClient发起请求获取动态IP列表了:
4.3 过滤抓取来的代理IP
从网上抓取来的代理IP一般质量都不高,所以需要进行过滤。
常用的方法就是尝试使用代理IP去访问一个常用网站,如百度等,如果在短时间内正常返回则认为可用。
开始我也采用了这种方式,访问百度,但是发现可能会出现一种情况,就是百度可以访问,但是访问云音乐接口却出现错误。
既然如此,不如一步到位,过滤时就访问歌曲信息接口,如果能正常获取歌曲信息,那么认为代理可用。
先给出获取歌曲信息的Client:
接下来需要一个定制OkHttp,添加代理IP,这里创建一个工厂:
由于每次请求的代理IP都不同,不能直接注入到bean,而是使用工厂方法,每次请求都重新构造一个OkHttpClient
,然后生成MusicClient
。
注意在添加converterFactory时,先添加ScalarsConverterFactory
,再JacksonConverterFactory
,原因在doc中:
Because Jackson is so flexible in the types it supports, this converter assumes that it can handle all types. If you are mixing JSON serialization with something else (such as protocol buffers), you must add this instance last to allow the other converters a chance to see their types.
接下来就可以使用代理IP请求歌曲信息了:
剩下的过滤工作也就简单了,对于爬去来的代理IP列表,使用多个线程去过滤一下就好了:
4.4 定时抓取
上面我们实现了一次代理IP抓取,而抓取来的IP也会失效,所以我们需要定时抓取,更新代理IP池。
定时任务可以使用Spring的Schedule注解实现,也可以使用quartz,这里使用quartz。
引入依赖:
application.yml
添加配置:
这里使用简单的内存存储job,也可以使用数据库,配置项会麻烦些。
然后创建一个定时任务job,继承QuartzJobBean
:
有了任务,还要将它调度起来,我想20分钟跑一次:
这里我们先注册一个JobDetail
,需要一个名字和分组,然后设置为durably,即没有trigger指向时,也保留。
接下来注册一个trigger去触发上面的job,设置cron表达式,绑定job即可。
4.5 定时删除失效IP
上面我们实现了定时抓取新代理IP的功能,接下来还需要定时从库中删除失效的代理IP,不能光等到使用时再删除。
方法是从库中读取所有代理IP(如果代理IP量很大,可以分页处理),然后启动多个任务去校验代理IP有效性,将无效的删除:
同样需要一个定时任务,15分钟执行一次即可,代码与上面抓取代理任务类似。
4.6 使用代理IP获取歌曲信息
|
|