動態擷取網頁內容

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

The Economist

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

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

在如何獲取資料:靜態擷取網頁內容小節中討論如何從常見資料來源(檔案、資料庫與網頁)中的第三種來源:網頁的 html 檔案中擷取資料,即為人耳熟能詳的爬蟲技巧,我們知道如何使用 CSS 選擇器與 XPath 定位網頁資料,然後分別使用 pyquery 模組與 rvest 套件分別解析至 Python 以及 R 語言中,不過在文末面對到一個問題:無法憑藉電影名稱對應到該電影資訊的頁面。於是我們求助 selenium 自動化網頁測試工具連接 Python 與 R 語言,協助兩者的程式碼操控瀏覽器,進而前往含有指定電影資訊的網頁。

修飾擷取電影資訊的函數

首先我們將先前撰寫過的函數再修飾一番,原本擷取電影特定電影的評分(Rating)、劇情類型(Genre)、海報圖片連結(Poster)和演員名單(Cast)是分開的四個函數;修飾成 get_movie_info(movie_url) 函數,可以將四個電影資訊儲存在一個 Python 的 dict 中或一個 R 語言的 list 中。

from pyquery import PyQuery as pq

def get_movie_info(movie_url):
  """
  Get movie info from certain IMDB url
  """
  # 指定電影資訊的 CSS 選擇器
  rating_css = "strong span"
  genre_css = ".subtext a"
  poster_css = ".poster img"
  cast_css = ".primary_photo+ td a"
  
  movie_doc = pq(movie_url)
  # 擷取資訊
  rating_elem = movie_doc(rating_css)
  movie_rating = float(rating_elem.text())
  genre_elem = movie_doc(genre_css)
  movie_genre = [x.text.replace("\n", "").strip() for x in genre_elem]
  movie_genre.pop()
  movie_poster_elem = movie_doc(poster_css)
  movie_poster = movie_poster_elem.attr('src')
  movie_cast_elem = movie_doc(cast_css)
  movie_cast = [x.text.replace("\n", "").strip() for x in movie_cast_elem]
  
  # 回傳資訊
  movie_info = {
      "rating": movie_rating,
      "genre": movie_genre,
      "poster": movie_poster,
      "cast": movie_cast
  }
  return movie_info

