PythonとGASでAtCoderのContest予定をGoogleCalendar上に作成する

#初めに
AtCoderのコンテストが不定期に開催されているが、時々予定に入れ忘れたりして、家族に迷惑をかけそうだったので自動化することにした（できるかは分からない）。
実行にはGoogleColaboratoryを使用した。
プログラミング初心者なのでコードが煩雑だったり間違ってたりするかもしれません。ごめんなさい。
#AtCoderからGoogleCalendarまで
###手順
1. AtCoderからPythonのrequestsやBeautifulSoupなどを使ってスクレイピング
2. スクレイピングしたデータをPythonのgspreadなどを使いGoogleSpreadSheetsにまとめる
3. GoogleSpreadSheetsからGASを使ってGoogleCalendarに予定を作成

##1. スクレイピング
実際のコード
AtCoderの開催予定コンテストの名前、開催日、リンクのみを、AtCoderホームページの「コンテスト一覧」から取得してlistにする。

```python 
from bs4 import BeautifulSoup # BeautifulSoupのインポート
import requests # requestsのインポート
import datetime
import re

url = "https://atcoder.jp/contests/"
response = requests.get(url).text
soup = BeautifulSoup(response, 'html.parser') # BeautifulSoupの初期化
tags = soup.select("tbody a") # tbody下のaタグを全て選択

l=[]
l_n=[]
l_link=[]
l_n1=[]

now=datetime.datetime.now().strftime('%Y-%m-%d %H:%M') # 現在の年、月、日、時間を取得

for i in tags:
  l.append(i.text) # aタグ内のテキストを取得
  l.append(i.get("href")) # aタグに付いてるリンクを取得
l.remove("practice contest") 
l.remove("/contests/practice")
l.remove("AtCoder Library Practice Contest")
l.remove("/contests/practice2")
l=[l[i:i + 4] for i in range(0,len(l), 4)]
for i in range(len(l)):
  l[i][0]=l[i][0][0:16]
  del l[i][1]
  if (l[i][0][0:4] > now[0:4]) or (l[i][0][0:4] == now[0:4] and l[i][0][5:7] > now[5:7])
 or (l[i][0][0:4] == now[0:4] and l[i][0][5:7] == now[5:7] and l[i][0][8:10] >= now[8:10]): # 過去のコンテストをリストから削除
    l_n.append(l[i])
for i in range(len(l_n)):
  l_link.append("https://atcoder.jp"+l_n[i][2]) # 相対リンクを絶対リンクに変更
```
コンテストの開始時間、終了時間、Ratedやペナルティなどは取得しづらかったので、それぞれのリンクに飛んで取得した。

