美文网首页Python学习资料整理DD收藏文章
Python爬虫实战:自动化漫画检索及下载-实现本地阅读(干货满

Python爬虫实战:自动化漫画检索及下载-实现本地阅读(干货满

作者: 越大大雨天 | 来源:发表于2019-04-26 20:05 被阅读112次

    更新日志:
    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()函数.
    • 漫画图片链接使用了异步加载,不能直接在主页获取.
    • 请求图片链接必须携带对应章节referer信息才有返回数据.
    • 部分漫画缺少资源,需增加判定.
    • 部分漫画为付费漫画,需增加判定.
    • 部分漫画为限制级漫画,需模拟点击验证才能返回数据.
    • 需分章节创建目录,并判定目录是否存在.
    • 漫画图片需按顺序命名.

    正文开始:

    2. 目标网页结构分析(思路分析,具体代码下一章)

    爬虫编写建议逆向分析网页,即:从自己最终需求所在的数据网页开始,分析网页加载形式, 请求类型, 参数构造 ,再逆向逐步推导出构造参数的来源.分析完成后再从第一个网页出发, 以获取构造参数为目的,逐步请求得到参数,构造出最终的数据页链接并获取所需数据.
    因此,我这次的爬取先从漫画图片所在页开始分析.

    • 找到漫画图片链接
      随意选择一部漫画进入任意一页,这里还是以<<约定梦幻岛>>为例吧,我随便点击进了第85话:
      http://www.1kkk.com/ch103-778911/#ipg1
      常规操作,首先使用谷歌浏览器,按F12打开开发者工具,选择元素,点击漫画图片,自动定位到图片地址源码位置.如图所示:

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

      异步确认
      通过上面的操作,我们得出结论,图片链接时=是通过异步加载得到的。因此需要找到它的数据来源,经过一段时间的寻找,我,放弃了。没有找到结构化且明确的链接所在,最终考虑到并非进行大规模爬取,决定用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

    相关文章

      网友评论

        本文标题:Python爬虫实战:自动化漫画检索及下载-实现本地阅读(干货满

        本文链接:https://www.haomeiwen.com/subject/dzqegqtx.html