Rのパッケージ chromote

Rのパッケージ chromote を確認します。

chromoteは、RからGoogle Chrome(またはChromium系ブラウザ)をプログラム的に操作するためのパッケージです。

ボタンをクリックして、JavaScriptによって動的に表示されるデータを取得

HTMLページの作成

まずは、テスト用のHTMLページを作成してローカル環境に保存します。

作成しましたテスト用ページはこちらで確認してください。

# HTMLソースコードを文字列として定義
chromote_samplepage01 <- '
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Dynamic Page Test</title>
</head>
<body>
    <h1>JavaScript操作テスト</h1>
    
    <!-- 初期状態のテキスト -->
    <div id="data-container">初期データ(未クリック)</div>
    
    <!-- クリックするとJavaScriptを実行するボタン -->
    <button id="load-btn" onclick="loadData()">データを読み込む</button>

    <script>
        function loadData() {
            // ボタンがクリックされたらテキストを書き換える
            document.getElementById("data-container").innerText = "JavaScriptによって読み込まれたシークレットデータ";
        }
    </script>
</body>
</html>
'

# 一時ファイルとしてHTMLを保存
tmp_html <- tempfile(fileext = ".html")
writeLines(chromote_samplepage01, tmp_html)

今回の目的は、ボタンをクリックした後に表示される「JavaScriptによって読み込まれたシークレットデータ」という文字列を取得することです。

Rコード

chromoteを利用して、裏側で実際のChromeブラウザを動かし、「ボタンをクリックさせる」という操作をJavaScript経由で実行し、変化後のデータを取得します。

library(chromote)

# 裏側でHeadless Chromeのセッションを立ち上げる
session <- ChromoteSession$new()

# ローカルファイルのパスをURL形式(file://...)に変換してブラウザで開く
file_url <- paste0("file://", normalizePath(tmp_html, winslash = "/"))
session$Page$navigate(file_url)

# ページの読み込み完了を待つ
Sys.sleep(1)

# ブラウザ上でJavaScriptを実行し、ボタンをクリックさせる
session$Runtime$evaluate('document.getElementById("load-btn").click();')

# ボタンクリック後、変化した要素のテキストをJavaScript経由で取得する
res <- session$Runtime$evaluate('document.getElementById("data-container").innerText;')

# 取得した結果から値を抽出
text_chromote <- res$result$value

結果の確認

cat("chromoteでの取得結果:\n", text_chromote, "\n\n")

# 最後にセッション(ブラウザ)を閉じる
session$close()
chromoteでの取得結果:
 JavaScriptによって読み込まれたシークレットデータ 

[1] TRUE

実際のブラウザが裏で動いているため、ボタンクリック(click())が発火し、JavaScriptの関数が実行されました。その結果、目的のテキストを取得できています。

テキストボックスに文字を入力し、検索ボタンを押す

HTMLページの作成

作成しましたテスト用ページはこちらで確認してください。

chromote_samplepage02 <- '
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Input Test</title>
</head>
<body>
    <h1>検索フォーム入力テスト</h1>
    
    <!-- 文字を入力するテキストボックス -->
    <input type="text" id="search-box" value="">
    
    <!-- 検索ボタン -->
    <button id="search-btn" onclick="executeSearch()">検索する</button>

    <br><br>
    <!-- 結果が表示されるエリア -->
    <div id="result-area">【検索結果はここに表示されます】</div>

    <script>
        function executeSearch() {
            // テキストボックスの中身を取得
            var keyword = document.getElementById("search-box").value;
            
            // 入力があった場合のみ、結果を書き換える
            if(keyword !== "") {
                document.getElementById("result-area").innerText = 
                    "「" + keyword + "」の極秘データをデータベースから取得しました";
            } else {
                document.getElementById("result-area").innerText = 
                    "キーワードが入力されていません。";
            }
        }
    </script>
</body>
</html>
'

tmp_html <- tempfile(fileext = ".html")
writeLines(chromote_samplepage02, tmp_html)

今回の目的は、テキストボックスに任意のキーワード(例:“地球温暖化”)を入力してボタンを押し、動的に生成される検索結果のテキストを取得することです。

Rコード

chromoteを利用して、JavaScript経由で「テキストボックスに文字を代入する」→「検索ボタンをクリックする」という一連の操作を行います。

library(chromote)

# セッションを立ち上げてページを開く
session <- ChromoteSession$new()
file_url3 <- paste0("file://", normalizePath(tmp_html, winslash = "/"))
session$Page$navigate(file_url3)
Sys.sleep(1)

