作者简介:孙亖,软件工程师,长期从事企业信息化系统的研发工作,主要擅长后台业务功能的设计开发。
本文来自作者在 GitChat 上分享「如何用 Python 爬取网页制作电子书」主题内容。

使用 Scrapy 抓取电子书

爬虫思路

怎么抓取数据,首先我们要看从哪里取,打开《修真小主播》的页面,如下:

30f897d0c160afaf7f07c3ff7c051e69.png

有个目录页签,点击这个页签可以看见目录,使用浏览器的元素查看工具,我们可以定位到目录和每一章节的相关信息,根据这些信息我们就可以爬取到具体的页面:

7b79ce456553610f0cfb825352158938.png

获取章节地址

现在我们打开 xzxzb.py 文件,就是我们刚刚创建的爬虫:

# -*- coding: utf-8 -*-
import scrapy
class XzxzbSpider(scrapy.Spider):
    name = 'xzxzb'
    allowed_domains = ['qidian.com']
    start_urls = ['http://qidian.com/']
    def parse(self, response):
        pass

start_urls 就是目录地址,爬虫会自动爬这个地址,然后结果就在下面的 parse 中处理。现在我们就来编写代码处理目录数据,首先爬取小说的主页,获取目录列表:

   def parse(self, response):
        pages = response.xpath('//div[@id="j-catalogWrap"]//ul[@class="cf"]/li')
        for page in pages:
            url = page.xpath('./child::a/attribute::href').extract()
            print url
        pass

获取网页中的 DOM 数据有两种方式,一种是使用 CSS 选择子,另外一种是使用 XML 的 xPath 查询。

这里我们用 xPath,相关知识请自行学习,看以上代码,首先我们通过 ID 获取目录框,获取类 cf 获取目录列表:

pages = response.xpath('//div[@id="j-catalogWrap"]//ul[@class="cf"]/li')

接着,遍历子节点,并查询 li 标签内 a 子节点的 href 属性,最后打印出来:

for page in pages:
            url = page.xpath('./child::a/attribute::href').extract()
            print url

这样,可以说爬取章节路径的小爬虫就写好了,使用如下命令运行 xzxzb 爬虫查看结果:

scrapy crawl xzxzb

这个时候我们的程序可能会出现如下错误:

…
ImportError: No module named win32api
…

运行下面的语句即可:

pip install pypiwin32

屏幕输出如下:

