原本爬蟲使用 PTT 作為練習標的,但是年輕人好像已經不流行用這個(?),而且另一方面也不想在頁面 parsing 上花太多功夫。
這次想要練習的東西是爬蟲架構以及其他後續應用,而不再拘泥處理資料本身;所以選擇了與 PTT 相似性質且擁有 API 的 Dcard 作為實驗目標。
網站 API 規則
網路上流傳多種 Dcard
API 與各式 API wrapper 程式套件,不過網站本身似乎在改版中 (看起來是為了因應前端的技術 stack 改變,轉換至 React.js 環境開發吧) 變得大量使用非同步請求 (ajax) 來獲得資料並繪製畫面。所以前述的那些套件及早期公開的 API 幾乎通通失效。
不過重新到網站上透過開發者工具重新觀察便能獲得一些規則,重新組合出網站實際在使用的新版 API。當然這些資訊或許隨著網站改變會失效也不一定 (尤其是 _api
這樣子的命名,明顯是過度產物的感覺)
API root URL
API |
URL |
看板資訊(meta) |
http://dcard.tw/_api/forums |
文章資訊(meta) |
http://dcard.tw/_api/forums/{看板名稱}/posts |
文章內文 |
http://dcard.tw/_api/posts/{文章編號} |
文章內引用連結 |
http://dcard.tw/_api/posts/{文章編號}/links |
文章內留言 |
http://dcard.tw/_api/posts/{文章編號}/comments |
*文章資訊(meta)
與 文章內留言
預設使用熱門度 (popularity) 作為排序,而且一次請求 (request) 中只回應 30 筆
。
- 想要讓這兩項使用
時間
排序,可在 GET
參數中加入 popular=false
- 欲取得更多的
文章資訊(meta)
,可以使用 before={某文章編號}
來獲得早於 #{某文章編號} 的另外 30 筆
資訊。
- 欲取得更多的
文章內留言
,可以使用 after={某樓層}
來獲得大於 #{某樓層} 的另外 30 筆
留言。
開始寫簡易爬蟲
首先定義好剛剛提到的基本 URL 資訊
1 2 3 4 5
| class Dcard: API_ROOT = 'http://dcard.tw/_api' FORUMS = 'forums' POSTS = 'posts'
|
以及一些基本功能 @(utils.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import requests import logging def get(url, verbose=False, **kwargs): response = requests.get(url, **kwargs) if verbose: logging.info(response.url) return response.json() def filter_general(forums): for forum in forums: if not forum['isSchool']: yield forum
|
Step1. 替 class Dcard
新增 method,取得所有看板資訊 (metadata)
1 2 3 4 5 6 7 8 9 10 11
| @staticmethod def get_forums(**kwargs): url = '{api_root}/{api_forums}'.format( api_root=Dcard.API_ROOT, api_forums=Dcard.FORUMS) forums = get(url) if kwargs.get('no_school'): return [forum for forum in filter_general(forums)] return forums
|
參數中加入 no_school
來控制是否過濾掉學校看板。之後想做的分析會著重於比較熱門的一般性看板,所以這邊做個開關決定是否去掉這些特定看板。
Step2. 替 class Dcard
新增 method,取得看板內文章資訊 (metadata)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @staticmethod def get_post_metas(forum, params): url = '{api_root}/{api_forums}/{forum}/{api_posts}'.format( api_root=Dcard.API_ROOT, api_forums=Dcard.FORUMS, api_posts=Dcard.POSTS, forum=forum ) article_metas = get(url, params=params) return article_metas @staticmethod def get_post_ids(forum, pages=3): ''' 為了一次取的更多頁的文章 (可以把一次 request 取得 30 筆,視作取得一頁) 使用此 method 將 `get_post_metas` 做包裝,提供一次抓取多頁文章資訊, 且通常是為了之後用途而抓取 {文章編號}。 ''' params = {'popular': False} ids = [] for _ in range(pages): metas = Dcard.get_post_metas(forum, params) ids += [e['id'] for e in metas] params['before'] = ids[-1] return ids
|
Step3. 最終的重點,取得單篇文章內的所有資訊,分為:內文、引用連結、留言三個部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @staticmethod def get_post_content(post_meta): post_url = '{api_root}/{api_posts}/{post_id}'.format( api_root=Dcard.API_ROOT, api_posts=Dcard.POSTS, post_id=post_meta['id'] ) links_url = '{post_url}/links'.format(post_url=post_url) comments_url = '{post_url}/comments'.format(post_url=post_url) params = {} content = get(post_url) links = get(links_url) comments = [] while True: _comments = get(comments_url, params=params, verbose=True) if len(_comments) == 0: break comments += _comments params['after'] = comments[-1]['floor'] return { 'content': content, 'links': links, 'comments': comments }
|
透過 Dcard 現正偷偷使用中
的官方 API,完成了我們的基本爬蟲。
*
Salas