MAGAZINE

ルーターマガジン

クローリング/スクレイピング

chrome_remoteでiframe内のコンテンツを取得する方法

2023.10.13
Pocket

はじめまして、今年の5月から入社しましたエンジニアのmiyakawaです。

WEBスクレイピングにはブラウザを直接立ち上げないと情報を収集できないサイトがあります。そこでブラウザの自動操縦できるライブラリが必要になります。ここではライブラリの1つであるchrome_remoteというgemライブラリを使用します。chrome_remoteの説明、使用方法はこちらのリンクをご覧ください。 chrome_remoteという選択(脱Selenium大作戦)

ブラウザの自動操縦にはHTML中のiframeを扱うことがあります。 例えば今週のルーターのプレスリリース記事DX Forum 2023に株式会社ルーターCTO山本ゆうごが登壇しましたのfacebookのシェア数を取得したいです。 しかし、この部分は画像でもわかるようにiframeで作られているため、URL先のHTMLだけでは取得することができません。

そこでchrome_remoteの機能を使ってiframeの中にある情報であるfacebookのシェア数を取得する方法を紹介します。

環境

Ruby 3.1.3

chrome_remote 0.3.0

手法

前準備

まずchrome_remoteを使用するためにchromeをdebugging-portつきでgoogle chromeを起動します。

google-chrome --remote-dubgging-port=9222 --disable-site-isolation-trials

Macの場合は次のコマンドになります。

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --disable-site-isolation-trials

最後にhttp://localhost:9222/jsonにアクセスできるかも確認しましょう。アクセスできたらdebugging-portつきでgoogle chromeを起動できています。

サンプルコード

シェア数を取得するコードになります。

require 'chrome_remote'
require 'Nokogiri'

def wait_for_complete
  loop do
    sleep(1)
    response = @chrome.send_cmd 'Runtime.evaluate', expression: 'document.readyState;'
    break if response['result']['value'] == 'complete'
  end
end

@chrome = ChromeRemote.client
context_datas = []
@chrome.send_cmd('Runtime.enable')
@chrome.on('Runtime.executionContextCreated') do |params|
  context_datas << params['context']
end
@chrome.send_cmd('Page.navigate', url: 'https://rooter.jp/news/dx-forum-2023/')
wait_for_complete
sleep(10)
js = 'document.querySelector(".fb_iframe_widget iframe").name;'
response = @chrome.send_cmd('Runtime.evaluate', expression: js)
fb_iframe_tag_name = response['result']['value']
fb_sharing_frame = @chrome.send_cmd('Page.getFrameTree')['frameTree']['childFrames'].find do |child_frame|
  child_frame['frame']['name'] == fb_iframe_tag_name
end

fb_sharing_context_datas = context_datas.select do |context_data|
  context_data['auxData']['frameId'] == fb_sharing_frame['frame']['id']
end
js = 'document.documentElement.outerHTML;'
fb_sharing_context_datas.each do |fb_sharing_context_data|
  response = @chrome.send_cmd('Runtime.evaluate', expression: js, contextId: fb_sharing_context_data['id'])
  next if response.nil?

  html = response['result']['value']
  doc = Nokogiri::HTML.parse(html)
  puts fb_sharing_number = doc.at_css('#icon-button ._5n6h').text
end

出力結果は画像と同じくシェア数20を取得できています。

$ ruby fb_number.rb
20

ではコードの流れを順に説明していきます。

execution context

chrome_remoteでiframeの操作のイメージを掴むためにDevtoolsのConsoleの画面を説明します。topと書かれているプルダウンの名称はexecution context selectorというものです。execution contextとは実行環境のことで、このプルダウンを開くと、execution contextがDevToolsのConsole画面の赤枠部分のように表示され、iframeを操作できます。 chrome_remoteの場合、このexecutionContextを選択するためにはexecutionContextIdというパラメータを用いて、どのフレームにJavaScriptを実行するかを選択することができます。executionContextIdの取得するには次の設定をする必要があります。

@chrome.send_cmd('Runtime.enable')
@chrome.on('Runtime.executionContextCreated') do |params|
  context_datas << params['context']
end

次に指定したiframeでJavaScriptを実行するにはexecutionContextIdに紐づくiframeを見つける必要があります。

executionContextIdを紐づけるframeId

ではどのようにexecutionContextIdに紐づくiframeを見つけるのかというと、executionContextのパラメータにはframeIdというものがあり、これを用いてexecutionContextIdが結びつけることができます。 frameIdとは現在開いているページやiframeに与えられる固有のIDのことです。iframeのframeIdの取得方法は@chrome.send_cmd('Page.getFrameTree')の戻り値に含まれています。

