【实战】使用asyncio爬取gitbook内容输出pdf

文末附有github源码连接~css

梳理一下流程

用到HTML+css转pdf是 weasyprint.readthedocs.io/en/stable/i…html

def output_pdf(html_text,css_text):
	html = weasyprint.HTML(string=html_text)
	css = weasyprint.CSS(string=css_text)
	html.write_pdf(fname, stylesheets=[css])
复制代码

因此咱们须要作的,就是获取css文件和html源代码,而后传入output_pdf这个函数就好了。html5

获取css

css很简单,由于不一样的gitbook page使用到的css文件都是同样的,能够复制下来保存到本地的文件,以后从文件中读取就行。java

具体内容见:github.com/fuergaosi23…python

获取html

须要的html是其中的正文部分,经过页面源代码分析可知,这部分是被<section class='normal markdown-section'></section>包裹住的,这能够很容易得使用bs4或者lxml等工具提取出来。git

知道了怎么获取一个页面的内容,接下来要作的就是获取全部章节页面的连接,这部份内容就在左边的侧边栏。github

由页面源代码分析,可知这些章节都是一个带有header或chapter的li标签,这也能够经过简易的脚本抓取。markdown

获取了全部章节连接以后,就能够爬取各个页面得正文内容了,而后组装起来。session

输出pdf

这部分很简单,上面提到过,就不赘述了。app

开始动手

首先是一个提取单页面正文的函数:

def get_content(index,path):
    ''' return path's html '''
    url = urljoin(BASE_URL, path)
    content = requests.get(url,headers=headers).text
    tree = etree.HTML(content)
    context = tree.xpath('//section[@class="normal markdown-section"]')[0]
    context.remove(context.find('footer'))
    text = etree.tostring(context).decode()
    return text
复制代码

获取章节连接的函数:

def collect_toc(self, start_utocrl):
    text = requests.get(start_url, headers=self.headers).text
    soup = BeautifulSoup(text, 'html.parser')
    lis = ET.HTML(text).xpath("//ul[@class='summary']//li")
    for li in lis:
        element_class = li.attrib.get('class')
    
        if not element_class:
            continue
        if 'header' in element_class:
            title = self.titleparse(li)
            data_level = li.attrib.get('data-level')
            level = len(data_level.split('.')) if data_level else 1
            content_urls.append({
                'url': "",
                'level': level,
                'title': title
            })
        elif "chapter" in element_class:
            data_level = li.attrib.get('data-level')
            level = len(data_level.split('.'))
            if 'data-path' in li.attrib:
                data_path = li.attrib.get('data-path')
                url = urljoin(self.start_url, data_path)
                title = self.titleparse(li)
                if url not in found_urls:
                    content_urls.append(
                        {
                            'url': url,
                            'level': level,
                            'title': title
                        }
                    )
                    found_urls.append(url)
    
            # Unclickable link
            else:
                title = self.titleparse(li)
                content_urls.append({
                    'url': "",
                    'level': level,
                    'title': title
                })

复制代码

一个gitbook page的章节可能会不少,若是是经过循环一个一个爬的话,那效率过低了,这里咱们使用python3.6的新feature asyncio来进行异步抓取。

示例代码以下:

这里还要注意一点,requests自己是block的,要使用asyncio,还须要对对这部分进行一下处理。这里用的是aiohttp。

async def request(url, headers, timeout=None): async with aiohttp.ClientSession() as session: async with session.get(url, headers=headers, timeout=timeout) as resp: return await resp.text() 复制代码

主函数:

async def main():
    text_tree, content_urls = collect_toc()
    tasks = []
    for index, url in enumerate(content_urls):
        tasks.append(
            get_content(index, url)
        )
    await asyncio.gather(*tasks)
    print("crawl : all done!")

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()
复制代码

其余一些细节

  • 如何生成pdf目录

Weasyprint默认是将h1-h6标签和目录锚点进行对应的,这个和咱们的需求不符。

咱们想要的目录结构是要和gitbook page左边目录栏一致。在研究了一阵源码以后,咱们用monkey patch(猴子补丁的方式)将这部份内容改了一下。

def local_ua_stylesheets(self):
    return [weasyprint.CSS('./html5_ua.css')]
weasyprint.HTML._ua_stylesheets = local_ua_stylesheets
复制代码

这个html5_ua.css的内容在文末给出的github地址里面有。

  • 如何让爬到的内容有序?

这个项目和普通的爬虫有点不同的地方,那就是最终生成的html是要和章节内容顺序一致的。若是是经过一个for循环的话,这个很容易解决。用到asyncio的话,就要相对复杂不少。 这里咱们的解决方案是先获取全部的url列表,而后生成一个同样长度的全局变量CONTENT_LIST列表

for index, url in enumerate(content_urls):
        tasks.append(
            get_content(index, url)
        )
复制代码

经过enumerate函数,咱们遍历的同时获取这个url对应的索引,将这个索引信息传入到get_content函数,这个函数再也不返回值,而是把数据写入到全局变量CONTENT_LIST相应的index位置上去。

  • 调整代码结构

全局变量的处理是不太好的,一个每次只运行一次的脚本却是问题不大,若是要作为一个module给其余程序调用的话,这个全局变量会代码不少问题。因此咱们抽象成了一个类,改为在__init__里面初始化这个列表。

github项目地址

想直接取工具的小伙伴点这里:github.com/fuergaosi23…

相关文章
相关标签/搜索