GCE(VM インスタンス) の Python(Ubuntu) で Selenium を使ったスクレイピング+α

## やりたいこと
- 【[こちらから](https://qiita.com/kawagoe6884/items/940d87d9a0b7d919f1dd)】無料枠でGCEを起動したい
- 【[こちらから](https://qiita.com/kawagoe6884/items/940d87d9a0b7d919f1dd)】SSHの22番ポートを3333番ポートに変更したい
- 【このページ】GCE(仮想マシン) 内の Python(Ubuntu) でSeleniumを使ったスクレイピング
- 【このページ】取得したファイルをGoogle Driveにアップロード
- 【[こちらから](https://qiita.com/kawagoe6884/items/944424e553eab9159b98)】スクレイピングを cron で定期的に実行する
- 【[こちらから](https://qiita.com/kawagoe6884/items/944424e553eab9159b98)】指定の時間にインスタンスをオン/オフする

※※※※※※※※※※※※※※※※※※※※※
※**GCEは作成済みという前提で話を進めます** ※
※※※※※※※※※※※※※※※※※※※※※


## 全体図
![cloud.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/6c42035e-15d4-66d7-537c-b8edad69f16e.jpeg)


## 環境
| アイテム | バージョン |
|:-|:-|
| Ubuntu | 22.04.1 LTS |
| Python | 3.10.6 |
| Selenium | 3.141.0 |
| webdriver-manager | 3.8.3 |
| ChromeDriver | 106.0.5249.91 |



## 準備編
- 動的ページをスクレイピングできるようにしたい
- webdriver-managerのパッケージを使いたい

### ① まずは接続先にSSH接続
```shell:ユーザー名@cloudshell:~ (プロジェクト名)$
ssh -i ~/.ssh/my-ssh-key -p 3333 hogehoge@[外部 IP アドレス]
```

### ② pip がインストールされてなければインストール
```shell:hogehoge@vm-hoge:~$
python --version
# Python 3.10.6

pip3 list
# Command 'pip3' not found, but can be installed with: apt install python3-pip

sudo apt update
sudo apt install python3-pip
# After this operation, 273 MB of additional disk space will be used.
# Do you want to continue? [Y/n]
# ディスクを273MB分使いますよ的なことを聞かれたら `Y`

pip3 --version
# pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)
```

### ③ 必要なパッケージをインストールする
```shell:hogehoge@vm-hoge:~$
pip3 install selenium==3.141.0
pip3 install webdriver-manager
pip3 install pandas
pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
```

### ④ GoogleChrome のインストール
```shell:hogehoge@vm-hoge:~$
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb
```

##### 以下のエラーが出た場合
```Errors were encountered while processing```
```google-chrome-stable```
```shell:hogehoge@vm-hoge:~$
sudo apt install -f
sudo dpkg -i google-chrome-stable_current_amd64.deb
```

### ⑤ Google Drive APIの有効化、鍵の作成
##### 1. 画面左上、Google Cloud ロゴの左にある **ナビゲーション メニュー** から **すべてのプロダクトを表示**
![**ナビゲーション メニュー**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/905ec8b6-5113-79f1-4da7-1335f4100d0e.jpeg)
![**すべてのプロダクトを表示**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/5f5caefb-2f98-b89f-8dd7-f97d2c8f92b6.jpeg)

##### 2. 管理の **API とサービス** を選択
![**API とサービス**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/c17aa343-1974-48f3-343f-32c8f4e8f7c5.jpeg)

##### 3. **ライブラリ** を選択、**一覧 or 検索ボックス** から **Google Drive API** をクリック
![**ライブラリ**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/86f1de8d-4022-96e1-f988-5441b833c73f.jpeg)
![**一覧 or 検索ボックス**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/0c75d929-e920-b566-5b6b-fa8de06869c2.jpeg)

##### 4. Compute Engine APIを **有効にする**
![**有効にする**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/c6160039-8540-f69d-2885-f0aa104c8e83.jpeg)

##### 5. **認証情報** ＞ **認証情報を作成** ＞ **サービス アカウント**
![**認証情報** ＞ **認証情報を作成** ＞ **サービス アカウント**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/d65f903a-698a-231b-7bee-10db8cad6b8c.jpeg)

##### 6. わかりやすい **サービス アカウント名** を入力して **作成して続行**
![**サービス アカウント名** を入力して **作成して続行**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/364d63d5-5784-1c29-7938-674977ad402f.jpeg)

##### 7. **ロールを選択**
![**ロールを選択**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/830c0683-a694-f519-fbae-afb3d7336114.jpeg)

##### 8. **基本** から **編集者** を選択
![**基本** から **編集者**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/398dfb9b-081c-2859-dc68-9167810d566f.jpeg)

##### 9. **続行** して **完了**
![**続行** して **完了**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/4e128e64-143c-c410-1e67-625afeded23a.jpeg)

##### 10. 一覧に戻るので、作成した **サービス アカウント名** を選択
![**サービス アカウント名**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/bf751299-cbb6-bbfc-7193-1ebf037a4fb5.jpeg)

##### 11. **キー** から **鍵を追加**、**新しい鍵を作成** を選択
![**キー** から **鍵を追加**、**新しい鍵を作成** を選択](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/efe0771a-7aea-bef4-6ff0-2ecbc84dcdcc.jpeg)

##### 12. **作成**
![**作成**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/b05b1c5c-750d-0093-5bbd-c3bbc7f531a9.jpeg)

##### 13. ダウンロードされたログを閉じて、**サービス アカウント名** の **左矢印** をクリック
![**左矢印**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/e00ba76c-b369-91d5-8698-f09ac3dc3435.jpeg)

##### 14. **メールアドレスをコピーしておく**
![**メールアドレスをコピーしておく**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/0563fee9-d901-6382-b1ed-8d9fc6b4d80f.jpeg)

### ⑥ Google Driveとの連携
##### 1. Google Drive にフォルダを作って開く
ここでは **scrape** というフォルダを作ります
##### 2. **アクセスを管理**
![**アクセスを管理**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/0f4f60ae-0a34-17ab-c0de-eac485e079fe.jpeg)
##### 3. **ユーザーやグループを追加** に [**⑤ の 14.**](https://qiita.com/kawagoe6884/items/709826bcb1d84957e88f/#14-メールアドレスをコピーしておく) でコピーしたメールアドレスを貼り付ける
![**ユーザーやグループを追加**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/cba95390-61dc-14bd-65ad-e5f4c95f6ac7.jpeg)
##### 4. **送信**
![**送信**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/ee4ed8de-8f0d-cba9-c2d2-08d15bdfcda3.jpeg)



## スクレイピング編
### ① Pythonファイルとjsonファイルのアップロード
##### 1. Pythonファイルを用意
<details><summary>コピペですぐに使えるサンプルコード (折りたたみ)</summary><div>

- ```folder_id``` をフォルダのURLへ変える必要がある　https\://drive.google.com/drive/folders/<font color="red">**XXXXXXXXXXXXXXXXXXXX**　← この部分だけ</font>
- 関数 ```ext_to_mime``` はファイルの拡張子からMIMEタイプを返す処理
  ```.png```と```.csv```だけ記述したので、必要に応じて以下のURLから探して追加すること
  - https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
  - https://github.com/google/google-drive-proxy/blob/master/DriveProxy/API/MimeType.cs

   ```python:qiita.py
   # coding:utf-8
   import glob
   import os
   import time
   from datetime import datetime
   
   import pandas as pd
   from google.oauth2 import service_account
   from googleapiclient.discovery import build
   from googleapiclient.errors import HttpError
   from googleapiclient.http import MediaFileUpload
   from selenium import webdriver
   from selenium.webdriver.chrome.options import Options
   from webdriver_manager.chrome import ChromeDriverManager
   
   
   def start():
       """処理の開始時間
   
       end() - start() で実行時間を測る
   
       """
       global start_time
       start_time = time.time()
   
   
   def end(tag="経過時間"):
       """処理の終了時間
   
       end() - start() で実行時間を測る
   
       """
       if "start_time" in globals():
           print("{}: {:.3f} [sec]".format(tag, time.time() - start_time))
       else:
           print("start関数が呼ばれていない")
   
   
   def ext_to_mime(file_name):
       """`extension` -> `mime type`
       Returns : mime type
       """
       ext = os.path.splitext(file_name)[1]
       if ext == ".csv":
           return "text/csv"
       elif ext == ".png":
           return "image/png"
   
   
   def upload_basic(upload_list):
       """Insert new file.
       Returns : Id's of the file uploaded
   
       Load pre-authorized user credentials from the environment.
       TODO(developer) - See https://developers.google.com/identity
       for guides on implementing OAuth2 for the application.
       """
       creds = service_account.Credentials.from_service_account_file(f"{home_dir}/key.json")
       creds = creds.with_scopes(["https://www.googleapis.com/auth/drive"])
   
       try:
           # create drive api client
           service = build("drive", "v3", credentials=creds)
   
           folder_id = "XXXXXXXXXXXXXXXXXXXX"
           mime_types = [ext_to_mime(file_name=file_name) for file_name in upload_list]
   
           for file_name, mime_type in zip(upload_list, mime_types):
               file_metadata = {"name": f"{file_name}", "parents": [folder_id]}
               media = MediaFileUpload(f"{home_dir}/{file_name}", mimetype=mime_type)
               # pylint: disable=maybe-no-member
               file = (
                   service.files().create(body=file_metadata, media_body=media, fields="id").execute()
               )
               print(f'File ID: {file.get("id")}')
       except HttpError as error:
           print(f"An error occurred: {error}")
   
   
   if __name__ == "__main__":
       # 計測開始
       start()
   
       # 保存先
       home_dir = f"{os.path.expanduser('~')}/scrape"
   
       # fmt: off
       options = Options()
       options.add_argument('--blink-settings=imagesEnabled=false')                    # 画像の非表示
       options.add_argument('--disable-blink-features=AutomationControlled')           # navigator.webdriver=false とする設定
       options.add_argument('--disable-browser-side-navigation')                       # Timed out receiving message from renderer: の修正
       options.add_argument('--disable-dev-shm-usage')                                 # ディスクのメモリスペースを使う
       options.add_argument('--disable-extensions')                                    # すべての拡張機能を無効
       options.add_argument('--disable-gpu')                                           # GPUハードウェアアクセラレーションを無効
       options.add_argument('--headless')                                              # ヘッドレスモードで起動
       options.add_argument('--incognito')                                             # シークレットモードで起動
       # fmt: on
       driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)
       driver.implicitly_wait(10)
   
       # URL アクセス
       url = "https://qiita.com/kawagoe6884/items/cea239681bdcffe31828"
       driver.get(url)
   
       # 画像 保存
       img = driver.find_element_by_tag_name("a")
       with open(f"{home_dir}/qiita.png", "wb") as f:
           f.write(img.screenshot_as_png)
   
       # DataFrame 作成
       df = pd.read_html(driver.page_source)[0]
   
       # DataFrame 保存
       filename = "qiita_table"
       today = datetime.today()
       month = datetime.strftime(today, "%Y%m")
       df.to_csv(f"{home_dir}/{filename}_{month}.csv", index=False, encoding="utf_8_sig")
   
       # driver 閉じる
       driver.quit()
       print("スクレイピング終了")
   
       # ファイルをアップロード
       upload_list = []
       for f in glob.glob(f"{home_dir}/*"):
           if os.path.splitext(f)[1] != ".py" and os.path.splitext(f)[1] != ".json":
               upload_list.append(os.path.split(f)[1])
       upload_basic(upload_list=upload_list)
       print("アップロード完了")
   
       # アップロードしたファイルを削除
       for f in glob.glob(f"{home_dir}/*"):
           if f.split("/")[-1] in upload_list:
               os.remove(f)
   
       # 計測終了
       end()
   ```
</div></details>

##### 2. [**⑤ の 13.**](https://qiita.com/kawagoe6884/items/709826bcb1d84957e88f/#13-ダウンロードされたログを閉じてサービス-アカウント名-の-左矢印) でダウンロードしたjsonファイルを **key.json** へリネーム
   ※便宜上、**key.json** としただけで特に指定なし。

##### 3. ２つのファイルをフォルダに入れる
scrape
　├ key.json
　└ qiita.py

##### 4. 画面右下、**さらに表示**
   ![01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/412bd030-392f-059b-bd4f-352d2b759bca.jpeg)

##### 5. **アップロード**を選択
   ![02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/143fc714-c28c-c558-8baa-edb558bd400f.jpeg)

##### 6. アップロードするフォルダを選択
   フォルダをアップロードする前にポップアップで確認される
   ![**ポップアップ**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/5591c6c2-1dd9-1162-0709-49752494ed28.jpeg)

##### 7. フォルダを**アップロード**
   ![**アップロード**](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/a9322444-75af-6992-6f3d-ce89457013a6.jpeg)

##### 8. コンソールに以下の1行を入力
   ```shell:ユーザー名@cloudshell:~ (プロジェクト名)$
   scp -i ~/.ssh/my-ssh-key -P 3333 -r ~/scrape hogehoge@[外部 IP アドレス]:~/scrape
   ```

### ② スクレイピングしてみる
##### 1. コンソールに以下の2行を入力
   ```shell:ユーザー名@cloudshell:~ (プロジェクト名)$
   ssh -i ~/.ssh/my-ssh-key -p 3333 hogehoge@[外部 IP アドレス]
   ```
   ```shell:hogehoge@vm-hoge:~$
   python3 ~/scrape/qiita.py
   ```

##### 2. ```File ID: XXXXXXXXXXXXXXX``` が2回表示されていればアップロードできてます
![Cloud Shell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/b3564df8-c445-b2bf-5d83-90221adc069b.jpeg)
![Google Drive](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1934278/0bf8ca7d-c5c1-e475-c8b9-16fa93ddfe79.jpeg)

### エラーでつまったこと
- scp のポート指定時は **大文字 <font color="red">-P</font>**、ssh のポート指定時は **小文字 <font color="red">-p</font>**

- webdriver の '--headless' オプションの付け忘れ
  ```The process started from chrome location /usr/bin/google-chrome is no longer running, so ChromeDriver is assuming that Chrome has crashed.``` というエラーが出て、seleniumが起動しない

- 保存先のパス指定が間違っていた
   ```diff_Python:qiita.py
   - with open(f"~/scrape/qiita.png", "wb") as f:
   + with open(f"{os.path.expanduser('~')}/scrape/qiita.png", "wb") as f:
   ```

　
1. Chrome 起動
1. スクレイピングしたいURLにアクセス
1. 取得したデータを画像とCSVで保存
1. Google Driveにアップロード
1. アップロードしたファイルは削除

ここまでで一連の流れが完成したので、あとは定期実行。


## メモ
- 6000を超えるページ(<font color="blue">静的ページ</font>) をスクレイピングしてみた結果
  - かかった費用： **0円**
  - かかった時間：3時間20分
  - CPU使用率は20%を推移、メモリ使用率は不明（Ops Agent入れると250MBくらいメモリ食われるってどこかで見た）

- 4000を超えるページ(<font color="red">動的ページ</font>) をスクレイピングしてみた結果
  - かかった費用： **0円**
  - かかった時間： 3時間
  - CPU使用率は80%を推移、メモリ使用率は不明、1回だけCPU バーストした
    - CPU バーストした理由は ```print()``` のコメントアウトし忘れ。~~2万を超えるリストを print() した~~ なので **実質0回**

- 25000を超えるページ(<font color="blue">静的ページ</font>) をスクレイピングしてみた結果
  - かかった費用： **19円**
  - かかった時間： 13時間30分
  - CPU使用率は30%を推移、メモリ使用率は不明、1回だけCPU バーストした
    - CPU バーストした理由は ```df.to_csv()```？ 出力されたCSVが80MBもあったので書き込みにCPUパワーを使った可能性

無料枠のスペックでも十分やれそう。
<br>

- gspread でも似たようなことができる。ただ、ファイルが複数あるとその数だけスプレッドシートを用意しないといけない。事前設定・リクエスト回数制限・０落ち対策・画像の保存先は？　気にすべきことが多すぎる。~~要はめんどい~~
- Google Drive を Ubuntu にマウントする方法もあるらしい
- サンプルの```qiita.py```は ```service.files().create()``` を使っているのでファイル名が重複していてもアップロードする。
  - 以下は、```新しい.txt```を2回アップロードしたディレクトリの状態。```新しい.txt``` ```新しい (2).txt```とはならない。
    ```
        マイドライブ　> フォルダ
                          ├ 新しい.txt
                          └ 新しい.txt
    ```
  - 新規アップロードではなく、更新をしたい場合は```service.files().update()``` を使う必要がある
    参考サイト
    - https://isgs-lab.com/416/
    - https://developers.google.com/drive/api/v3/reference#Files
    - https://stackoverflow.com/search?q=update+file+google+drive+api+v3+python


## 参考先
https://eng-entrance.com/linux-ssh-scp

https://qiita.com/inoken0926/items/65be0fabee0752d02658

https://developers.google.com/drive/api/guides/manage-uploads#multipart

https://note.com/kohaku935/n/n99779e59561b