avenger_url = "https://www.imdb.com/title/tt4154756"
get_movie_info(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
25
26
27
28
29
30
31
32
33
34
35
## {'cast': ['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'],
## 'genre': ['Action', 'Adventure', 'Fantasy'],
## 'poster': 'https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MTUwNTM@._V1_UX182_CR0,0,182,268_AL_.jpg',
## 'rating': 8.6}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# install.packages(c("rvest", "magrittr"))
library(rvest)
library(magrittr) # 使用 %>% 運算子

get_movie_info <- function(movie_url) {
  # 指定電影資訊的 CSS 選擇器
  rating_css <- "strong span"
  genre_css <- ".subtext a"
  poster_css <- ".poster img"
  cast_css <- ".primary_photo+ td a"
  
  movie_doc <- movie_url %>% 
    read_html()
  # 擷取資訊
  movie_rating <- movie_doc %>% 
    html_nodes(css = rating_css) %>%
    html_text() %>%
    as.numeric()
  movie_genre <- movie_doc %>% 
    html_nodes(css = genre_css) %>% 
    html_text() %>% 
    trimws() %>% 
    gsub(pattern = "\n", replacement = "")
  movie_genre_len <- length(movie_genre)
  movie_genre <- movie_genre[-movie_genre_len]
  movie_poster <- movie_doc %>% 
    html_nodes(css = poster_css) %>% 
    html_attr("src")
  movie_cast <- movie_doc %>% 
    html_nodes(css = cast_css) %>% 
    html_text() %>% 
    trimws() %>% 
    gsub(pattern = "\n", replacement = "")
  
  # 回傳資訊
  movie_info <- list(
    "rating" = movie_rating,
    "genre" = movie_genre,
    "poster" = movie_poster,
    "cast" = movie_cast
  )
  return(movie_info)
}

avenger_url <- "https://www.imdb.com/title/tt4154756"
get_movie_info(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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
## $rating
## [1] 8.6
## 
## $genre
## [1] "Action"    "Adventure" "Fantasy"  
## 
## $poster
## [1] "https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MT UwNTM@._V1_UX182_CR0,0,182,268_AL_.jpg"
## 
## $cast
##  [1] "Robert Downey Jr."    "Chris Hemsworth"      "Mark Ruffalo"        
##  [4] "Chris Evans"          "Scarlett Johansson"   "Don Cheadle"         
##  [7] "Benedict Cumberbatch" "Tom Holland"          "Chadwick Boseman"    
## [10] "Zoe Saldana"          "Karen Gillan"         "Tom Hiddleston"      
## [13] "Paul Bettany"         "Elizabeth Olsen"      "Anthony Mackie" 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

遭遇到的問題

將函數修飾過後,我們想利用它一次擷取多部電影資訊,像是 Marvel 漫威系列的復仇者聯盟、黑豹或鋼鐵人,這時就會發現一個問題,每一部電影資訊頁面的網址皆是 IMDB 資料庫的一個 id,例如 Avengers: Infinity War 的 id 是 tt4154756;而 Black Panther 的 id 是 tt1825683,因此要取得網址,得先在 IMDB 的搜尋對話框中輸入電影名稱,再前往電影資訊頁面,假如要大量擷取電影資訊,手動做法會耗費太多的力氣,得想一個更好的方法才行:這時我們求助可以操控瀏覽器的 Selenium 解決方案。

什麼是 Selenium

Selenium 是瀏覽器自動化的解決方案,主要是網頁應用程式測試目的,在資料科學團隊中運用於解決擷取網頁資料所碰到的問題,例如面對到需要登入、填寫表單或者點選按鈕後才會顯示出資料的網站。Python 與 R 語言採用的是 Selenium 中的 WebDriver 元件,運作的方式略有不同。

Python 透過 Selenium WebDriver 呼叫瀏覽器驅動程式,再由瀏覽器驅動程式去呼叫瀏覽器;R 語言透過 Selenium Server 直接呼叫瀏覽器,本篇文章採用的是 Selenium Server Standalone 的 .jar 檔案,電腦必須安裝有 java 才能夠執行。如果使用者的電腦沒有安裝,請先前往 java 下載頁面下載後安裝。

Selenium WebDriver 對 Google Chrome 與 Mozilla Firefox 兩個主流瀏覽器的支援最好,為了確保使用上不會碰到問題,建議都使用最新版的瀏覽器、瀏覽器驅動程式與模組。

下載瀏覽器

前往官方網站下載最新版的瀏覽器。

前往官方網站下載最新版的 Google Chrome

前往官方網站下載最新版的 Mozilla Firefox

安裝 Selenium

Python

前往官方網站下載最新版的瀏覽器驅動程式,Chrome 瀏覽器的驅動程式名稱為 ChromeDriver,Firefox 瀏覽器的驅動程式名稱為 geckodriver。

前往官方網站下載最新版的 ChromeDriver

前往官方網站下載最新版的 geckodriver

下載完成以後解壓縮在熟悉的路徑讓後續的指派較為方便,我習慣放在使用者家目錄的下載資料夾,因此路徑會是 /Users/YOURUSERNAME/Downloads/chromedriver 以及 /Users/YOURUSERNAME/Downloads/geckodriver,相同路徑 Windows 的使用者應該是 C:/Users/YOURUSERNAME/Downloads/chromedriver.exe 以及 C:/Users/YOURUSERNAME/Downloads/geckodriver.exe

接著在終端機安裝 Selenium 模組。

pip install selenium
1

接著測試用程式碼透過 ChromeDriver 與 geckodriver 分別操控 Chrome 瀏覽器以及 Firefox 瀏覽器前往 IMDB 首頁並將首頁的網址印出再關閉瀏覽器。

from selenium import webdriver

imdb_home = "https://www.imdb.com/"
driver = webdriver.Chrome(executable_path="YOURCHROMEDRIVERPATH") # Use Chrome
driver.get(imdb_home)
print(driver.current_url)
driver.close()
1
2
3
4
5
6
7
from selenium import webdriver

imdb_home = "https://www.imdb.com/"
driver = webdriver.Firefox(executable_path="YOURGECKODRIVERPATH") # Use Firefox
driver.get(imdb_home)
print(driver.current_url)
driver.close()
1
2
3
4
5
6
7
## https://www.imdb.com/
1

用程式碼透過 ChromeDriver 操控 Chrome 瀏覽器前往 IMDB 首頁

用程式碼透過 geckodriver 操控 Firefox 瀏覽器前往 IMDB 首頁

R 語言

前往官方網站下載最新版的 Selenium Standalone Driver,並且在終端機啟動。

java -jar selenium-server-standalone-X.X.X.jar
1

接著測試用程式碼分別操控 Chrome 瀏覽器以及 Firefox 瀏覽器前往 IMDB 首頁並將首頁的網址印出再關閉瀏覽器,我們使用 RSelenium 套件從 R 語言操控 Selenium Driver。

if (!require(RSelenium)) {
  install.packages("devtools")
  devtools::install_github("johndharrison/binman")
  devtools::install_github("johndharrison/wdman")
  devtools::install_github("ropensci/RSelenium")
}
if (!require(magrittr)) {
  install.packages("magrittr")
}
library(RSelenium)

test_selenium <- function(browser = "chrome") {
  remDr <- remoteDriver(remoteServerAddr = "localhost" 
                      , port = 4444L
                      , browserName = browser
                      )
  remDr$open()
  remDr$navigate("https://www.imdb.com")
  print(remDr$getCurrentUrl())
  remDr$close()
}

test_selenium()
test_selenium(browser = "firefox")
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]]
## [1] "https://www.imdb.com/"
1
2

