DullCodes’s diary

programming,c++,python,MachineLearning,Math,Django,Competitive

Scraping - CodeForcesのExampleを取得したい

Scraping

ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。

ウェブスクレイピング - Wikipedia

Python だといろいろパッケージあるけど、初心者向けのやつは

  • requests

requests-docs-ja.readthedocs.io

  • BeautifulSoup

とても公式サイトとは思えないほどとっつきにくい見た目

www.crummy.com

というのが二大巨頭らしい
CodeForcesのサイトから Examples を取得してin.txt out.txtみたいなやつを
作って c++ から出る出力を比べてテストをする感じ

requests

url から HTTP response を request オブジェクトとして取得する
使い方は超かんたん
htmlがほしいだけなので http get メソッドで終了

import requests

# requests オブジェクト となる res : response
res = requests.get('https://www.crummy.com/software/BeautifulSoup/')

print(res)
print(res.url)
print(res.status_code)
print(res.headers)
print(res.encoding)
print(res.text)

出力

$ py main.py 

# res
<Response [200]>

# res.url
https://www.crummy.com/software/BeautifulSoup/

# res.status_code
200

# res.headers
{'Date': 'Sun, 15 Mar 2020 21:15:41 GMT', 'Server': 'Apache/2.4.18 (Ubuntu) OpenSSL/1.0.2g mod_wsgi/4.3.0 Python/2.7.12', 'Last-Modified': 'Sun, 15 Mar 2020 21:00:01 GMT', 'ETag': '"28d6-5a0eafc724a73-gzip"', 'Accept-Ranges': 'bytes', 'Vary': 'Accept-Encoding', 'Content-Encoding': 'gzip', 'Content-Length': '4328', 'Keep-Alive': 'timeout=15, max=100', 'Connection': 'Keep-Alive', 'Content-Type': 'text/html; charset=UTF-8'}

# res.encoding
UTF-8

# res.text
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
"http://www.w3.org/TR/REC-html40/transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Beautiful Soup: We called him Tortoise because he taught us.</title>
<link rev="made" href="mailto:leonardr@segfault.org">

...

<form method="get" action="/search/">
   <input type="text" name="q" maxlength="255" value=""></input>
   </form>
   </td>
</tr>

</table>
</body>
</html>

か、簡単 中身はいともたやすくgetすることが出来た
あとはこの status_code や html 本体をパースして欲しい情報を得る
この一連の流れが スクレイピング = scraping という

BeautifulSoup

スクレイピング専用みたいな扱われ方してる 中身は高機能なパーサー
HTTP レスポンスやそのままローカルのHTML を BeautifulSoupオブジェクトにして
中身を抽出する

from bs4 import BeautifulSoup as bs

with open('res.html', 'r') as f:
    # ファイルオブジェクトを渡してあげる
    # lxml はパーサーのエンジンらしい なんだそれは
    soup = bs(f, 'lxml')
    print(soup)

出力
html のタグを formatting して出力するには

from bs4 import BeautifulSoup as bs

with open('res.html', 'r') as f:
    soup = bs(f, 'lxml')
    # vscode の extension にもあるやつ
    print(soup.prettify())

これはすごい
あとは こんな感じでsoup オブジェクトに腐るほどメソッドが
あるので必要なタグや属性を取得して表示する
しかも簡単 というわけで本格的にスクレイピング

細かいメソッドは無制限にブログなりチュートリアルなり動画なりが出てくるので
詳細はggrとして大雑把な概要を

いざCodeForceへ

CodeForces の Problemsページは

f:id:DullCodes:20200316072100p:plain
code forces problem page

こんな感じになっとる
最終的に提出まで行きたいけども、今の所Exampleが取得できればいいので
Exampleのタグがどうなっているかを確認

<div class="problemindexholder" problemindex="A">
  <div class="ttypography">
    <div class="problem-statement">
      <div>
        <p>Mahmoud and Ehab play a game called the even-odd game...</p>
        <p>If the current player can't choose ... </p>
      </div>

      ... (中略) ...

      <!-- ここらへん -->
      <div class="sample-tests">
        <div class="section-title">Examples</div>
        <div class="sample-test">
          <div class="input">
            <div class="title">Input</div>
            <pre>1<br /></pre>
          </div>
          <div class="output">
            <div class="title">Output</div>
            <pre>Ehab</pre>
          </div>
          <div class="input">
            <div class="title">Input</div>
            <pre>2<br /></pre>
          </div>
          <div class="output">
            <div class="title">Output</div>
            <pre>Mahmoud</pre>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

