转载请注明来源, 原文链接 :
https://www.cnblogs.com/Laplacedoge/p/11828622.html
讲真的, 手机看漫画翻页总是会手残碰到页面上的广告好吧, 再碰上站点的带宽还很低, 无疑是雪上加霜, 要是指定漫画的主页URL就能给我返回整本漫画的所有图片并且整理好存放在指定目录就好了...
这促使我产生了使用Python 3来实现, 做一个 ComicReaper(漫画收割者) 的想法!
本文所用漫画链接 : http://www.manhuadb.com/manhua/2317
总体流程
那就开始吧
做一些准备工作
导入将会使用到Python的两个库, re 与 urllib
1 # 导入正则表达式 2 import re 3 # 导入 urllib.request 4 import urllib.request
先用字符串存储两个链接, 一个是本次漫画网站站点的域名URL, 另一个是当前我们要爬取的漫画主页URL
再定义一个 header 字典, 用于存储我们的 User-Agent 和 Referer Referrer (由于早期HTTP规范的拼写错误, 为了保持向后兼容就将错就错了)
1 url_domainame = r'https://www.manhuadb.com' 2 url_host = r'https://www.manhuadb.com/manhua/2317' 3 header = { 4 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0', 5 'Referer' : '' 6 }
首部字段 User-Agent
首部字段 User-Agent 告诉服务器当前创建请求的浏览器是什么(有的网站会针对不同的浏览器提供不同的页面, 比如如果是手机浏览器提出的请求, 服务器就向客户端提供网站的手机版页面)
比如说同样是请求 GitHub 的主页, 左边是使用笔记本电脑浏览器请求的页面, 右边是在安卓手机上请求的
首部字段 Referer
首部字段 Referer 告诉服务器当前请求的页面是从哪个Web页面发起的(一般情况下 Referer 字段用于防盗链)
有的网站不允许直接访问站内的URL, 只能通过从主页点击链接来进行跳转, 或者...我们在请求之前构建请求头把 User-Agent 字段设置为主页或发起页即可
获取章节目录
一次性获取所有的章节信息将会是一个不错的选择, 因为发起一次请求的代价很高(当网速较慢或者网站带宽较低时, 延时很高)
我们要获取当前漫画所有章节的标题与URL(标题用于后期存储时文件夹的命名, URL用于跳转到当前章节的开始页面)并且打包成字典存储在列表中
对在浏览器中按下 [F12] 键打开开发者工具来对漫画的章节页面进行分析
我们可以看到页面中有很多章节, 也就是章节跳转链接, 每个链接的<a>标签中正好具有我们需要的标题和URL, 分别是<a>标签的 title 属性与 href 属性, 我们将使用字典来存储它
先不慌着前进, 考虑到整个HTML中有非常多的链接, 那么也就意味着页面中具有大量的<a>标签, 如果我们只是单纯地从HTML中过滤出<a>标签, 这样我们会得到大量我们并不需要的<a>标签, 这是不明智的, 我们必须只过滤出章节跳转链接的<a>标签, 仔细观察, 发现章节跳转链接的<a>标签们都具有一个特点, 那就是它们都具有 class 属性并且属性值为 "fixed-a-es" , 这就找到了一个可以定位章节<a>标签的依据, 把这一点加入到我们的正则表达式的匹配规则中去
现在就可以定义一个正则表达式匹配字符串了(什么是正则表达式?)(在线正则表达式练习) :
pat = r'<a class="fixed-a-es" href="(.*?)" title="(.*?)"'
为什么要这么写 :
- 在Python中, 在字符串常量的开头加一个 'r' 表示本字符串中的 '\' 字符将不会用来作转义字符使用, 保留了它原本的含义, 也就是反斜杠字符
- 在正则表达式中, '.' 字符用于匹配任何字符(当匹配时具有 're.S' 标志时此话成立, 否则只能匹配任意但除了 '\n' 以外的字符)
- 在正则表达式中, '*' 字符用于描述它左边的匹配字符的出现次数为0次或若干次
- 在正则表达式中, '(.*?)' 的组合用来表示一个贪婪匹配(并且会被捕捉到), 至于什么是贪婪匹配, 可以看这位博主的这篇文章
使用这个正则表达式, 就可以匹配到 title 属性与 href 属性的属性值中的双引号里面的内容了
具体实现是 chapterIndexReaper 函数, 主要用来"收割"当前漫画的所有章节并存储为字典列表
代码如下 :
1 #获取一本漫画的所有章节的目录 2 def chapterIndexReaper(url_host, header): 3 # 定义一个临时字典, 用于临时存储一个章节的标题与url 4 dic_temp = { 5 'Title' : '', 6 'Url' : '' 7 } 8 # 章节字典列表, 存储当前漫画的所有章节字典 9 set_dic = [] 10 # 构建Request对象 11 req = urllib.request.Request(url = url_host, headers = header) 12 # 读取所请求的req并用utf-8编码来进行解码, 所得到的的字符串赋值给html 13 html = urllib.request.urlopen(req).read().decode('utf-8') 14 # 爬取漫画章节标题与url的正则表达式 15 pat = r'<a class="fixed-a-es" href="(.*?)" title="(.*?)"' 16 # 使用pat在html中进行进行匹配(re.S参数是为了让"."除了能够匹配本身规定的字符, 17 # 另外也能匹配"\n"), 返回一个结果列表res 18 res = re.findall(pat, html, re.S) 19 for i in res: 20 dic_temp['Title'] = i[1] 21 dic_temp['Url'] = url_head + i[0] 22 # 向当前的章节字典列表的后面追加新的章节, 注意, 此处要使用浅拷贝 23 # (因为dic_temp是一个临时变量, 需要创建它的副本并追加到set_dic中去, 24 # 否则当dic_temp刷新时set_dic中的元素会相应发生改变) 25 set_dic.append(dic_temp.copy()) 26 return set_dic
获取章节全部页面的URL目录
我们有了所有章节的跳转链接, 现在需要做的是获取章节里面的所有页面, 因为每一个页面都包含一张我们所需要爬取的图片
对第一章的页面URL进行分析 :
第一页(也是章节跳转链接) :
https://www.manhuadb.com/manhua/2317/2860_52789.html
第二页 :
https://www.manhuadb.com/manhua/2317/2860_52789_p2.html
第三页 :
https://www.manhuadb.com/manhua/2317/2860_52789_p3.html
想必都能看出规律, 交给 pageIndexReaper 来生成并打包它即可 :
1 #获取一个章节的所有页码的目录 2 def pageIndexReaper(url, number): 3 set_page = [url] 4 url = url.replace('.html', '') 5 for i in range(2, number + 1): 6 set_page.append(url + '_p' + str(i) + '.html') 7 return set_page
这时候可能就有小朋友会问了 : "pageIndexReaper(url, number) 的 number 参数怎么来的? "
的确, 虽然我们能够确定漫画有多少个章节, 但我们却不能确定每章有多少页(每章的页面数量是不确定的), 这时候我们就需要在页面上寻找有没有能够提供总页面数量的标签了
正好这里有一个 :
分析其 HTML :
丢给 totalPageNumberReaper 去解决吧 :
1 #爬取一个章节的页码数 2 pat_totalPageNumber = r'<li class="breadcrumb-item active" aria-current="page".*?共 (.*?) 页' 3 def totalPageNumberReaper(url, html = None): 4 if html == None: 5 hdr = header 6 hdr['Referer'] = url_host 7 req = urllib.request.Request(url = url, headers = hdr) 8 html = urllib.request.urlopen(req).read().decode('utf-8') 9 res = re.search(pat_totalPageNumber, html, re.S) 10 return int(res.group(1))
这里的 totalPageNumberReaper 函数拥有两种调用方案 :
- 最原始的方法, 给出指定章节页面的URL, 经过构建请求头, 请求并读取然后解码, 最后再使用正则表达式匹配到章节总页码数
- 利用现有的 HTML 直接开始匹配章节总页码数(实在是因为请求一次页面非常耗时间)
Laplacedoge说 : 能实时看到进度的爬虫才是好爬虫! 加上进度条来让你掌控全局!
1 #进度条显示 2 def showProgressBar(value, tip): 3 if value < 1: 4 print('\r', ' [{} : {:.2%}]'.format(tip, value), end = '', flush = True) 5 else: 6 print('\r', ' [{} : {:.2%}]'.format(tip, value), end = '' + '\n' + '[Done]' + '\n', flush = True)
事实上Python有很多第三方进度条库, 各种实用而且炫酷的都有(比如受人称赞的tqdm库)
下载图片并存储到指定指定目录
传给 GO 函数漫画的主页URL以及在准备存储在系统中的位置, 把剩下的事情交给 GO 去做吧!
1 #开始 2 pat_image = r'<img class="img-fluid show-pic" src="(.*?)"' 3 def GO(url_comic, path_save): 4 # 获取所有章节目录 5 index_chapter = chapterIndexReaper(url_comic, header) 6 # 对所有章节进行遍历 7 for chapter in index_chapter: 8 # 获取当前章节的页面总数量 9 number = totalPageNumberReaper(chapter['Url']) 10 # 获取当前章节的页面URL目录 11 index_page = pageIndexReaper(chapter['Url'], number) 12 # 编辑当前章节的路径名, 即将以当前章节名为文件名在 path_save 路径下创建一个文件夹 13 path_current = path_save + '/' + chapter['Title'] 14 # 使用os模块创建目录并给出777的权限(最开放的权限) 15 # 检测章节目录是否已经创建 16 if not os.path.exists(path_current): 17 os.makedirs(path_current) 18 os.chmod(path_current, 0o777) 19 # 用于标记当前页码 20 page_current = 1 21 print('\n[{} : 共{}页]'.format(chapter['Title'], number)) 22 # 遍历当前章节的所有页面 23 for page in index_page: 24 # 编辑当前页面图片的路径名 25 path_currentImage = path_current + '/' + chapter['Title'] + '_' + str(page_current) + '.jpg' 26 # 使用os模块判断是否存在当前照片 27 if os.path.exists(path_currentImage): #若是当前图片存在 28 showProgressBar(page_current / number, 'Downloading ' + chapter['Title']) #显示当前进度 29 page_current = page_current + 1 30 # 不存在就请求并存储这张照片 31 else: 32 hdr = header 33 hdr['Referer'] = chapter['Url'] 34 req = urllib.request.Request(url = page, headers = hdr) 35 html = urllib.request.urlopen(req).read().decode('utf-8') 36 # 爬取当前页面图片 37 url_currentImage = re.search(pat_image, html, re.S).group(1) 38 # 下载当前图片到指定目录下 39 urllib.request.urlretrieve(url_currentImage, path_currentImage) 40 os.chmod(path_currentImage, 0o777) 41 # 显示当前进度 42 showProgressBar(page_current / number, 'Downloading ' + chapter['Title']) 43 # 当前页码 + 1 44 page_current = page_current + 1 45 # 延时 1s, 太快会出大问题 46 time.sleep(1) 47 time.sleep(1)
正如之前的<a>标签的情况一样, 这张图片使用的<img>标签也在页面中大量存在, 使用下图中黄框的 class 属性来对<img>标签进行定位确保唯一, 红框中的URL是我们要提取的内容
1 pat_image = r'<img class="img-fluid show-pic" src="(.*?)"'
最终爬取效果
在桌面的指定存储路径下 :
所有章节文件夹均以它们在原网页上的标题命名 :
每一张图片是章节 + 图片标号的命名方式, 可在代码中修改 :
关于断点续爬功能
首先说说为什么需要这个功能?
事实上要知道, 如果在爬取的过程中断网了, 那将会变成一件非常揪心的事情——这意味着你要全部从头开始重新爬(会覆盖原来已经爬取的内容)
所以断点续爬提供了存在图片自动检测功能, 让您爬得舒心, 用得安心