用程式碼透過 Selenium Server 操控 Chrome 瀏覽器前往 IMDB 首頁

用程式碼透過 Selenium Server 操控 Firefox 瀏覽器前往 IMDB 首頁

盤點手動操控的動作順序

測試完畢確認可以利用 Python 與 R 語言啟動 Chrome 以及 Firefox 瀏覽器之後,接著是盤點從 IMDB 首頁前往指定電影資訊頁面過程中,手動用滑鼠、鍵盤所操控的動作:

  1. 前往 IMDB 首頁
  2. 在搜尋欄位輸入電影名稱
  3. 點選搜尋按鈕
  4. 點選符合度最高的連結(最上方)
  5. 來到指定電影資訊頁面

前往 IMDB 首頁

在搜尋欄位輸入電影名稱

點選搜尋按鈕

點選符合度最高的連結(最上方)

來到指定電影資訊頁面

盤點要使用到的方法

Python 的 selenium 模組與 R 語言的 RSelenium 套件中對方法的命名不同,因此我們分開盤點。

Python 要使用的方法

  • driver.get() :前往 IMDB 首頁
  • driver.find_element_by_xpath()driver.find_element_by_css_selector() :定位搜尋欄位、搜尋按鈕與搜尋結果連結
  • driver.current_url :取得當下瀏覽器的網址
  • elem.send_keys() :輸入電影名稱
  • elem.click() :按下搜尋按鈕與連結

R 語言要使用的方法

  • remDr$navigate() :前往 IMDB 首頁
  • remDr$findElement(using = "css")remDr$findElement(using = "xpath"):定位搜尋欄位、搜尋按鈕與搜尋結果連結
  • remDr$getCurrentUrl() :取得當下瀏覽器的網址
  • elem$sendKeysToElement() :輸入電影名稱
  • elem$clickElement() :按下搜尋按鈕與連結

擷取多部電影資訊的函數