@chrome.send_cmd('Page.getFrameTree')
=> 
{"frameTree"=>
  {"frame"=>
    {"id"=>"1D4E052547FFFA307C1A505DA511D6E7",
     "loaderId"=>"639FCD14F0AB2411803FE599356C83B6",
     "url"=>"https://rooter.jp/news/dx-forum-2023/",
     "domainAndRegistry"=>"rooter.jp",
     "securityOrigin"=>"https://rooter.jp",
     "mimeType"=>"text/html",
     "adFrameStatus"=>{"adFrameType"=>"none"},
     "secureContextType"=>"Secure",
     "crossOriginIsolatedContextType"=>"NotIsolated",
     "gatedAPIFeatures"=>[]},
   "childFrames"=>
    [{"frame"=>
       {"id"=>"81847BCAFBFB3851C1BB55698DF17928",
        "parentId"=>"1D4E052547FFFA307C1A505DA511D6E7",
        "loaderId"=>"E7AB656D6B83E582A9ED239283282DA0",
        "name"=>"3tipt|https://rooter.jp/news/dx-forum-2023/",
        "url"=>"https://b.hatena.ne.jp/entry/button/?url=https%3A%2F%2Frooter.jp%2Fnews%2Fdx-forum-2023%2F&layout=simple-balloon",
        "domainAndRegistry"=>"hatena.ne.jp",
        "securityOrigin"=>"https://b.hatena.ne.jp",
        "mimeType"=>"text/html",
        "adFrameStatus"=>{"adFrameType"=>"none", "explanations"=>[]},
        "secureContextType"=>"Secure",
        "crossOriginIsolatedContextType"=>"NotIsolatedFeatureDisabled",
        "gatedAPIFeatures"=>[]}},
     {"frame"=>
       {"id"=>"4208D48B34E2C98F49BDB1C0DFE0644D",
        "parentId"=>"1D4E052547FFFA307C1A505DA511D6E7",
        "loaderId"=>"8F17C04EA07EF29B016E1D7932161690",
        "name"=>"",
        "url"=>"https://www.youtube.com/embed/5Ll35FuML5M?si=tbsoFcJUE2tE0XK_",
        "domainAndRegistry"=>"youtube.com",
        "securityOrigin"=>"https://www.youtube.com",
        "mimeType"=>"text/html",
        "adFrameStatus"=>{"adFrameType"=>"none", "explanations"=>[]},
        "secureContextType"=>"Secure",
        "crossOriginIsolatedContextType"=>"NotIsolatedFeatureDisabled",
        "gatedAPIFeatures"=>[]}},
.
.
.
     {"frame"=>
       {"id"=>"9F02CCB4DF198722CFACD3901F44C6E4",
        "parentId"=>"1D4E052547FFFA307C1A505DA511D6E7",
        "loaderId"=>"1D0B4A640CB55CB605D2CC05AFEAE6F8",
        "name"=>"rufous-sandbox",
        "url"=>"about:blank",
        "domainAndRegistry"=>"",
        "securityOrigin"=>"://",
        "mimeType"=>"text/html",
        "adFrameStatus"=>{"adFrameType"=>"none", "explanations"=>[]},
        "secureContextType"=>"Secure",
        "crossOriginIsolatedContextType"=>"NotIsolated",
        "gatedAPIFeatures"=>[]}}]}}

childFramesのFrameのidがiframeのframeIdになります。 またどのiframeを使用するかは、以下のコードのように親ページのHTMLから導き出せます。

js = 'document.querySelector(".fb_iframe_widget iframe").name;'
response = @chrome.send_cmd('Runtime.evaluate', expression: js)
fb_iframe_tag_name = response['result']['value']
fb_sharing_frame = @chrome.send_cmd('Page.getFrameTree')['frameTree']['childFrames'].find do |child_frame|
  child_frame['frame']['name'] == fb_iframe_tag_name
end

このことからiframeのframeIdを取得することができました。 次にframeIdに紐づくexecutionContextIdを取得します。

fb_sharing_context_datas = context_datas.select do |context_data|
  context_data['auxData']['frameId'] == fb_sharing_frame['frame']['id']
end

executionContextの情報は以下の通りです。

context_datas
=> 
[{"id"=>48,
  "origin"=>"https://rooter.jp",
  "name"=>"",
  "uniqueId"=>"-2409670390877541474.-8501997218644170573",
  "auxData"=>{"isDefault"=>true, "type"=>"default", "frameId"=>"1D4E052547FFFA307C1A505DA511D6E7"}},
 {"id"=>49,
  "origin"=>"https://rooter.jp",
  "name"=>"",
  "uniqueId"=>"6517981812984711340.5131865020398391733",
  "auxData"=>{"isDefault"=>true, "type"=>"default", "frameId"=>"81847BCAFBFB3851C1BB55698DF17928"}},
.
.
.
{"id"=>70,
  "origin"=>"https://rooter.jp",
  "name"=>"",
  "uniqueId"=>"-3584878143739262143.8401766484312389282",
  "auxData"=>{"isDefault"=>true, "type"=>"default", "frameId"=>"9F02CCB4DF198722CFACD3901F44C6E4"}}]

注意点としては@chrome.send_cmd('Page.getFrameTree')を実効しないとexecutionContextの情報が取得できません。

JavaScript実行時にExecutionContextIdをcontextIdパラメータに渡す

あとは取得したExecutionContextIdを用いることで、フレームを選択してJavaScriptを実行できるようになりました。Runtime.evaluateにcontextIdのパラメータにExecutionContextIdを与えることで指定したiframeの操作が可能になります。

js = 'document.documentElement.outerHTML;'
@chrome.send_cmd('Runtime.evaluate', expression: js, contextId: fb_sharing_context_data['id'])

@chrome.send_cmd('Runtime.evaluate', expression: [jsコード]と書くことでJavaScriptのコードを実行できます。そこに追加でcontextIdのパラメータを渡すことでiframeにもJavaScriptのコードを実行することができるということです。

終わりに

少し複雑になってしまいましたが少しでもchrome_remoteの問題が解決したら幸いです。 また、これを機にchrome_remoteでの自動化を試してみてはいかがでしょうか。

Pocket

CONTACT

お問い合わせ・ご依頼はこちらから