Seleniumを使わず動的ページをスクレイピングする

作者

Shintaro Watanabe

公開

2025年4月27日

はじめに

RSA Conferenceの講演の座席予約は Full Agendaから行ないます。17コマに300以上のセッションが立ち並びますから、興味のあるものを厳選しなくてはいけません。ところがRSACのサイトはページの応答速度が芳しくなく、一覧性もよくないため、選定作業は難儀します。それで私は毎年ウェブスクレイピングしてExcelファイルに書き出し、それを見ながら選定していました。

今年はデザインに変更が入り、一覧ページから抽出できる情報が少なくなりました。たとえば概要(Abstract)を得るためには、個別のセッションのページにもアクセスする必要があります。さらに、これらのページはJavaScriptで動的に描画されるので、単純にHTTPリクエストを投げるだけでは適切なスクレイピングができません。

観念してSeleniumの使用を検討していたところ、rvestread_html_live()という関数が生まれていることを知りました(2024年2月リリースのrvest 1.0.4から利用可能だった)。

既に「rvestで動的サイトをスクレイピングする(Seleniumを使わずに)」という記事があります。この記事も同じ趣向で、RSACのページを題材にしてread_html_live()を使ったデータの取得を取り上げます。

講演一覧から個別ページへのリンクを抽出する

まずはFull Agendaから一覧ページを取得することを考えましょう。たとえば28日月曜のTrack Sessionを取得するなら、次のようなURLを組み立てます。

pacman::p_load(tidyverse, httr2, rvest, chromote)
base_url <- "https://path.rsaconference.com/"

url <-  url_parse(base_url)
url$path <- "flow/rsac/us25/FullAgenda/page/catalog"
url$query <- lst(
  "tab.day" = "20250428",
  "search.typeformat" = "16330260516930029l9w"
)
  
url_0428 <- url_build(url)
url_0428
[1] "https://path.rsaconference.com/flow/rsac/us25/FullAgenda/page/catalog?tab.day=20250428&search.typeformat=16330260516930029l9w"

次に、一覧ページへのリクエストを発生させて、個別ページへのリンクを抽出します。それには開発者ツールを使い、必要なクラス名などを把握しておかなくてはいけません。ここでは分量を節約するため、最初の3ページ分だけ出力することにします。

session <- read_html_live(url_0428)
Sys.sleep(5)
# 「CLICK HERE TO VIEW MORE SESSIONS」ボタンをクリック
session$click("button.mdBtnR.mdBtnR-primary.show-more-btn") 
Sys.sleep(5)
details_path <- 
  session$html_elements(".catalog-result-title.session-title.rf-simple-flex-frame") |> 
  html_elements("a") |> 
  html_attr("href") |> 
  head(3)

details_path
[1] "/flow/rsac/us25/FullAgenda/page/catalog/session/1727292939934001IAdE"
[2] "/flow/rsac/us25/FullAgenda/page/catalog/session/1727005184409001IAvU"
[3] "/flow/rsac/us25/FullAgenda/page/catalog/session/1725992567518001moQM"

個別ページをファイルに書き出す

書き出すディレクトリ名は「details_files」とし、存在しなければ作るようにします。 fsパッケージは、Tidyverseとともにインストールされています。

walk()やwalk2()purrrパッケージに 含まれる関数で、map()と異なり戻り値がありません。ファイル操作のような副作用が発生するときに使用します。 walk2()の中で、個別ページの

タグを抽出し、文字列ベクトルから文字列に変換し、最後にファイルに書き出しています。単にhtmlオブジェクトを丸ごとテキストにするには迂遠なので、read_html_live()の機能拡充を期待したいところです。

フォーミュラ式(~{...})の代わりにR 4.1の無名関数(\(x) {...})を使ってみましたが、これはちょっと格好をつけただけでして、普段はフォーミュラ式を使っています。

dir_name <- "detail_files"
if (!fs::dir_exists(dir_name)) {
  fs::dir_create(dir_name)
}
details_filepath <- str_c(dir_name, "/", basename(details_path), ".htm")
details_url <- str_c("https://path.rsaconference.com", details_path)

walk2(details_url,
      details_filepath,
      \(url, filepath) {
          session <- read_html_live(url)
          Sys.sleep(5)
          session$html_elements("html") |> 
            map_chr(as.character) |> 
            str_c(collapse = "\n") |> 
            write_file(filepath)
      }
)

