【Jupyter Notebook】Atcoderの標準入力を楽に受け取る環境作り

Jupyter Notebookで競プロの入力例を受け取る、快適な環境作りを紹介します。

## 記事でやること

* 競プロしやすいファイル構成を作る
* 標準入力セットをスニペットに登録
* Atcoderから入力例を自動で読み込む


## 環境

- OS : macOS
- エディター : Visual Studio Code
- 言語 : Python

## 1. 競プロしやすいファイル構成を作る
まずは、以下のようにフォルダ内に２つのファイルを作成します。

```html
atcoder
	|- main.ipynb
	|- input.txt
```

`input.txt`ファイルには、入力例を「２つの改行で区切る」形で書き込みます。また、「１つの改行」がある場合、それはある入力例の続きとみなします。自分で作成した入力例も、ここに書き込むことができます。なんかWAになるとき、簡単にコードテストできるので良き。

```txt:input.txt
入力例1(１行目)
入力例1(２行目)

入力例2(１行目)
入力例2(２行目)

入力例3(１行目)
入力例3(２行目)

:
```

## 2. 標準入力セットをスニペットに登録

次に、標準入力を受け取るためのスニペットを登録します。スニペットを使うと、入力例が受けとる呪文を毎回書く必要がなくなります。登録の仕方は以下ような感じです。

1. vscodeで`cmd+shift+p`。コマンドパレットを開く。
2. コマンドパレットに`User snippet`を入力
3. 言語はpython.jsonを選択
4. 開いたpython.jsonファイルに以下を追加

```json:python.json
{
    "cp load input": {
        "prefix": "cp load input",
        "body": [
            "input_case = 1",
            "with open('./input.txt') as file:",
            "    input_lines = file.read().split('\\n\\n')[input_case - 1].split('\\n')",
            "def input(): return input_lines.pop(0)"
        ]
    },
    "cp int": {
        "prefix": "cp int",
        "body": ["int(input())"]
    },
    "cp map": {
        "prefix": "cp map",
        "body": ["map(int, input().split())"]
    },
    "cp s list": {
        "prefix": "cp s list",
        "body": ["input().split()"]
    },
    "cp n list": {
        "prefix": "cp n list",
        "body": ["list(map(int, input().split()))"]
    },
    "cp 2d n list": {
        "prefix": "cp 2d n list",
        "body": ["[list(map(int, input().split())) for i in range(n)]"]
    },
    "cp 2d s list": {
        "prefix": "cp 2d s list",
        "body": ["[list(input()) for i in range(n)]"]
    },
}
```

`prefix`は、入力サジェストされるワードの指定です。
より正確なルールや言葉に従って命名したい人は、設定を変更してみて下さい。

### 登録したスニペットを使ってみる