> ...
> [u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/wrrduN6auIlOBDFlr9quQA2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/Jh-J5usgyW62uJcMpdsVgA2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/5YXHdBvg1ImaGfXRMrUjdw2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/fw5EBeKat-76ItTi_ILQ7A2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/KsFh5VutI6PwrjbX3WA1AA2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/-mpKJ01gPp1p4rPq4Fd4KQ2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/MlZSeYOQxSPM5j8_3RRvhw2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/5TXZqGvLi-3M5j8_3RRvhw2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/sysD-JPiugv4p8iEw--PPw2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/xGckZ01j64-aGfXRMrUjdw2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/72lHOJcgmedOBDFlr9quQA2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/cZkHZEYnPl22uJcMpdsVgA2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/vkNh45O3JsRMs5iq0oQwLQ2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/ge4m8RjJyPH6ItTi_ILQ7A2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/Y33PuxrKT4dp4rPq4Fd4KQ2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/MDQznkrkiyXwrjbX3WA1AA2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/A2r-YTzWCYj6ItTi_ILQ7A2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/Ng9CuONRKei2uJcMpdsVgA2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/Q_AxWAge14pMs5iq0oQwLQ2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/ZJshvAu8TVVp4rPq4Fd4KQ2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/hYD2P4c5UB2aGfXRMrUjdw2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/muxiWf_jpqTgn4SMoDUcDQ2']
[u'//read.qidian.com/chapter/MuRzJqCY6MyoLoerY3WDhg2/OQQ5jbADJjVp4rPq4Fd4KQ2']
> ...

爬取章节路径的小爬虫就写好了,但我们的目的不仅于此,我们接下来使用这些地址来抓取内容:

章节页面分析

我们接下来分析一下章节页面,从章节页面我们要获取标题和内容。

如果说章节信息爬取使用的 parser 方法,那么我们可以给每一个章节内容的爬取写一个方法,比如:parser_chapter,先看看章节页面的具体情况:

fa9b290348e22a7a57e255a7e10e2862.png

可以看到,章节的整个内容在类名为 main-text-wrap 的 div 标签内,标题是其中类名为j_chapterName的 h3 标签,具体内容是类名为read-content j_readContent的 div 标签。

试着把这些内容打印出来:

# -*- coding: utf-8 -*-
import scrapy
class XzxzbSpider(scrapy.Spider):
    name = 'xzxzb'
    allowed_domains = ['qidian.com']
    start_urls = ['https://book.qidian.com/info/1010780117/']
    def parse(self, response):
        pages = response.xpath('//div[@id="j-catalogWrap"]//ul[@class="cf"]/li')
        for page in pages:
            url = page.xpath('./child::a/attribute::href').extract_first()
            # yield scrapy.Request('https:' + url, callback=self.parse_chapter)
            yield response.follow(url, callback=self.parse_chapter)
        pass
    def parse_chapter(self, response):
        title = response.xpath('//div[@class="main-text-wrap"]//h3[@class="j_chapterName"]/text()').extract_first().strip()
        content = response.xpath('//div[@class="main-text-wrap"]//div[@class="read-content j_readContent"]').extract_first().strip()
        print title
        # print content
        pass

上一步,我们获取到了一个章节地址,从输出内容来看是相对路径,因此我们使用了yield response.follow(url, callback=self.parse_chapter),第二个参数是一个回调函数,用来处理章节页面,爬取到章节页面后我们解析页面和标题保存到文件。

next_page = response.urljoin(url)
yield scrapy.Request(next_page, callback=self.parse_chapter)

scrapy.Request 不同于使用 response.follow,需要通过相对路径构造出绝对路径,response.follow 可以直接使用相对路径,因此就不需要调用 urljoin 方法了。

注意,response.follow 直接返回一个 Request 实例,可以直接通过 yield 进行返回。

数据获取了之后是存储,由于我们要的是 html 页面,因此,我们就按标题存储即可,代码如下:

   def parse_chapter(self, response):
        title = response.xpath('//div[@class="main-text-wrap"]//h3[@class="j_chapterName"]/text()').extract_first().strip()
        content = response.xpath('//div[@class="main-text-wrap"]//div[@class="read-content j_readContent"]').extract_first().strip()
        # print title
        # print content
        filename = './down/%s.html' % (title)
        with open(filename, 'wb') as f:
            f.write(content.encode('utf-8'))
        pass

至此,我们已经成功的抓取到了我们的数据,但还不能直接使用,需要整理和优化。

数据整理

首先,我们爬取下来的章节页面排序不是很好,如果人工去排需要太多的时间精力;另外,章节内容包含许多额外的东西,阅读体验不好,我们需要优化内容的排版和可读性。

我们先给章节排个序,因为目录中的章节列表是按顺序排列的,所以只需要给下载页面名称添加一个顺序号就行了。

可是保存网页的代码是回调函数,顺序只是在处理目录的时候能确定,回调函数怎么能知道顺序呢?因此,我们要告诉回调函数它处理章节的顺序号,我们要给回调函数传参,修改后的代码是这样的:

   def parse(self, response):
        pages = response.xpath('//div[@id="j-catalogWrap"]//ul[@class="cf"]/li')
        for page in pages:
            url = page.xpath('./child::a/attribute::href').extract_first()
            idx = page.xpath('./attribute::data-rid').extract_first()
            # yield scrapy.Request('https:' + url, callback=self.parse_chapter)
            req = response.follow(url, callback=self.parse_chapter)
            req.meta['idx'] = idx
            yield req
        pass
    def parse_chapter(self, response):
        idx = response.meta['idx']
        title = response.xpath('//div[@class="main-text-wrap"]//h3[@class="j_chapterName"]/text()').extract_first().strip()
        content = response.xpath('//div[@class="main-text-wrap"]//div[@class="read-content j_readContent"]').extract_first().strip()
        # print title
        # print content
        filename = './down/%s_%s.html' % (idx, title)
        cnt = '<h1>%s</h1> %s' % (title, content)
        with open(filename, 'wb') as f:
            f.write(cnt.encode('utf-8'))
        pass

使用 Sigil 制作电子书

加载 html 文件

要制作 ePub 电子书,我们首先通过 Sigil 把我们的抓取的文件加载到程序中,在添加文件对话框中我们全选所有文件:

55131cdd4c48d5698de0a513b38d5165.png

制作目录

文件中存在 HTML 的 h 标签时,点击生成目录按钮就可以自动生成目录,我们在前面数据抓取时已经自动添加了 h1 标签:

3ff4589047053e78f5b27b421fb4267c.png

制作封面

83ac4a696be088afcea06953512aea29.png

封面本质上也是 HTML,可以编辑,也可以从页面爬取,就留给大家自己实现吧。

366a3d6946ce1f8b1f463acb238b01a3.png

.

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