Python爬虫实战:自动化漫画检索及下载-实现本地阅读(干货满满带分析思路及源码)

更新日志:
2019.4.28 更新检索模块

自从追完约定梦幻岛就念念不忘,想着追下漫画,可是,电脑上看太不方便,手机一看,广告太多而且翻页什么的都太不方便了,于是乎,就有了今天的爬虫实战了。
我这次爬取的漫画目标网站为: http://www.1kkk.com/

1.写在前面

求点赞,求点赞,求点赞~(小声)
(觉得啰嗦可直接跳到正文部分)
(还是觉得啰嗦的可以直接跳到最后整合后完整代码部分~)
漫画下载需求确认:

  1. 用户输入漫画名,程序自动完成检索,打印检索漫画信息,含漫画名、作者、连载情况、摘要等;

  2. 判断漫画是否为付费漫画,并打印提示,选择仅下载免费章节或退出下载;

  3. 判断是否为限制级漫画,打印提示,并自动完成校验进入下一步;

  4. 用户确认信息是否检索准确,是则下载,否则退出;

  5. 能完整下载所有章节所有漫画页高清图片;

  6. 能根据不同章节打包,文件夹漫按漫画章节名命名;

  7. 每章节内漫画页按顺序命名;

  8. 尽可能提升下载效率。

先给大家看下成果,爬取到的漫画结合本地漫画app "Perfect Viewer"观看的效果:

在手机上可实现触屏点击自动翻页、跳章,还能记录当前看到的位置,可以说很爽了~

本地app内效果
高清漫画图页

写在前面的总结

本爬虫使用到的模块如下:

  • 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打开开发者工具,选择元素,点击漫画图片,自动定位到图片地址源码位置.如图所示:

    开发者工具定位漫画图片链接

  • 确定返回该链接的源网址
    简单的找到漫画图片链接后,需要确定返回该链接的请求网址。
    最简单的情况是图片没有异步加载,链接随主页网址返回,怎么确定它是不是异步加载的呢?
    很简单,1.通过Preview查看渲染后的网页中是否包含漫画图片;2.在Response响应中直接搜索是否包含图片链接.具体示例图如下:

    异步确认

    通过上面的操作,我们得出结论,图片链接是通过异步加载得到的。因此需要找到它的数据来源,经过一段时间的寻找,我,放弃了。没有找到结构化且明确的链接所在,确认是通过JS渲染得到的,最终考虑到并非进行大规模爬取,决定用selenium模拟来完成图片链接获取的工作。这样,获取一页图片链接的步骤就没问题了。

  • 实现章节内翻页获取全部图片链接
    既然已经确定采用seleni模拟浏览器来获取图片链接,那翻页的网页结构分析步骤也省略了,只需获取"下一页"节点,模拟浏览器不断点击下一页操作即可。

    下一页节点

    这样就确定了每一章所有页的图片获取方式.接下来需要做的是获取所有章节的链接.

  • 获取章节链接
    当然,可能会与人想,章节也可以继续用selenium来模拟浏览器翻页点击啊,这样是可以没错,但是......selenium的效率是真的低,能不用就不要用,不然一部漫画的下载时间可能需要很长。
    我们来分析该部漫画主页,其实一下就能看出,获取章节链接和信息是很简单的。

    章节链接结构

    这里只需要构造一个requests请求再解析网页即可获得所有章节的链接及章节名.
    其实到这里,我们就已经可以完成单个指定漫画的爬虫简单版了,为什么叫简单版,因为还有很多判定,很多自动化检索功能未添加进去..

3.编写漫画爬虫简单版

何为简单版?

  1. 没有检索功能,不能自动检索漫画并下载。
  2. 漫画名、漫画主页链接需要手工给定输入。
  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、章节名、章节内漫画顺序索引的字典信息,并同时存进了数据库。
运行输出如下:

获取漫画图片信息
  • 下载并保存图片
    所有图片信息已经获取完成,后续的下载保存逻辑就很简单了,代码逻辑如下:

    1. 从数据库取出图片信息数据,或者直接使用get_page函数的返回值。

    2. requests构造请求,须携带Referer,保存图片数据。


      在浏览器中直接访问图片链接不能获得正确图片
    3. 按漫画名创建总文件夹,按章节名创建子文件夹,按索引名命名下载图片并放入对应章节名文件夹内。

    4. 为提高效率,漫画图片下载同样采用多进程

    实现代码如下,此处取漫画信息数据方式采用的从数据库获取:

# 传入漫画图片字典信息
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()

运行上述下载代码,漫画图片将被快速的下载并结构化的保存下来.这样,漫画下载的主体已经全部完成
我们只需需稍微重构下代码,将所有代码整合在一起即可,整合后,两处多进程方法不变,即:

  1. 使用多进程将漫画图片信息保存到数据库.
  2. 储存完成后,自动从数据库读取数据,采用多进程下载漫画图片并结构挂保存.

完整重构整合代码如下:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342