由好奇心日報備份計劃教大家備份網站 技术

大家好,我是 习猪习 a.k.a. 新品蔥 和 Github 上的 PincongBot。有論壇用戶邀請我搶救性備份 Qdaily 好奇心日報,因此就趁這個機會,淺談一下我一直在做的網站備份,同時手把手地教大家寫一點代碼。

PincongBot 這個名字來自於小二使用的 TerminusBot

我寫這篇文章的原意是作為自己的日常筆記,但是希望這能夠成為為 2047 論壇引流的優質內容。

我先假設讀者們不懂技術,因此我會嘗試使用儘可能直白的語言和代碼為大家解釋。


和一些人想象的不同,我在備份網站時優先考慮使用的不是爬蟲,而是會尋找是否有 API 可供使用,因為 API 往往可以提供比網頁更多的信息。

如何找到需要的 API ?

首先,搜尋 API 文檔。如果網站願意開放 API,都會提供詳細的 API 文檔。

如果沒有,則尋找頁面發出的異步(和頁面自身不是同時加載)网络请求。

XHR,或者現代化的 fetch API 請求

http://www.qdaily.com/articles/65148.html 這一個頁面為例。按 開啟開發者工具中的網路監視器,點選工具條中的 XHR ,以過濾出 XHR 請求。

將頁面下拉以加載 lazy-loading 的剩餘內容,可以看到幾個 XHR 請求,其中看起來有用的是 http://www.qdaily.com/comments/article/65148/0.json。其中 65148 可以知道是 article id ,0 的意義目前還未知。

後面知道這個是 :key 分頁索引 時間戳,第一頁為 0 ,下一頁的索引為返回結果中的 last_key 字段

通過同樣的方法我還找到了下列 API
http://www.qdaily.com/homes/articlemore/:key.json
http://www.qdaily.com/tags/tagmore/:tag_id/:key.json
http://www.qdaily.com/comments/paper/:paper_id/:key.json
http://www.qdaily.com/labs/papermore/:key.json

吐槽一下,都 2020 年了還沒有部署 HTTPS

行動端 APP 往往會使用 API 獲取數據,因此讓我們來解包 Android apk

通過解包 Android apk 檔案(如何解包 apk 就是另外一個話題了,挖坑待填),檢索代碼中的關鍵字,如 api,真的有。

發現的 API 列表(有些是不能使用的)

前面都要加上 http://app3.qdaily.com/

app3/articles/detail/%s.json
app3/articles/info/%s.json
app3/authors/index/%s/%s.json
app3/boot_advertisements.json
app3/categories/index/%s/%s.json
app3/column_ads/info/%s.json
app3/columns/all_columns_index/%s.json
app3/columns/article_in_all_columns/%s.json
app3/columns/index/%s/%s.json
app3/columns/info/%s.json
app3/comments/create_comment
app3/devices/android
app3/feedbacks
app3/homes/index/%s.json
app3/homes/left_sidebar.json
app3/options/mob_create_option
app3/options/mob_create_praise
app3/paper/choices
app3/paper/choices/%s
app3/paper/choices/result/%s
app3/paper/tot_results/%s
app3/paper/tots
app3/paper/tots/%s
app3/paper/whos
app3/paper/whos/%s
app3/paper/whos/result/%s
app3/papers/detail/%s.json
app3/papers/done
app3/papers/index/%s.json
app3/praises/create_praise
app3/radars/index/%s/%s.json
app3/read_the_statistics
app3/reads
app3/searches/post_list
app3/shares
app3/subscribes/create_subscribe
app3/subscribes/remove_subscribe
app3/tags/index/%s/%s.json
app3/user_feedbacks
app3/users/center
app3/users/comment_on_my
app3/users/find_password
app3/users/my_comments
app3/users/my_praises
app3/users/my_subscription
app3/users/paper_detail?paper_id=%s
app3/users/papers
app3/users/praise_on_my
app3/users/praises
app3/users/profiles/message_number.json?uuid=%s
app3/users/radar
app3/users/scan_history
app3/users/setting
app3/users/sync_to_phone_praises
app3/users/sync_to_server_praises
app3/users/system_message_on_my
app3/users/tourist_praises
app3/users/update_my_personal_information

嘗試其中一個,如 http://app3.qdaily.com/app3/authors/index/589058/0.json 。WTF! 這就把行動端的 API 全部暴露給我們了。

這就是在生產環境將環境變量 (在這裡是 PASSENGER_APP_ENV) 設為 development 忘記改回來的後果。在生產環境中開發調試是一個不好的習慣。