# JavaScript経由でテキストボックス(inputタグ)に「地球温暖化」という文字を入力する
session$Runtime$evaluate('document.getElementById("search-box").value = "地球温暖化";')

# 検索ボタンをクリックさせる
session$Runtime$evaluate('document.getElementById("search-btn").click();')

# 処理が完了するのを少し待つ
Sys.sleep(1)

# ボタンクリック後の結果を取得する
res_input <- session$Runtime$evaluate('document.getElementById("result-area").innerText;')

結果の確認

cat("chromoteでの取得結果:\n", res_input$result$value, "\n\n")

# 最後にセッションを閉じる
session$close()
chromoteでの取得結果:
 「地球温暖化」の極秘データをデータベースから取得しました 

[1] TRUE

動的に生成されたテキストを取得できています。

同様に、Webスクレイピングにおける、「ログインIDとパスワードを入力してログインボタンを押す」「検索窓にキーワードを入れて検索する」といった操作が、chromoteの利用により可能となります。

<canvas>要素に描画された画像データ(ピクセルデータ)の取得

Webサイトによっては、HTMLのテキストとしてではなく、JavaScriptを使って<canvas>という領域にグラフ、画像、あるいは文字を表示している場合があります。

chromoteは「ブラウザの画像レンダリング機能」を使えるため、この「描かれた絵」を画像データとして抽出することができます。

HTMLページの作成

HTML上には空の<canvas>タグだけを用意し、JavaScriptでそこに「Secret_Code: XYZ-777」という文字を描画するページを作成します。

作成しましたテスト用ページはこちらで確認してください。

chromote_samplepage03 <- '
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Canvas Test</title>
</head>
<body>
    <h1>Canvas画像データ抽出テスト</h1>
    <p>以下の枠内には、JavaScriptによって文字が「画像として」描かれています。</p>
    
    <!-- Canvas要素(静的なHTML上ではただの空のタグです) -->
    <canvas id="myCanvas" width="300" height="100"></canvas>

    <script>
        // ブラウザの機能を使って、Canvasに文字を描画する
        var canvas = document.getElementById("myCanvas");
        var ctx = canvas.getContext("2d");
        
        // 背景を薄いグレーで塗りつぶす
        ctx.fillStyle = "#f0f0f0";
        ctx.fillRect(0, 0, 300, 100);
        
        // 青い文字でシークレットコードを描画する
        ctx.font = "24px sans-serif";
        ctx.fillStyle = "blue";
        ctx.fillText("Secret_Code: XYZ-777", 20, 50);
    </script>
</body>
</html>
'

tmp_html <- tempfile(fileext = ".html")
writeLines(chromote_samplepage03, tmp_html)

今回の目的は、Canvasに描かれた内容を、画像データ(Base64形式の文字列)として取得することです。

Rコード

chromoteは実際のブラウザエンジンを動かしているため、Canvasに描かれたピクセルデータを認識できます。

JavaScriptの toDataURL() という標準機能を使うことで、描画内容を「PNG画像のデータ(Base64文字列)」として取得することができます。

library(chromote)

session <- ChromoteSession$new()
file_url4 <- paste0("file://", normalizePath(tmp_html, winslash = "/"))
session$Page$navigate(file_url4)
Sys.sleep(1)

# JavaScript経由で、Canvasの描画内容をPNG画像データ(Base64形式)として取得する
res_canvas <- session$Runtime$evaluate('document.getElementById("myCanvas").toDataURL("image/png");')

# 取得した画像データ(文字列)を取り出す
base64_img <- res_canvas$result$value

結果の確認

cat("chromoteでの取得結果:\n")
# データが非常に長いため、最初の60文字だけを表示します
cat(substring(base64_img, 1, 60), " ... (以下略)\n\n")

# 最後にセッションを閉じる
session$close()
chromoteでの取得結果:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABkCAYAAA  ... (以下略)

[1] TRUE

“data:image/png;base64,…” という文字列は画像データですので、実際のPNGファイルとして出力してみます。

library(magick)
library(base64enc)

# 1. ヘッダーを削除してバイナリにデコード
pure_b64_string <- sub("^data:image/png;base64,", "", base64_img)
raw_data <- base64decode(pure_b64_string)

# 2. magickパッケージでバイナリを画像として読み込む(アスペクト比を維持してリサイズ)
img <- image_read(raw_data) %>% image_resize("600")

# 3. 画像を表示(または画像情報を確認)する
print(img)

# 4. バイナリデータを直接ファイルとして書き出す場合
# writeBin(raw_data, "secret_code.png")
  format width height colorspace matte filesize density
1    PNG   600    200       sRGB  TRUE        0   72x72
Figure 1

以上です。