靜態擷取網頁內容

The world’s most valuable resource is no longer oil, but data.

The Economist

獲取資料在資料科學專案中扮演發起點,如果這個資料科學專案目的是協助我們制定資料驅動的策略(data-driven strategy),而非倚賴直覺,那麼為專案細心盤點資料來源與整理獲取方法,可以為將來的決策奠基穩固的基礎。資料常見的來源包含三種:

  1. 檔案
  2. 資料庫
  3. 網頁資料擷取

這個小節我們要討論第三種資料來源:網頁,從網頁擷取資料的方法另外一個更為眾人耳熟能詳的名稱即是爬蟲。

如何定位網頁資料

定位網頁中特定資料的位址,就像是在地圖上標記一般,我們需要景點或者建築物的位址,可以是詳細地址,亦或者是精準的經緯度。而在網頁中有非常多方法能夠表示出資料位址,常見的像是使用:

  • html 的標籤名稱
  • html 標籤中給予的 id
  • html 標籤中給予的 class
  • 資料所在的 CSS 選擇器(CSS Selector)
  • 資料所在的 XPath

在地圖上標記

定位網頁中特定資料的位址

考量多數資料科學愛好者皆不是網頁工程師背景,透過 Chrome 瀏覽器的外掛來取得資料所在的 CSS 選擇器或者 XPath 是快速入門的好方法,也是我們推薦的做法;對於使用 HTML 標籤名稱、id 與 class 來取得資料所在有興趣的資料科學愛好者,可以另外花時間學習 HTML 與 CSS 的相關知識。

安裝 Selector Gadget

透過下列步驟將 Selector Gadget 外掛加入 Chrome 瀏覽器:

  1. 前往 Chrome Web Store,點選外掛(Extensions)
  2. 搜尋 Selector Gadget 並點選加入到 Chrome 瀏覽器
  3. 確認要加入 Selector Gadget
  4. 完成安裝

前往 Chrome Web Store,點選外掛(Extensions)

搜尋 Selector Gadget 並點選加入到 Chrome 瀏覽器

確認要加入 Selector Gadget

完成安裝

使用 Selector Gadget

透過下列步驟定位 Avengers: Infinity War (2018) 的評分:

  1. 點選 Selector Gadget 的外掛圖示
  2. 留意 Selector Gadget 的 CSS 選擇器
  3. 移動滑鼠到想要定位的元素
  4. 在想要定位的評分上面點選左鍵,留意此時的 CSS 選擇器位址定位為 span,網頁上有很多的資料都同時被選擇到(以黃底標記),Clear 後面數字表示有多少個元素被選擇到
  5. 接著移動滑鼠點選不要選擇的元素(改以紅底標記),並同時注意 CSS 選擇器位址(.ratingValue span)與 Clear 後面數字(3)
  6. 繼續移動滑鼠點選不要選擇的元素(改以紅底標記),注意 CSS 選擇器位址(strong span)與 Clear 後面數字(1),這時表示我們已經成功定義到評分的 CSS 選擇器:strong span

點選 Selector Gadget 的外掛圖示

留意 Selector Gadget 的 CSS 選擇器

移動滑鼠到想要定位的元素

在想要定位的評分上面點選左鍵

接著移動滑鼠點選不要選擇的元素(改以紅底標記)

繼續移動滑鼠點選不要選擇的元素(改以紅底標記)

讓我們再練習一次,透過下列步驟定位 Avengers: Infinity War (2018) 的電影類型:

  1. 移動滑鼠到想要定位的元素
  2. 在想要定位的評分上面點選左鍵,留意此時的 CSS 選擇器位址定位為 .itemprop,網頁上有很多的資料都同時被選擇到(以黃底標記),Clear 後面數字表示有多少個元素被選擇到
  3. 接著移動滑鼠點選不要選擇的元素(改以紅底標記),並同時注意 CSS 選擇器位址(.subtext a)與 Clear 後面數字(4),這時表示我們已經成功定義到電影類型的 CSS 選擇器:.subtext a

移動滑鼠到想要定位的元素

在想要定位的評分上面點選左鍵

接著移動滑鼠點選不要選擇的元素(改以紅底標記)