暴露的 API 中能找到一些對我們有用的。

使用 API 備份

準備

  1. 配置 Python3 環境
  2. 安裝依賴
pip3 install requests
  1. 新建 main.py 文件
  2. 編輯 main.py 文件

代碼中使用到的 request_json, format_user_info, format_category_info, format_article_info, format_comment_info, format_child_comment, write_json 等函數,需要根據需求和實際情況自行實現

備份文章

使用的 API 為 /wxapp/articles/info/:id

為什麼要用這個而不是 /app3/articles/info/:id? 因為它一次性提供的信息最全

定義 URL 模板

BASE_URL = "http://app3.qdaily.com"
ARTICLE_URL_TPL = BASE_URL + "/wxapp/articles/info/{id}"

定義函數 backup_article, 參數為 article_id

def backup_article(article_id):
    # 用實際的 article id 替換 URL 模板中的 `:id`
    url = ARTICLE_URL_TPL.format(id=article_id)

    # API 請求
    json = request_json(url)
    if json is None:
        return

    post = json['response']['post']

    # 格式化作者信息
    author = format_user_info(json['response']['author'])

    # 格式化分類信息
    category = format_category_info(post['category'])

    # 格式化文章數據,只保留我們想要的數據
    article = format_article_info(post, author['id'])

    ###
    # 寫入文章數據
    ###
    write_json(article)

    ###
    # 寫入作者信息
    ###
    write_json(author)

    ###
    # 寫入分類信息
    ###
    write_json(category)

恰巧文章的 id 是遞增的整數。我們用暴力方法,備份所有文章(因為一些文章根本就不會出現在文章索引中)

我們需要從 article id 為 1 開始嘗試備份,一直到目前最新的 65270

因此寫一個 for range 循環

for article_id in range(1, 65270+1):
    backup_article(article_id)

備份文章評論

使用的 API 為 /wxapp/comments/index/:comment_type/:id/:last_key,其中 :last_key 是分頁索引,第一頁為 0 ,下一頁的索引為返回結果中的 last_key 字段

定義 URL 模板

COMMENT_URL_TPL = BASE_URL + "/wxapp/comments/index/{comment_type}/{id}/{last_key}"

定義函數 backup_comments, 參數為 parent (backup_article 中的 article 文章數據)

def backup_comments(parent):
    last_key = 0  # 分頁索引第一頁為 0

    # 不斷備份下一頁,直到跳出循環
    while True:
        url = COMMENT_URL_TPL.format(
            comment_type=parent['type'], id=parent['id'],
            last_key=last_key
        )

        json = request_json(url)
        comments = json['response']['comments']

        # 處理這一頁上的每一個評論
        for c in comments:
            # 格式化作者信息
            author = format_user_info(c['author'])
            # 格式化評論數據
            comment = format_comment_info(c)
            # 寫入作者信息
            write_json(author)
            # 寫入評論數據
            write_json(comment)
            # 備份子評論
            for cc in c['child_comments']:
                # 寫入子評論數據
                write_json(format_child_comment(cc))
                # 寫入子評論的作者信息
                write_json(format_user_info(cc['author']))

        # 有更多評論 (分頁)
        if json['response']['has_more']:
            # 下一頁的分頁索引為返回結果中的 last_key 字段
            last_key = int(json['response']['last_key'])
            # 備份下一頁
            continue
        else:
            # 沒有更多頁了
            # 跳出循環
            break

backup_article 函數的最後加上

    if article['comment_count'] > 0:
        backup_comments(article)

備份圖片

由於圖片都儲存在好奇心日報主站的服務器上,如果主站挂了,圖片就全部丟失了。

這是小二曾提出的問題

其中一種解決方案是,在格式化文章數據時,使用 正則表達式 查找正文中的圖片,非阻塞地下載並保存

備份好奇心研究所

TODO



備份的成果在 https://github.com/PincongBot/qdaily ,欢迎 star 和 fork 。

12
10月4日 157 次浏览
4个评论
习猪习 抵抗者运动

@thphd 可以為 code block 加上代碼高亮嗎?

thphd 2047站长

@习猪习 #101681 todo_list++;

共识网 21ccom.net,在archive.org上有比较完整的备份,个人认为有很高的恢复价值。如果能把上面的文章恢复成一个备份站就好了。

消极 (男)消极自由需要积极的个人主义来维护
标记为删除

欲参与讨论,请 登录注册

勉从虎穴暂栖身,说破英雄惊煞人。巧借雷声来掩饰,随机应变信如神。 ——林彪(中国,PRC)