[スクレイピング] Pythonでwebサイトの情報を丸裸にしよう！！

# 概要
Pythonのスクレイピングについて1からまとまっている記事等がみつからなかったので自分で作成することにした。
今回は特にseleniumについての内容だが、この記事を通して一通りpythonのスクレイピングが出来るようになる。

(スクレイピングで学んだことがあるたびに追加していきます！)

(エラーハンドリングについて近々追記予定)

#目次

**1. 初期設定**

- ライブラリのimport
- 初期optionの設定
- driverのセットアップ
- はじめのurlへジャンプ

**2. seleniumの基本動作**

- 要素の取得
    - 要素のテキストを取得
    - 要素のattributeを取得
- 文字列を送信する
- クリックする
- javascriptの実行
- タブ移動

**3.seleniumの応用動作**
- ログイン
- ページネーション
- 無限スクロール


**Tips**
- Current browser version is ・・・
- time.sleep(秒)
- driver.quit()とdriver.close()の違い
- x_pathの最強の指定方法
- セットアップのベース



##1.初期設定
driverの初期設定を行う。

###ライブラリのimport
まずseleniumやbeautifulsoupなどの主要なスクレイピングのツールの他,
ファイルの読み込みや可視化のためのライブラリをimportする。

```
import requests
from selenium import webdriver
from webdriver_manager.utils import chrome_version
from webdriver_manager.chrome import ChromeDriverManager
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 bs4 import BeautifulSoup
import time
import pandas as pd
# これをimportしておくことで自分でcheromedriverをインストールする必要がない
import chromedriver_binary
import cv2
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
```

###初期optionの設定
次に初期optionの設定である。ここでは、webdriverをバックグラウンドで起動したり、画像を読み込まないことで動作を軽量にしたりなどの設定をすることが出来る。
以下のoptionを設定しておくことでほとんどのサイトでスクレイピングが出来るように設定できる。

```
op = Options()
op.add_argument("no-sandbox")
op.add_argument("--disable-extensions")
# 動作確認のときはoffにする
# op.add_argument("--headless")
op.add_argument('--disable-gpu')  
op.add_argument('--ignore-certificate-errors')
op.add_argument('--allow-running-insecure-content')
op.add_argument('--disable-web-security')
op.add_argument('--disable-desktop-notifications')
op.add_argument("--disable-extensions")
op.add_argument('--lang=ja')
# 画像を保存したいときはoffにする
op.add_argument('--blink-settings=imagesEnabled=false')
op.add_argument('--disable-dev-shm-usage')
op.add_argument('--proxy-server="direct://"')
op.add_argument('--proxy-bypass-list=*')    
op.add_argument('--start-maximized')
```
###driverのセットアップ
次にdriverのセットアップを行う。ここでdriverを宣言することによりwebdriverが使えるようになる。

```
# driverのセットアップ
driver = webdriver.Chrome(chrome_options=op)
```

### はじめのurlにジャンプ
そしてはじめに設定したdriverを使ってurlにジャンプできるか検証する。

```
driver_path =  "ジャンプしたいurl"

# 初期のurlに移行できるか確認(ここで開かれるwindowをインスタンスwindowという)
driver.get(driver_path)

#urlの読み込みに時間がかかる可能性があるため
time.sleep(5)

#driverの起動停止
driver.quit()
```

##2. seleniumの動作
seleniumで出来る動作についてまとめる
###要素の取得
seleniumでurlの要素の情報を取得する方法には以下のようなものがある。
基本的にこれらを組み合わせることで一部例外はあるものの全ての要素の取得が可能。

要素を取得する属性で試す試す順番としては、以下を個人的には推奨する。

①idで取得 (重複がないから)
②class か name で取得
③x_pathで取得  (確実に取得できるが複雑)

```
#最強　最終手段
driver.find_element_by_xpath("x_path")
#複数要素がある時
driver.find_elements_by_xpath("x_path")

#idより取得　
driver.find_element_by_id("ID")

#name属性から取得
driver.find_elements_by_name("name")

#classより取得
driver.find_elements_by_class_name("class")
```

#### 要素のテキストの取得
以下のようにpタグの中の文字列を取得したい場合使用できる。

```:dom
<p class="element">あああ</p>¥
```

```:pタグのコードの取得
driver.find_element_by_class_name("element").text
```
#### 要素のattributeの取得
例えばaタグのリンク先が知りたいときに役に立つ。

