聖誕快樂!你這骯髒的小畜生!
《小鬼當家2》
昨天我們把所有寫爬蟲時可能會需要的先備知識都學起來了,今天就要直接進入爬蟲主題啦!以往我在教同學寫網路爬蟲的時候,我都會用一個我自己原創的比喻:聖誕節大採購!且聽我娓娓道來吧!
爬蟲出動!
過去從來沒有接觸過網路爬蟲的人,可能會覺得爬蟲是一個很難理解的技術,對初學者而言就更不用說了。但其實爬蟲的概念很簡單,就是:
把所有要爬網頁瀏覽一遍,把我們要的資訊整理成結構化資料,接著儲存成我們想要的檔案形式。
好像還是有點抽象。沒關係,我們從了解一個網路的架構開始談起。網頁通常會分成兩種:
動態網頁不是我們平常會看到的那些有很精美的動畫,才能叫做動態網頁。有時候,一個靜態網頁也可能會有很厲害的動畫。決定一個網頁是靜態還是動態,取決於他是否有連接至資料庫。
對爬蟲而言,抓取靜態跟動態網頁各自有不一樣的寫法。動態網頁稍微複雜一點,今天先講抓取靜態網頁的網路爬蟲寫法。
網頁架構
一個網站的基本架構,通常是 html + CSS (+ javascript)
首先,我們在網路上看到的所有網站,都是透過一個一個的 html 檔所組成的,這些 html 檔又被稱為標籤語言,這是因為你可以看到這是由很多的 <label>
組成的語言,這些標籤中又各自帶有不同的特性。我們先來看 html 檔長什麼樣子,我通常都會把 html 檔稱為聖誕樹。
1 2 3 4 5 6 7 8 9 10 11 12
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> </body> </html>
|
之後,在網頁上有時候會再加上 CSS 檔,來裝飾網頁,讓網頁變得更加繽紛。而我通常會稱這些 CSS 檔為聖誕樹裝飾品還有裝飾燈。
有些網站會有 Javascript ,有些沒有。Javascript 主要是用來讓網站回應訪客的要求,並作出適當回饋;又或者是,可以透過 Javascript 來跟後端的資料庫進行溝通。因此我通常稱 Javascript 為聖誕樹裝飾燈的開關。
所以說,爬蟲就是聖誕節搶購!
為什麼是聖誕節的大採購!?
所以,如果要用聖誕節大採購來比喻爬蟲的話,基本上就是這樣:
- 你可以將一個一個的網頁當作聖誕樹。
- 網頁中的文字、連結,則是聖誕樹的裝飾品,也就是你需要的資料。
- 你要做的,就是找到一棵一棵的聖誕樹,接著從一棵一棵聖誕樹的樹枝上找到你要的裝飾品。
- 接著,將裝飾品裝到你的籃子裡,打包帶回家。
什麼意思?我們來寫寫程式碼
首先,我們要下載需要的套件。請先在終端機執行 pip install
,爬蟲會用到的套件為 BeautifulSoup
1
| pip install BeautifulSoup
|
如果說今天我們想爬 PTT 電影版的資料作為簡單範例,我們先執行以下程式:
1 2 3 4 5
| import requests from bs4 import BeautifulSoup as bs r = requests.get("https://www.ptt.cc/bbs/movie/index.html") page = bs(r.text, "html.parser") print(page)
|
就可以得到那個網頁的 html 檔!(注意,如果是動態網頁,很有可能就抓不到需要的內容,那時就需要另外一種寫法)以下貼一部分的輸出即可(我有特別擷取某一段,所以你的輸出會跟我長得不太一樣):
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <!DOCTYPE html>
<html> <head> <meta charset="utf-8"/> <meta content="width=device-width, initial-scale=1" name="viewport"/> <title>看板 movie 文章列表 - 批踢踢實業坊</title> <link href="//images.ptt.cc/bbs/v2.27/bbs-common.css" rel="stylesheet" type="text/css"/> <link href="//images.ptt.cc/bbs/v2.27/bbs-base.css" media="screen" rel="stylesheet" type="text/css"/> <link href="//images.ptt.cc/bbs/v2.27/bbs-custom.css" rel="stylesheet" type="text/css"/> <link href="//images.ptt.cc/bbs/v2.27/pushstream.css" media="screen" rel="stylesheet" type="text/css"/> <link href="//images.ptt.cc/bbs/v2.27/bbs-print.css" media="print" rel="stylesheet" type="text/css"/> <script async="" src="/cdn-cgi/challenge-platform/h/b/scripts/invisible.js?ts=1652421600"></script></head> <body> <div id="topbar-container"> <div class="bbs-content" id="topbar"> <a href="/bbs/" id="logo">批踢踢實業坊</a> <span>›</span> <a class="board" href="/bbs/movie/index.html"><span class="board-label">看板 </span>movie</a> <a class="right small" href="/about.html">關於我們</a> <a class="right small" href="/contact.html">聯絡資訊</a> </div> </div> <div id="main-container"> <div id="action-bar-container"> <div class="action-bar"> <div class="btn-group btn-group-dir"> <a class="btn selected" href="/bbs/movie/index.html">看板</a> <a class="btn" href="/man/movie/index.html">精華區</a> </div> <div class="btn-group btn-group-paging"> <a class="btn wide" href="/bbs/movie/index1.html">最舊</a> <a class="btn wide" href="/bbs/movie/index9510.html">‹ 上頁</a> <a class="btn wide disabled">下頁 ›</a> <a class="btn wide" href="/bbs/movie/index.html">最新</a> <div class="r-ent"> <div class="nrec"><span class="hl f2">6</span></div> <div class="title"> <a href="/bbs/movie/M.1652075354.A.59B.html">[新聞] 民族誌紀錄片工作者,胡台麗逝世</a> </div> <div class="meta"> <div class="author">mysmalllamb</div> <div class="article-menu"> <div class="trigger">⋯</div> <div class="dropdown"> <div class="item"><a href="/bbs/movie/search?q=thread%3A%5B%E6%96%B0%E8%81%9E%5D+%E6%B0%91%E6%97%8F%E8%AA%8C%E7%B4%80%E9%8C%84%E7%89%87%E5%B7%A5%E4%BD%9C%E8%80%85%EF%BC%8C%E8%83%A1%E5%8F%B0%E9%BA%97%E9%80%9D%E4%B8%96">搜尋同標題文章</a></div> <div class="item"><a href="/bbs/movie/search?q=author%3Amysmalllamb">搜尋看板內 mysmalllamb 的文章</a></div> </div> </div> <div class="date"> 5/09</div> <div class="mark"></div> </div> </div>
|
注意,我們現在目錄頁!但我們想要的資料在文章的網頁裡面。所以現在要找的是文章網頁的網址。仔細觀察一篇一篇的文章中,超連結通常會存在前面的標籤中,所以需要透過以下程式碼得到需要的那段 html:
1
| page.find("div", "r-ent")
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <div class="r-ent"> <div class="nrec"><span class="hl f2">6</span></div> <div class="title"> <a href="/bbs/movie/M.1652075354.A.59B.html">[新聞] 民族誌紀錄片工作者,胡台麗逝世</a> </div> <div class="meta"> <div class="author">mysmalllamb</div> <div class="article-menu"> <div class="trigger">⋯</div> <div class="dropdown"> <div class="item"><a href="/bbs/movie/search?q=thread%3A%5B%E6%96%B0%E8%81%9E%5D+%E6%B0%91%E6%97%8F%E8%AA%8C%E7%B4%80%E9%8C%84%E7%89%87%E5%B7%A5%E4%BD%9C%E8%80%85%EF%BC%8C%E8%83%A1%E5%8F%B0%E9%BA%97%E9%80%9D%E4%B8%96">搜尋同標題文章</a></div> <div class="item"><a href="/bbs/movie/search?q=author%3Amysmalllamb">搜尋看板內 mysmalllamb 的文章</a></div> </div> </div> <div class="date"> 5/09</div> <div class="mark"></div> </div> </div>
|
.find
雖然可以找到第一篇文章的部分 html 檔可是我們需要的是所有文章,這時可以用另外一個函式 .find_all()
回傳的就會是一個list
,這裡就不貼輸出了。
1
| page.find_all("div", "r-ent")
|
但記得,我們需要的是網址,但並不是每個標籤都有我們想要的網址,這時 try...except...
就派上用場了。
1 2 3 4 5 6 7
| divs = page.find_all("div", "r-ent") for div in divs: try: href = div.find('a')['href'] print(href) except: pass
|
這時,我們就可以得到網址的「後半部分」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| /bbs/movie/M.1652075354.A.59B.html /bbs/movie/M.1652076208.A.B5C.html /bbs/movie/M.1652077025.A.469.html /bbs/movie/M.1652077434.A.D63.html /bbs/movie/M.1652077492.A.F65.html /bbs/movie/M.1652078673.A.2B7.html /bbs/movie/M.1652079315.A.0AC.html /bbs/movie/M.1652079447.A.571.html /bbs/movie/M.1652081468.A.575.html /bbs/movie/M.1652081735.A.D22.html /bbs/movie/M.1652083107.A.CED.html /bbs/movie/M.1652086174.A.524.html /bbs/movie/M.1652087613.A.7D7.html /bbs/movie/M.1652088346.A.D6C.html /bbs/movie/M.1652089057.A.132.html /bbs/movie/M.1652089740.A.BA4.html /bbs/movie/M.1652089754.A.A1E.html /bbs/movie/M.1652092096.A.6BC.html /bbs/movie/M.1652097109.A.975.html /bbs/movie/M.1630756788.A.1FE.html /bbs/movie/M.1636002497.A.7EC.html
|
重新整理一遍,讓輸出的結果是完整網址。
1 2 3 4 5 6 7 8
| divs = page.find_all("div", "r-ent") baseURL = "https://www.ptt.cc" for div in divs: try: href = baseURL+div.find('a')['href'] print(href) except: pass
|
現在試著把他包成函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def indexGrabber(inputURL): r = requests.get(inputURL) page = bs(r.text, "html.parser") divs = page.find_all("div", "r-ent") UrlLIST = [] baseURL = "https://www.ptt.cc" for div in divs: try: href = baseURL + div.find('a')['href'] UrlLIST.append(href) except: pass return UrlLIST
indexGrabber("https://www.ptt.cc/bbs/movie/index9501.html")
|
我們接下來抓取目錄頁的網址,這樣才能將前面的函式用上。
可以發現,一個一個的目錄頁,是由後面那段數字組成,所以到時候我們用for ... in range():
寫就可以了。(大部分目錄頁都會是用這種寫法,仔細觀察!)
要找前面那些標籤位置好像很容易?
前面要找那些資訊的時候,你可能發現在參數中,我加上了("div", "r-ent")
這是因為,PTT的結構相對簡單,可是如果我們碰到結構複雜的網頁,該怎麼辦?我們可以用以下工具,你可以根據你的瀏覽器選擇對應的套件:
- Chrome: ChroPath
- (網頁任意處右鍵) -> 檢查 -> 元素 -> (右側「樣式」列的最右側,若沒看到則按”>>”) -> (檢查元素) -> (複製CSS或XPath)
- Firefox: SelectorsHub
- (網頁任意處右鍵) -> 檢測 -> 檢測器 -> (右側「樣式」列的最右側,若沒看到則按向下箭頭樣式) -> (檢查元素) -> (複製CSS或XPath)
- Alternatives: 移動游標至欲檢測之目標上方 -> (右鍵) -> SelectorsHub -> “Copy Rel cssSelector” or “Copy Rel XPath”
現在來練習 imdb 的網站,我們要將標題跟分數抓下來,透過以上方式,我們可以找到標題跟分數的位置如下:
1 2
| titlePath = ".sc-b73cd867-0.eKrKux" scorePath = "div[class='sc-db8c1937-0 eGmDjE sc-94726ce4-4 dyFVGl'] span[class='sc-7ab21ed2-1 jGRxWM']"
|
那我們執行跟前面一樣的動作:
1 2 3 4 5 6 7 8 9 10
| import requests from bs4 import BeautifulSoup as bs r = requests.get("https://www.imdb.com/title/tt3783958/?ref_=fn_al_tt_1") page = bs(r.text, "html.parser") rawTitle = page.select(titlePath) rawScore = page.select(scorePath) print(rawTitle) print(rawScore) print(type(rawTitle)) print(type(rawScore))
|
1 2 3 4
| [<h1 class="sc-b73cd867-0 eKrKux" data-testid="hero-title-block__title" textlength="10">La La Land</h1>] [<span class="sc-7ab21ed2-1 jGRxWM">8.0</span>] <class 'list'> <class 'list'>
|
可以發現輸出的資料型態都是 list
。所以要得到我們需要的資訊,除了指定索引值之外,需要再加上.text
,告訴爬蟲,我們只需要中間的字串即可。我們可以這麼做:
1 2 3 4
| title = rawTitle[0].text score = rawScore[0].text print(title) print(score)
|
好,寫到這邊,大家應該都對爬蟲有概念了,若有任何問題可以一起來討論喔!我們明天見。