MAGAZINE

ルーターマガジン

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

chrome_remoteでframeやiframe内の要素をスクレイピングする方法|Facebookシェア数の取得手順

2024.11.08
Pocket

frame,iframe内を参照するには

今回はスクレイピングを行う上で、frameやiframe内の要素を参照する方法を紹介します。 結論、Page.getFrameTree、Page.createIsolatedWorldを用いると、参照できるようになります。

今回はDX Forum 2023に株式会社ルーターCTO山本ゆうごが登壇しました にて、facebookのShare数を取得したいと思います。

実行環境

ruby 3.2.2
chrome_remote 1.3

実行準備

Linux/Windows(WSL2)の例

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

Macの例

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

chromeをremote-debugging-portつきで起動しておきます。

curl http://127.0.0.1:9222/json

を実行し、jsonが返ってきたら確認完了です。

注意していただきたいのが、iframeやframeを取得する際は --disable-site-isolation-trials オプションも指定しましょう。 指定しないと、frame構造を正しく取得できない場合があります。

サンプルコード

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

def dig_frame_context_id(frame_name)
  frames = @chrome.send_cmd('Page.getFrameTree')['frameTree']['childFrames']
  target_frame = frames.find{ |frame| frame.dig('frame', 'name') == frame_name}
  @chrome.send_cmd('Page.createIsolatedWorld', frameId: target_frame['frame']['id'])['executionContextId']
end

@chrome = ChromeRemote.client
@chrome.send_cmd('Runtime.enable')
@chrome.send_cmd 'Page.navigate', url:'https://rooter.jp/news/dx-forum-2023/'
wait_for_complete

js = "document.querySelector('.fb_iframe_widget iframe').name"
response = @chrome.send_cmd 'Runtime.evaluate', expression: js
frame_name = response.dig('result', 'value')

context_id = dig_frame_context_id(frame_name)
js = "document.getElementsByTagName('html')[0].outerHTML"
response = @chrome.send_cmd 'Runtime.evaluate', expression: js, contextId: context_id

html = response.dig('result', 'value')
doc = Nokogiri::HTML.parse(html)
puts doc.at_css('button#icon-button span._5n6h').text  #=> 27

解説

上記サンプルコードは以下のような処理になっています。

  1. クロール対象のページに遷移する
  2. 対象iframeのname属性の値を取得する
  3. 対象iframeのname属性の値から対象iframeのcontextIdを取得する
  4. contextIdを指定してjavascriptを実行しHTMLを取得する
  5. HTML内からfacebookのShare数を取得する

以下でそれぞれ解説します。

@chrome = ChromeRemote.client
@chrome.send_cmd('Runtime.enable')
@chrome.send_cmd 'Page.navigate', url:'https://rooter.jp/news/dx-forum-2023/'

Page.navigateは、Chrome DevTools Protocolのメソッドで、現在のページを指定されたURLへ移動します。

2. 対象iframeのname属性の値を取得する

js = "document.querySelector('.fb_iframe_widget iframe').name"
response = @chrome.send_cmd 'Runtime.evaluate', expression: js
frame_name = response.dig('result', 'value') #=> "f667e8e090a6beb0f"

参照したい要素をもつiframeを探します。 chrome remoteでは次のようにjsを実行することができます。

@chrome.send_cmd 'Runtime.evaluate', expression: js

3. 対象iframeのname属性の値から対象iframeのcontextIdを取得する

context_id = dig_frame_context_id(frame_name)

にてcontextIdを取得します。

def dig_frame_context_id(frame_name)
  frames = @chrome.send_cmd('Page.getFrameTree')['frameTree']['childFrames']
  target_frame = frames.find{ |frame| frame.dig('frame', 'name') == frame_name}
  @chrome.send_cmd('Page.createIsolatedWorld', frameId: target_frame['frame']['id'])['executionContextId']
end

Page.getFrameTreeは、Chrome DevTools Protocolのメソッドで、現在のページのすべてのframeの情報をツリー構造で返します。