```:dom
<a href="http://sample/following.jp" class="follow">フォロー</a>
```

```:aタグのhrefの取得
driver.find_element_by_class_name("follow").get_attribute("href")
```


###文字列の送信
主にinputタグへの操作になる。これにより文字列の送信ができ、ログイン等の操作を実行できる。

```
#inputタグを持つ要素を取得して文字列を送信する
login_element=driver.find_elements_by_class_name("login")
login_element.send_keys("入力したい文字列")
```

###クリック
これにより、リンクのクリックをすることが出来る。

```
#要素を取得した後にジャンプ
jamp_element=driver.find_elements_by_class_name("login")
jamp_element.click()

#ジャンプするときは読み込みがあるのでsleepする
time.sleep(3)
```

### javascriptの実行
pythonでseleniumを使ってjavascriptのコードを使って操作をすることも出来る。これにより、例えばタブ移動やスクロール、スクリーンショットなどの操作も実行することが出来る。

```
# 一番上から一番下までスクロール
driver.execute_script('window.scrollTo(0 ,document.body.scrollHeight);')

# スクリーンショット
# w:横幅いっぱい h:縦幅いっぱい
w = driver.execute_script("return document.body.scrollWidth;")
h = driver.execute_script("return document.body.scrollHeight;")
# スクリーンショットするサイズの選択
driver.set_window_size(w,h)
# スクリーンショットをしてsample.pngという名前で保存
driver.save_screenshot("sample.png")
```

### タブ移動
場合によってはaタグをクリックしたときに新しいタブが出てくることもある。このような場合は以下のようなコードを書くことで対処が可能。

```
# tab移動
# driver.window_handles[タブの番号]
driver.switch_to.window(driver.window_handles[1])
driver.switch_to.window(driver.window_handles[0])
# 初めに開いていたウインドウを閉じる
driver.close()
# 新しく開かれたタブを新規にインスタンスウインドウとする これをしないとNosuchElementエラーになる
driver.switch_to.window(driver.window_handles[0])
```

## 3. seleniumの応用動作
実際にseleniumを使ってできる応用動作について説明する。

### ログイン
twitterやrakuteなど、場合によってはログインしないとサイトの操作ができないものもある。
そんなときのためにログインを実装する処理を記述する。

```
def login(id,password):
        driver = webdriver.Chrome(chrome_options=.options)
        driver.get(home_url)
        time.sleep(5)
        
        # ユーザーidのinput要素を取得する
        input_name=  driver.find_element_by_name("usernameのxpath")
        input_name.send_keys(id)

        #  パスワードのinput要素を取得する
        input_pass = driver.find_element_by_name("passwordのxpath")
        input_pass.send_keys(password)

        #  ログインボタンの要素を取得する
        login_button = driver.find_element_by_xpath('ログインボタンのxpath')
        login_button.click()

        time.sleep(1)
        return driver
```

### ページネーション
特定のページまで移行してページにつき次第処理を終了する動作はこのように書く。

以下のようなpagenationを仮定している。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656158/b570153f-5769-8575-eb2e-a43a78ac67f3.png)

この関数ではページ番号が期待するページ番号に到達したときに処理を終了するように設定している。

```
#ページの判定の要素
judge= True

#ページネーションの関数 finish:ページ番号
def page_move(driver,finish):
        judge = True
        while judge==True:
            # pagenationの要素を取得
            page = driver.find_elements_by_xpath('ページネーションの要素')
            
            #finishがページ番号以下だったたら繰り返し処理
            if int(page[1].text) <= finish:
                print("進行度:{}".format(page[1].text))
                page[11].click()
                time.sleep(3)
                
            #finishがページ番号を超えたら処理を終わる judge
            elif int(page[1].text) > finish:
                print("進行度{},ページネーションを停止します".format(page[1].text))
                judge=False
            
        return driverジネーションを停止します".format(page[1].text))
        
        
judge= True
```

### 無限スクロール
ページをスクロールしていると次の要素が出てくる無限スクロールには次のように対処する。
これは以前noteユーザーの情報をスクレイピングシた際のコードである。
スクロールをしていくとユーザーの情報が更新されo-searchResultUser__itemというclassをもつtagsの数が増えていくがページ最下層になると更新が止まる。
whileを使って更新が止まったときに処理を終了するようにしている。