安裝 XPath Helper

透過下列步驟將 XPath Helper 外掛加入 Chrome 瀏覽器:

  1. 前往 Chrome Web Store,點選外掛(Extensions)
  2. 搜尋 XPath Helper 並點選加入到 Chrome 瀏覽器
  3. 確認要加入 XPath Helper
  4. 完成安裝

前往 Chrome Web Store,點選外掛(Extensions)

搜尋 XPath Helper 並點選加入到 Chrome 瀏覽器

確認要加入 XPath Helper

完成安裝

使用 XPath Helper

透過下列步驟定位 Avengers: Infinity War (2018) 的評分:

  1. 點選 XPath Helper 的外掛圖示
  2. 留意 XPath Helper 介面左邊的 XPath 與右邊被定位到的資料
  3. 按住 shift 鍵移動滑鼠到想要定位的元素
  4. 試著縮減 XPath,從最前面開始刪減,我們會發現可以刪減為 //strong/span 依然還可以對應到評分,這時表示我們已經成功定義到評分的 XPath://strong/span

點選 XPath Helper 的外掛圖示

留意 XPath Helper 介面左邊的 XPath 與右邊被定位到的資料

按住 shift 鍵移動滑鼠到想要定位的元素

刪減為 //strong/span 依然還可以對應到評分

讓我們再練習一次,透過下列步驟定位 Avengers: Infinity War (2018) 的電影類型:

  1. 按住 shift 鍵移動滑鼠到想要定位的元素,由於電影類型有三個分類,我們分別將滑鼠移動到上面觀察 XPath
  2. 觀察到 a 後面中括號中的數字由 1 變更為 3,這時刪減掉整個中括號,就可以用一個 XPath 選擇到三個分類
  3. 試著縮減 XPath,從最前面開始刪減,我們會發現可以刪減為 //div[@class='subtext']/a 依然還可以對應到電影類型,這時表示我們已經成功定義到電影類型的 XPath://div[@class='subtext']/a

電影類型有三個分類,我們分別將滑鼠移動到上面觀察 XPath

電影類型有三個分類,我們分別將滑鼠移動到上面觀察 XPath

電影類型有三個分類,我們分別將滑鼠移動到上面觀察 XPath

刪減掉整個中括號,就可以用一個 XPath 選擇到三個分類

刪減為 //div[@class='subtext']/a 依然還可以對應到電影類型

擷取網頁內容

擷取網頁內容的主要任務有兩個,一是取得網頁中所有資料,二是利用 CSS 選擇器或 XPath 解析出我們所需要的部分。在 Python 中我們使用 pyquery 模組的 PyQuery() 函數取得網頁中所有資料,接著再指派 CSS 選擇器即可(注意 pyquery 模組目前只接受 CSS 選擇器);在 R 語言中我們使用 rvest 套件的 read_html() 函數取得網頁中所有資料,接著在 html_nodes() 函數中指派 CSS 選擇器或 XPath。

在 Python 中使用 pyquery

開始之前我們得先在終端機安裝好 pyquery 模組:

pip install pyquery
1

在終端機安裝好 pyquery 模組

