shine-Notes

ゆるふわ思考ダンプ

Python+Selenium+Beautifulsoupでスクレイピング

夏の自由研究としてGarmin Connectの健康データスクレイピングに挑戦した。

本エントリは今回のユースケースに関する技術面の試行錯誤プロセスを記録する。

【サマリ】

  • 乱暴に言えば、大体Seleniumでイケる。
  • webの知識がなくともChrome開発者ツールを使い倒せば要素指定もなんとかなる。
  • Seleniumで手が届かないならBeautifulsoupの併用も検討する。

環境はMacBook Pro+VS Code+Google Chrome

【1:Seleniumを使う】

  • pipでSeleniumをインストール。ここでの苦労は前エントリを参照。
 pip3 install selenium 
  • driverを導入する。Homebrewを入れたのでこれを使うことにする。
 brew cask install chromedriver 

(「cask」を付ける必要があるらしい。)

さて、ひとまずサンプルでもやっているGoogleへのアクセス、検索をやってみる。

from selenium import webdriver 
from selenium.webdriver.common.keys import Keys 
from time import sleep 
driver = webdriver.Chrome() 
driver.get("https://www.google.co.jp/") 
driver.find_element_by_id("lst-ib").send_keys("shine-note") 
driver.find_element_by_id("lst-ib").send_keys(Keys.ENTER) 
sleep(10) 
driver.close() 

参考にした記事 Selenium ChromeDriver & PythonをMacで動かす準備メモ

無事動いた。

【2:Chrome開発者ツールを使う】

…が、当方html+cssはてんで疎く、前項のサンプルで入力文字列を選択する時に使った「lst-ib」が何なのかわからない。このままだと目的のサイト(Garmin Connect)をスクレイプしようにも応用が効かなくなるのは必至…

と、そういえば先日業務で偶然Chrome開発者ツールを使ったことを思い出し、もしかして…と気付きもう少し調べてみた。

4. 要素を見つける — Selenium Python Bindings 2 ドキュメント Seleniumで使用するXPathをChromeで取得する

なるほど!先程のサンプルではhtmlの要素idを指定していたのだ。 そしてid以外にも使える指定方法が幾つか有ることもわかった。

driver.find_element_by_id([id名]) 
driver.find_element_by_class([class名]) 
driver.find_element_by_xpath([xpath名]) 

こんだけ種類があればidが振られていない要素でも取得できる、つまり大体イケるはず!

【ところがどっこい】

