今回は、Yahoo(ヤフー)ファイナンスをターゲットにします。
初めは株価をスクレイピングしようと考えました。
ただ、それだと当たり前過ぎます。
また、静的ページのみでたいした工夫も必要ではありません。
それであれば、スクレイピングのスキル向上は望めません。
そもそも、株価であればヤフーである必要性もなくなります。
ヤフーファイナンスで価値ある情報と言えば、掲示板になるでしょう。
ということで、ヤフーファイナンスの掲示板のコメントをスクレイピングします。
本記事の内容
- ヤフーファイナンスのスクレイピングに対する態度は?
- ヤフーファイナンスをスクレイピングしてもいいの?
- ヤフーファイナンスをさくっとスクレイピングする
- ヤフーファイナンスをスクレイピングするサンプルコード
それでは、上記の解説を行っていきます。
ヤフーファイナンスのスクレイピングに対する態度は?
まずは、ヤフーファイナンスのスクレイピングに対する態度を確認しましょう。
Googleで「ヤフー スクレイピング」を検索すれば、真っ先に以下のページがヒットします。
https://support.yahoo-net.jp/PccFinance/s/article/H000011276
明確にスクレイピングに対して禁止していますね。
あれだけの情報が集まれば、お金になるでしょう。
だから、その商売の邪魔をするなと言うことです。
ただ、ヤフーのサーバーがダウンするとは思えません。
ヤフーファイナンスは、スクレイピングを明示的に禁止しています。
じゃあ、スクレイピングしたらダメではないのか?
この疑問に以下で答えます。
ヤフーファイナンスをスクレイピングしてもいいの?
そもそも、スクレイピング自体は何の犯罪でもありません。
そして、スクレイピング禁止法のような法律は、世界のどこを探しても存在しません。
ただし、スクレイピングができるとついついやってしまうことがあります。
早く情報を集めようと過度なアクセスをしてしまうのです。
この過度なアクセスが、問題になってしまいます。
過度なアクセスにより、相手側のサーバーに過度な負荷を与える可能性があるのです。
その結果、安定したサービス提供を邪魔してしまうことがありえます。
そうなると、法的問題になりかねません。
それも以下のような刑事罰です。
- 刑法233条 偽計業務妨害罪
- 刑法234条 電子計算機損壊等業務妨害罪
話を整理しましょう。
問題は、スクレイピングではありません。
問題となるのは、過度なアクセスです。
そのような内容がヤフーのページにも書いてあります。
プログラム等を用いて機械的に取得する行為(スクレイピング等)について、システムに過度の負荷がかかり、安定したサービス提供に支障をきたす恐れがある
よって、ヤフーが利用規約にスクレイピング禁止を記載していても何ら法的な根拠はありません。
ということは、スクレイピングをしても法的問題にもなり得ません。
あと、気をつける必要があるのは著作権です。
取得した情報を「転用、複製および外部配信ならびに販売」した場合は、問題があるでしょう。
でも、これは別にスクレイピングだけの問題ではありません。
手動でコピーした場合でも、著作権の問題は発生します。
まとめると、以下になります。
- スクレイピング行為自体は問題なし
- 過度なアクセスが問題である
- 取得した情報には著作権があることを注意する
以上より、ヤフーファイナンスをスクレイピングしてもOKです。
ただし、過度なアクセスと著作権には気をつけましょう。
ヤフーファイナンスをさくっとスクレイピングする
スクレイピングの対象
本記事では、「9984 – ソフトバンクグループ(株) 2020/08/11」ページをスクレイピングします。
https://finance.yahoo.co.jp/cm/message/1009984/a5bda5ua5ha5pa5sa5af/706
基本的には、記事で公開するのは単体のページのスクレイピングのやり方です。
単体のページであれば、スクレイピングに詳しくない人が同じことをしても過度なアクセスは防げます。
その意味でも、記事で公開する場合は単体ページまでとしています。
過去には、AmazonやTwitterのスクレイピング方法を公開しています。
そのときも単体ページのスクレイピングまでです。
スクレイピングの難易度
難易度的には単体のメインページが最も難解となります。
そのため、単体ページのスクレイピング方法を公開することに意味はあります。
実際、上記の記事にはアクセスが多く、需要はあるのだと実感しています。
さて、今回のターゲットとなるヤフーファイナンスのページの難易度はどうでしょうか?
Amazonより難しく、Twitterより簡単というところです。
と言っても、そこそこ面倒です。
Twitterが異常なほどに難しいだけなので。
でも、そのTwitterのスクレイピングで学んだことを存分に活かすことができます。
先にTwitterのスクレイピングで苦労して良かったと思いました。
スクレイピングする上でのポイント
Twitterと同じく、1ページ内でスクロールするたびにコンテンツが追記されるパターンです。
アクセスした時点では、コメントが20個まで表示されています。
そして、20個までは静的ページでの表示となります。
つまり、20個までのコメントは簡単にスクレイピングできるということです。
しかし、それ以降は非同期で20個づつコメントが追記されていきます。
「百聞は一見にしかず」です。
次の動画で実際の動きを確認してみてください。
このスクロール処理をPythonでどう実現するのか?
これが一番のポイントです。
でも、これはすでにTwitterのスクレイピングの際にマスターしています。
詳細は、Twitterのスクレイピングに関する記事をご覧ください。
技術に関してだけ言えば、以下の記事の方がわかりやすいかもしれません。
ヤフーファイナンスをスクレイピングするサンプルコード
サンプルコードです。
2020年08月12日時点は動いています。
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains from bs4 import BeautifulSoup import time import pandas as pd import urllib.parse import re from datetime import datetime chromedriver = "chromedriver.exeのパス" s_code = "9984" file_path = "./yahoo_data/" + s_code + ".json" scroll_wait_time = 1 def get_comment(url): # ヘッドレスモードでブラウザを起動 options = Options() options.add_argument('--headless') print(datetime.now().strftime("%Y/%m/%d %H:%M:%S")) # ブラウザーを起動 driver = webdriver.Chrome(chromedriver, options=options) driver.get(url) # 見つからないときは、10秒まで待つ driver.implicitly_wait(10) meta_info = get_meta(driver) last_com_number = 99999 while last_com_number > 1: com_number = scroll_to_elem(driver) time.sleep(scroll_wait_time) if last_com_number == com_number: break else: last_com_number = com_number print(datetime.now().strftime("%Y/%m/%d %H:%M:%S")) # 情報取得 comment_list = get_list(driver, meta_info) # ブラウザ停止 driver.quit() return comment_list def get_meta(driver): title = driver.find_element_by_tag_name("h1").text try: list = title.split('-') # 証券コード security_code = list[0] # 日付 list_2 = title.split('〜') if len(list_2)==2: start_date = extract_date(list_2[0]) end_date = extract_date(list_2[1]) else: start_date = extract_date(title) end_date = "" info = {} info["security_code"] = security_code info["start_date"] = start_date info["end_date"] = end_date return info except Exception as e: return None def get_list(driver, meta_info): # ページリスト list = [] elems = driver.find_elements_by_class_name("comment") for elem in elems: tag = elem.get_attribute("innerHTML") comment_id = elem.get_attribute("data-comment") info = get_info(comment_id, tag) try: info.update(meta_info) list.append(info) except Exception as e: #print(info) #print("not update") return list def scroll_to_elem(driver): # 最後の要素の一つ前までスクロール elems = driver.find_elements_by_class_name("comment") last_elem = elems[-1] # comNum com_number = last_elem.find_element_by_class_name("comNum").text com_number = int(com_number) actions = ActionChains(driver); actions.move_to_element(last_elem); actions.perform(); return com_number def get_info(comment_id, data): soup = BeautifulSoup(data, features="lxml") try: comNum = get_text_by_elem(soup.find(class_="comNum")) comNum = re.sub("\\D", "", comNum) # ユーザー user_elem = soup.find(class_="comWriter").find("a") user_id = user_elem["data-user"] user_name = get_text_by_elem(user_elem) # 感情 emotion = "" emotion_elem = soup.find(class_="comWriter").find(class_="emotionLabel") if emotion_elem: emotion = get_text_by_elem(emotion_elem) # 投稿日時 datetime_elem = soup.find(class_="comWriter").find_all("span")[-1] datetime = get_text_by_elem(datetime_elem) # 返答 commnet_reply_target = 0 commnet_reply_dsp = "" commnet_reply_elem = soup.find(class_="comReplyTo") if commnet_reply_elem: commnet_reply_target = commnet_reply_elem.find("a")["data-parent_comment"] commnet_reply_dsp = get_text_by_elem(commnet_reply_elem) # 投稿 comment_text = get_text_by_elem(soup.find(class_="comText")) info = {} info["comment_id"] = comment_id info["comment_number"] = comNum info["user_id"] = user_id info["user_name"] = user_name info["emotion"] = emotion info["datetime"] = datetime info["commnet_reply_target"] = commnet_reply_target info["commnet_reply_dsp"] = commnet_reply_dsp info["comment_text"] = comment_text return info except Exception as e: return None def get_text_by_elem(elem): try: text = elem.text text = text.strip() return text except Exception as e: return None def extract_date(s): date_pattern = re.compile('(\d{4})/(\d{1,2})/(\d{1,2})') result = date_pattern.search(s) if result: y, m, d = result.groups() return str(y) + str(m.zfill(2)) + str(d.zfill(2)) else: return None if __name__ == '__main__': url = "https://finance.yahoo.co.jp/cm/message/1009984/a5bda5ua5ha5pa5sa5af/706" result_list = get_comment(url) json_data = json.dumps(result_list) with open(file_path, mode='w',encoding="utf-8") as f: f.write(json_data)
1秒間隔でスクロール(20個分)しています。
このサンプルコードを実行すれば、対象ページの全コメントをスクレイピングできます。
そして、スクレイピングした情報をjsonに保存します。
このサンプルコードの実行時間は、2分43秒です。
過度なアクセスにならないように1秒間の間隔を設けています。
なぜ、1秒間の間隔を設けることが過度なアクセスではないと言い切れるのか?
このような疑問を思う方がいるかもしれません。
その疑問に答えておきます。
なぜなら、実際に手でスクロールした時間とほぼ同じだからです。
記事内で参考に上げた動画の再生時間を見てください。
全部で2分52秒です。
動画を見ればわかりますが、スクロールしながら最後のコメントまで表示させています。
プログラムでやった場合と手でスクロールした場合とでは、たった9秒の差しかありません。
よって、スクレイピングしたからと言って過度なアクセスになるという訳ではありません。
そんなものは、いくらでも調整可能です。
むしろ、それを調整するのがスクレイピングの腕の見せ所でもあります。
それでも心配な人は、以下の値を2に変更してください。
scroll_wait_time = 1
2に変更すると、2秒間隔でスクロールをすることになります。
もう、こうなると人間が手でスクロールするよりも遅くなります。