【Crawler】Day 28: 其實就是聖誕節大採購嘛!網路爬蟲速成班(下)

聖誕快樂!你這骯髒的小畜生!
《小鬼當家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)
1
2
La La Land
8.0

好,寫到這邊,大家應該都對爬蟲有概念了,若有任何問題可以一起來討論喔!我們明天見。