接著建立 get_movies() 函數,輸入電影名稱、利用 Selenium 瀏覽到指定電影頁面最後再呼叫一開始修飾過的 get_movie_info() 函數,然後將多部電影的結果儲存到 Python 的 dict 中或者 R 語言的 list 中並以電影名稱作為標籤。

Python

from selenium import webdriver
from random import randint
import time
from pyquery import PyQuery as pq

def get_movie_info(movie_url):
  """
  Get movie info from certain IMDB url
  """
  # 指定電影資訊的 CSS 選擇器
  rating_css = "strong span"
  genre_css = ".subtext a"
  poster_css = ".poster img"
  cast_css = ".primary_photo+ td a"
  
  movie_doc = pq(movie_url)
  # 擷取資訊
  rating_elem = movie_doc(rating_css)
  movie_rating = float(rating_elem.text())
  genre_elem = movie_doc(genre_css)
  movie_genre = [x.text.replace("\n", "").strip() for x in genre_elem]
  movie_genre.pop()
  movie_poster_elem = movie_doc(poster_css)
  movie_poster = movie_poster_elem.attr('src')
  movie_cast_elem = movie_doc(cast_css)
  movie_cast = [x.text.replace("\n", "").strip() for x in movie_cast_elem]
  
  # 回傳資訊
  movie_info = {
      "rating": movie_rating,
      "genre": movie_genre,
      "poster": movie_poster,
      "cast": movie_cast
  }
  return movie_info

def get_movies(*args):
  """
  Get multiple movies' info from movie titles
  """
  imdb_home = "https://www.imdb.com/"
  driver = webdriver.Firefox(executable_path="YOURGECKODRIVERPATH") # Use Firefox
  movies = dict()
  for movie_title in args:
    # 前往 IMDB 首頁
    driver.get(imdb_home)
    # 定位搜尋欄位
    search_elem = driver.find_element_by_css_selector("#navbar-query")
    # 輸入電影名稱
    search_elem.send_keys(movie_title)
    # 定位搜尋按鈕
    submit_elem = driver.find_element_by_css_selector("#navbar-submit-button .navbarSprite")
    # 按下搜尋按鈕
    submit_elem.click()
    # 定位搜尋結果連結
    first_result_elem = driver.find_element_by_css_selector("#findSubHeader+ .findSection .odd:nth-child(1) .result_text a")
    # 按下搜尋結果連結
    first_result_elem.click()
    # 呼叫 get_movie_info()
    current_url = driver.current_url
    movie_info = get_movie_info(current_url)
    movies[movie_title] = movie_info
    time.sleep(randint(3, 8))
  driver.close()
  return movies

movies = get_movies("Avengers: Infinity War", "Black Panther")
print(movies["Avengers: Infinity War"])
print(movies["Black Panther"])
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
## {'rating': 8.8,
##  'genre': ['Action', 'Adventure', 'Fantasy'],
##  'poster': 'https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MTUwNTM@._V1_UX182_CR0,0,182,268_AL_.jpg',
##  'cast': ['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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

R 語言

# install.packages(c("rvest", "magrittr"))
if (!require(RSelenium)) {
  install.packages("devtools")
  devtools::install_github("johndharrison/binman")
  devtools::install_github("johndharrison/wdman")
  devtools::install_github("ropensci/RSelenium")
}
library(RSelenium)
library(rvest)
library(magrittr) # 使用 %>% 運算子

get_movie_info <- function(movie_url) {
  # 指定電影資訊的 CSS 選擇器
  rating_css <- "strong span"
  genre_css <- ".subtext a"
  poster_css <- ".poster img"
  cast_css <- ".primary_photo+ td a"
  
  movie_doc <- movie_url %>% 
    read_html()
  # 擷取資訊
  movie_rating <- movie_doc %>% 
    html_nodes(css = rating_css) %>%
    html_text() %>%
    as.numeric()
  movie_genre <- movie_doc %>% 
    html_nodes(css = genre_css) %>% 
    html_text() %>% 
    trimws() %>% 
    gsub(pattern = "\n", replacement = "")
  movie_genre_len <- length(movie_genre)
  movie_genre <- movie_genre[-movie_genre_len]
  movie_poster <- movie_doc %>% 
    html_nodes(css = poster_css) %>% 
    html_attr("src")
  movie_cast <- movie_doc %>% 
    html_nodes(css = cast_css) %>% 
    html_text() %>% 
    trimws() %>% 
    gsub(pattern = "\n", replacement = "")
  
  # 回傳資訊
  movie_info <- list(
    "rating" = movie_rating,
    "genre" = movie_genre,
    "poster" = movie_poster,
    "cast" = movie_cast
  )
  return(movie_info)
}

