文末附有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很簡單,由於不一樣的gitbook page使用到的css文件都是同樣的,能夠複製下來保存到本地的文件,以後從文件中讀取就行。java
具體內容見:github.com/fuergaosi23…python
須要的html是其中的正文部分,經過頁面源代碼分析可知,這部分是被<section class='normal markdown-section'></section>
包裹住的,這能夠很容易得使用bs4或者lxml等工具提取出來。git
知道了怎麼獲取一個頁面的內容,接下來要作的就是獲取全部章節頁面的連接,這部份內容就在左邊的側邊欄。github
由頁面源代碼分析,可知這些章節都是一個帶有header或chapter的li標籤,這也能夠經過簡易的腳本抓取。markdown
獲取了全部章節連接以後,就能夠爬取各個頁面得正文內容了,而後組裝起來。session
這部分很簡單,上面提到過,就不贅述了。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()
複製代碼
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.com/fuergaosi23…