iT邦幫忙

0

PTT 爬蟲

  • 分享至 

  • xImage
  •  

前言

良葛格過世的消息對我來說十分衝擊,筆者從國中開始學 C 語言,就是一路看良哥的筆記長大,乃至於後來學的 Java, Python 以及很多軟體設計的思維都受到良哥很大影響。人生短暫而脆弱,我們雖然無法帶走任何東西,但良哥留下的著作卻能夠深深影響我們。他的文字流傳於世,仿佛他仍然活著。有感於此,取之於網路,用之於網路,決定在這裡展開筆者的技術分享之旅。由於筆者過去只有在擔任課堂助教時才有編寫教學講義的經驗,出社會後的歷練也還不夠深厚,如果在文章中有發現不好的地方,請大家多多包涵,並歡迎提出建議來討論。

介紹

本文透過簡單的 Python 爬蟲程式,爬取 PTT 的文章標題。筆者其實一直想試試看製作一個文章分類的分類器,在訓練分類器之前,需要先有個訓練資料,而 PTT 論壇就是個爬蟲友善的網站,討論區的資訊量非常豐富,非常適合練習爬蟲。本文以西洽板 (C_Chat) 為例,因為西洽板討論量豐富且沒有年齡限制,在實做上更為友善。

目標

希望可以製作一個資料集,獲得類似以下格式的資料:

[
    {"Category": "問題", "Title": "這是什麼神奇寶貝"},
    {"Category": "討論", "Title": "有哪些太晶屬性能近乎完美反制啊"},
    {"Category": "閒聊", "Title": "SHIROBAKO 白箱聖地巡禮 第四彈"},
    ...
]

之後我們可以運用這樣的資料集,來嘗試訓練 NLP 模型,例如使用 Title 來預測 Category 是什麼。

檢查網頁元件

  • 在設計爬蟲程式之前,要先瞭解爬取的目標有哪些資訊。
    • 標記 (Tag)
      • e.g. div, span, img, a, ..., etc.
    • 屬性 (Attribute)
      • e.g. id, class, href, ..., etc.
  • 要觀察目標的資訊,可以簡單的透過瀏覽器對該目標(例如連結、按鈕)按右鍵,選擇檢查:
  • 以 Chrome 來說,快捷鍵是 Ctrl + Shift + C,使用 Ctrl + Shift + I 也可以開啟類似的介面,但不會追蹤游標指著的物件資訊。

環境

首先我們需要安裝 requests, beautifulsoup4 兩個套件:

pip install requests beautifulsoup4

爬取網頁原始碼

透過 requests 套件發送 HTTP Request 並取得網頁原始碼

import requests

url = "https://www.ptt.cc/bbs/C_Chat/index.html"
resp = requests.get(url)
# 將結果存在 Source.html 裡面
with open("Source.html", "w", encoding="UTF-8") as f:
    f.write(resp.text)

基於爬蟲的道德倫理,可以只爬一次的東西,就不要爬兩次。在練習爬蟲的過程中難免會多次發送 Request,在迴圈裡面使用爬蟲,一個不慎可能會對目標伺服器造成很大的負擔,所以我們最好養成把需要操作的資料先存在本機的習慣。

剖析網頁原始碼

Beautiful Soup 4 這個 Python 套件可以讓開發者輕鬆分析 HTML, XML 這種標記語言格式的資料,在爬蟲程式這種需要大量操作 HTML 的應用裡面有相當大的用處。接下來用 Beautiful Soup 4 把剛剛存下來的 Source.html 進行分析,並嘗試提取裡面的標題:

from bs4 import BeautifulSoup

with open("Source.html", "r", encoding="UTF-8") as f:
    bs = BeautifulSoup(f.read(), features="html.parser")

links = bs.find_all("div", attrs={"class": "title"})
for link in links:
    print(link.text.strip())

以上程式碼可能會印出類似以下的結果

[閒聊] 如果虛白是大奶正妹的話,一護會?
[閒聊] 其實當五絕也不是那麼得好。
Re: [閒聊] 世界各國最受歡迎的初代寶可夢
[公告] C_Chat板板規 v.16.8 暨好文補M區
[公告] 看板活動公告彙整 & 置底推文閒聊區
[名人] 批踢踢推廣中心-看板知名人物題目募集中
[公告] 4-11選舉期間從嚴條款
[公告] C_Chat 板 開始舉辦樂透!

標題字串處理