@chrome.send_cmd('Page.getFrameTree')
#=>
{"frameTree"=>
  {"frame"=>
    {"id"=>"7F1C0794AB165008E80B036B1D7EBBB2",
     "loaderId"=>"E64B43C51A4F80CC4EF083C2E873C7A6",
     "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"=>"5EDF7A765B7038928D6E422BAB9FAE76",
        "parentId"=>"7F1C0794AB165008E80B036B1D7EBBB2",
        "loaderId"=>"8BE0E4E192518D0B1B597F5E4B6803EE",
        "name"=>"",
        "url"=>"https://www.youtube.com/embed/GygtJrN9_gI?si=5ThGnDMXSitqwwDO",
        "domainAndRegistry"=>"youtube.com",
        "securityOrigin"=>"https://www.youtube.com",
        "mimeType"=>"text/html",
        "adFrameStatus"=>{"adFrameType"=>"none", "explanations"=>[]},
        "secureContextType"=>"Secure",
        "crossOriginIsolatedContextType"=>"NotIsolatedFeatureDisabled",
        "gatedAPIFeatures"=>[]}},
     {"frame"=>
       {"id"=>"449FB1784E866963D88ED71ACFAF375E",
        "parentId"=>"7F1C0794AB165008E80B036B1D7EBBB2",
        "loaderId"=>"4B884188877349C1F9C1F653DF863D4D",
        "name"=>"",
        "url"=>"https://www.youtube.com/embed/rmdV1Zqz8K8?si=Ipp5CNW2PHIHfHEI",
        "domainAndRegistry"=>"youtube.com",
        "securityOrigin"=>"https://www.youtube.com",
        "mimeType"=>"text/html",
        "adFrameStatus"=>{"adFrameType"=>"none", "explanations"=>[]},
        "secureContextType"=>"Secure",
        "crossOriginIsolatedContextType"=>"NotIsolatedFeatureDisabled",
        "gatedAPIFeatures"=>[]}},
・
・
・
     {"frame"=>
       {"id"=>"51806929EBF558D32A41D7D2BD92BDED",
        "parentId"=>"7F1C0794AB165008E80B036B1D7EBBB2",
        "loaderId"=>"F42C913CCDF7785C2B9FCDD9C7EB11E6",
        "name"=>"f667e8e090a6beb0f",
        "url"=>
         "https://www.facebook.com/v2.7/plugins/share_button.php?app_id=&channel=https%3A%2F%2Fstaticxx.facebook.com%2Fx%2Fconnect%2Fxd_arbiter%2F%3Fversion%3D46%23cb%3Df0935fdad00f7c80e%26domain%3Drooter.jp%26is_canvas%3Dfalse%26origin%3Dhttps%253A%252F%252Frooter.jp%252Ffd10e6e3564a59b6f%26relation%3Dparent.parent&container_width=0&href=https%3A%2F%2Frooter.jp%2Fnews%2Fdx-forum-2023%2F&locale=en_US&sdk=joey&type=button_count",
        "domainAndRegistry"=>"facebook.com",
        "securityOrigin"=>"https://www.facebook.com",
        "mimeType"=>"text/html",
        "adFrameStatus"=>{"adFrameType"=>"none", "explanations"=>[]},
        "secureContextType"=>"Secure",
        "crossOriginIsolatedContextType"=>"NotIsolatedFeatureDisabled",
        "gatedAPIFeatures"=>[]}},
     {"frame"=>
       {"id"=>"9AE62DC8C0E1F9BE4A62A3EDD0C62284",
        "parentId"=>"7F1C0794AB165008E80B036B1D7EBBB2",
        "loaderId"=>"555EA75075FECD34F3A362E69E71A3B7",
        "name"=>"ff6d2c6da650c48b9",
        "url"=>
         "https://www.facebook.com/v2.7/plugins/share_button.php?app_id=&channel=https%3A%2F%2Fstaticxx.facebook.com%2Fx%2Fconnect%2Fxd_arbiter%2F%3Fversion%3D46%23cb%3Dfef86bbc87be8dbfb%26domain%3Drooter.jp%26is_canvas%3Dfalse%26origin%3Dhttps%253A%252F%252Frooter.jp%252Ffd10e6e3564a59b6f%26relation%3Dparent.parent&container_width=0&href=https%3A%2F%2Frooter.jp%2Fnews%2Fdx-forum-2023%2F&locale=en_US&sdk=joey&type=button_count",
        "domainAndRegistry"=>"facebook.com",
        "securityOrigin"=>"https://www.facebook.com",
        "mimeType"=>"text/html",
        "adFrameStatus"=>{"adFrameType"=>"none", "explanations"=>[]},
        "secureContextType"=>"Secure",
        "crossOriginIsolatedContextType"=>"NotIsolatedFeatureDisabled",
        "gatedAPIFeatures"=>[]}},
・
・
・
 frames = @chrome.send_cmd('Page.getFrameTree')['frameTree']['childFrames']
  target_frame = frames.find{ |frame| frame.dig('frame', 'name') == frame_name}

先ほど取得したnameタグが一致するframeを探します。

@chrome.send_cmd('Page.createIsolatedWorld', frameId: target_frame['frame']['id'])
#=>{"executionContextId"=>123}

Page.createIsolatedWorldは、Chrome DevTools Protocolのメソッドで、与えられたframeのcontextIdを取得します。

4. contextIdを指定してjavascriptを実行しHTMLを取得する

js = "document.getElementsByTagName('html')[0].outerHTML"
response = @chrome.send_cmd 'Runtime.evaluate', expression: js, contextId: context_id

contextIdを指定して、特定のframeに対してjsを実行することができます。

5. HTML内からfacebookのShare数を取得する

html = response.dig('result', 'value')
doc = Nokogiri::HTML.parse(html)
puts doc.at_css('button#icon-button span._5n6h').text #=> 27

最後に、Nokogiriを用いて目的の要素を取得すれば完成です!

あとがき

いかがだったでしょうか。私はchromeの起動オプションの--disable-site-isolation-trialsを指定し忘れ、frameの情報を正しく取得できず苦労しました。--disable-site-isolation-trialsの指定を忘れないでくださいね。

Pocket

CONTACT

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