スクレイピングのメモ

Pythonで簡単にスクレイピングするときのメモ。

# 最低限必要なライブラリの準備

RequestsとBeautifulSoup4が必須です。

```
python3 -m pip install requests
python3 -m pip install beautifulsoup4
```

# 最も基本的な使い方

ダウンロードと解析の最も簡単な方法は次の通り。

```basic.py
import requests
from bs4 import BeautifulSoup

# URLを取得
url = 'https://uta.pw/sakusibbs/index.php?action=fav_ranking'
html = requests.get(url).text

# BeautifulSoup4で解析
soup = BeautifulSoup(html, 'html.parser')

# CSSセレクタで a タグの class=indextile の要素一覧を取得
a_list = soup.select("a.indextitle")

# 取得した後、繰り返し a タグの一覧を繰り返す
for a in a_list:
    # リンク先(href属性)を表示
    print(a.attrs['href'])
```

## リンク先の相対URLを絶対URLに変換する

なお、リンクは相対URLとなっています。例えば、「./aaa/bbb/ccc」のようなものです。改めて別のリンク先のページを取得する時には、絶対URLが必要です。そこで、相対URLを絶対URLに変換しましょう。
そのために、`urllib.parse.urljoin(基本URL, 相対URL)`を使います。

```basic2_parts.py
# ...抜粋 ...
for a in a_list:
    # リンク先(href属性)を表示
    href = a.attrs['href']
    full = urllib.parse.urljoin(url, href)
    print("url=", full)
```


# 取得結果をキャッシュ - 試行錯誤のために

上記が簡単だが、スクレイピングのプログラムを完成させるまでには、何度もURLからHTMLをダウンロードして解析がうまくいくかを試すことになるはずです。そこで、最初の1回だけダウンロードして、後はキャッシュしたファイルから結果を確認できると、相手先のサーバーに迷惑をかけずに済みます。そこで、以下のように簡単なモジュールを用意して、キャッシュする関数を定義しておくと便利です。

```requests_cache.py
import requests, os, urllib.parse, time
from bs4 import BeautifulSoup

def get(url):
    # キャッシュの確認
    cache_dir = './.cache_data'
    if not os.path.exists(cache_dir): os.mkdir(cache_dir)
    fname = urllib.parse.quote(url)
    fname = os.path.join(cache_dir, fname.replace('/', '__'))
    if os.path.exists(fname):
        print('### cache=', url)
        with open(fname, 'rt') as fp:
            return fp.read()
    # データを取得したら1秒待つ
    print('### download=', url)
    req = requests.get(url)
    with open(fname, 'wt') as fp:
        fp.write(req.text)
    time.sleep(1) # 迷惑をかけないために重要な待機時間
    return req.text

def soup(url):
    return BeautifulSoup(get(url), 'html.parser')
```

## 利用例1 - 簡単な例

このモジュールを使うには、以下のように記述する。以下は、現在時刻を返すページを取得するが、キャッシュしているため、最初に取得した時間がずっと表示されることを確認しよう。

```test_cache.py
import requests_cache as req

url = "https://api.aoikujira.com/time/get.php"
print(req.get(url))
```

## 利用例2 - パースしてリンク一覧を取得

なお、上記のモジュールにはオマケで、BeautifulSoup4のオブジェクトを返すsoupメソッドを用意したので、取得したHTMLをBeautifulSoup4ですぐにパースできます。

以下は、[作詞掲示板](https://uta.pw/sakusibbs/)のランキングに掲載されている作品一覧のタイトルとそのリンクを表示するプログラムです。

```test_cache2.py

import requests_cache as req
import urllib.parse

# URLを取得してBeautifulSoup4オブジェクトを得る
url = 'https://uta.pw/sakusibbs/index.php?action=fav_ranking'
soup = req.soup(url)

# リンク一覧を得る
a_list = soup.select("a.indextitle")
for a in a_list:
    # リンク先(href属性)を表示
    href = a.attrs['href']
    label = a.text.strip()
    full = urllib.parse.urljoin(url, href)
    print('*', label)
    print('- link=', full)
```


## ファイルのダウンロード

これまでダウンロードには、`urllib.request.urlretrieve`が便利と思っていましたが、利用は非推奨とのこと。

```download_regacy.py
import urllib
url = '...'
save_path = '...'
urllib.request.urlretrieve(url, save_path)
```

- (参考) [Pythonのファイルダウンロードにはurlretrieveではなくurlopenを使うべき理由](https://qiita.com/Kanahiro/items/dd585599f36a77540439)

そこで、requestsのストリーミングを使うとエレガントかと思います。

```download_requests.py
import requests, shutil

def download_to_file(url, save_path):
    res = requests.get(url, stream=True)
    if res.status_code != 200:
        raise IOError('Could not download:', url)
    with open(save_path, 'wb') as fp:
        res.raw.decode_content = True
        shutil.copyfileobj(res.raw, fp)

download_to_file('https://...', '/path/to/file')
```

もちろん、小さなファイルなら、メモリに取得してファイルに直接保存する方法が面倒がないでしょう。

```download_easy.py
import requests
url = 'https://...'
save_path = '...'
# (1) 2行で書く
with open(save_path, 'wb') as fp:
     fp.write(requests.get(url).content)
# (2) 1行で書く
open(save_path, 'wb').write(requests.get(url).content)
```

- (参考) [stack overflow - How to download image using requests](https://stackoverflow.com/questions/13137817/how-to-download-image-using-requests)


# 参考書籍

スクレイピングに関する本を書きました。基本から応用まで、全体的に書いています。良かったら見てください！

https://www.amazon.co.jp/%E5%A2%97%E8%A3%9C%E6%94%B9%E8%A8%82Python%E3%81%AB%E3%82%88%E3%82%8B%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0-%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92-%E9%96%8B%E7%99%BA%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF-%E3%82%AF%E3%82%B8%E3%83%A9%E9%A3%9B%E8%A1%8C%E6%9C%BA/dp/4802611927

## （追記) 長時間にわたってスクレイピングしたい時は？

- (1) なお、取得したいデータが結構な量がある場合、どのようにスクレイピングさせておくことができるでしょうか。一番簡単なのは、**PCを付けっぱなしにしておく**ことです。
- (2) とは言え、電気代も気になりますし、何よりその間PCが使えないのは不便です。そこで、**省電力なRaspberry Piなど小型のLinuxマシン**を使うのも手でしょう。スクレイピング程度の仕事なら、ほとんどCPUパワーも不要なので、格安のラズパイでも十分役立ってくれます。
- (3) また、素直に月数百円で使える、**VPSなどを契約**するのも手です。

## リモートマシンにSSH接続して、Pythonを実行する時

上記(2)のRaspberry Piや、(3)のVPSを使う場合に、SSH接続でマシンにリモート接続する場合、ターミナルを閉じると実行中のPythonも終了してしまいます。そこで、端末多重化アプリの`tmux`を使います。

- (1)tmuxを起動してから、`python3 プログラム`でスクレイピングのプログラムを実行します。
- (2) そして、その後、`[プレフィックスキー(ctrl+bなど)] d`を押すとtmuxからデタッチします。（デタッチすると、プロセスは終了するのではなく実行中のまま残ります。）
- (3) 作業が終わったか確認したい場合、SSHでマシンに接続した後、`tmux a`で改めて前回起動したセッションに接続できます。