htmlのタグが確認できたらあとはBeautifulSoupを弄って中身をもらう
結果的にこんな感じで取得できた

def convert(sp: bs4):
    # get input samples
    input_samples = []
    for elem in sp.select('.sample-tests > div > .input > pre'):
        input_samples.append(replace_pre_br(elem))

    # get output samples
    output_samples = []
    for elem in sp.select('.sample-tests > div > .output > pre'):
        output_samples.append(replace_pre_br(elem))

    # collect sample from input/output samples
    samples = []
    for idx, (i, o) in enumerate(zip(input_samples, output_samples), start=1):
        samples.append({idx: {'in': i, 'out': o}})
    return samples


def replace_pre_br(elem):
    return str(elem).replace('<pre>', '').replace('</pre>', '').replace('<br/>', '\n')

おそらく色々無駄手間があるコードなんだろうけどしょうがない
とくに replace は絶対にもっとうまく出来るはず
文章中の
タグは \n にしないとおかしな表示になるがbsだけでの
解決手段が見当たらなかったのでやむなく無理やり文字列にして replace という形に

BeautifulSoup の select メソッドがとても使いやすい
基本的に何でも出来るのでこれを使って処理
リンクがたくさん貼ってあるようなページでは
soup.find_all('a') などで全部取得すればいい感じに取れる

スクレイピングで一番重要なことはタグを抽出するところ
つまり sp.select('.sample-tests > div > .input > pre')
これが適切にできたら後はすごく簡単
タグの中身はそのままExampleなので、文字列として受け取って
辞書にするなりなんなりしてから、あとはファイルに書き込み保存するだけ

パッケージ化

どうにかうまくいったのであとはパッケージにしていつでも使えるようにする
自作のモジュールのパッケージ化もとっても簡単
python の import の仕組みをいまいち理解していないフシがあるけど
ググればわかりやすいサイトがいくらでも出てくる

Python: 自作ライブラリのパッケージングについて - CUBE SUGAR CONTAINER

Python 自作モジュールのパッケージ化 · GitHub

まともに見たのはこの2サイトくらい
ちょっと調べれば書籍出版するような上級なプログラマーの方が
わかりやすーく説明してくれているブログやサイトがでてくる
いい時代だ

最終構成はこんな感じ

$ tree CodeForcesTestPackage/
CodeForcesTestPackage/
├── entry_points.cfg
├── get_codeforces_examples
│   ├── __init__.py
├── setup.cfg
└── setup.py
[console_scripts]
# get_codeforces_examples パッケージの
# main() が呼ばれるようにしている
gcf=get_codeforces_examples.get_codeforces_examples:main


[metadata]
name=get_codeforces_examples
version=0.0.1
author=DullCodes
description=get codeforces examples

[options]
# zip されたパッケージを展開せずにスクリプトを実行すること
zip_safe=False
# ソースコード以外の成果物を含める
include_package_data=True
# ???
packages=find:
# このパッケージを起動するときにコマンドを使えるようにする
entry_points=file:entry_points.cfg
# 必要なライブラリを指定
install_requires=
    beautifulsoup4==4.8.2
    requests==2.23.0


from setuptools import setup

setup()


import os
import requests
from pathlib import Path
from bs4 import BeautifulSoup as bs4


def main():
    import sys
    get_codeforces_samples(sys.argv)


def get_codeforces_samples(args):
    url = check_argv(args)

    Message.welcome(url)
    try:
        os.mkdir('samples')
    except FileExistsError:
        pass

    # url is BeautifulSoup obj
    if type(url) == str and url.find('.html') != -1:
        objs = scraping_html(htmlname=url)
        make_samples(objs)
    else:
        # url is local html file
        res = get_request(url)
        samples = scraping_bs4(res)
        make_samples(samples)


def check_argv(args):
    if len(args) != 2:
        raise ValueError
    return args[1]


def get_request(url):
    res = requests.get(url)
    if res.status_code != 200:
        raise ValueError
    return res


def scraping_bs4(res):
    sp = bs4(res.text, 'html.parser')
    return convert(sp)


def scraping_html(htmlname):
    with open(htmlname, 'r') as f:
        sp = bs4(f, 'html.parser')
        return convert(sp)


def convert(sp: bs4):
    """Parser"""
    # get input samples
    input_samples = []
    for elem in sp.select('.sample-tests > div > .input > pre'):
        input_samples.append(replace_pre_br(elem))

    # get output samples
    output_samples = []
    for elem in sp.select('.sample-tests > div > .output > pre'):
        output_samples.append(replace_pre_br(elem))

    # collect sample from input/output samples
    samples = []
    for idx, (i, o) in enumerate(zip(input_samples, output_samples), start=1):
        samples.append({idx: {'in': i, 'out': o}})
    return samples


