Note of Pediatric Surgery

腸内細菌、R、ときどき小児外科

SeleniumでNCD検索システムから全経験手術症例を1つのCSVファイルに抽出する

はじめに

とある事情により、自分の経験症例数を算出する必要がありました。一般外科医であれば、自分の登録症例はすべてNCDに登録されていると思います。自分の経験症例を検索するにはNCD検索システムにアクセスする必要があります。

NCD検索システム

手術症例数の件数表示は、施設別、術式別、術者別に一覧表示できるのですが、ここで大きな問題が。

  1. 施設・術者・術式の複数を組み合わせたフィルタリングができない
  2. 手術は20件ずつ表示されて、それ以上の設定はできない

というクソ仕様になっていました。NCDには術者助手に関わらず、自分が入った手術がカウントされていると思いますが、自分が術者として執刀したそれぞれの手術のカウントをするには、20件ずつクリックしてカウント、もしくはExcelにコピペしていなきゃいけないということになります。1500件近くあったので、この作業を75回も繰り返すのは嫌だ!

たまらずNCDの事務局にCSVなどで自分の経験症例を一括ダウンロードできないものかと問い合わせましたが、秒でだめですと返信が返ってきました。というわけで、せっかく勉強したのでPythonスクレイピングしてやりましょう。

www.pediatricsurgery.site

CSVでリストが手に入れば後は、Rでいかようにも料理できますので。

ChromeでNCD検索システムを立ち上げる

import time
from selenium import webdriver
import chromedriver_binary # PATHを通す

driver = webdriver.Chrome()
driver.get('https://user.ncd.or.jp/member/memberLogin.html') # ページを立ち上げる
time.sleep(3) # 読み込み時間を考慮して5秒待つ

ログインIDとパスワードを入力する

ChromeDevelopper Toolsを眺めてもコードがよくわからないのです。これがJavaScriptってやつでしょうか?素人にはよくわかりません。

driver.page_source

を実行すると、大量の文字列が取得でき、各要素がどんな風に表現されているかわかります。すると

# ログインID
<input type="text" name="user" style="width:120px;" maxlength="10">

# パスワード
<input type="password" name="password" style="width:116px;" maxlength="50">

となっていることがわかります。name="user"name="password"でそれぞれの入力フォームが規定されているので、find_element_by_name()で指定してあげます。send_keysというのがフォームに文字列を入力する方法みたいですね。

user_box = driver.find_element_by_name("user") # "user"という名前でフォームを認識する
user_box.send_keys('自分のログインID') 

pass_box = driver.find_element_by_name("password")
pass_box.send_keys('自分のパスワード')

driver.find_element_by_xpath('button')

ログインボタンをクリックする

# ログインボタン
<input type="button" value="ログイン" onclick="return goLogin();">

ログインボタンは上記のように規定されているのですが、value="ログイン"で探してあげる方法を探すのに難渋しましたが、この記事が参考になりました。

stackoverflow.com

driver.find_element_by_xpath("//input[@value='ログイン']").click()

これを実行すると次の画面にジャンプすると思います

手術症例一覧をクリックする

この画面に来たら次は、手術症例一覧をクリックするようにコードを書きます。

driver.page_source

でじっくり探すと、手術症例一覧のボタンは下記であることがわかります。

<input type="button" onclick="return goShujyuShoureiIchiran();" value="手術症例\u3000一覧">

なので、このように入力すれば次のページに遷延します。

driver.find_element_by_xpath("//input[@value='手術症例\u3000一覧']").click()

Table情報を取得する

さてようやくスクレイピングしたい手術リストのページに到達することができました。driver.page_sourceソースコードを探すと、

<table border="0" cellspacing="0" cellpadding="3" width="96%">

というのが手術症例の表のtableなのですが、valueもnameもついていないので、このままでは指定できません。このサイトによると

tanuhack.com

id、class、nameがちゃんと設置されてないページでも、CSSセレクタXPathをゴリゴリ書けば基本的になんでも要素を取得できます。CSSセレクタXPathディベロッパーツールで指定の要素を右クリックすればまるごとコピーできます。

