更新日志:
2019.4.28 更新检索模块
自从追完约定梦幻岛就念念不忘,想着追下漫画,可是,电脑上看太不方便,手机一看,广告太多而且翻页什么的都太不方便了,于是乎,就有了今天的爬虫实战了。
我这次爬取的漫画目标网站为: http://www.1kkk.com/
1.写在前面
求点赞,求点赞,求点赞~(小声)
(觉得啰嗦可直接跳到正文部分)
(还是觉得啰嗦的可以直接跳到最后整合后完整代码部分~)
漫画下载需求确认:
-
用户输入漫画名,程序自动完成检索,打印检索漫画信息,含漫画名、作者、连载情况、摘要等;
-
判断漫画是否为付费漫画,并打印提示,选择仅下载免费章节或退出下载;
-
判断是否为限制级漫画,打印提示,并自动完成校验进入下一步;
-
用户确认信息是否检索准确,是则下载,否则退出;
-
能完整下载所有章节所有漫画页高清图片;
-
能根据不同章节打包,文件夹漫按漫画章节名命名;
-
每章节内漫画页按顺序命名;
-
尽可能提升下载效率。
先给大家看下成果,爬取到的漫画结合本地漫画app "Perfect Viewer"观看的效果:
在手机上可实现触屏点击自动翻页、跳章,还能记录当前看到的位置,可以说很爽了~
本地app内效果 高清漫画图页写在前面的总结
本爬虫使用到的模块如下:
- Selector : scrapy的解析库
- selector提取内容的方法,本文使用
getall()
和get()
替代了旧的extract()
和extract_first()
- requests:请求库
- selenium:浏览器模拟工具
- time:时间模块
- re:正则表达式
- multiprocessing: 多进程
- pymongo: mongodb数据库
本爬虫使用的工具如下:
- 谷歌浏览器
- 解析插件:xpath helper
- postman
本爬虫遇到的值得强调的问题如下:
- 多进程不共享全局变量,不能使用input()函数.
- 漫画图片链接使用了异步加载,不能直接在主页获取.
- 请求图片链接必须携带对应章节referer信息才有返回数据.
- 部分漫画缺少资源,需增加判定.
- 部分漫画为付费漫画,需增加判定.
- 部分漫画为限制级漫画,需模拟点击验证才能返回数据.
- 需分章节创建目录,并判定目录是否存在.
- 漫画图片需按顺序命名.
正文开始:
2. 目标网页结构分析(思路分析,具体代码下一章)
爬虫编写建议逆向分析网页,即:从自己最终需求所在的数据网页开始,分析网页加载形式, 请求类型, 参数构造 ,再逆向逐步推导出构造参数的来源.分析完成后再从第一个网页出发, 以获取构造参数为目的,逐步请求得到参数,构造出最终的数据页链接并获取所需数据.
因此,我这次的爬取先从漫画图片所在页开始分析.
-
找到漫画图片链接
开发者工具定位漫画图片链接
随意选择一部漫画进入任意一页,这里还是以<<约定梦幻岛>>为例吧,我随便点击进了第85话:
http://www.1kkk.com/ch103-778911/#ipg1
常规操作,首先使用谷歌浏览器,按F12
打开开发者工具,选择元素,点击漫画图片,自动定位到图片地址源码位置.如图所示:
-
确定返回该链接的源网址
异步确认
简单的找到漫画图片链接后,需要确定返回该链接的请求网址。
最简单的情况是图片没有异步加载,链接随主页网址返回,怎么确定它是不是异步加载的呢?
很简单,1.通过Preview查看渲染后的网页中是否包含漫画图片;2.在Response响应中直接搜索是否包含图片链接.具体示例图如下:
通过上面的操作,我们得出结论,图片链接时=是通过异步加载得到的。因此需要找到它的数据来源,经过一段时间的寻找,我,放弃了。没有找到结构化且明确的链接所在,最终考虑到并非进行大规模爬取,决定用selenium模拟来完成图片链接获取的工作。这样,获取一页图片链接的步骤就没问题了。 -
实现章节内翻页获取全部图片链接
下一页节点
既然已经确定采用seleni模拟浏览器来获取图片链接,那翻页的网页结构分析步骤也省略了,只需获取"下一页"节点,模拟浏览器不断点击下一页操作即可。
这样就确定了每一章所有页的图片获取方式.接下来需要做的是获取所有章节的链接. -
获取章节链接
章节链接结构
当然,可能会与人想,章节也可以继续用selenium来模拟浏览器翻页点击啊,这样是可以没错,但是......selenium的效率是真的低,能不用就不要用,不然一部漫画的下载时间可能需要很长。
我们来分析该部漫画主页,其实一下就能看出,获取章节链接和信息是很简单的。
这里只需要构造一个requests请求再解析网页即可获得所有章节的链接及章节名.
其实到这里,我们就已经可以完成单个指定漫画的爬虫简单版了,为什么叫简单版,因为还有很多判定,很多自动化检索功能未添加进去..
3.编写漫画爬虫简单版
何为简单版?
- 没有检索功能,不能自动检索漫画并下载。
- 漫画名、漫画主页链接需要手工给定输入。
- 下载的漫画不能为付费漫画、限制级漫画。
其余功能,包括多进程下载都正常包含。
实现代码模块将在下面分别讲解:
- 获取全部章节信息
from scrapy.selector import Selector
import requests
# 约定梦幻岛漫画链接
start_url = "http://www.1kkk.com/manhua31328/"
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
}
def get_chapter_list(start_url):
res = requests.get(start_url, headers=header)
selector = Selector(text=res.text)
items = selector.xpath("//ul[@id='detail-list-select-1']//li")
# 用于存放所有章节信息
chapter_list = []
for item in items:
# 构造绝对链接
chapter_url = "http://www.1kkk.com" + item.xpath("./a/@href").get()
title = item.xpath("./a/text()").get().rstrip()
# 若上述位置未匹配到标题,则换下面的匹配式
if not title:
title = item.xpath("./a//p/text()").get().rstrip()
dic = {
"chapter_url": chapter_url,
"title": title
}
chapter_list.append(dic)
# 按章节正需排序
chapter_list.reverse()
total_len = len(chapter_list)
print("\n【总共检索到 {} 个章节信息如下】:\n{}".format(total_len, chapter_list))
return chapter_list
if __name__ == '__main__':
get_chapter_list(start_url)
输出结果:
得到包含所有章节链接和标题数据。
【总共检索到 103 个章节信息如下】:
[{'chapter_url': 'http://www.1kkk.com/ch1-399698/', 'title': '第1话 GFhouse'}, {'chapter_url': 'http://www.1kkk.com/ch2-400199/', 'title': '第2话 出口'}, {'chapter_url': 'http://www.1kkk.com/ch3-402720/', 'title': '第3话 铁之女'}, {'chapter_url': 'http://www.1kkk.com/ch4-404029/', 'title': '第4话 最好'}, {'chapter_url': 'http://www.1kkk.com/ch5-405506/', 'title': '第5话 被算计了!'}, {'chapter_url': 'http://www.1kkk.com/ch6-406812/', 'title': '第6话 卡罗露和克洛涅'}, {'chapter_url': 'http://www.1kkk.com/ch7-407657/', 'title': '第7话 全靠你了'}, {'chapter_url': 'http://www.1kkk.com/ch8-409649/', 'title': '第8话 我有个主意'}, {'chapter_url': 'http://www.1kkk.com/ch9-411128/', 'title': '第9话 一起来玩捉迷藏吧'}, {'chapter_url': 'http://www.1kkk.com/ch10-418782/', 'title': '第10话 掌控'}, {'chapter_url': 'http://www.1kkk.com/ch11-421753/', 'title': '第11话 内鬼①'}, {'chapter_url': 'http://www.1kkk.com/ch12-422720/', 'title': '第12话 内鬼➁'}, {'chapter_url': 'http://www.1kkk.com/ch13-424435/', 'title': '第13话 内鬼3'}, {'chapter_url': 'http://www.1kkk.com/ch14-425751/', 'title': '第14话 杀手锏'}, {'chapter_url': 'http://www.1kkk.com/ch15-427433/', 'title': '第15话 不要有下次了'}, {'chapter_url': 'http://www.1kkk.com/ch16-428613/', 'title': '第16话 秘密的房间和W.密涅尔巴'}, {'chapter_url': 'http://www.1kkk.com/ch17-429698/', 'title': '第17话 秘密的房间和W.密涅瓦 ➁'}, {'chapter_url': 'http://www.1kkk.com/ch18-430916/', 'title': '第18话 觉悟'}, {'chapter_url': 'http://www.1kkk.com/ch19-432001/', 'title': '第19话 厨具'}, {'chapter_url': 'http://www.1kkk.com/ch20-452160/', 'title': '第20话 “携手共战”'}, {'chapter_url': 'http://www.1kkk.com/ch21-452161/', 'title': '第21话 被看穿的策略'}, {'chapter_url': 'http://www.1kkk.com/ch22-453011/', 'title': '第22话 诱饵'}, {'chapter_url': 'http://www.1kkk.com/ch23-453852/', 'title': '第23话 砸个粉碎!!'}, {'chapter_url': 'http://www.1kkk.com/ch24-454970/', 'title': '第24话 预先调查①'}, {'chapter_url': 'http://www.1kkk.com/ch25-455408/', 'title': '第25话 预先调查②'}, {'chapter_url': 'http://www.1kkk.com/ch26-456937/', 'title': '第26话 想活下去'}, {'chapter_url': 'http://www.1kkk.com/ch27-459192/', 'title': '第27话 不会让你死'}, {'chapter_url': 'http://www.1kkk.com/ch28-463002/', 'title': '第28话 潜伏'}, {'chapter_url': 'http://www.1kkk.com/ch29-469845/', 'title': '第29话 潜伏②'}, {'chapter_url': 'http://www.1kkk.com/ch30-470068/', 'title': '第30话 抵抗'}, {'chapter_url': 'http://www.1kkk.com/ch31-471022/', 'title': '第31话 空虚'}, {'chapter_url': 'http://www.1kkk.com/ch32-471987/', 'title': '第32话 决行①'}, {'chapter_url': 'http://www.1kkk.com/ch33-475979/', 'title': '第33话 决行②'}, {'chapter_url': 'http://www.1kkk.com/ch34-477581/', 'title': '第34话 决行③'}, {'chapter_url': 'http://www.1kkk.com/ch35-478788/', 'title': '第35话 决行④'}, {'chapter_url': 'http://www.1kkk.com/ch36-480532/', 'title': '第36话 决行⑤'}, {'chapter_url': 'http://www.1kkk.com/ch37-484169/', 'title': '第37话 逃脱'}, {'chapter_url': 'http://www.1kkk.com/ch38-487071/', 'title': '第38话 誓言之森'}, {'chapter_url': 'http://www.1kkk.com/ch39-489256/', 'title': '第39话 意料之外'}, {'chapter_url': 'http://www.1kkk.com/ch40-491112/', 'title': '第40话 阿尔巴比涅拉之蛇'}, {'chapter_url': 'http://www.1kkk.com/ch41-492519/', 'title': '第41话 袭来'}, {'chapter_url': 'http://www.1kkk.com/ch42-495364/', 'title': '第42话 怎么可能让你吃掉'}, {'chapter_url': 'http://www.1kkk.com/ch43-497162/', 'title': '第43话 81194'}, {'chapter_url': 'http://www.1kkk.com/ch44-498952/', 'title': '第44话 戴兜帽的少女'}, {'chapter_url': 'http://www.1kkk.com/ch45-500306/', 'title': '第45话 救援'}, {'chapter_url': 'http://www.1kkk.com/ch46-501983/', 'title': '第46话 颂施与缪西卡'}, {'chapter_url': 'http://www.1kkk.com/ch47-503551/', 'title': '第47话 昔话'}, {'chapter_url': 'http://www.1kkk.com/ch48-505288/', 'title': '第48话 两个世界'}, {'chapter_url': 'http://www.1kkk.com/ch49-508300/', 'title': '第49话 请教教我'}, {'chapter_url': 'http://www.1kkk.com/ch50-514639/', 'title': '第50话 朋友'}, {'chapter_url': 'http://www.1kkk.com/ch51-521408/', 'title': '第51话 B06-32①'}, {'chapter_url': 'http://www.1kkk.com/ch52-523467/', 'title': '第52话 B06-32②'}, {'chapter_url': 'http://www.1kkk.com/ch53-525733/', 'title': '第53话 B06-32③'}, {'chapter_url': 'http://www.1kkk.com/ch54-527909/', 'title': '第54话 B06-32④'}, {'chapter_url': 'http://www.1kkk.com/ch55-540686/', 'title': '第55话 B06-32⑤'}, {'chapter_url': 'http://www.1kkk.com/ch56-542516/', 'title': '第56话 交易①'}, {'chapter_url': 'http://www.1kkk.com/ch57-544193/', 'title': '第57话 交易②'}, {'chapter_url': 'http://www.1kkk.com/ch58-545650/', 'title': '第58话 判断'}, {'chapter_url': 'http://www.1kkk.com/ch59-547841/', 'title': '第59话 任你挑选'}, {'chapter_url': 'http://www.1kkk.com/ch60-551884/', 'title': '第60话 金色池塘'}, {'chapter_url': 'http://www.1kkk.com/ch61-552877/', 'title': '第61话 活下去看看呀'}, {'chapter_url': 'http://www.1kkk.com/ch62-558935/', 'title': '第62话 不死之身的怪物'}, {'chapter_url': 'http://www.1kkk.com/ch63-559580/', 'title': '第63话 HELP'}, {'chapter_url': 'http://www.1kkk.com/ch64-559739/', 'title': '第64话 如果是我的话'}, {'chapter_url': 'http://www.1kkk.com/ch65-560418/', 'title': '第65话 SECRET.GARDEN'}, {'chapter_url': 'http://www.1kkk.com/ch66-563262/', 'title': '第66话 被禁止的游戏①'}, {'chapter_url': 'http://www.1kkk.com/ch67-563263/', 'title': '第67话 被禁止的游戏②'}, {'chapter_url': 'http://www.1kkk.com/ch68-566491/', 'title': '第68话 就是这么回事'}, {'chapter_url': 'http://www.1kkk.com/ch69-567669/', 'title': '第69话 想让你见的人'}, {'chapter_url': 'http://www.1kkk.com/ch70-573812/', 'title': '第70话 试看版'}, {'chapter_url': 'http://www.1kkk.com/ch71-573813/', 'title': '第71话 试看版'}, {'chapter_url': 'http://www.1kkk.com/ch72-575487/', 'title': '第72话 试看版'}, {'chapter_url': 'http://www.1kkk.com/ch73-626152/', 'title': '第73话 顽起'}, {'chapter_url': 'http://www.1kkk.com/ch74-629319/', 'title': '第74话 特别的孩子'}, {'chapter_url': 'http://www.1kkk.com/ch75-629320/', 'title': '第75话 倔强的华丽'}, {'chapter_url': 'http://www.1kkk.com/ch76-629321/', 'title': '第76话 开战'}, {'chapter_url': 'http://www.1kkk.com/ch77-629322/', 'title': '第77话 无知的杂鱼们'}, {'chapter_url': 'http://www.1kkk.com/ch78-629323/', 'title': '第78话 新解决一双'}, {'chapter_url': 'http://www.1kkk.com/ch79-629324/', 'title': '第79话 一箭必定'}, {'chapter_url': 'http://www.1kkk.com/ch80-629219/', 'title': '第80话 来玩游戏吧,大公!'}, {'chapter_url': 'http://www.1kkk.com/ch81-633406/', 'title': '第81话 死守'}, {'chapter_url': 'http://www.1kkk.com/ch82-633407/', 'title': '第82话 猎场的主人'}, {'chapter_url': 'http://www.1kkk.com/ch83-633409/', 'title': '第83话 穿越13年的答复'}, {'chapter_url': 'http://www.1kkk.com/ch84-633410/', 'title': '第84话 停'}, {'chapter_url': 'http://www.1kkk.com/ch85-633411/', 'title': '第85话 怎么办'}, {'chapter_url': 'http://www.1kkk.com/ch86-633290/', 'title': '第86话 战力'}, {'chapter_url': 'http://www.1kkk.com/ch87-633867/', 'title': '第87话 境界'}, {'chapter_url': 'http://www.1kkk.com/ch88-708386/', 'title': '第88话 一雪前耻'}, {'chapter_url': 'http://www.1kkk.com/ch89-709622/', 'title': '第89话 汇合'}, {'chapter_url': 'http://www.1kkk.com/ch90-710879/', 'title': '第90话 赢吧'}, {'chapter_url': 'http://www.1kkk.com/ch91-711639/', 'title': '第91话 把一切都'}, {'chapter_url': 'http://www.1kkk.com/ch92-715647/', 'title': '第92话'}, {'chapter_url': 'http://www.1kkk.com/ch93-720622/', 'title': '第93话 了断'}, {'chapter_url': 'http://www.1kkk.com/ch94-739797/', 'title': '第94话 大家活下去'}, {'chapter_url': 'http://www.1kkk.com/ch95-750533/', 'title': '第95话 回去吧'}, {'chapter_url': 'http://www.1kkk.com/ch96-754954/', 'title': '第96话 欢迎回来'}, {'chapter_url': 'http://www.1kkk.com/ch97-755431/', 'title': '第97话 所期望的未来'}, {'chapter_url': 'http://www.1kkk.com/ch98-758827/', 'title': '第98话 开始的声音'}, {'chapter_url': 'http://www.1kkk.com/ch99-764478/', 'title': '第99话 Khacitidala'}, {'chapter_url': 'http://www.1kkk.com/ch100-769132/', 'title': '第100话 到达'}, {'chapter_url': 'http://www.1kkk.com/ch101-774024/', 'title': '第101话 过来吧'}, {'chapter_url': 'http://www.1kkk.com/ch102-776372/', 'title': '第102话 找到寺庙!'}, {'chapter_url': 'http://www.1kkk.com/ch103-778911/', 'title': '第103话 差一步'}]
-
selenium模拟浏览器获取漫画图片链接
定义一个从章节内获取每页图片信息的函数,其接受参数为函数get_chapter_list返回值列表中的字典。
经过上面的分析,我们已确定该处要采用selenium进行图片链接获取,因此,在函数定义之前,还需要初始化selenium,并设置不加载图片,不开启可视化的选项,提高效率。
在此之前,你除了pip安装好所需模块外,还需要安装对应谷歌浏览器版本的chromedriver,64位向下兼容,所以下载32位的是没问题的。下载地址http://chromedriver.storage.googleapis.com/index.html。
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_argument('blink-settings=imagesEnabled=false') # 不加载图片
chrome_options.add_argument('--headless') # 不开启可视化
browser = webdriver.Chrome(options=chrome_options)
为增加爬取效率,我的当前考虑时不在获取图片链接信息后直接下载图片,而是持久化存入数据库保存,随时可以再次下载,不用同一部漫画每次都采用selenium从头获取图片链接。
因此,这里我用到了Mongodb数据库,同样,使用前需要先初始化数据库,我们将要下载的漫漫画名用一个变量来表示:
import pymongo
CARTOON_NAME = "约定梦幻岛"
client = pymongo.MongoClient("localhost", 27017)
db = client["1kkk_cartoon"]
collection = db[CARTOON_NAME]
初始化selenium和数据库完,下面编写获取漫画页信息的函数:
def get_page(chapter_dic):
chapter_title = chapter_dic.get("title")
chapter_url = chapter_dic.get("chapter_url")
image_info = []
browser.get(chapter_url)
time.sleep(2)
source = browser.page_source
selector = Selector(text=source)
# 获取总页数
total_page = selector.xpath("//div[@id='chapterpager']/a[last()]/text()").get()
print(" ", chapter_name, "--总页数:", total_page)
# 循环点击下一页次数等于总页数
for index in range(1, int(total_page) + 1):
page_source = browser.page_source
selector2 = Selector(text=page_source)
image_url = selector2.xpath("//div[@id='cp_img']/img/@src").get()
# 如网络不稳定,图片信心有丢失,可加如下备注代码,增加等待时常直至获取数据
# while image_url is None:
# time.sleep(1)
# page_source = browser.page_source
# selector2 = Selector(text=page_source)
# image_url = selector2.xpath("//div[@id='cp_img']/img/@src").get()
# 以索引顺序命名图片
f_name = str(index)
# 下一页标签
next_page = browser.find_element_by_xpath("//div[@class='container']/a[contains(text(),'下一页')]")
# 模拟点击下一页
next_page.click()
time.sleep(2)
# 将漫画图片关键信息存入字典,用以后续批量下载
# 重要:此处保存了页面来源的章节链接,因为后续爬取将会知道,此Referer必不可少,否则将会被判定为异常访问,拿不到图片数据。
page_info = {
"chapter_title": chapter_title,
'Referer': chapter_url,
'img_url': image_url,
'img_index': f_name
}
image_info.append(page_info)
print(page_info)
print("-----已下载{},第{}页-----".format(chapter_title, index))
# 存入数据库
collection.insert_one(page_info)
# 其实数据都已经写入数据库了,也可以不用再return,这里return后完整运行代码后可不连接数据库读取图片信息。
return image_info
-
设计多进程运行get_page()函数
上述两个函数get_chapter_list、及get_chapter_list()组合运行后,便能完成爬取所有章节全部漫画页的详情信息并存入数据库中。
为了提高爬取效率,这里我直接用了多进程进程池-multiprocessing.Pool(),有不了解多进程的可以参考我之前的文章或网上了解下,这里不多阐述。
调用get_chapter_list(start_url)函数,得到章节信息返回值, 开启多进程运行get_page(chapter_dic):
if __name__ == '__main__':
# 运行get_chapter_list(start_url) 得到返回章节信息列表
chapter_list = get_chapter_list(start_url)
# 实例化进程池,不传参数将默认以你当前计算机的cpu核心数来创建进程数,比如我的电脑默认为Pool(4)
p = Pool()
for chapter_dic in chapter_list:
# 开启非阻塞式多进程
p.apply_async(get_page,(chapter_dic,)) # 传参那里不要漏了逗号,参数要求必须是元组
p.close()
p.join()
# 关闭浏览器,回收设备资源
browser.close()
这样就得到了所有包含图片URL、对应章节链接:Referer、章节名、章节内漫画顺序索引的字典信息,并同时存进了数据库。
运行输出如下:
-
下载并保存图片
所有图片信息已经获取完成,后续的下载保存逻辑就很简单了,代码逻辑如下:-
从数据库取出图片信息数据,或者直接使用get_page函数的返回值。
-
requests构造请求,须携带Referer,保存图片数据。
在浏览器中直接访问图片链接不能获得正确图片 -
按漫画名创建总文件夹,按章节名创建子文件夹,按索引名命名下载图片并放入对应章节名文件夹内。
-
为提高效率,漫画图片下载同样采用多进程
实现代码如下,此处取漫画信息数据方式采用的从数据库获取:
-
# 传入漫画图片字典信息
def save_img(info_dict):
chapter_title = info_dict.get('chapter_title')
referer = info_dict.get('Referer')
img_url = info_dict.get('img_url')
f_name = info_dict.get('img_index')
# 重新构造请求头,请求头必须加入Referer来源,否则将被反爬拦截无法获取数据
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Referer": referer
}
res = requests.get(img_url, headers=headers)
if res.status_code == 200:
img = res.content
# ./代表当前目录
path1 = "./%s" % CARTOON_NAME
# 判断是否存在文件夹,否则创建新文件夹
if not os.path.exists(path1):
os.makedirs(path1)
print("创建目录文件夹--%s 成功" % CARTOON_NAME)
path2 = "./%s/%s" % (CARTOON_NAME, chapter_title)
if not os.path.exists(path2):
os.makedirs(path2)
print("创建漫画目录文件夹--%s 成功" % chapter_title)
# 保存图片,索名命名
with open("./%s/%s/%s.jpg" % (CARTOON_NAME, chapter_title, f_name), 'wb') as f:
f.write(img)
print("%s--第%s页 保存成功" % (chapter_title, f_name))
else:
print("该页下载失败")
if __name__ == '__main__':
CARTOON_NAME = "贤者之孙"
client = pymongo.MongoClient("localhost", 27017)
db = client["1kkk_cartoon"]
collection = db[CARTOON_NAME]
# 从数据库中取出漫画页信息,并转换为列表
infos = list(collection.find())
p = Pool()
for info in infos:
p.apply_async(save_img, (info,))
p.close()
运行上述下载代码,漫画图片将被快速的下载并结构化的保存下来.这样,漫画下载的主体已经全部完成
我们只需需稍微重构下代码,将所有代码整合在一起即可,整合后,两处多进程方法不变,即:
- 使用多进程将漫画图片信息保存到数据库.
- 储存完成后,自动从数据库读取数据,采用多进程下载漫画图片并结构挂保存.
完整重构整合代码如下:
from scrapy.selector import Selector
import requests
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
import re
from multiprocessing import Pool
import pymongo
import os
# 约定梦幻岛漫画链接
CARTOON_NAME = "约定梦幻岛"
client = pymongo.MongoClient("localhost", 27017)
db = client["1kkk_cartoon"]
collection = db[CARTOON_NAME]
chrome_options = Options()
chrome_options.add_argument('blink-settings=imagesEnabled=false')#不加载图片
chrome_options.add_argument('--headless')#不开启可视化
browser = webdriver.Chrome(options=chrome_options)
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
}
start_url = "http://www.1kkk.com/manhua31328/"
def get_chapter_list(start_url):
res = requests.get(start_url, headers=header)
selector = Selector(text=res.text)
items = selector.xpath("//ul[@id='detail-list-select-1']//li")
# 用于存放所有章节信息
chapter_list = []
for item in items:
# 构造绝对链接
chapter_url = "http://www.1kkk.com" + item.xpath("./a/@href").get()
title = item.xpath("./a/text()").get().rstrip()
# 若上述位置未匹配到标题,则换下面的匹配式
if not title:
title = item.xpath("./a//p/text()").get().rstrip()
dic = {
"title": title,
"chapter_url": chapter_url,
}
chapter_list.append(dic)
# 按章节正需排序
chapter_list.reverse()
total_len = len(chapter_list)
print("\n【总共检索到 {} 个章节信息如下】:\n{}".format(total_len, chapter_list))
return chapter_list
def get_page(chapter_dic):
chapter_title = chapter_dic.get("title")
chapter_url = chapter_dic.get("chapter_url")
image_info = []
browser.get(chapter_url)
time.sleep(2)
source = browser.page_source
selector = Selector(text=source)
# 获取总页数
total_page = selector.xpath("//div[@id='chapterpager']/a[last()]/text()").get()
print(" ", chapter_title, "--总页数:", total_page)
# 循环点击下一页次数等于总页数
for index in range(1, int(total_page) + 1):
page_source = browser.page_source
selector2 = Selector(text=page_source)
image_url = selector2.xpath("//div[@id='cp_img']/img/@src").get()
# 遇到加载缓慢时等待时间加长
# while image_url is None:
# time.sleep(1)
# page_source = browser.page_source
# selector2 = Selector(text=page_source)
# image_url = selector2.xpath("//div[@id='cp_img']/img/@src").get()
# 以索引顺序命名图片
f_name = str(index)
# 下一页标签
next_page = browser.find_element_by_xpath("//div[@class='container']/a[contains(text(),'下一页')]")
# 模拟点击
next_page.click()
time.sleep(2)
# 将漫画图片关键信息存入字典,用需后续批量下载
page_info = {
"chapter_title": chapter_title,
'Referer': chapter_url,
'img_url': image_url,
'img_index': f_name
}
image_info.append(page_info)
print("-----已下载{},第{}页-----".format(chapter_title, index))
# 存入数据库
collection.insert_one(page_info)
return image_info
def save_img(info_dict):
chapter_title = info_dict.get('chapter_title')
referer = info_dict.get('Referer')
img_url = info_dict.get('img_url')
f_name = info_dict.get('img_index')
# 重新构造请求头,请求头必须加入Referer来源,否则将被反爬拦截无法获取数据
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Referer": referer
}
res = requests.get(img_url, headers=headers)
if res.status_code == 200:
img = res.content
# ./代表当前目录
path1 = "./%s" % CARTOON_NAME
# 判断是否存在文件夹,否则创建新文件夹
if not os.path.exists(path1):
os.makedirs(path1)
print("创建目录文件夹--%s 成功" % CARTOON_NAME)
path2 = "./%s/%s" % (CARTOON_NAME, chapter_title)
if not os.path.exists(path2):
os.makedirs(path2)
print("创建漫画目录文件夹--%s 成功" % chapter_title)
# 保存图片,索名命名
with open("./%s/%s/%s.jpg" % (CARTOON_NAME, chapter_title, f_name), 'wb') as f:
f.write(img)
print("%s--第%s页 保存成功" % (chapter_title, f_name))
else:
print("该页下载失败")
def main_info_to_database():
chapter_list = get_chapter_list(start_url)
# 实例化进程池,不传参数将默认以你当前电脑的cpu核心数来创建进程数量,比如我的电脑默认为Pool(4)
p = Pool()
for chapter_dic in chapter_list:
p.apply_async(get_page,(chapter_dic,))
p.close()
p.join()
browser.close()
def main_download_from_database():
collection = db[CARTOON_NAME]
# 从数据库中取出漫画页信息,并转换为列表
infos = list(collection.find())
p = Pool()
for info in infos:
p.apply_async(save_img, (info,))
p.close()
if __name__ == '__main__':
main_info_to_database()
main_download_from_database()
运行结果如下:
漫画名文件夹下载后的目录结构:
章节目录image.png
章节页目录
借助本地阅读软件看起漫画来就很开心了!
这篇爬虫难度不大,但是很多必不可少分析思路,和一些常用爬取手段的使用,如遇到js加载时可用selenium、解析库Selector的使用、多进程库multiprocessing的使用,MongoDB数据库的存取操作等。
当然,不想研究代码的直接拷过去也能使用(前提是库和webdriver都安装好了)。
—————————————————————————————————————————————————————
本文先写到这,好累啊,后续的检索模块、付费漫画、限制级漫画处理我先挖坑,休息了再补上~
自己写个爬虫要不了多久,写文是真费时啊,哭!
如果本文对你有些帮助,请务必点个赞或者收藏下,跪求!!!
内容有不明白的地方或建议欢迎留言交流。
4. 增加检索功能模块
之前的代码只能完成下载给定漫画名和完整漫画链接的情况。
但更好的情况是模拟主页内的检索功能,用户输入漫画名即可打印漫画详情,并完成自动下载。
分析漫画搜索请求:
通过主页内搜索并跟踪链接,很容易就找到搜索请求的链接及参数构成:
搜索信息请求参数构成
试验删掉
language: 1
的参数并未影响数据返回,因此构造参数只需要传递漫画名title
即可最终检索url构成为: http://www.1kkk.com/search?title={}
后续要做的事情仅仅就是构造请求,解析返回数据。
# 检索功能
def search(name):
# 利用传递的参数构造检索链接
search_url = "http://www.1kkk.com/search?title={}".format(name)
print("正在网站上检索您输入的漫画:【{}】,请稍后...".format(name))
res = requests.get(search_url, headers=header)
if res.status_code == 200:
# 解析响应数据,获取需要的漫画信息并打印
selector = Selector(text=res.text)
title = selector.xpath("//div[@class='info']/p[@class='title']/a/text()").get()
link = "http://www.1kkk.com" + selector.xpath("//div[@class='info']/p[@class='title']/a/@href").get()
author = "|".join(selector.xpath("//div[@class='info']/p[@class='subtitle']/a/text()").getall())
types = "|".join(selector.xpath("//div[@class='info']/p[@class='tip']/span[2]/a//text()").getall())
block = selector.xpath("//div[@class='info']/p[@class='tip']/span[1]/span//text()").get()
content = selector.xpath("//div[@class='info']/p[@class='content']/text()").get().strip()
print("【检索完毕】")
print("请确认以下搜索信息是否正确:")
print("-------------------------------------------------------------------------------------------------")
print("漫画名:", title)
print("作者:", author)
print("类型:", types)
print("状态:", block)
print("摘要:", content)
print("-------------------------------------------------------------------------------------------------")
print("漫画【%s】链接为:%s" % (title,link))
# 用户检查检索信息,确认是否继续下载
conf = input("确认下载?Y/N:")
if conf.lower() != "y":
print("正在退出,谢谢使用,再见!")
return None
else:
print("即将为您下载:%s" % title)
# 返回该漫画链接
return link
else:
print("访问出现错误")
单独运行效果检查:
serch("约定梦幻岛")
输出如下:
正在网站上检索您输入的漫画:【约定梦幻岛】,请稍后...
【检索完毕】
请确认以下搜索信息是否正确:
-------------------------------------------------------------------------------------------------
漫画名: 约定的梦幻岛
作者: 白井カイウ|出水ぽすか
类型: 冒险|科幻|悬疑
状态: 连载中
摘要: 约定的梦幻岛漫画 ,妈妈说外面的世界好可怕,我不信;
但是那一天、我深深地体会到了妈妈说的是真的!
因为不仅外面的世界、就连妈妈也好可怕……
-------------------------------------------------------------------------------------------------
漫画【约定的梦幻岛】链接为:http://www.1kkk.com/manhua31328/
确认下载?Y/N:
输入y或者Y都将正确return漫画的链接,达到预期要求。
现只需将之前代码中的start_url由指定链接变更为该函数即可,即:
将start_url = "http://www.1kkk.com/manhua31328/"
替换为:start_url = search(CARTOON_NAME)
当需要下载漫画时,只需改变参数CARTOON_NAME即可,后续的检索下载、目录命名、数据库表名称都不用操心,将会自动完成更改创建。
到此,基本的检索模块也完成了。
坑位二:付费漫画处理
pass
坑位三: 限制级漫画处理
pass
网友评论