def replace_pre_br(elem):
    return str(elem).replace('<pre>', '').replace('</pre>', '').replace('<br/>', '\n')


def make_samples(obj: list):
    """make samples in.txt out.txt"""
    here = Path.cwd()
    example_dir = here / 'samples'

    # make in/out
    for line in obj:
        for key, val in line.items():
            for k, v in val.items():
                sample_path = example_dir / f'{k}{key}.txt'
                with open(sample_path, 'w')as f:
                    f.write(v)
    Message.welldone()


class Message:
    @staticmethod
    def welcome(url):
        here = Path(url)
        print(f'>>> running get_codeforces_samples')
        print(f'>>>')
        print(f'>>> now getting "{here}" ...')
        print(f'>>>')

    @staticmethod
    def welldone():
        print(f'>>> well done D;')
        print(f'>>>')
        print(f'>>> we got below')
        Message.show_dir()
        print(f'')

    @staticmethod
    def show_dir():
        here = Path.cwd()
        samples = here / 'samples'
        files = [str(f.resolve()) for f in samples.glob('**/*')]
        for f in files:
            print(f'>>> {f}')


if __name__ == "__main__":
    main()

デコレータを使いたかったけど、まるで理解できなかったため
とても見づらいMessage クラスができた しょうがないのでまたいつか

setup.py のあるファイルで pip install -U . すれば自分のpip環境に
自分で作ったpythonファイルが登録できる

$ ls
entry_points.cfg  get_codeforces_examples  setup.cfg  setup.py

$ pip install -U .
Defaulting to user installation because normal site-packages is not writeable
Processing /home/hoge/hogege/hage/hige/CodeForcesTestPackage

... (60行くらい) ...

Successfully built get-codeforces-examples
Installing collected packages: get-codeforces-examples
Successfully installed get-codeforces-examples-0.0.1


$ pip freeze | grep codeforce
get-codeforces-examples==0.0.1

テスト用にローカルに保存したhtml から Examplesを読み込むための
処理をそのまま組み込んでしまい、修正するのを忘れたまま完成したので
わけのわからん感じに 全く必要ない
基本的な使い方は

CodeForces/ProblemSet/Round473
$ ls


CodeForces/ProblemSet/Round473
$ gcf https://codeforces.com/problemset/problem/959/A

>>> running get_codeforces_samples
>>>
>>> now getting "https:/codeforces.com/problemset/problem/959/A" ...
>>>
>>> well done D;
>>>
>>> we got below
>>> ~/Competitive/CodeForces/ProblemSet/Round473/samples/out2.txt
>>> ~/Competitive/CodeForces/ProblemSet/Round473/samples/in1.txt
>>> ~/Competitive/CodeForces/ProblemSet/Round473/samples/in2.txt
>>> ~/Competitive/CodeForces/ProblemSet/Round473/samples/out1.txt


CodeForces/ProblemSet/Round473
$ cd samples/

CodeForces/ProblemSet/Round473
$ ls
in1.txt  in2.txt  out1.txt  out2.txt

意外とスクレイピングしてる感出ててよかった

何がすごいかというと標準入力からファイルを受け取って
その結果を標準出力に出して out.txt と比べる処理は
bash のコマンドに委ねているというところ

各問題につき一つディレクトリと複数のファイルが増えていくのはさすがに非効率
一つのファイルで出来たんじゃないかこれ感が強い
っていうかできるな簡単に
現状こういう感じでディレクトリ/ファイルが増えていく

$ tree Round473/
Round473/
├── A
│   ├── main.cpp
│   └── samples
│       ├── in1.txt
│       ├── in2.txt
│       ├── out1.txt
│       └── out2.txt
├── B
│   └── samples
│       ├── in1.txt
│       ├── in2.txt
│       ├── out1.txt
│       └── out2.txt
├── C
│   └── samples
│       ├── in1.txt
│       ├── in2.txt
│       ├── out1.txt
│       └── out2.txt
└── D
    └── samples
        ├── in1.txt
        ├── in2.txt
        ├── out1.txt
        └── out2.txt

8 directories, 17 files

いかんでしょ
とにかくスクレイピングの基礎の基礎は学べたんじゃないかということで終了

まとめ

テスト中は百回くらい get 飛ばした気がする
requests は節度を守って使用しよう