定義一個函數 get_movie_rating(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得該電影的評分,值得注意的地方有:

  • 定位到的網頁資料都會伴隨 html 標籤,使用 .text() 方法可以只擷取資料內容
  • 從網頁擷取下來的資料為字串,我們可以使用 float() 函數轉換為浮點數
from pyquery import PyQuery as pq

def get_movie_rating(movie_url):
  rating_css = "strong span"               # 評分的 CSS 選擇器
  
  movie_doc = pq(movie_url)                # 取得網頁中所有的資料
  rating_elem = movie_doc(rating_css)      # 擷取評分資料
  movie_rating = float(rating_elem.text()) # 將標籤去除後轉換為浮點數
  return movie_rating

avenger_url = "https://www.imdb.com/title/tt4154756"
get_movie_rating(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
## 8.6
1

再練習一次,定義一個函數 get_movie_genre(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得電影類型,值得注意的地方是類型有三個,因此我們使用 list comprehension 將每一個有 html 標籤的電影類型都取 text 屬性。

from pyquery import PyQuery as pq

def get_movie_genre(movie_url):
  genre_css = ".subtext a"                                             # 電影類型的 CSS 選擇器
  
  movie_doc = pq(movie_url)                                            # 取得網頁中所有的資料
  genre_elem = movie_doc(genre_css)                                    # 擷取評分資料
  movie_genre = [x.text.replace("\n", "").strip() for x in genre_elem] # 將標籤去除
  movie_genre.pop()                                                    # 將最後一個元素上映日期拋出
  return movie_genre

avenger_url = "https://www.imdb.com/title/tt4154756"
get_movie_genre(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
13
## ['Action', 'Adventure', 'Fantasy']
1

再練習一次,定義一個函數 get_movie_cast(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得演員名單,值得注意的地方是演員名單同樣是複數個,因此我們使用 list comprehension 將每一個有 html 標籤的演員名單都取 text 屬性。

from pyquery import PyQuery as pq

def get_movie_cast(movie_url):
  cast_css = ".primary_photo+ td a"                                  # 演員名單的 CSS 選擇器
  
  movie_doc = pq(movie_url)                                          # 取得網頁中所有的資料
  cast_elem = movie_doc(cast_css)                                    # 擷取演員名單資料
  cast_genre = [x.text.replace("\n", "").strip() for x in cast_elem] # 將標籤去除
  return cast_genre

avenger_url = "https://www.imdb.com/title/tt4154756"
get_movie_cast(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
## ['Robert Downey Jr.', 'Chris Hemsworth', 'Mark Ruffalo', 'Chris Evans', 'Scarlett Johansson', 'Don Cheadle', 'Benedict Cumberbatch', 'Tom Holland', 'Chadwick Boseman', 'Zoe Saldana', 'Karen Gillan', 'Tom Hiddleston', 'Paul Bettany', 'Elizabeth Olsen', 'Anthony Mackie']
1

最後一個練習,定義一個函數 get_movie_poster(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得電影海報連結,值得注意的是電影海報連結是在 html 標籤的屬性中,因此我們改採用 .attr() 方法取出連結。

from pyquery import PyQuery as pq

def get_movie_poster(movie_url):
  poster_css = ".poster img"          # 電影海報的 CSS 選擇器
  
  movie_doc = pq(movie_url)           # 取得網頁中所有的資料
  poster_elem = movie_doc(poster_css) # 擷取電影海報資料
  poster = poster_elem.attr('src')    # 將標籤去除,保留連結
  return poster

avenger_url = "https://www.imdb.com/title/tt4154756"
get_movie_poster(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
## 'https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MTUwNTM@._V1_UX182_CR0,0,182,268_AL_.jpg'
1

在 R 語言中使用 rvest

定義一個函數 get_movie_rating(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得該電影的評分,值得注意的地方有:

  • 使用 read_html() 函數取得網頁中所有資料
  • 使用 html_nodes() 函數利用 CSS 選擇器或 XPath 擷取出伴隨 html 標籤的評分
  • 使用 html_text() 函數可以去除 html 標籤,只留下資料內容
  • 從網頁擷取下來的資料為字串,可以使用 as.numeric() 函數轉換為浮點數
# install.packages(c("rvest", "magrittr"))
library(rvest)
library(magrittr) # 使用 %>% 運算子

get_movie_rating <- function(movie_url) {
  rating_css <- "strong span"
  rating_xpath <- "//strong/span"
  
  movie_rating <- movie_url %>% 
    read_html() %>%                        # 取得網頁中所有的資料
    html_nodes(css = rating_css) %>%       # 擷取評分資料
    # html_nodes(xpath = rating_xpath) %>% # 亦可以使用 XPath 擷取評分資料
    html_text() %>%                        # 去除 html 標籤
    as.numeric()                           # 轉換為浮點數
  
  return(movie_rating)
}

avenger_url <- "https://www.imdb.com/title/tt4154756"
get_movie_rating(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## [1] 8.6
1

再練習一次,定義一個函數 get_movie_genre(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得電影類型。

# install.packages(c("rvest", "magrittr"))
library(rvest)
library(magrittr) # 使用 %>% 運算子

get_movie_genre <- function(movie_url) {
  genre_css <- ".subtext a"
  genre_xpath <- "//div[@class='subtext']/a"
  
  movie_genre <- movie_url %>% 
    read_html() %>%                             # 取得網頁中所有的資料
    html_nodes(xpath = genre_xpath) %>%         # 使用 XPath 擷取評分資料
    # html_nodes(css = genre_css) %>%           # 亦可以使用 CSS 選擇器擷取評分資料
    html_text()                                 # 去除 html 標籤
  movie_genre_len <- length(movie_genre)
  movie_genre <- movie_genre %>%
    `[` (-movie_genre_len) %>%                  # 將最後一個元素上映日期刪去
    gsub(pattern = "\n", replacement = "") %>%  # 去除換行符號
    trimws(which = "both")                      # 去除前後空白
  
  return(movie_genre)
}

avenger_url <- "https://www.imdb.com/title/tt4154756"
get_movie_genre(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## [1] "Action"    "Adventure" "Fantasy"
1

再練習一次,定義一個函數 get_movie_cast(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得演員名單。

# install.packages(c("rvest", "magrittr"))
library(rvest)
library(magrittr) # 使用 %>% 運算子

get_movie_cast <- function(movie_url) {
  cast_css <- ".primary_photo+ td a"
  cast_xpath <- "//td[2]/a"
  
  movie_cast <- movie_url %>% 
    read_html() %>%                             # 取得網頁中所有的資料
    html_nodes(xpath = cast_xpath) %>%          # 使用 XPath 擷取演員名單
    # html_nodes(css = cast_css) %>%            # 亦可以使用 CSS 選擇器擷取演員名單
    html_text() %>%                             # 去除 html 標籤
    gsub(pattern = "\n", replacement = "") %>%  # 去除換行符號
    trimws(which = "both")                      # 去除前後空白
  return(movie_cast)
}

avenger_url <- "https://www.imdb.com/title/tt4154756"
get_movie_cast(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## [1] "Robert Downey Jr."    "Chris Hemsworth"     
## [3] "Mark Ruffalo"         "Chris Evans"         
## [5] "Scarlett Johansson"   "Don Cheadle"         
## [7] "Benedict Cumberbatch" "Tom Holland"         
## [9] "Chadwick Boseman"     "Zoe Saldana"         
## [11] "Karen Gillan"         "Tom Hiddleston"      
## [13] "Paul Bettany"         "Elizabeth Olsen"     
## [15] "Anthony Mackie"
1
2
3
4
5
6
7
8

最後一個練習,定義一個函數 get_movie_poster(movie_url) 讓使用者輸入不同電影的 IMDB 網址,就可以取得電影海報連結,值得注意的是電影海報連結是在 html 標籤的屬性中,因此我們改採用 html_attr() 函數取出連結。

# install.packages(c("rvest", "magrittr"))
library(rvest)
library(magrittr) # 使用 %>% 運算子

avenger_url <- "https://www.imdb.com/title/tt4154756"
get_movie_cast(avenger_url)

get_movie_poster <- function(movie_url) {
  poster_css <- ".poster img"
  
  movie_poster <- movie_url %>% 
    read_html() %>%                  # 取得網頁中所有的資料
    html_nodes(css = poster_css) %>% # 使用 CSS 選擇器擷取電影海報連結
    html_attr("src")                 # 去除 html 標籤
  
  return(movie_poster)
}

avenger_url <- "https://www.imdb.com/title/tt4154756"
get_movie_poster(avenger_url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## [1] "https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MTUwNTM@._V1_UX182_CR0,0,182,268_AL_.jpg"
1

小結

在這個小節中我們簡介如何使用 Chrome 瀏覽器的外掛 Selector Gadget 與 XPath Helper 定位網頁中資料的位址,並且利用 Python 的 pyquery 模組與 R 語言的 rvest 套件擷取並解析網頁中的資料;但是使用者會發現每一部電影的網址,都是 IMDB 資料庫的一個流水編號,並無法透過電影標題獲得網址,在下一個小節如何獲取資料:擷取網頁內容(下)我們會介紹 Selenium 這個可以操控瀏覽器的解決方案來因應這個問題。

延伸閱讀