```
def scroll(driver):
    judge=False
    while judge == False:
        user_url_list = []
        name_list = []
        note_len_before = driver.find_elements_by_class_name('o-searchResultUser__item')
        #100回スクロールの動作を行う
        for i in range(100):
            # ページの一番下までスクロールする
            driver.execute_script('window.scrollTo(0 ,document.body.scrollHeight);')
            time.sleep(0.3)
        note_len_after = driver.find_elements_by_class_name('o-searchResultUser__item')
        #ユーザーのリストを取得
#         実験用
#         if len(note_len_before) < 300:
#         本番陽
        if len(note_len_before) <len(note_len_after):
            print(len(note_len_after))
            print(name_list)
#             実験用
#         elif len(note_len_before) >= 300:
#     本場用
        # スクロールしてもユーザーリストが増えなかったらそれ以上スクロールできないので終了
        elif len(note_len_before)==len(note_len_after):
            for i in range(len(note_len_after)):
                print(i)
                user_url_list.append(note_len_after[i].find_elements_by_tag_name('a')[0].get_attribute('href'))
                name_list.append(note_len_after[i].find_element_by_class_name('m-userListItem__nameLabel').text)
                judge = True
    return user_url_list,name_list
```


##Tips
ここではpythonでスクレイピングする際に知っておくと役に立つことを記述する。

###Current browser version is ・・・
chromedriver-binaryとgoogle chromeのバージョンの不一致によるエラーである。
エラー文は以下のようになる。

```
SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version {chromedriver-binaryのバージョン}
Current browser version is {使っているchromeのバージョン} with binary path /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
```