```python
for i in l_link:
  url_n=i
  response_n = requests.get(url_n).text
  soup_n = BeautifulSoup(response_n, 'html.parser') # BeautifulSoupの初期化
  tags2=soup_n.select("span.mr-2")+soup_n.select("small.contest-duration") # Ratedやペナルティ、開催時間などを取得
  for j in tags2:
    l_n1.append(j.text)
l_n1=[l_n1[i:i + 4] for i in range(0,len(l_n1), 4)]
for i in range(len(l_n1)):
  l_n1[i][1]=l_n1[i][1][13:]
  l_n1[i][1]=l_n1[i][1].replace("-","~")
  l_n1[i][2]=l_n1[i][2][9:]
  l_n1[i][3]=re.sub("\n","",l_n1[i][3])
  l_n1[i][3]=re.sub("\t","",l_n1[i][3])
  l_n1[i][3]=l_n1[i][3][54:60]
  del l_n1[i][0]
for i in range(len(l_n)):
  l_n[i]+=l_n1[i]
```
（参考：[https://dividable.net/programming/python/python-scraping](https://dividable.net/programming/python/python-scraping)）

##2. データをスプレッドシートへ
取得したデータをスプレッドシートに貼り付ける。

```python
from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())
worksheet = gc.open('AtCoderNewContestList').get_worksheet(0) # AtCoderNewContestListという名前のスプレッドシートの1シート目を指定

for i in range(len(l_n)):
  if worksheet.update_acell("B"+str(i+2),l_n[i][1]) in worksheet.range('B2:B10'): # コンテストがもう追加されていたら処理をしない
    continue
  else:
    worksheet.update_acell("B"+str(i+2),l_n[i][1])
    worksheet.update_acell("C"+str(i+2), l_n[i][0][0:4]+"/"+l_n[i][0][5:7]+"/"+l_n[i][0][8:10])
    worksheet.update_acell("D"+str(i+2),l_n[i][0][11:])
    worksheet.update_acell("E"+str(i+2),l_n[i][-1])
    worksheet.update_acell("F"+str(i+2),l_n[i][3])
    worksheet.update_acell("G"+str(i+2),l_n[i][4])
    worksheet.update_acell("H"+str(i+2),"https://atcoder.jp"+l_n[i][2])
  worksheet.update_acell("A"+str(i+2),"")
```
結果が以下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499174/8c167512-57ca-6ad4-8d72-6830b6e57a40.png)
割とうまくいっている。
##3. GASで予定を追加
GASを使ってGoogleCalendarに予定を追加する。
諸々の事情から、新しいGoogleアカウントを作って、作成したスプレッドシートにアクセス、GASでデータを抜き取り予定を作成、とすることにした。
ら、初めに作ったアカウントにアクセスできなくなった。なんで？
スプレッドシートの「ツール」から「スクリプトエディタ」を選択するとGASのコードが書ける。
（GASはコードの内容は理解できるが書けはしないので、下のサイトのコードをコピーして編集した。）
（参考：[https://qiita.com/cazimayaa/items/5fdfbc060dff7a11ee15](https://qiita.com/cazimayaa/items/5fdfbc060dff7a11ee15)）


```

function myFunction() {
  // 今選択中のスプレッドシートのシートを取得
  var sheet = SpreadsheetApp.getActiveSheet();
  // 取得したシートから、セルの中身を取得
  var values = sheet.getDataRange().getValues();
  var calendar = CalendarApp.getDefaultCalendar();
  // ※var i の0番目はヘッダーになるので、1からスタートします。
  for (var i = 1; i < values.length; i++) {
    var status = values[i][0];
    if (
      status != "済" // 連携の欄が済になっていなかったら処理を行う
    ) {
      // 予定日
      var date = values[i][2];

      // 開始時間
      var startTime = values[i][3];
      var startDateTime = new Date(date.getFullYear(),
                                   date.getMonth(),
                                   date.getDate(), 
                                   startTime.getHours(),
                                   startTime.getMinutes(), 0);
      // 終了時間
      var endTime = values[i][4];
      var endDateTime = new Date(date.getFullYear(),
                                 date.getMonth(),
                                 date.getDate(),
                                 endTime.getHours(),
                                 endTime.getMinutes(), 0);
      // タイトル
      var title = values[i][1]+values[i][8];
      var options = {
        description: values[i][7]
      }
      // var event = calendar.createEvent(title, startDateTime, endDateTime);
      // 引数にoptionsを追加するだけ
      var event = calendar.createEvent(title, startDateTime, endDateTime, options);
      // カレンダーへ登録
      sheet.getRange(i + 1, 1).setValue("済"); // 連携の欄を済にする
    }
  }
}
```
このままでもいいかもしれないが、スプレッドシートが更新されたらカレンダーも連動させたいので、トリガーを設定する。（参考：[https://auto-worker.com/blog/?p=1646](https://auto-worker.com/blog/?p=1646https://auto-worker.com/blog/?p=1646)）
「現在のプロジェクトのトリガー」から、シート変更時に関数を実行するようにする。

実行後
カレンダー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499174/01c43832-25f2-6aa9-17cd-13068ed417f5.png)
スプレッドシート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499174/886a7cf5-04c2-4640-53d2-594173a025af.png)
やったぜ。
##4. 追記
まさかのコンテスト終了時間が1時になることでカレンダー追加できない問題発生。無理やり時間の方に24を足すことでどうにかならないかな...