例として、[ABCコンテスト312のC問題](https://atcoder.jp/contests/abc312/tasks/abc312_c)の入力例を受け取ってみましょう。
まずはinput.txtに、入力例を貼り付けます。（後にこれも自動化する方法を紹介します）

```:input.txt
3 4
110 90 120
100 80 120 10000

5 2
100000 100000 100000 100000 100000
100 200

3 2
100 100 100
80 120
```

次に`main.ipynb`で、先ほど登録したスニペットを利用して、入力例を受け取りましょう。

:::note warn
入力例を受け取るときは、最初に`cp load input`スニペットを使用します。これで、input.txtの内容を読み込むコードが記述されます。提出時は、この記述を削除してください。
:::

```python:main.ipynb
# cp load inputを使用
input_case = 1
with open('./input.txt') as file:
    input_lines = file.read().split('\n\n')[input_case - 1].split('\n')
def input(): return input_lines.pop(0)
# cp mapを使用
n, m = map(int, input().split())
# cp listを使用
a = list(map(int, input().split()))
b = list(map(int, input().split()))
```
`input_case`の数字を変更することで、何番目の入力例を受け取るかを指定できます。


## 3. Python+Automatorで入力例を自動取得
Atcoderから入力例をポチポチコピーするの面倒ですよね。
それをAutomatorを使って自動化してみましょう。
自動化すると、Atocderの問題URLから自動でinput.txtに入力例を書き込めます。

<iframe width="560" height="430" src="https://www.youtube.com/embed/7Y409_Fm8h4?loop=1&playlist=7Y409_Fm8h4&rel=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>


### Atcoderから入力例を取得するPythonコードを作成
まずは、`input_reader.py`という名前のPythonファイルを作成します。保存場所はどこでも大丈夫ですが、私の場合は`/Users/username/python/app/automator/input_reader.py`に保存しました。

次に、このPythonファイルに以下のコードを書き込みます。

:::note warn
コード内の`file_path`変数に`input.txt`のフルパスを設定して下さい。これは、スクレイピングした文字列を`input.txt`に書き込むのに必要です。
:::

```python:input_reader.py
import sys
import requests
from bs4 import BeautifulSoup


def get_problem_examples(url):
    try:
        r = requests.get(url)
        r.raise_for_status()  # HTTPエラーがあればここで例外を引き起こすよ
    except requests.exceptions.RequestException as err:
        print(f"リクエストに問題があったみたいだよ💁‍♀️: {err}")
        sys.exit(1)

    soup = BeautifulSoup(r.text, "html.parser")
    data = soup.find_all("div", class_="part")
    input_data = []
    output_data = []

    for d in data:
        h3 = d.find("h3")
        if h3:
            pre = d.find("pre")
            if h3.string.startswith("入力例"):
                input_data.append(pre.text)
            if h3.string.startswith("出力例"):
                output_data.append(pre.text)

    return input_data, output_data


def write_to_file(input_data, output_data, file_path):
    if input_data and output_data:
        input_data = "\n".join(input_data)
        output_data = "\n".join(output_data)
        res = input_data + "\n--------Answer--------\n" + output_data
        with open(file_path, mode="w") as f:
            f.write(res)


def main():
    url = sys.argv[1]
    # ---- ここ大事!　input.txtのフルパスを設定 ----
    file_path = "~/input.txt"
    # ----------------------------------------
    if url.startswith("https://atcoder.jp/contests/"):
        input_data, output_data = get_problem_examples(url)
        write_to_file(input_data, output_data, file_path)


if __name__ == "__main__":
    main()
```
BeautifulSoupも使うので、pipでインストールしておきましょう。
```bash
pip install beautifulsoup4
```
### ショートカットキーで、Automator->Python実行の設定をする
次に、Automatorを使ってPythonスクリプトを実行する設定を行います。
これにより、ショートカットキーを押すだけで先ほど作成したinput_reader.pyが実行されます。

まず、Automatorを起動して、`cmd + n`で新規から「クイックアクション」を選択し作成します。それから、ワークフローから「シェルスクリプトを実行」を追加し、以下のスクリプトを入力します。

```shell
# クリップボードの内容を取得
clip_content=$(pbpaste)

# input_reader.pyを実行
/opt/homebrew/Caskroom/miniforge/base/envs/python3_8/bin/python /Users/username/python/app/automator/input_reader.py "$clip_content"
```

:::note warn
ただし、`# input_reader.pyを実行`と書かれているところは、ユーザー環境に合わせて記述します。
```
{pythonコマンドのフルパス} {input_reader.pyのフルパス} "$clip_content"
```
Pythonコマンドのフルパスはターミナルで`which python`などと実行し、取得してください。
:::

そしたら 「ワークフローを受け取る項目」を「入力なし」に設定し、保存して完了です。
    ![s３.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727397/0cb3c114-80fd-357d-638d-61ff841354f1.png)

最後にショートカットキーの設定をします。Macの「システム設定」から「キーボード->キーボードショートカット」と進み、さらに「サービス->一般」からinput_readerをダブルクリックし、ショートカットを設定します。私は`cmd + shift + ctrl + r`と設定しました。
    ![s5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727397/1c3f8067-1719-a199-cb43-c69cfb84e0a5.png)

これでおしまいです。

### 実際に使ってみる
1. Atcoderの問題ページに行き、URLを`cmd + c`などでコピーする
1. 先ほど作成したショートカット`cmd + shift + ctrl + r`を実行する

こうすることで、クリップボードにコピーされたURLを入力として、`input_reader.py`が実行されます。結果、`input.txt`に問題の入力例が出力されます。


## 最後に
最初はちょっと面倒かもしれませんが、一度設定すれば結構快適です。
もっと良い方法や、ここがわからない、これがうまくいかないということがあれば、ぜひコメントください。