get_movies <- function(movie_titles) {
  remDr <- remoteDriver(remoteServerAddr = "localhost" 
                        , port = 4444L
                        , browserName = "firefox"
  )
  remDr$open()
  movies <- list()
  for (i in 1:length(movie_titles)) {
    # 前往 IMDB 首頁
    remDr$navigate("https://www.imdb.com")
    # 定位搜尋欄位
    search_elem <- remDr$findElement(using = "css", "#navbar-query")
    # 輸入電影名稱
    search_elem$sendKeysToElement(list(movie_titles[i]))
    # 定位搜尋按鈕
    submit_elem <- remDr$findElement(using = "css", "#navbar-submit-button .navbarSprite")
    # 按下搜尋按鈕
    submit_elem$clickElement()
    # 定位搜尋結果連結
    first_result_elem <- remDr$findElement(using = "css", "#findSubHeader+ .findSection .odd:nth-child(1) .result_text a")
    # 按下搜尋結果連結
    first_result_elem$clickElement()
    # 呼叫 get_movie_info()
    current_url <- remDr$getCurrentUrl()
    movie_info <- get_movie_info(current_url[[1]])
    movies[[i]] <- movie_info
    Sys.sleep(sample(3:8, size = 1))
  }
  remDr$close()
  names(movies) <- movie_titles
  return(movies)
}

movie_titles <- c("Avengers: Infinity War", "Black Panther")
movies <- get_movies(movie_titles)
movies$`Avengers: Infinity War`
movies$`Black Panther`
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
## movies$`Avengers: Infinity War`
## $rating
## [1] 8.8
## $genre
## [1] "Action"    "Adventure" "Fantasy"
## $poster
## [1] "https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MTUwNTM@._V1_UX182_CR0,0,182,268_AL_.jpg"
## $cast
##  [1] "Robert Downey Jr."    "Chris Hemsworth"      "Mark Ruffalo"         ## "Chris Evans"         
##  [5] "Scarlett Johansson"   "Don Cheadle"          "Benedict Cumberbatch" "Tom Holland"         
##  [9] "Chadwick Boseman"     "Zoe Saldana"          "Karen Gillan"         "Tom Hiddleston"      
## [13] "Paul Bettany"         "Elizabeth Olsen"      "Anthony Mackie"
## movies$`Black Panther`
## $rating
## [1] 7.5
## $genre
## [1] "Action"    "Adventure" "Sci-Fi"
## $poster
## [1] "https://m.media-amazon.com/images/M/MV5BMTg1MTY2MjYzNV5BMl5BanBnXkFtZTgwMTc4NTMwNDI@._V1_UX182_CR0,0,182,268_AL_.jpg"
## $cast
##  [1] "Chadwick Boseman"  "Michael B. Jordan" "Lupita Nyong'o"    "Danai Gurira"     
##  [5] "Martin Freeman"    "Daniel Kaluuya"    "Letitia Wright"    "Winston Duke"     
##  [9] "Sterling K. Brown" "Angela Bassett"    "Forest Whitaker"   "Andy Serkis"     
## [13] "Florence Kasumba"  "John Kani"         "David S. Lee"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

小結

在這個小節中我們簡介如何使用 selenium 自動化網頁測試工具連接 Python 與 R 語言,協助兩者的程式碼操控瀏覽器,進而前往含有指定電影資訊的網頁。

延伸閱讀