1. 程式人生 > >Python 抓取「知識星球」內容生成電子書

Python 抓取「知識星球」內容生成電子書

是不是有時星球內容太多刷不過來?是不是想把星球精華內容擼下來做個電子書?

本文就帶你實現用 Python 抓取星球內容,並生產 PDF 電子書。

有需要Python學習資料的小夥伴嗎?小編整理【一套Python資料、原始碼和PDF】,感興趣者可以加小編微信:【mmp9972】,反正閒著也是閒著呢,不如學點東西啦~~

先上效果:

1 過程分析

1.1 模擬登入

這個網站並不是依靠 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",
}

1.2 頁面內容分析

登入成功後,一般我習慣右鍵、檢查或者檢視原始碼。

但是這個頁面比較特殊,它不把內容放到當前位址列 URL 下,而是通過非同步載入(XHR),只要找對介面就可以了。

這個介面是最新 20 條資料的,還有後面數據對應不同介面,這就是後面要說的翻頁。

1.3 製作 PDF 電子書

需安裝模組:

  1. wkhtmltopdf,用作匯出 PDF,安裝完成後可用命令生成 PDF,例如:wkhtmltopdf http://www.google.com.hk google.pdf
  2. pdfkit,是 python 對 wkhtmltopdf 呼叫的封裝,支援 URL、本地檔案、文字內容到 PDF 的轉換,實際轉換還是最終呼叫 wkhtmltopdf 命令

本來精華區是沒有標題的,我把每個問題的前 6 個字元當做標題,以此區分不同問題。

1.4 爬取圖片

很明顯,在返回的資料中的 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)

1.5 製作精美 PDF

通過 css 樣式來控制字型大小、佈局、顏色等,詳見 test.css 檔案。

再將此檔案引入到 options 欄位中。

options = {
    "user-style-sheet": "test.css",
    ...
    }

2 難點分析

2.1 翻頁邏輯

爬取地址是:{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 我沒找到直接處理的辦法,只能這樣曲線救國了。

2.2 判斷最後一頁

翻頁到最後返回的資料是:

{"succeeded":true,"resp_data":{"topics":[]}}

故以 next_page = rsp.json().get('resp_data').get('topics') 來判斷是否有下一頁。

2.3 評論爬取

發現評論裡也有很多有用的內容,評論的格式如下:

{
    "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>'

最終評論展示效果:

3 優化

3.1 中文問題

生成 PDF 後,打開發現中文全部顯示為方框,如下圖:

這表示伺服器未安裝中文字型,安裝字型即可,安裝下面說明;

1. 檢視目前安裝字型:fc-list
2. 下載所需字型,例如 fangsong_GB2312.ttf
3. mkdir /usr/share/fonts/zh_CN
4. cp fangsong_GB2312.ttf /usr/share/fonts/zh_CN
5. 執行fc-cache -fv
6. 檢視是否安裝成功:fc-list,檢視是已安裝

重新生成後,一切 OK:

3.2 頁面空白優化

生成成功,但是返現每篇文章預設都是新的一頁,出現大塊空白,閱讀時非常彆扭,於是我想著是否可以進行優化。

之所以是一篇星球推文顯示成單獨的一頁,是因為原作者處理時,是將推文分別儲存成 HTML 檔案進行處理,然後再轉換成 PDF。

那麼我的思路就是,將每篇推文放在單獨的 <body> 裡,最後合併成一個 HTML,這樣最終顯示的就是連續的頁面了。

修改前:

修改後:

3.3 超連結優化

在原作者的程式碼中,沒有對超連結進行處理,而星球中有大部分都是進行超連結的分享。

沒有處理的原因是,抓取到的程式碼中,超連結是這種形式的:

<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 裡,這裡我就不搞了,有興趣的嘗試下吧。

3.4 換行處理

換行在星球上顯示是這樣的:

但是到 PDF 後,換行全部沒了,大段文字看起來很費勁:

這個處理起來就比較簡單了,只要將返回資料中的 \n 替換為 HTML 中的換行標籤 <br> 即可:

例如獲取精華正文時:

text = handle_link(unquote(content.get('text', '').replace("\n", "<br>")))

需要注意的是,需要在解碼之前進行替換。

效果:

至此,基本需求已完成。