おぼつかない知識でWebスクレイピングしてみた

# はじめに
ひよっこエンジニアがGoogle先生頼りでスクレイピングしました。ただの自分の知識整理用ページです。

# 目的
Google Colaboratoryを使って、ログインが必要なサイトで、検索を行った後、スクレイピングを実行し、スプレッドシートに出力したい。

# 環境
- python3
- Google Colabratory(無料版)

# 手順
1. 事前準備
1. ログイン処理
1. 検索
1. スクレイピング
1. データベースにまとめる
1. 出力

# 事前準備

### Googleドライブマウント

忘れないように、最初にGoogleDriveをマウントしておきます。
~~~Python3
from google.colab import drive
drive.mount('/content/drive')
~~~
上記を実行すると、別タブが開いて、Googleアカウントログインやらパスワードやらを毎回求められます。Google Colabは時間が経過したりページを閉じたりするとランタイム接続が切れるので、コード実行する際は毎回やる必要があります。



### ライブラリ
そしたらライブラリのインストールをします。
スクレイピングをする場合、beautifulsoup、seleninum、scrapyなどをよく使うらしい。
今回はスクレイピングにbeautifulsoup、ログイン作業にseleninumを使用します。
テーブル情報も取りたいし使い慣れてて楽なのでpandasも入れておきます。
[参考サイト：【Pythonでスクレイピング】ライブラリどれ使う？Scrapy・Selenium・Beautiful Soup](https://popoco.net/python-scrapy-selenium-beautiful-soup/)

ライブラリ一覧※スプレッドシート関連以外
~~~Python3
# いつものスクレイピングセット
import requests
from bs4 import BeautifulSoup
import pandas as pd
import re

# ログイン用<selenium>
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
~~~



### Chomeドライバー
seleninumでChomeのサイトを扱う場合、Chomeのドライバーインストールが必要らしいです。こちらのサイトを参考にして、インストールを行いました。

https://zenn.dev/robes/articles/b0542824ec61a6


# ログイン処理
先ほどのサイトをガン見しながらログインしていきます。今回は、ログイン後、サイト内で欲しい内容を検索するところまでを行う必要があるので、ログインまではこちら（[参考サイト：Google Colabratoryでseleniumを使ってスクレイピングする](https://zenn.dev/robes/articles/b0542824ec61a6)）のサイトを参照してもらって、そこから先の処理についてまとめます。

ログインしたら、検索窓から検索ワードを入力し、検索ボタンを押します。
~~~python3
# 検索ワードの入力
kensaku_keyword = "検索ワード" #検索ワード指定

# 検索タブクリック
kensaku_btn = driver.find_element(By.XPATH,'XPATH')
kensaku_btn.click()

time.sleep(2)

# 検索ワードを入力
driver.find_element(By.XPATH,'XPATH').send_keys(kensaku_keyword)

# 検索ボタンクリック（確定）
kakutei_btn = driver.find_element(By.XPATH,'XPATH')
kakutei_btn.click()

time.sleep(2)
html = driver.page_source
BeautifulSoup(html,"lxml")
~~~
サーバーに負荷をかけないように`time.sleep`するのが大事って書いてありました。そらそう。

ここでは実行はしませんが、ログアウトはどうやるのかメモしておきます。
~~~Python3
# ログアウト
driver.close()
driver.quit()
~~~

# スクレイピング
### html取得
やっとhtmlを取得してスクレイピングします。
本来は`requests.get(url)`でhtmlを取得してからパースしますが、先ほどのログイン作業時にそれは済んでいるので、
いきなり`soup = BeautifulSoup(driver.page_source,"lxml")`します。

### 欲しい情報を収集
目的ページのURLが取れたので、それぞれのURLに対し、欲しい情報を取得していきます。
今回欲しい情報は大きく分けて5種類です。
- 各ページのURL
- ページタイトル
- ページ掲載者（食べログなら店の名前）
- ページ掲載者URL(食べログなら店のURL)
- テーブルデータ

## URL取得
今回は、食べログみたいな１ページに複数の結果のURLが並んでいるページでスクレイピングを行うので、まず各ページのurlだけ取得します。このページには各ページURLの他にも、関連サイトだったり今回は必要ないサイトのURLが並んでいたりするのですが、URLを見比べてみると欲しいURLには規則性があったので、以下のようにして必要なものだけ抽出します。
~~~Python3
all_atags = soup.find_all(href=re.compile("特徴となるURL規則"+r"\d"))
~~~
これで`all_tags`の中に取得できました。でも今回のサイトは相対パスっぽくURLが書かれていたのでちゃんと使えるように、`https://~~~`の形に直して`url`というリストに格納しておきます。

## テキストデータの取得
### ページタイトルの取得
今回、ページタイトルは`h3`タグで書かれています。しかしページタイトル以外の情報も`h3`タグで書かれているので、`class`タグで絞り込みを行います。
htmlの記述内容は以下のような感じです。
~~~html
<h3 class="クラス名">
    <a data-hoge="" target="_blank" rel="noopener noreferrer" href="/hogehoge/wakwak">ここにタイトルが書いてある</a>
</h3>
~~~
これに対しタイトルが書いてある部分のみをテキストで取得し、リストに入れていきます。
~~~python3
elems_title = soup.select(".クラス名")
title_list = []

for elem in elems_title:
  #print(elem.getText())
  title_list.append(elem.getText())

print(title_list)
print(len(title_list))
~~~

### ページ掲載者の取得
ページタイトルの取得と同様に、`class`タグで取得します。ページ掲載者は`h3`ではなく`span`で記述されていましたが、これも同様に取得することができます。

## ページ掲載者URLの取得
掲載者の詳細情報が書かれたURLを取得します。ここも前述したURLの取得とほぼ同じ手法で取得したので省略します。

## テーブルデータの取得
今回取得したいURLは取得した各URLページの先にありました。各URLページの先にはテーブルデータが３つずつあり、全ての情報を取得する必要がありました。
必要なURLはすでにリスト化して取得済みなので、それぞれのURLのhtmlを取得して、Dataframeを使ってそれぞれのテーブルデータを取得していきます。

~~~python3
for i in range(url_num):
  list_element = []
  
  # html取得
  # パースして表示
  html_detail.append(requests.get(url[i]))
  
  html_detail[i].raise_for_status()
  soup.append(BeautifulSoup(html_detail[i].content,"html.parser"))
  
  # 二次元配列でtable情報を記録
  page.append(pd.read_html(url[i] , encoding='utf-8'))
  
  # dataframeにする
  df_temp_gaiyou = pd.DataFrame(page[i][0])
  df_temp_oubo = pd.DataFrame(page[i][1])
  df_temp_syousai = pd.DataFrame(page[i][2])

  # dataframeを結合
  df_temp = pd.concat([df_temp_gaiyou,df_temp_oubo], axis=1, sort=False, join='outer')
  df_temp = pd.concat([df_temp,df_temp_syousai], axis=1, sort=False, join='outer')

  # URLを追記
  df_temp['URL'] = url[i]
  list_element.append('URL')


  # columsに追加
  df_temp.columns = list_element
  
  # df_ansと結合
  df_ans = pd.merge(df_ans,df_temp,how="outer")
  type_dataframe.append(type(df_ans))


  # df_temp_syousaiの処理
  df_temp_syousai_ans = pd.concat([df_temp_syousai_ans,df_temp_syousai], axis=1, sort=False, join='outer')

~~~
テーブルデータが１つのページに複数ある場合、dataframeは配列の形で、それぞれの要素の中に１つのテーブルデータを格納します。よってそれぞれを一度別のdataframeに格納し直して、結合させました。maegeの仕様を忘れていて最初はそのまま結合させたのですが、全く同じデータが来ると後から来たデータが消えてしまったので、キーとしてURLを追加し、データが消えないようにしました。
`df_ans`はこれを記述する前に、必要な項目のみ宣言した状態で作成してあります。そのまま結合結果とするとエラーが出ます。

# データベースにまとめる
テキストデータとテーブルデータを１つのdataframeにまとめます。今回は使用しているサイトの各URL内の記述内容に異なりが多く、欠損値が多くありました。このあとスプレッドシートに出力する際に欠損値`NaN`をそのままにして出力しようとすると、「floatは出力できません」的なエラーが出てしまうので、データの結合を行った後`NaN`を文字列に変換します。
~~~python3
df_syuturyoku = df_ans.astype(str)
~~~


# 出力
dataframeからスプレッドシートに出力します。gspreadというライブラリをインストールします。
~~~python3
!pip install --upgrade -q gspread
~~~
GoogleColabからスプレッドシートへのアクセスを許可するためのコードを書きます。
~~~python3
# スプレッドシートへのアクセス許可
from google.colab import auth
auth.authenticate_user()

import gspread
from google.auth import default
creds, _ = default()

gc = gspread.authorize(creds)
~~~
最初ここの認証でエラーが出て悩んでいたのですが、gspread側の仕様のためのようでした。こちら↓参考にさせていただきました。

https://qiita.com/toraaa/items/36b4a3daa84619dd7f2e

その後、スプレッドシートを作成したり開いたりします。こちら参考にさせていただきました。

https://kirinote.com/python-pivot-spread/

せっかくデータを１つのdataframeにまとめましたが、出力するためにリストに直します。すごく非効率できな気がしているので別の方法を考えても良かったのかもしれない、、、
でもやっと出力！これでスクレイピング完了です！！！！
~~~python3
# PandasのDataframeをリストに変換し出力
sheet = sh.worksheet("シート1")
worksheet.update([df_syuturyoku.columns.values.tolist()] + df_syuturoyku.values.tolist())
~~~

# 感想
初めてにしては難易度の高い（？）スクレイピングから挑戦してしまった気がします。忘れている知識の補完はできて気がしますが、もっと時間のかからず効率的な方法を考えるべきだったかもしれない。。。。