chromedriver-binaryのバージョンを{使っているchromeのバージョン}に変更することでこのエラーは解決することが出来る。
chromedriver-binaryのバージョンは[PyPl](https://pypi.org/project/chromedriver-binary/#history)から確認可能だ。

```
pip install chromedriver-binary=={使っているchromeのバージョン}
```

###time.sleep(秒)
seleniumを使っているとurlの読み込みが完了しないうちに要素を取得しようとするためにエラーになることが多くある。(特に長時間seleniumを使用しているとだんだん動作が遅くなりやって来た処理が水の泡になりかねない)

そのため、`ページの移動の間に適切にスリープを入れることが大切となる。`
そこで`time.sleep(秒)`を指定することで引数に入力した数字と同じ秒数だけ処理を停止するので適度に入れるのが好ましい。



###driver.quit()とdriver.close()の違い
driver.quit()はseleniumを通して作成した`全てのwindowを閉じる`commandでdriver.close()は`インスタンスウィンドウのみを閉じる`commandである。

なのでタブ移動をした際にはじめに作ったウィンドウを消したいと言ったときはdriver.close()が有効になる。対称的に、処理が終了して完全に終了したので全てのタブを閉じたいというときはdriver.quit()が有効になる。

###x_pathの最強の指定方法

x_pathにcontainsを使うことでタグ内の記述から要素を取得することができる。
これにより検証→x_pathをコピーの方法ではdomの順序が少し変更するだけで作成したスクレイピングのコードが動作しなくなってしまうがdomの中身が変更されない限り動作するコードにすることが出来る。

x_path配下のように記述する。

```
//タグ[contains(@要素,'含む文字列')]"
```

具体例

```
<a href="http://sample/following.jp" class="follow">フォロー</a>
```

```
#属性の値で取得するときは @属性 とする　
driver.find_element_by_xpath("//a[contains(@href,'following')]")

#このようにテキストで取得も可能
driver.find_element_by_xpath('//a[contains(text(), "フォロー")]')
```

またこの記述で`orやnot or, and`を使うことも出来る。

```
<a href="http://sample/following.jp" class="follow">フォロー</a>
<a href="http://sample/follower.jp" class="follow">フォロワー</a>
```

```:取得

#or
#両方の要素を取得できる
driver.find_elements_by_xpath("//a[contains(@href,'follower') or contains(@href, 'following')]")

#not or
#follwingとfollower以外の文字列を含むhrefをaタグを取得
driver.find_elements_by_xpath("//a[not(contains(@href,'follower') or contains(@href, 'following'))]")

#and
#classがfollowでhrefにfollowerを含むaタグを指定
driver.find_elements_by_xpath("//a[contains(@href,'follower') and contains(@class, 'follow')]")
```

### except KeyboardInterrupt:
途中で処理を中断したいときに`except KeyboardInterrupt:`とすることでその段階までの処理を保存しておくことが出来る。

```
try:
    処理
except KeyboardInterrupt:
    止める処理
```

# セットアップのベース

```
class SeleniumSetUp:
    def __init__(self, home_url:str, user_id = "none", password="none"):
        self.op = Options()
        # --headlessだけではOSによって動かない、プロキシが弾かれる、
        # CUI用の省略されたHTMLが帰ってくるなどの障害が出ます。
        # 長いですが、これら6行あって最強かつどんな環境でも動きますので、必ず抜かさないようにしてください。
        self.op.add_argument("no-sandbox")
        self.op.add_argument("--disable-extensions")
        # self.op.add_argument("--headless")
        self.op.add_argument('--disable-gpu')  
        self.op.add_argument('--ignore-certificate-errors')
        self.op.add_argument('--allow-running-insecure-content')
        self.op.add_argument('--disable-web-security')
        self.op.add_argument('--disable-desktop-notifications')
        self.op.add_argument("--disable-extensions")
        self.op.add_argument('--lang=ja')
#         self.op.add_argument('--blink-settings=imagesEnabled=false')
        self.op.add_argument('--disable-dev-shm-usage')
        self.op.add_argument('--proxy-server="direct://"')
        self.op.add_argument('--proxy-bypass-list=*')    
        self.op.add_argument('--start-maximized')
        
        self.home_url = home_url
        
        self.user_id = user_id
        self.password = password  
        
    def driver_set(self):
        driver = webdriver.Chrome(chrome_options = self.op)
        driver.get(self.home_url)
        time.sleep(1)
        return driver
    
    def login(self, input_id, input_pass, login_button):
        # ユーザーidのinput要素を取得する
        input_name.send_keys(self.user_id)

        #  パスワードのinput要素を取得する
        input_pass.send_keys(self.password)

        #  ログインボタンの要素を取得する
        login_button.click()

        time.sleep(1)
        return driver
    
    #取得したデータを統合
    def auto_csv(self, datas: list, cols: list, file_name: str, make=False):
        if len(datas) != len(cols):
            return "希望のデータのカラムと作成したデータのカラムの数が異なります"

        else:
            pd_datas = []

            for data in datas:
                pd_datas.append(pd.DataFrame(data))

            concat = pd.concat([li.T for li in pd_datas]).T
            concat.columns = cols

            if make == True:
                concat.to_csv("{}.csv".format(file_name))
            else:
                pass

            return concat
    
    # 元のタブを閉じて２つ目のタブをメインタブにする
    def tab_move(self, driver, close=True):
        # tab移動
        # driver.window_handles[タブの番号]
        driver.switch_to.window(driver.window_handles[1])
        driver.switch_to.window(driver.window_handles[0])
        if close == True:
            
            # 初めに開いていたウインドウを閉じる
            driver.close()
            
            # 新しく開かれたタブを新規にインスタンスウインドウとする これをしないとNosuchElementエラーになる
            driver.switch_to.window(driver.window_handles[0])
        
        else:
            driver.switch_to.window(driver.window_handles[1])
        
        return driver
    
    # 元のタブを閉じて２つ目のタブをメインタブにする
    def tab_return(self, driver, close=True):
        # tab移動
        # driver.window_handles[タブの番号]
        driver.switch_to.window(driver.window_handles[0])
        driver.switch_to.window(driver.window_handles[1])
        if close == True:
            driver.switch_to.window(driver.window_handles[0])
            # 初めに開いていたウインドウを閉じる
            driver.close()
            
            # 新しく開かれたタブを新規にインスタンスウインドウとする これをしないとNosuchElementエラーになる
            driver.switch_to.window(driver.window_handles[0])
        
        else:
            driver.switch_to.window(driver.window_handles[0])
        
        return driver
```

#参考文献

・[Python + Selenium で Chrome の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693)

・[便利なXPatyまとめ](https://techblog.zozo.com/entry/xpath)

・[Pypl chromedriver-binary](https://pypi.org/project/chromedriver-binary/#history)

・[Python Seleniumでスクリーンショットを撮る](https://qiita.com/shutokawabata0723/items/431e9fd0c1cf709347da)

・[Seleniumチートシート](https://qiita.com/DisneyAladdin/items/be771591c0a06b30a369)