なので、ChromeViewからDevelopper Toolsを開き、右クリックからCopy XPathをすると、メインのテーブルは

/html/body/form/div[3]

に存在することがわかります。なので

table = driver.find_element_by_xpath("/html/body/form/div[3]")

で表を丸ごと抽出してtableに格納してあげます。これを各列ごとの要素に分解するには

from selenium.webdriver.common.by import By
rows = table.find_elements(By.TAG_NAME, "tr")

とすると表のデータが列ごとのリストとしてrowsに格納されます。どの行にどの情報が入っているのか1つ1つ試していくと、

print(rows[9].text) # 列名
print(rows[10].text) # 手術名最初
print(rows[29].text) # 手術名最後

となっていることがわかります。11列目から30列目が必要な情報 = 手術のリストになっていることがわかります。これをそれぞれ分解してデータフレームに格納していきましょう。

手術リストをデータフレームに格納する

正直、ここが一番時間を喰った箇所でした。Rでは一瞬でできる操作がPythonだとすごく面倒。R万歳!最終的にはこちらの記事を参考にさせていただきました。

qiita.com

1行抜き出し、各項目を列の値として格納してdfに格納するという作業です。

# 結果を入れる配列を用意
facility = []
ncd = []
date = []
method = []
operator = []
status = []
df = []

# 1行の各項目を抽出してデータフレームにする
for i in range(10,30):
  cols = rows[i].find_elements(By.TAG_NAME, "td")
  facility += [cols[0].text]
  ncd += [cols[1].text]
  date += [cols[2].text]
  method += [cols[3].text]
  operator += [cols[4].text]
  status += [cols[5].text]
  df = pd.DataFrame(data={'Facility': facility, 'NCD': ncd,
  'Date': date, 'Method': method, 'Operator': operator, 'Status': status},
  columns=['Facility', 'NCD', 'Date', 'Method', 'Status'])
len(df)

で確認しておくと、20と返ってくるので、1ページ内の手術をすべて抽出できていることになります。

次ページをクリックする

次ページをクリックすることで次の20件のリストのページに遷延することができます。driver.page_sourceをつぶさに見ていくと、

<a href="javascript:void(0);" onclick="return pageJump(2);">次ページ</a>

が次ページのリンクとなっています。リンクを文字列で認識してクリックするには、

driver.find_element_by_link_text(u"次ページ").click()

と書けばいいようです。このuって何だ?って話しですが気にしない。これをページ数分繰り返せばいいわけですね。

for loopで回す

これをページ数分繰り返せばいいわけです。表の下の方にページ数が書いてあり、これを取得する方法もないわけじゃないのですが、面倒だったので、手入力してしまいました。

facility = []
ncd = []
date = []
method = []
operator = []
status = []
df = []

for k in range('手動でページ数を入力する'): 
  for i in range(10,30):
    table = driver.find_element_by_xpath("/html/body/form/div[3]")
    rows = table.find_elements(By.TAG_NAME, "tr")
    cols = rows[i].find_elements(By.TAG_NAME, "td")
    facility += [cols[0].text]
    ncd += [cols[1].text]
    date += [cols[2].text]
    method += [cols[3].text]
    operator += [cols[4].text]
    status += [cols[5].text]
    df = pd.DataFrame(data={'Facility': facility, 'NCD': ncd,
    'Date': date, 'Method': method, 'Operator': operator, 'Status': status},
    columns=['Facility', 'NCD', 'Date', 'Method', 'Operator', 'Status'])
  
  # 次ページをクリック  
  driver.find_element_by_link_text(u"次ページ").click()
  time.sleep(2)

これを実行するとdfの中に手術リストが格納されていると思います。

csvファイルにして出力

df.to_csv('~/Desktop/NCD.csv', encoding='utf_8_sig')

encodingを指定しないと文字化けするので注意してください。これで自分の経験症例をすべてCSVファイルとして手に入ると思います。時間にして2秒×ページ数!今回、やってみてPythonは業務効率化にものすごいインパクトがあるんだなと思いました。実際の仕事にどう活かせるかはおいといて。