大家好,我是 习猪习 a.k.a. 新品蔥 和 Github 上的 PincongBot。有論壇用戶邀請我搶救性備份 Qdaily 好奇心日報,因此就趁這個機會,淺談一下我一直在做的網站備份,同時手把手地教大家寫一點代碼。
PincongBot 這個名字來自於小二使用的 TerminusBot
我寫這篇文章的原意是作為自己的日常筆記,但是希望這能夠成為為 2047 論壇引流的優質內容。
我先假設讀者們不懂技術,因此我會嘗試使用儘可能直白的語言和代碼為大家解釋。
和一些人想象的不同,我在備份網站時優先考慮使用的不是爬蟲,而是會尋找是否有 API 可供使用,因為 API 往往可以提供比網頁更多的信息。
如何找到需要的 API ?
首先,搜尋 API 文檔。如果網站願意開放 API,都會提供詳細的 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 備份
準備
- 配置 Python3 環境
- 安裝依賴
pip3 install requests
- 新建
main.py
文件 - 編輯
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 。