個別ページから要素を抽出する

下に掲げるextract_event()は、上で書き出した個別ページから必要な要素を抽出し、読みやすいように加工したものです。これも開発者ツールを使ってクラスを見つけるところから始めなくてはいけません。

extract_event <- function(html_file_path){
  pacman::p_load(tidyverse, rvest)
  html <- read_html(html_file_path)
  date <- 
    html |> 
    html_element(".session-date") |> 
    html_text() |> 
    str_split_i(",", 2) |> 
    str_c(" 2025") |> 
    str_trim() |> 
    mdy(locale = "C")  
  
  time_start <- 
    html |> 
    html_element(".session-time") |> 
    html_text() |> 
    str_extract_all("\\d{1,2}:\\d{2} [AP]M") |> 
    pluck(1, 1) |> 
    parse_time()  

  time_end <- 
    html |> 
    html_element(".session-time") |> 
    html_text() |> 
    str_extract_all("\\d{1,2}:\\d{2} [AP]M") |> 
    pluck(1, 2) |> 
    parse_time()
  
  topic <- 
    html |> 
    html_element(".attribute-TopicTrack") |> 
    html_text() |> 
    str_split_i(":", 2) |> 
    str_trim() |> 
    str_replace("Governance,", "Governance__") |>
    str_replace(", ", "\n") |>
    str_replace("Governance__", "Governance,")
    
  level <- 
    html |> 
    html_element(".attribute-SessionClassification") |> 
    html_text() |> 
    str_split_i(":", 2) |> 
    str_trim()
  
  id <- 
    html |> 
    html_element(".title-text") |> 
    html_text() |>
    str_replace_all(c("\\[" = "___", "\\]" = "___")) |>
    str_split_i("___", 2)
  
  title <- 
    html |> 
    html_element(".title-text") |> 
    html_text() |> 
    str_replace_all(c("\\[" = "___", "\\]" = "___")) |>
    str_split_i("___", 1) |> 
    str_trim() |> 
    str_sub(1, -2) |> 
    str_trim()

  speaker <- 
    html |> 
    html_elements(".profile-container") |> 
    html_text() |> 
    str_flatten("\n")

  abstract <- 
    html |> 
    html_element(".abstract-component") |> 
    html_text()

  basedir <- "https://path.rsaconference.com/flow/rsac/us25/FullAgenda/page/catalog/session/"
  linkurl <- 
    html_file_path |> 
    fs::path_file() |> 
    fs::path_ext_remove() |> 
    (\(text) {str_c(basedir, text)})()
    
  tribble(
    ~date, ~time_start, ~time_end, ~topic, ~level, ~id, ~title, ~speaker, ~abstract, ~linkurl,
    date, time_start, time_end, topic, level, id, title, speaker, abstract, linkurl
  ) 
}

個別ファイルをデータフレームに集約

以上で準備が整ったので、1つのデータフレームに載せます。このときに、先に定義した関数extract_event()を使っています。

本来はCSVファイルに出力して眺めますが、ここでは4列だけ抜き出したデータフレームを表示させるにとどめます。

html_files <- fs::dir_ls(path = dir_name, glob = "*.htm")

df_events <- 
  html_files |> 
  map_df(extract_event)

df_events |> 
  select(date, time_start, id, title)
# A tibble: 3 × 4
  date       time_start id      title                                           
  <date>     <time>     <chr>   <chr>                                           
1 2025-04-28 08:30      TPV-M01 Cracks in the Fortress: How Major Companies Lea…
2 2025-04-28 08:30      HTA-M01 Beyond the Black Box: Revealing Adversarial Neu…
3 2025-04-28 08:30      DSA-M01 A Stuxnet Moment for Supply Chain Security?     
# write_excel_csv(df_events, file = "rsac2025_tracks.csv")

おわりに

以上で見てきたように、read_html_live()を使うと動的ページのスクレイピングも割と気軽に実施できます。ただ、まだ日が浅い関数なので、不足を感じる機能もあります。たとえばwaitを指定するオプションは搭載してほしいところです。

read_html_live()は、chromoteというChrome DevTools Protocol(CDP)を扱うパッケージをバックエンドとしています。CDPはSeleniumが使うWebDriverよりも低レベルなAPIで、直接ブラウザーを制御できるものです。

よって、read_html_live()に不足を感じたときには、chromoteパッケージを触りに行くことになるでしょう。