寒假闲来无聊,又正好在学Scrapy,于是便爬个p站来玩玩。在爬的过程中,总的来说p站反爬措施不强,貌似只有防盗链的存在,连模拟登陆都不需要的样子。但还是在其中遇到了许多坑,于是就记录下来了
分析
p站的加载采取的是异步ajax,分析一下接口,如图
很容易批量生成url,且这些接口返回的都是json数据,也很容易提取其中的数据,如图
![]()
具体参数如下,p参数为请求的页数,但参数tt似乎并没有什么卵用
![]()
里面的json数据
![]()
图上所示的url是240x480或同规格的缩略图,如果想要爬取原尺寸图片,则还需要对原尺寸图片的url进行分析,索性格式大致一样,只有后面的后缀名会有些许差别,跟缩略图的url也大体相近,如下所示
https://i.pximg.net/img-original/img/2019/02/21/01/22/24/(作品id)_p0(.png)或(.jpg)
由于是两个后缀二选一,所以需要自己重写个RetryMiddleware,如当请求.jpg后缀失败的时候改用请求.png后缀
但是如果没做任何措施,爬取图片时会报403,这也就是p站防盗链在作祟,这里p站通过判断referer来作出判断,而referer经过判断,需要来源于作品展示页面,url格式大体相同,即
https://www.pixiv.net/member_illust.php?mode=medium&illust_id=(作品id)
于是给请求头添上referer即可成功爬取图片
开爬
爬json数据url:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def start_requests(self): SSR_POSITION=self.settings.get('SSR_POSITION') win32api.ShellExecute(0, 'open', SSR_POSITION, '','',1) base_url='https://www.pixiv.net/ranking.php?' params={ 'mode': 'daily', 'content': 'illust', 'p':1, 'format': 'json', } MAX_PAGE=self.settings.get('MAX_PAGE',5) for i in range(1,MAX_PAGE+1): params['p']=i url=base_url+urlencode(params) yield scrapy.Request(url,self.parse)
|
重写ImagesPipeline:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class ImagesPivixPipeline(ImagesPipeline): def get_media_requests(self,item,info): #没有refer会报403 referer='https://www.pixiv.net/member_illust.php?mode=medium&illust_id='+item['illust_id'] headers={ 'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36', 'referer':referer } yield Request(item['url'],headers=headers,dont_filter=True)
def item_completed(self,results,item,info): image_paths=[x['path'] for ok,x in results if ok] if not image_paths: raise DropItem return item
def file_path(self, request, response=None, info=None): url = request.url file_name = url.split('/')[-1] return file_name
|
重写RetryMiddleware:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class MyRetryMiddleware(RetryMiddleware): def process_response(self, request, response, spider): if request.meta.get('dont_retry', False): return response if response.status in self.retry_http_codes: reason = response_status_message(response.status) part_url=request.url.split('.') if part_url[-1]=="jpg": part_url[-1]='png' else: part_url[-1]='jpg' url='.'.join(part_url) request=request.replace(url=url) return self._retry(request, reason, spider) or response return response
|
遇到的坑
在重写自己的重写RetryMiddleware中间件时,总是不执行自己的MyRetryMiddleware,而是去执行默认的RetryMiddleware,查资料发现在特殊情况下(如请求失败时),会执行默认的Middleware(包括RetryMiddleware),如果值低于550,则逻辑便是先执行RetryMiddleware,再执行自己的RetryMiddleware,但由于默认的的RetryMiddleware会将retry_time用光,致使自己的RetryMiddleware白写。
总结
反爬措施+1:判断referer
重写RetryMiddleware中间件时,需要注意其在settings.py中所设置的值,不然很有可能白写,如果其值低于550(即默认的RetryMiddleware值)。
完整代码请见 github