@coder-pig
2018-04-02T11:54:07.000000Z
字数 6741
阅读 1861
Python
引言:
关于爬小姐姐的脚本示例,在我的Gayhub仓库:ReptileSomething
里已经有好几个了,基本都是没什么技术含量的,直接解析HTML拿到
图片的URL,然后下载,特别开一篇写爬取花瓣网的小姐姐的实战教程,
是因为爬这个网站的时候会遇到好几个问题,第一感受到了反爬虫的套路,
(折腾了我将近2天):
不信的话可以试试。
http://img.hb.aicdn.com/36b521f717741a4e3e024fd29606f61b8f960318f3763-WzUoLC_fw658
我觉得算是爬图片里稍微有点难度的站点了,强烈建议跟着我
一起回顾这个过程!
Chrome抓包的时候,抓到的数据和Elements的内容不一样,
js动态加载数据,前面已经见识过这种反爬虫的套路了,
有Selenium在手,根本不虚,模拟一波浏览器请求
加载下就能得到和Elements一样的内容了。
两个问题:瀑布流和Ajax动态加载数据。
怎么说?且听我一一道来:
没事喜欢练手的我意外发现了花瓣网,F12 Chrome抓一波包:
随手写个代码看看:
先Elements看下我们想扒的是什么:
这里尽管有个img,但是明显是个小图,应该是要点开a那个
/pins/1433175317链接里才有大图,点开:
http://huaban.com/pins/1433175317/
看下Elements,这个就是我们想要的图片url:
恩,一如既往的简单套路,搞到批量的列表url,然后下载图片。
看回我们的利用Selenium得到的网页代码,可以很稳。
接下来的事情本该就水到渠成的了,然后这时候发生了一件
令人猝不及防的事情:
在网页那里滚到底部发现会加载更多的图片,越滚越多,但是
我们的Selenium只抓到了30个,咦,这种在之前抓某个网站
的时候就遇到过了,写个简单的滚动到底部的js,然后Selenium
循环执行这个脚本图片数/30次就好了,中途可以休眠1s给他加载,
执行完毕后,再去调用page_source得到最终的页面代码,然后走波
BeautifulSoup把我们1000多个小姐姐扒出来就好。
但是实际运行的结果却出乎我们的意料,最后得到的小姐姐列表
还是30个,卧槽,什么鬼,接着打开我们的浏览器,滚动的时候
发现,列表内容竟然是动态变化的,打开图片列表节点,滚动网
页,不禁又发出一句,卧槽,什么鬼,列表是动态变化的???
这些刚还不在的,突然就蹦出来了,就好像你刷着即刻刷着
刷着就蹦出一个x子。动态加载?想想野路子,要不我们自己
量化下滚动偏移量,比如滚动100,我们抓一波页面,存一下,
最后做下去重?这野路子不是一般的野:
单不说怎么量化这个偏移量了,浏览器宽度不一样时加载的
数目还不一定是30,然后那么多画板,你每个画板这样玩?
效率巨低。
苦苦寻觅后,发现了两个关键词:瀑布流和Ajax动态加载数据
瀑布流:参差不齐的多栏布局以及到达底部自动加载的方式
Ajax动态加载数据:在不重新加载整个网页的情况下,对网页的某
部分进行更新
简单点说就是:
图片通过JS加载成瀑布流的形式,当到达底部后,会请求后台拿到更多
的数据,解析后通过Ajax,可以在不关闭不转跳不刷新浏览器的情况下
部分更新页面内容。
So,我们我们重新抓抓包,在滚动到底部的时候看下抓到的数据,
点击筛选XHR(XMLHttpRequest),这个是浏览器后台与服务之间
交换数据的文件,一般为json格式:
点开,右侧看看Headers,果然是json格式的:
发现有这样的请求头,先放着,等下再研究规律:
点开右侧Previews,发现了传回来的一大串Json,这里的
pins应该就是新增加的妹子图的相关数据了。
为了方便研究,我又滚动了几下,加载更多的数据,以方便
找出规律:
从上面我们可以得到一些这样的信息:
首先是Get请求,固定的基地址:http://huaban.com/boards/18907029/
这个18907029是画板id,后面的参数,jcx1ki7y和wfl=1应该是固定的,
limit这个是加载数量,一般是分页用的,就是一次加载20条新数据,
最后这个max:1348131400 暂时不知道是什么?不过应该是某个什么id
吧,看下第二个的max是1263961742,复制到第一个返回的json里搜搜:
卧槽,刚好最后一个,不会那么巧吧?然后把第三个max:1247629077复制
到第二个的Json里看看:
果然,好家伙,这个max就是每次拿到的最后一个图片的pin_id
知道规则了,模拟一波请求,解析一波json,每次拿到最后这个
pin_id,用作下次请求,当返回的pins里没东西,说明已经加载
完所有的了,来,写一波代码,先要处理刚进来时加载的列表,
得到最后的一个图的pin_id,然后才能开始执行上面那个拿
json的操作。
在我准备用Selenium模拟请求主页的时候,我发现了用urllib
模拟请求,里面就能拿到最后一个pin_id,只不过他是写在js里
的,我们可以通过正则表达式拿到我们想要的pin_id们:
还有点开一个具体的大图页,发现他的url规则竟然是:
http://huaban.com/pins/926502853/
就是http://huaban.com/pins/ + pin_id,所以我们只要获得pin_id就可以了!
规则清楚了,接下来一步步写代码来获取我们想要的数据吧!
1)首页数据的获取
进来的时候会加载一次,不是通过Ajax加载,默认是30个,需要处理
一波网页获得这个30个数据,然后30个数据的最后一个用于请求Ajax。
通过正则拿到pins这段json。
代码如下:
测试下代码:
打开网页加载更多确认下这个最后的pid是否正确:
可以正确,接着就来处理json数据了~
(这要注意正则匹配用的是search,我一开始没留意用的是match,
用一些真这个校验工具测试自己的正则一直是对的,但是丢程序
里缺一直不匹配,返回None,要注意!!!)
2)处理Ajax数据
首先是请求头的设置,和我们普通的请求不一样,如果你直接
用浏览器打开ajax加载数据的那个url发现返回的并不是json!!!
随手找个链接试试,就是解析json而已,
简单测试下:
运行结果:
可以,拿到数据了,接下来要优化下点东西,如果每次都要去拼接:
这串东西不就很麻烦了,可以通过正则来进行替换:
这里用到前面没有细讲的re.sub()替换方法和向前和向后界定
这两个东西什么时候用到,当前这个场景就能用到,比如我们想替换
pin_id,用一个括号括着想替换的部分,感觉应该就能替换了:
结果:
是的,前面一大段东西没了,如果你用了向前(?<=...)和向后界定(?=),
就可以让正则只匹配和替换这两个中间部分的字符串了~
好的,替换成功,小小整理一下代码:
好的,pin_id都能够拿到了,接下来通过这些想办法拿到图片啦,
点开一个图片的详情页,比如:
http://huaban.com/pins/1272982736/
查看页面结构,得到图片url:
http://img.hb.aicdn.com/36b521f717741a4e3e024fd29606f61b8f960318f3763-WzUoLC_fw658
然后写个简单的模拟请求下这个网址,看下返回的数据有没有这个图片Url相匹配
的内容,全部搜没有,然后把后面的分成三段,一段段搜:
先搜:36b521f717741a4e3e024fd29606f61b8f960318f3763,秒找到,
有六处匹配的,这后面跟着-WzUoLC,就剩下最后的fw658,全局搜搜不到
难道是固定的?打开另一个详情页看看,果然,fw658是固定的:
http://img.hb.aicdn.com/7dc8cfc5e00f4cd78efba3f61c3b0b00f345a01a13b901-vaX0ho_fw658
卧槽,key???前面拿json的数据就能拿到key,卧槽,该不会就
这样拼接就可以了吧,找到前面一个:
...令人无法接受,折腾了那么旧,原来拿到key就可以拼接得出图片url了
啧啧,正确的图片url拿到了,浏览器打开也是没问题的,接着就是无脑
拼url,然后一个个下载了,正当我以为一切已经结束的时候,才发现了
最后的隐藏关卡:"图片并不能下载"???右键另存为保存直接失败,
py代码直接崩溃。
然后,在那个图片的详情页倒是可以下载,我的天,难不成要我每个
图片都用Selenium加载,然后处理页面数据,在这里下?
讲真,我是非常抗拒用Selenium的,慢不说,还耗内存,
代理也不怎么好设置,而且别人会说我low,百度谷歌搜了
一大堆,基本都是说了等于没说...正在我万念俱灰,想用回
Selenium这种Low比方式的时候,我萌生了一个想法:
会不会是需要登录后才能下载,于是乎我把链接发给我组
的UI,然后她默认浏览器竟然是ie,然后奇迹发现了,没有
登录,直接用ie浏览器打开了,然后他么的,可以右键保存
到本地?接着我又把链接发给我隔壁的后台小哥,同样用ie
打开,可以。此时熟悉的BGM响起:
真相:花瓣没有做ie系列浏览器的兼容
所以,模拟ie浏览器的User-Agent就可以下载图片了!!!
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)'
立马试试,当看到第一张图片下载到了我的仓库里的时候,
我就知道我猜对了:
剩下的就是组织一波代码,批量下载了~
import urllib.request
import urllib.error
import re
import json
import coderpig
import os
# 图片拼接url后,分别是前缀后缀
img_start_url = 'http://img.hb.aicdn.com/'
img_end = '_fw658'
# 获取pins的正则
boards_pattern = re.compile(r'pins":(.*)};')
# 修改pin_id的正则
max_pattern = re.compile(r'(?<=max=)\d*(?=&limit)')
# 图片输出文件
pin_ids_file = 'pin_ids.txt'
# 图片输出路径
pic_download_dir = 'output/Picture/HuaBan/'
json_headers = {
'Host': 'huaban.com',
'Accept': 'application/json',
'X-Request': 'JSON',
'X-Requested-With': 'XMLHttpRequest'
}
# 获得borads页数据,提取key列表写入到文件里,并返回最后一个pid用于后续查询
def get_boards_index_data(url):
print(url)
proxy_ip = coderpig.get_proxy_ip()
resp = coderpig.get_resp(url, proxy=proxy_ip).decode('utf-8')
result = boards_pattern.search(resp)
json_dict = json.loads(result.group(1))
for item in json_dict:
coderpig.write_str_data(item['file']['key'], pin_ids_file)
# 返回最后一个pin_id
pin_id = json_dict[-1]['pin_id']
return pin_id
# 模拟Ajax请求更多数据
def get_json_list(url):
proxy_ip = coderpig.get_proxy_ip()
print("获取json:" + url)
resp = coderpig.get_resp(url, headers=json_headers, proxy=proxy_ip).decode('utf-8')
if resp is None:
return None
else:
json_dict = json.loads(resp)
pins = json_dict['board']['pins']
if len(pins) == 0:
return None
else:
for item in pins:
coderpig.write_str_data(item['file']['key'], pin_ids_file)
return pins[-1]['pin_id']
# 下载图片的方法
def download_pic(key):
proxy_ip = coderpig.get_proxy_ip()
coderpig.is_dir_existed(pic_download_dir)
url = img_start_url + key + img_end
resp = coderpig.get_resp(url, proxy=proxy_ip, ie_header=True)
try:
print("下载图片:" + url)
pic_name = key + ".jpg"
with open(pic_download_dir + pic_name, "wb+") as f:
f.write(resp)
except (OSError, urllib.error.HTTPError, urllib.error.URLError, Exception) as reason:
print(str(reason))
if __name__ == '__main__':
coderpig.init_https()
if os.path.exists(pin_ids_file):
os.remove(pin_ids_file)
# 一个画板链接,可自行替换
boards_url = 'http://huaban.com/boards/27399228/'
board_last_pin_id = get_boards_index_data(boards_url)
board_json_url = boards_url + '?jcx38c3h&max=354569642&limit=20&wfl=1'
while True:
board_last_pin_id = get_json_list(max_pattern.sub(str(board_last_pin_id), board_json_url))
if board_last_pin_id is None:
break
pic_url_list = coderpig.load_data(pin_ids_file)
for key in pic_url_list:
download_pic(key)
print("下载完成~")
输出结果:
参见吾王~
磕磕碰碰,总算是把代码给撸出来了,成功又收获了一大波小姐姐,
爬虫技能点+1,建议还是少用无脑Selenium吧,另外刚发现,
Chrome直接支持右键导出XPath,就不用自己慢慢扣了(如果你用lxml的话)。
好的,就说那么多~
====== 修正 ======
本节源码下载:
https://github.com/coder-pig/ReptileSomething
来啊,Py交易啊
想加群一起学习Py的可以加下,智障机器人小Pig,验证信息里包含:
Python,python,py,Py,加群,交易,屁眼 中的一个关键词即可通过;
验证通过后回复 加群 即可获得加群链接(不要把机器人玩坏了!!!)~~~
欢迎各种像我一样的Py初学者,Py大神加入,一起愉快地交流学♂习,van♂转py。