【實戰】使用asyncio爬取gitbook內容輸出pdf

liaochangjiang發表於2019-03-18

文末附有github原始碼連結~

梳理一下流程

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

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這個函式就行了。

獲取css

css很簡單,因為不同的gitbook page使用到的css檔案都是一樣的,可以複製下來儲存到本地的檔案,之後從檔案中讀取就行。

【實戰】使用asyncio爬取gitbook內容輸出pdf

具體內容見:github.com/fuergaosi23…

獲取html

【實戰】使用asyncio爬取gitbook內容輸出pdf

需要的html是其中的正文部分,通過頁面原始碼分析可知,這部分是被<section class='normal markdown-section'></section>包裹住的,這可以很容易得使用bs4或者lxml等工具提取出來。

知道了怎麼獲取一個頁面的內容,接下來要做的就是獲取所有章節頁面的連結,這部分內容就在左邊的側邊欄。

【實戰】使用asyncio爬取gitbook內容輸出pdf

由頁面原始碼分析,可知這些章節都是一個帶有header或chapter的li標籤,這也可以通過簡易的指令碼抓取。

獲取了所有章節連結之後,就可以爬取各個頁面得正文內容了,然後組裝起來。

輸出pdf

這部分很簡單,上面提到過,就不贅述了。

開始動手

首先是一個提取單頁面正文的函式:

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…

相關文章