Python爬虫实战:自动化漫画检索及下载-实现本地阅读(干货满满带分析思路及源码)
更新日志:
2019.4.28 更新检索模块自从追完约定梦幻岛就念念不忘,想着追下漫画,可是,电脑上看太不方便,手机一看,广告太多而且翻页什么的都太不方便了,于是乎,就有了今天的爬虫实战了。
我这次爬取的漫画目标网站为: http://www.1kkk.com/1.写在前面
求点赞,求点赞,求点赞~(小声)
(觉得啰嗦可直接跳到正文部分)
(还是觉得啰嗦的可以直接跳到最后整合后完整代码部分~)
漫画下载需求确认:用户输入漫画名,程序自动完成检索,打印检索漫画信息,含漫画名、作者、连载情况、摘要等;
判断漫画是否为付费漫画,并打印提示,选择仅下载免费章节或退出下载;
判断是否为限制级漫画,打印提示,并自动完成校验进入下一步;
用户确认信息是否检索准确,是则下载,否则退出;
能完整下载所有章节所有漫画页高清图片;
能根据不同章节打包,文件夹漫按漫画章节名命名;
每章节内漫画页按顺序命名;
尽可能提升下载效率。
先给大家看下成果,爬取到的漫画结合本地漫画app "Perfect Viewer"观看的效果:
在手机上可实现触屏点击自动翻页、跳章,还能记录当前看到的位置,可以说很爽了~
Selector : scrapy的解析库 selector提取内容的方法,本文使用 getall()
和get()
替代了旧的extract()
和extract_first()
requests:请求库 selenium:浏览器模拟工具 time:时间模块 re:正则表达式 multiprocessing: 多进程 pymongo: mongodb数据库 本爬虫使用的工具如下:
谷歌浏览器 解析插件:xpath helper postman 本爬虫遇到的值得强调的问题如下:
多进程不共享全局变量,不能使用input()函数. 漫画图片链接使用了JS渲染,不能直接在主页获取. 请求图片链接必须携带对应章节referer信息才有返回数据. 部分漫画缺少资源,需增加判定. 部分漫画为付费漫画,需增加判定. 部分漫画为限制级漫画,需模拟点击验证才能返回数据. 需分章节创建目录,并判定目录是否存在. 漫画图片需按顺序命名. 正文开始:
2. 目标网页结构分析(思路分析,具体代码下一章)
爬虫编写建议逆向分析网页,即:从自己最终需求所在的数据网页开始,分析网页加载形式, 请求类型, 参数构造 ,再逆向逐步推导出构造参数的来源.分析完成后再从第一个网页出发, 以获取构造参数为目的,逐步请求得到参数,构造出最终的数据页链接并获取所需数据.
因此,我这次的爬取先从漫画图片所在页开始分析.找到漫画图片链接
随意选择一部漫画进入任意一页,这里还是以<<约定梦幻岛>>为例吧,我随便点击进了第85话:
http://www.1kkk.com/ch103-778911/#ipg1
常规操作,首先使用谷歌浏览器,按F12
打开开发者工具,选择元素,点击漫画图片,自动定位到图片地址源码位置.如图所示:
通过上面的操作,我们得出结论,图片链接是通过异步加载得到的。因此需要找到它的数据来源,经过一段时间的寻找,我,放弃了。没有找到结构化且明确的链接所在,确认是通过JS渲染得到的,最终考虑到并非进行大规模爬取,决定用 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、章节名、章节内漫画顺序索引的字典信息,并同时存进了数据库。
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()
运行结果如下:
借助本地阅读软件看起漫画来就很开心了!
这篇爬虫难度不大,但是很多必不可少分析思路,和一些常用爬取手段的使用,如遇到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即可,后续的检索下载、目录命名、数据库表名称都不用操心,将会自动完成更改创建。
到此,基本的检索模块也完成了。坑位二:付费漫画处理
坑位三: 限制级漫画处理