MAGAZINE
ルーターマガジン
chrome_remoteでframeやiframe内の要素をスクレイピングする方法|Facebookシェア数の取得手順
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
解説
上記サンプルコードは以下のような処理になっています。
- クロール対象のページに遷移する
- 対象iframeのname属性の値を取得する
- 対象iframeのname属性の値から対象iframeのcontextIdを取得する
- contextIdを指定してjavascriptを実行しHTMLを取得する
- HTML内からfacebookのShare数を取得する
以下でそれぞれ解説します。
1. クロール対象のページに遷移する
@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の指定を忘れないでくださいね。
CONTACT
お問い合わせ・ご依頼はこちらから