Python 抓取「知識星球」內容生成電子書
是不是有時星球內容太多刷不過來?是不是想把星球精華內容擼下來做個電子書?
本文就帶你實現用 Python 抓取星球內容,並生產 PDF 電子書。
先上效果:
內容基於 ofollow,noindex" target="_blank">https://github.com/96chh/crawl-zsxq 進行的優化,主要優化內容在於,翻頁時間的處理、大段空白處理、評論抓取、超連結處理等。
涉及到隱私問題,這裡我們以免費星球「萬人學習分享群」為爬取物件。
過程分析
模擬登入
爬取的是網頁版知識星球, https://wx.zsxq.com/dweb/ 。
這個網站並不是依靠 cookie 來判斷你是否登入,而是請求頭中的 Authorization 欄位。
所以,需要把 Authorization,User-Agent 換成你自己的。(注意 User-Agent 也要換成你自己的)
一般來說,星球使用微信掃碼登入後,可以獲取到一個 Authorization,這個歌有效期很長反正,真的很長。
headers = { 'Authorization': 'C08AEDBB-A627-F9F1-1223-7E212B1C9D7D', 'x-request-id': "7b898dff-e40f-578e-6cfd-9687a3a32e49", 'accept': "application/json, text/plain, */*", 'host': "api.zsxq.com", 'connection': "keep-alive", 'referer': "https://wx.zsxq.com/dweb/", 'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", }
頁面內容分析
登入成功後,一般我習慣右鍵、檢查或者檢視原始碼。
但是這個頁面比較特殊,它不把內容放到當前位址列 URL 下,而是通過非同步載入(XHR),只要找對介面就可以了。
精華區的介面: https://api.zsxq.com/v1.10/groups/2421112121/topics?scope=digests&count=20
這個介面是最新 20 條資料的,還有後面數據對應不同介面,這就是後面要說的翻頁。
製作 PDF 電子書
需安裝模組:
wkhtmltopdf http://www.google.com.hk google.pdf
本來精華區是沒有標題的,我把每個問題的前 6 個字元當做標題,以此區分不同問題。
爬取圖片
很明顯,在返回的資料中的 images 鍵就是圖片,只需提取 large 的,即高清的 url 即可。
關鍵在於將圖片標籤 img 插入到 HTML 文件。我使用 BeautifulSoup 操縱 DOM 的方式。
需要注意的是,有可能圖片不止一張,所以需要用 for 迴圈全部迭代出來。
if content.get('images'): soup = BeautifulSoup(html_template, 'html.parser') for img in content.get('images'): url = img.get('large').get('url') img_tag = soup.new_tag('img', src=url) soup.body.append(img_tag) html_img = str(soup) html = html_img.format(title=title, text=text)
製作精美 PDF
通過 css 樣式來控制字型大小、佈局、顏色等,詳見 test.css 檔案。
再將此檔案引入到 options 欄位中。
options = { "user-style-sheet": "test.css", ... }
難點分析
翻頁邏輯
爬取地址是:{url}?scope=digests&count=20&end_time=2018-04-12T15%3A49%3A13.443%2B0800
路徑後面的 end_time
表示載入帖子的最後日期,以此達到翻頁。
這個 end_time
是經過 url 轉義了的,可以通過 urllib.parse.quote 方法進行轉義,關鍵是找出這個 end_time 是從那裡來的。
經過我細細觀察發現:每次請求返回 20 條帖子,最後一條貼子就與下一條連結的 end_time 有關係。
例如最後一條帖子的 create_time 是 2018-01-10T11:49:39.668+0800,那麼下一條連結的 end_time 就是 2018-01-10T11:49:39.667+0800,注意,一個 668,一個 667 , 兩者相差,於是我們便得到了獲取 end_time 的公式:
end_time = create_time[:20]+str(int(create_time[20:23])-1)+create_time[23:]
不過事情沒有那麼簡單,因為上一個 create_time 有可能是 2018-03-06T22%3A29%3A59.000%2B0800, -1
後出現了負數!
由於時分秒都有出現 0 的可能,看來最好的方法是,若出現 000
,則利用時間模組 datetime 獲取 create_time 的上一秒,然後在拼接 999
。
# int -1 後需要進行補 0 處理,test_str.zfill(3) end_time = create_time[:20]+str(int(create_time[20:23])-1).zfill(3)+create_time[23:] # 時間出現整點時需要特殊處理,否則會出現 -1 if create_time[20:23] == '000': temp_time = datetime.datetime.strptime(create_time, "%Y-%m-%dT%H:%M:%S.%f+0800") temp_time += datetime.timedelta(seconds=-1) end_time = temp_time.strftime("%Y-%m-%dT%H:%M:%S") + '.999+0800' end_time = quote(end_time) next_url = start_url + '&end_time=' + end_time
處理過程有點囉嗦,原諒我時間後面的 000
我沒找到直接處理的辦法,只能這樣曲線救國了。
判斷最後一頁
翻頁到最後返回的資料是:
{"succeeded":true,"resp_data":{"topics":[]}}
故以 next_page = rsp.json().get('resp_data').get('topics')
來判斷是否有下一頁。
評論爬取
發現評論裡也有很多有用的內容,評論的格式如下:
{ "comment_id": 15118288421852, "create_time": "2018-08-16T16:19:39.216+0800", "owner": { "user_id": 1484141222, "name": "xxx", "alias": "xxx", "avatar_url": "https://images.xxx" }, "text": "他這個資源不做投資才傻", "likes_count": 0, "rewards_count": 0, "repliee": { "user_id": 484552118, "name": "Kiwi", "avatar_url": "https://images.zsxqxxxTRQKsci9Q=" } }
我們主要解析其中的 owner.name
和 text
,其他資訊我們暫時不關心,比如是對誰誰誰的回覆,我們暫時只把評論列舉出來。
# 評論解析 comments = topic.get('show_comments') comments_str = '' if comments: for comment in comments: comments_str += comment.get('owner').get('name') + " : " + handle_link(comment.get('text')) comments_str += '<br><br>'
最終評論展示效果:
優化
中文問題
生成 PDF 後,打開發現中文全部顯示為方框,如下圖:
這表示伺服器未安裝中文字型,安裝字型即可,安裝下面說明;
- 檢視目前安裝字型:fc-list
- 下載所需字型,例如 fangsong_GB2312.ttf
- mkdir /usr/share/fonts/zh_CN
- cp fangsong_GB2312.ttf /usr/share/fonts/zh_CN
- 執行fc-cache -fv
- 檢視是否安裝成功:fc-list,檢視是已安裝
重新生成後,一切 OK:
頁面空白優化
生成成功,但是返現每篇文章預設都是新的一頁,出現大塊空白,閱讀時非常彆扭,於是我想著是否可以進行優化。
之所以是一篇星球推文顯示成單獨的一頁,是因為原作者處理時,是將推文分別儲存成 HTML 檔案進行處理,然後再轉換成 PDF。
那麼我的思路就是,將每篇推文放在單獨的 <body>
裡,最後合併成一個 HTML,這樣最終顯示的就是連續的頁面了。
修改前:
修改後:
超連結優化
在原作者的程式碼中,沒有對超連結進行處理,而星球中有大部分都是進行超連結的分享。
沒有處理的原因是,抓取到的程式碼中,超連結是這種形式的:
<e type="web" href="https%3A%2F%2Fmp.weixin.qq.com%2Fs%2Fw8geobayB8sIRWYcxmvCSQ" title="5000%E5%AD%97%E9%95%BF%E6%96%87%E5%91%8A%E8%AF%89%E4%BD%A0%EF%BC%8CSEO%E6%AF%8F%E6%97%A5%E6%B5%81%E9%87%8F%E5%A6%82%E4%BD%95%E4%BB%8E0%E5%88%B010000%2B" cache="http%3A%2F%2Fcache.zsxq.com%2F201808%2F732760494981a6500d8aadf0469efbf205c21d23ca472826f13e127799973455"/>
發現是用 <e>
標籤包圍的,這不是 HTML 原生標籤,所以導致無法解析,進而頁面也無法展示,我們要做的就是從中解析出超連結內容,並拼接成 HTML 中的超連結。
另外,發現超連結的 URL 是轉碼後的內容,我們也需要對其進行處理。
這部分的處理程式碼如下:
# 對文字進行 URL 解碼,要不後面解析處理也無法點選 def handle_link(text): # 星球中用 <e> 表示超連結 # 這裡我們進行轉換 soup = BeautifulSoup(text,"html.parser") links = soup.find_all('e', attrs={'type':'web'}) if len(links): for link in links: title = link.attrs['title'] href = link.attrs['href'] s = '<a href={}>{} </a>'.format(href,title) text += s # 清理原文中的 <e> 標籤 # 這裡不清理也沒問題,HTML 解析不了,頁面也不會顯示 # 但是,強迫症。 text = re.sub(r'<e[^>]*>', '', text).strip() return text
處理後:
當然,最好的方式是把超連結的內容也爬取出來,一併放在 PDF 裡,這裡我就不搞了,有興趣的嘗試下吧。
換行處理
換行在星球上顯示是這樣的:
但是到 PDF 後,換行全部沒了,大段文字看起來很費勁:
這個處理起來就比較簡單了,只要將返回資料中的 \n
替換為 HTML 中的換行標籤 <br>
即可:
例如獲取精華正文時:
text = handle_link(unquote(content.get('text', '').replace("\n", "<br>")))
需要注意的是,需要在解碼之前進行替換。
效果:
至此,基本需求已完成。
完整程式碼關注公眾號後回覆【星球】即可獲取。
如果覺得有用,歡迎關注我的微信,一起學習,共同進步,不定期推出贈書活動~
最近蒐集到傳智播客 2018 最新 Python 和 Java 教程!關注本公眾號,後臺回覆「2018」即可獲取下載地址。