夏の自由研究として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***で取り出せない
今回目的だったデータの一例がこれ。
だがこのデータ、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
(´・ω・`)データエクスポート要求、できるやん…… (登録してるmailにjson形式で送付された)
以上