一、前言
经过上两篇文章的学习,爬虫三步走:发起请求、解析数据、保存数据,已经掌握,算入门爬虫了吗?
远远不够!只掌握这些,还只能算门外汉级别。
今天,就来带大家继续学习,怎么爬的更优雅!
按照惯例,还是从实战出发,今天咱们就爬个图片,盘点那些遇到的问题,和优雅的解决方案。
本文男女老少皆宜,什么妹子图、肌肉男,学会了本文的方法,一切尽收囊中!
二、实战背景
动漫之家漫画下载!
动态加载、初级反爬
三、漫画下载
挑来挑去,找了本动漫之家排名靠前的一本《妖神记》,说实话,看了漫画第一章的内容,浓浓的火影气息。
URL:https://www.dmzj.com/info/yaoshenji.html
- 拿到所有章节名和章节链接
- 根据章节链接章节里的所有漫画图片
- 根据章节名,分类保存漫画
看似简单,实际做起来,可能遇到各种各样的问题,让我们一起优雅的解决这些问题吧!
div
div
存放不同的内容,如上图,有存放标题Jack Cui的div
,有存放菜单的div
,有存放正文内容的div
,有存放版权信息的div
。
瞧,不难发现,只要拿到class
属性为zj_list
的div
标签,就能拿到章节名和章节链接,都存放在这个div
标签下的a
标签中。
div
标签下还有个ul
标签,ul
标签是距离a
class
属性为list_con_li
的ul
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import requests from bs4 import BeautifulSoup target_url = "https://www.dmzj.com/info/yaoshenji.html" r = requests.get(url=target_url) bs = BeautifulSoup(r.text, 'lxml') list_con_li = bs.find('ul', class_="list_con_li") comic_list = list_con_li.find_all('a') chapter_names = [] chapter_urls = [] for comic in comic_list: href = comic.get('href') name = comic.text chapter_names.insert(0, name) chapter_urls.insert(0, href) print(chapter_names) print(chapter_urls) |
瞧,章节名和章节链接搞定了!
没有难度啊?别急,难的在后面。
我们先看第一章的内容。
URL:https://www.dmzj.com/view/yaoshenji/41917.html
打开第一章的链接,你会发现,链接后面自动添加了#@page=1。
但是,这些并不是图片的地址,而是这个展示页面的地址,要下载图片,首先要拿到图片的真实地址。
审查元素找图片地址,你会发现,这个页面不能右键!
这就是最最最最低级的反爬虫手段,这个时候我们可以通过键盘的F12调出审查元素窗口。
有的网站甚至把F12都禁掉,这种也是很低级的反爬虫手段,骗骗刚入门的手段而已。
面对这种禁止看页面源码的初级手段,一个优雅的通用解决办法是,在连接前加个view-source:
。
1 | view-source:https://www.dmzj.com/view/yaoshenji/41917.html |
用这个链接,直接看的就是页面源码。
F12依然可以调出调试窗口。
F12
我们可以在浏览器调试窗口中的Network里找到这个页面加载的内容,例如一些css文件啊、js文件啊、图片啊,等等等。
在Network中可以很轻松地找到我们想要的图片真实地址,调试工具很强大,Headers可以看一些请求头信息,Preview可以浏览返回信息。
搜索功能,过滤功能等等,应有尽有,具体怎么用,自己动手点一点,就知道了!
好了,拿到了图片的真实地址,我们看下链接:
https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
img
view-source:
1 | view-source:https://www.dmzj.com/view/yaoshenji/41917.html |
动态加载
view-source:
方法,就是看页面源码,并不管动态加载的内容。这里面没有图片链接,就说明图片是动态加载的。
遇到动态加载不要慌,使用JavaScript动态加载,无外乎两种方式:
外部加载就是在html页面中,以引用的形式,加载一个js,例如这样:
1 | <script type="text/javascript" src="https://cuijiahua.com/call.js"></script> |
内部加载就是Javascript脚本内容写在html内,例如这个漫画网站。
这时候,就可以用搜索功能了,教一个搜索小技巧。
https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
14395217739069
1 2 3 4 5 | <script type="text/javascript"> var arr_img = new Array(); var page = ''; eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('g f=\'{"e":"h","i":"0","l":"k\\/3\\/5\\/2\\/j.4\\r\\6\\/3\\/5\\/2\\/d.4\\r\\6\\/3\\/5\\/2\\/7.4\\r\\6\\/3\\/5\\/2\\/8.4\\r\\6\\/3\\/5\\/2\\/c.4\\r\\6\\/3\\/5\\/2\\/b.4\\r\\6\\/3\\/5\\/2\\/a.4\\r\\6\\/3\\/5\\/2\\/9.4\\r\\6\\/3\\/5\\/2\\/m.4\\r\\6\\/3\\/5\\/2\\/v.4\\r\\6\\/3\\/5\\/2\\/A.4\\r\\6\\/3\\/5\\/2\\/n.4\\r\\6\\/3\\/5\\/2\\/B.4\\r\\6\\/3\\/5\\/2\\/x.4\\r\\6\\/3\\/5\\/2\\/y.4","w":"p","o":"1","q":"\\s\\u \\t\\z"}\';',38,38,'||14237|chapterpic|jpg|3059|nimg|14395217891719|14395217893745|14395217913416|14395217908431|14395217904781|1439521790086|1439521788936|id|pages|var|41917|hidden|14395217739069|img|page_url|14395217918734|14395217931135|chapter_order|15|chapter_name||u7b2c01|u91cd|u8bdd|14395217923415|sum_pages|14395217940216|14395217943921|u751f|14395217926321|1439521793602'.split('|'),0,{})) </script> |
不出意外,你就能看到这段代码,14395217739069
就混在其中!
看不懂Javascript,怎么办啊?
没关系,说实话,我看着也费劲儿。
优雅的解决这个动态加载问题,我们再看这个图片链接:
https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
链接中的数字是不是眼熟?
这不就是这几个数字合成的吗?
好了,我有个大胆的想法!直接把这些长的数字搞出来,合成下链接试试看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import requests from bs4 import BeautifulSoup import re url = 'https://www.dmzj.com/view/yaoshenji/41917.html' r = requests.get(url=url) html = BeautifulSoup(r.text, 'lxml') script_info = html.script pics = re.findall('\d{13,14}', str(script_info)) chapterpic_hou = re.findall('\|(\d{5})\|', str(script_info))[0] chapterpic_qian = re.findall('\|(\d{4})\|', str(script_info))[0] for pic in pics: url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic + '.jpg' print(url) |
运行代码,你可以得到如下结果:
踏破铁鞋无觅处,得来全不费工夫!
但是有个问题,这么合成的的图片链接不是按照漫画顺序的,这下载下来漫画图片都是乱的啊!不优雅!
这个网站也是人写的嘛!是人,就好办!惯性思维,要是你,是不是小数放在前面,大数放在后面?这些长的数字里,有13位的,有14位的,并且都是以14开头的数字,那我就赌它末位补零后的结果,就是图片的顺序!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import requests from bs4 import BeautifulSoup import re url = 'https://www.dmzj.com/view/yaoshenji/41917.html' r = requests.get(url=url) html = BeautifulSoup(r.text, 'lxml') script_info = html.script pics = re.findall('\d{13,14}', str(script_info)) for idx, pic in enumerate(pics): if len(pic) == 13: pics[idx] = pic + '0' pics = sorted(pics, key=lambda x:int(x)) chapterpic_hou = re.findall('\|(\d{5})\|', str(script_info))[0] chapterpic_qian = re.findall('\|(\d{4})\|', str(script_info))[0] for pic in pics: if pic[-1] == '0': url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic[:-1] + '.jpg' else: url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic + '.jpg' print(url) |
程序对13位的数字,末位补零,然后排序。
在跟网页的链接按顺序比对,你会发现没错!就是这个顺序!
不用读懂Javascript合成链接代码,直接分析测试,够不够优雅
万事俱备,只欠东风!
1 2 3 4 | import requests from urllib.request import urlretrieve dn_url = 'https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg' urlretrieve(dn_url,'1.jpg') |
urlretrieve
方法,就可以下载,这是最简单的下载方法。第一个参数是下载链接,第二个参数是下载后的文件保存名。
不出意外,就可以顺利下载这张图片!
出现了HTTP Error,错误代码是403。
403表示资源不可用,这是又是一种典型的反扒虫手段。
打开这个图片链接:
URL:https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
这个地址就是图片的真实地址,在浏览器中打开,可能直接无法打开,或者能打开,但是一刷新就又不能打开了!
如果再打开章节页面,再打开这个图片链接就又能看到图片了。
章节URL:https://www.dmzj.com/view/yaoshenji/41917.html
Referer可以理解为来路,先打开章节URL链接,再打开图片链接。打开图片的时候,Referer的信息里保存的是章节URL。
动漫之家网站的做法就是,站内的用户访问这个图片,我就给他看,从其它地方过来的用户,我就不给他看。
是不是站内用户,就是根据Referer进行简单的判断。
这就是很典型的,反爬虫手段!
解决办法也简单,它需要啥,咱给它就完了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import requests from contextlib import closing download_header = { 'Referer': 'https://www.dmzj.com/view/yaoshenji/41917.html' } dn_url = 'https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg' with closing(requests.get(dn_url, headers=download_header, stream=True)) as response: chunk_size = 1024 content_size = int(response.headers['content-length']) if response.status_code == 200: print('文件大小:%0.2f KB' % (content_size / chunk_size)) with open('1.jpg', "wb") as file: for data in response.iter_content(chunk_size=chunk_size): file.write(data) else: print('链接异常') print('下载完成!') |
closing
下载完成!就是这么简单!
将代码整合在一起,下载整部漫画。编写代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | import requests import os import re from bs4 import BeautifulSoup from contextlib import closing from tqdm import tqdm import time """ Author: Jack Cui Wechat: https://mp.weixin.qq.com/s/OCWwRVDFNslIuKyiCVUoTA """ # 创建保存目录 save_dir = '妖神记' if save_dir not in os.listdir('./'): os.mkdir(save_dir) target_url = "https://www.dmzj.com/info/yaoshenji.html" # 获取动漫章节链接和章节名 r = requests.get(url = target_url) bs = BeautifulSoup(r.text, 'lxml') list_con_li = bs.find('ul', class_="list_con_li") cartoon_list = list_con_li.find_all('a') chapter_names = [] chapter_urls = [] for cartoon in cartoon_list: href = cartoon.get('href') name = cartoon.text chapter_names.insert(0, name) chapter_urls.insert(0, href) # 下载漫画 for i, url in enumerate(tqdm(chapter_urls)): download_header = { 'Referer': url } name = chapter_names[i] # 去掉. while '.' in name: name = name.replace('.', '') chapter_save_dir = os.path.join(save_dir, name) if name not in os.listdir(save_dir): os.mkdir(chapter_save_dir) r = requests.get(url = url) html = BeautifulSoup(r.text, 'lxml') script_info = html.script pics = re.findall('\d{13,14}', str(script_info)) for j, pic in enumerate(pics): if len(pic) == 13: pics[j] = pic + '0' pics = sorted(pics, key=lambda x:int(x)) chapterpic_hou = re.findall('\|(\d{5})\|', str(script_info))[0] chapterpic_qian = re.findall('\|(\d{4})\|', str(script_info))[0] for idx, pic in enumerate(pics): if pic[-1] == '0': url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic[:-1] + '.jpg' else: url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic + '.jpg' pic_name = '%03d.jpg' % (idx + 1) pic_save_path = os.path.join(chapter_save_dir, pic_name) with closing(requests.get(url, headers = download_header, stream = True)) as response: chunk_size = 1024 content_size = int(response.headers['content-length']) if response.status_code == 200: with open(pic_save_path, "wb") as file: for data in response.iter_content(chunk_size=chunk_size): file.write(data) else: print('链接异常') time.sleep(10) |
大约40分钟,漫画即可下载完成!
友好的爬虫。写爬虫,要谨慎,勿给服务器增加过多的压力,满足我们的获取数据的需求,这就够了。
你好,我也好,大家好才是真的好。
【JackCui-AI】
2021年8月21日 下午11:21 21楼
太强了,全程跟下来,每一步都有收获,最后那个用header信息的时候,我用的我前面爬下来的章节url,然后一直报404错误才发现自己爬下来的章节qian和hou的部分反了,本就打不开就没注意,搞得我找了好久
2021年9月22日 下午2:41 22楼
大佬,出现 KeyError: ‘content-length’ 要怎么解决
2021年9月26日 上午1:10 23楼
看图片时,真急死人,想放大却不行。
2022年1月10日 下午3:13 24楼
能多说点反反爬的方法吗?我自己爬个站,cookie有’,提前结束了cookie,都爬不动
2022年2月8日 上午10:15 25楼
想问下假设说title为blank的情况下怎么处理
2022年9月21日 下午5:17 26楼
诶,为什么我下载图片的时候直接越过Referer保存下来了
2022年9月22日 下午5:36 27楼
学会了
2022年10月27日 下午1:48 28楼
# 大佬为什么我运行这段代码然后电脑就死机了呢
import requests
from bs4 import BeautifulSoup
target_url = ‘https://www.dmzj.com/info/xiexiuyutianshadizi.html’
r = requests.get(url=target_url)
bs = BeautifulSoup(r.text, ‘lxml’)
chapter_a_list = bs.find(‘ul’, class_=’list_con_li’).find_all(‘a’)
chapter_list = []
for a in chapter_a_list:
# 下面三行有缩进
href = a.get(‘href’)
chapter_title = a.get(‘title’)
chapter_a_list.append({‘href’: href, ‘title’: chapter_title})
print(chapter_list)
2022年10月27日 下午2:18 1层
@爬爬 变量写错了,为什么不能删除了可恶