さて、今回の自由研究の目標としているGarmin Connectのページ(https://connect.garmin.com/ja-JP/signin)を確認したが、 2つの問題にぶち当たった。

問題1:Selenium経由でうまくログインできない

ログインサイトのテキストボックスにusername,passwordの要素がある。 なのでサンプルコードと同様に指定してみたが動かない。

試行錯誤してみたがうまくいかず、結論から言うと回避策を取った。 github.com

GitHubに上がっているコードをカンニングしてみた所、ログイン時は別のURLを利用している。どうもリポジトリ自体の古さも鑑みるに、別のログインサイト?が有るものと推測する。 であればどっこい、このURLで一度ログインして、そのままSSOで目的のサイトに行けば良いじゃん!で無理やり回避かつ解決した。

#config 
loginURL = "https://sso.garmin.com/sso/login?service=https%3A%2F%2Fconnect.garmin.com%2FminExplore&webhost=olaxpw-connect00&source=https%3A%2F%2Fconnect.garmin.com%2Fen-US%2Fsignin&redirectAfterAccountLoginUrl=https%3A%2F%2Fconnect.garmin.com%2Fpost-auth%2Flogin&redirectAfterAccountCreationUrl=https%3A%2F%2Fconnect.garmin.com%2Fpost-auth%2Flogin&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso&locale=en_US&id=gauth-widget&cssUrl=https%3A%2F%2Fstatic.garmincdn.com%2Fcom.garmin.connect%2Fui%2Fcss%2Fgauth-custom-v1.1-min.css&clientId=GarminConnect&rememberMeShown=true&rememberMeChecked=false&createAccountShown=true&openCreateAccount=false&usernameShown=false&displayNameShown=false&consumeServiceTicket=false&initialFocus=true&embedWidget=false&generateExtraServiceTicket=false" 
##chromeを開いてGarmin ConnectのSSOページからログイン 
driver = webdriver.Chrome() 
driver.get(loginURL) 
sleep(1) 
driver.find_element_by_id('username').send_keys(myid) 
driver.find_element_by_id("password").send_keys(mypass) 
driver.find_element_by_id("password").send_keys(Keys.ENTER) 
sleep(1) 
##目的のURLへそのままSSO 
driver.get(getDataUrl+handlId+dayURL+typeURL) 

※どなたかこの問題を他の方法でクリアした方が居るようなら是非知恵をお借りしたい。

問題2:欲しい数値データがdriver.find_element_by***で取り出せない

今回目的だったデータの一例がこれ。

f:id:shinebalance:20180822215335p:plain:w300

だがこのデータ、idでも、classでも、取れなかった。 残念ながら理由は理解できていないのだが、おそらくは動的に生成されている要素で、ある程度試行してこれは自分には難しいと匙を投げた。 表示はされているのだ…ソースにだって数値は出ているのだ…と悩んでいる内に、スマートさは無いが方策を思いついた。 qiita.com 下調べの時点でSelenium以外にも選択肢が有るらしいことは目にしていたが、其の中でもBeautifulsoupはhtml丸ごとを取得しているらしき動きをしている。 これなら絶対に数値は取れる筈だ。

【3:Beautifulsoupも使う】

手順自体はさっきと一緒だ。Chrome開発者ツールで該当の要素を右クリックして「検証」し、ソースを確認する。

##html取得 
html = driver.page_source.encode('utf-8') 
##BeautifulSoup用にパース 
soup = BeautifulSoup(html, "html.parser") 
###歩数 
steps = str(soup.find(class_='h2')) 
###睡眠 
sleepdeep = soup.find(class_='data-bit color-24 truncate') 
sleeplight= soup.find(class_='data-bit color-21 truncate') 
 

スマートさの欠片も無いが、とにかく値は取れた。

【最終的に】

あとは取れた値からタグを取り、csvライブラリを使ってcsv書き出しである。 本当に何のスマートさもないが、最終的に1月分のデータをcsv化できたコードが以下。

#config(ハードコーディング含) 
loginURL = "https://sso.garmin.com/sso/login?service=https%3A%2F%2Fconnect.garmin.com%2FminExplore&webhost=olaxpw-connect00&source=https%3A%2F%2Fconnect.garmin.com%2Fen-US%2Fsignin&redirectAfterAccountLoginUrl=https%3A%2F%2Fconnect.garmin.com%2Fpost-auth%2Flogin&redirectAfterAccountCreationUrl=https%3A%2F%2Fconnect.garmin.com%2Fpost-auth%2Flogin&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso&locale=en_US&id=gauth-widget&cssUrl=https%3A%2F%2Fstatic.garmincdn.com%2Fcom.garmin.connect%2Fui%2Fcss%2Fgauth-custom-v1.1-min.css&clientId=GarminConnect&rememberMeShown=true&rememberMeChecked=false&createAccountShown=true&openCreateAccount=false&usernameShown=false&displayNameShown=false&consumeServiceTicket=false&initialFocus=true&embedWidget=false&generateExtraServiceTicket=false" 
myid = [任意ID] 
mypass = [任意pass] 
gURL = "https://connect.garmin.com/modern/" 
getDataUrl = "https://connect.garmin.com/modern/daily-summary/" 
handlId = [GarminConnectID] 
typeURL="/sleep" 
#以下必要に応じて変更 
ymUrl = "2018-05-" 
dateURL= "01" 
dayURL=ymUrl+dateURL 
 
 
#import 
from selenium import webdriver 
from selenium.webdriver.common.keys import Keys 
from time import sleep 
from bs4 import BeautifulSoup 
import csv 
 
 
#ログイン 
##chromeを開いてGarmin ConnectのSSOページからログイン 
driver = webdriver.Chrome() 
driver.get(loginURL) 
sleep(1) 
driver.find_element_by_id('username').send_keys(myid) 
driver.find_element_by_id("password").send_keys(mypass) 
driver.find_element_by_id("password").send_keys(Keys.ENTER) 
 
 
#日数処理 
cnt = 1 

#30回ループ 
for num in range(30):  

    #日数処理
    dateURL = '{0:02d}'.format(cnt)
    print(dateURL)
    dayURL=ymUrl+dateURL
    #データ取得ページへ移動
    sleep(1)
    driver.get(getDataUrl+handlId+dayURL+typeURL)
    ##読み込みエラー回避のためリフレッシュ後sleep
    driver.refresh()
    sleep(7)
    ##html取得
    html = driver.page_source.encode('utf-8')
    ##BeautifulSoup用にパース
    soup = BeautifulSoup(html, "html.parser")
    ###日付(確認用)
    print(dayURL)
    ###歩数
    steps     = str(soup.find(class_='h2'))
    ####数値のみ残すために整形
    steps = steps.replace('<div class="h2 data-bit">','')
    steps = steps.replace('</div>','')
    steps = steps.replace(',','')
    ###睡眠
    sleepdeep = soup.find(class_='data-bit color-24 truncate')
    sleeplight= soup.find(class_='data-bit color-21 truncate')

    #csv処理
    f = open('output.csv','a')
    writer=csv.writer(f)
    ##データをリストに保持
    csvlist = []
    csvlist.append(dayURL)
    csvlist.append(steps)
    csvlist.append(sleepdeep.string)#.stringで数値のみ取得
    csvlist.append(sleeplight.string)
    ##出力
    writer.writerow(csvlist)
    ##ファイルクローズ
    f.close()
    ##カウント
    cnt += 1 
 
sleep(1) 
driver.close() 
 

【オチ】

さて、上記のような投げやりなコードで終わった言い訳を1つ。

https://www.garmin.com/en-US/legal/security f:id:shinebalance:20180822215817p:plain

(´・ω・`)データエクスポート要求、できるやん…… (登録してるmailにjson形式で送付された)

以上