接下來我們需要把標題的分類與標題本身切開來,處理並且跳過已被刪除的文章。這段算是相當單純的字串,不熟 Python 的朋友不妨自己實做看看,以下是筆者的做法:

def SplitTitle(title: str):
    if "本文已被刪除" in title:
        return
    if "[" not in title:
        return
    if "]" not in title:
        return

    a = title.index("[") + 1
    b = title.index("]")

    category = title[a:b].strip()
    title = title[b + 1 :].strip()

    return {"Category": category, "Title": title}

titles = [
    "[閒聊] 如果虛白是大奶正妹的話,一護會?",
    "[閒聊] 其實當五絕也不是那麼得好。",
    "Re: [閒聊] 世界各國最受歡迎的初代寶可夢",
    "[公告] C_Chat板板規 v.16.8 暨好文補M區",
    "[公告] 看板活動公告彙整 & 置底推文閒聊區",
    "[名人] 批踢踢推廣中心-看板知名人物題目募集中",
    "[公告] 4-11選舉期間從嚴條款",
    "[公告] C_Chat 板 開始舉辦樂透!",
]

for t in titles:
    print(SplitTitle(t))

會印出以下結果:

{'Category': '閒聊', 'Title': '如果虛白是大奶正妹的話,一護會?'}
{'Category': '閒聊', 'Title': '其實當五絕也不是那麼得好。'}
{'Category': '閒聊', 'Title': '世界各國最受歡迎的初代寶可夢'}
{'Category': '公告', 'Title': 'C_Chat板板規 v.16.8 暨好文補M區'}
{'Category': '公告', 'Title': '看板活動公告彙整 & 置底推文閒聊區'}
{'Category': '名人', 'Title': '批踢踢推廣中心-看板知名人物題目募集中'}
{'Category': '公告', 'Title': '4-11選舉期間從嚴條款'}
{'Category': '公告', 'Title': 'C_Chat 板 開始舉辦樂透!'}

造訪下一頁

只有一頁討論列表的標題,資料量顯然是不夠的。為了獲得更多的資料,我們需要造訪討論列表的「上一頁」

透過檢查網頁元件的方法,我們得知「上一頁」是個 classbtn widea 元件

但很不幸的,這裡至少有四個按鈕都是 btn wide,類似這樣的例子在網路爬蟲裡面其實很常見,需要多多觀察。這個例子,我們只需要簡單判斷元件的文字即可:

from bs4 import BeautifulSoup

with open("Source.html", "r", encoding="UTF-8") as f:
    bs = BeautifulSoup(f.read(), features="html.parser")

def FindNextPage(bs):
    links = bs.find_all("a", attrs={"class": "btn wide"})
    for link in links:
        if link.text == "‹ 上頁":
            return link.attrs["href"]

print(FindNextPage(bs))

輸出結果如下:

/bbs/C_Chat/index17572.html

這邊抓到的連結只包含網址的後半而已,記得在前面加上 "https://www.ptt.cc"

完整程式碼

整合以上步驟,完整的程式碼如下:

import json
import requests
from bs4 import BeautifulSoup as BS


def Main():
    base_url = "https://www.ptt.cc"
    sub_url = f"/bbs/C_Chat/index.html"

    data = list()
    for _ in range(1000):
        full_url = f"{base_url}{sub_url}"
        bs = BS(requests.get(full_url).text, features="lxml")

        titles = bs.find_all("div", attrs={"class": "title"})
        for title in titles:
            title = title.text.strip()
            title = SplitTitle(title)
            if title is None:
                continue
            data.append(title)

        sub_url = FindNextPage(bs)

    with open("Data.json", "w", encoding="UTF-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=4)


def SplitTitle(title: str):
    if "本文已被刪除" in title:
        return
    if "[" not in title:
        return
    if "]" not in title:
        return

    a = title.index("[") + 1
    b = title.index("]")

    category = title[a:b].strip()
    title = title[b + 1 :].strip()

    return {"Category": category, "Title": title}


def FindNextPage(bs):
    links = bs.find_all("a", attrs={"class": "btn wide"})
    for link in links:
        if link.text == "‹ 上頁":
            return link.attrs["href"]


if __name__ == "__main__":
    Main()

結論

透過學習網路爬蟲,可以瞭解基本的 HTML 架構與觀察方法,結合簡單的文字處理,可以讓爬蟲成為一個強大的資料工具。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言