水文水質データベースの1時間降水量データをスクレイピング

## はじめに
長期間の連続的な降雨データが必要となったため，水文水質データベースから1時間降水量データをスクレイピングすることにしました。
降水量データを（正攻法的に）取得する方法のひとつは，[気象庁アメダス](https://www.data.jma.go.jp/gmd/risk/obsdl/index.php)のデータを取得することです。
しかし，気象庁アメダスよりデータをダウンロードする際には，データ量に制限がかけられています。
例えば，1時間降水量データは1年強くらいしか一度にダウンロードできません。

最近，台風シーズンになるとよく「100年に一度の雨」とかニュースで耳にしますが，この年数（降雨の発生確率）を求めるためには，数十年程度の降水量データが必要です。
気象庁アメダスから例えば40年分の降水量データをダウンロードしようとすると，それはそれはまあ大変な作業になります。

<!--
### 気象庁アメダスからスクレイピングできないか？
気象庁のアメダスから正攻法でダウンロードするのが面倒なら，
「気象庁のアメダスそのもののページからスクレイピングすればいいのでは？」
的な発想が生まれると思います。自分もそう思い，そのプログラムも組んでみました。

しかし，

* 時々スクレイピングのミスが発生する。
* スクレイピングのミスは，各スクレイピング間の時間を長くとることで解決できる。
* でも，その分時間がかかる。

といった問題があり，時間も手間もかかります。
例えば，40年分の降水量データをダウンロードしようとすると，半日くらいかかります。
そして，取得失敗した箇所を自分で確認して，データを入れる作業が必要です。
何のためにスクレイピングしているのかよくわからないことになっちゃいます。
（気象庁のページって，自分の手を動かしてデータを見るときもアクセスに失敗することよくありますよね。）

### 水文水質データベースなら，スクレイピングミスがほとんどない
一方で，水文水質データベースならスクレイピングのミスがほとんどありません。
しかも，スクレイピングにかかる時間も短く，40年程度の降水量データも15分程度でスクレイピング可能です。

このページでは，水文水質データベースから降水量データをPythonでスクレイピングするコードをご紹介します。
-->

## 水文水質データベースから降水量データをクレピング
下記の説明には，正確性を欠いている部分があります。

### 水文水質データベースのURL構造
ここでは，山梨県の小淵沢を例に取り扱ってみます。
URLは下記の通りになっており，3つのことが読み取れます。

> URL: http://www1.river.go.jp/cgi-bin/DspRainData.exe?KIND=2&ID=103081283324005&BGNDATE=20210901&ENDDATE=20211231&KAWABOU=NO
> * ID：103081283324005
> * BGNDATE：20210901
> * ENDDATE:20211231

IDとBGNDATEとENDDATEの3つのパラメータがURLを構成していることがわかります。
これらのパラメータを適当にいじってやれば，任意のページが開けます。

### 基本的なスクレイピングのコード
ここでは，Pythonでスクレイピングする際によく利用されるライブラリ`BeautifulSoup`と`urllib`の`request`を使います。

```python
from bs4 import BeautifulSoup
from urllib import request
```

そして，URLをrequestライブラリで開いて，データを取得します。

```python
url = 'http://www1.river.go.jp/cgi-bin/DspRainData.exe?KIND=2&ID=103081283324005&BGNDATE=20210901&ENDDATE=20211231&KAWABOU=NO'

# 指定したurlを開く
response = request.urlopen(url)
#urlからBeautifulSoupオブジェクトをつくる
soup = BeautifulSoup(response, features='lxml')
#urlを閉じる
response.close()
```

`request.urlopen(url)`で，読んで字のごとく`url`を開きます。
`BeautifulSoup`とは，[公式ドキュメント](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)にある通り，HTMLやXMLファイルからデータを取ってくるPythonライブラリです。
> Beautiful Soup is a Python library for pulling data out of HTML and XML files. It works with your favorite parser to provide idiomatic ways of navigating, searching, and modifying the parse tree.
> 公式ドキュメント：https://www.crummy.com/software/BeautifulSoup/bs4/doc/

ここで，`soup`の中身はこんな感じになってます。

```python
print(soup)

#実行結果の一部
<tr>
<th bgcolor="#FFFFCC"><font size="-1">2021/09/30</font></th>
<td align="right"><font color="#0000ff" size="-1">0.0</font></td>
<td align="right" bgcolor="#e6e6e7"><font color="#0000ff" size="-1">0.0</font></td>
<td align="right"><font color="#0000ff" size="-1">0.0</font></td>
<td align="right" bgcolor="#e6e6e7"><font color="#0000ff" size="-1">0.0</font></td>
<td align="right"><font color="#0000ff" size="-1">0.0</font></td>
...
</tr>
```

このように，`tr`タグの中身に，それぞれの時刻の降水量が入っています。
今回は降水量をスクレイピングしたいので，`tr`タグに含まれるデータを取得します。

```python
tr_data = soup.find_all('tr')
tr_data = tr_data[4:len(tr_data)-1]
```

`soup.find_all('tr')`で，`tr`タグに含まれる要素を引っこ抜きます。
引っこ抜いた最初の数行は降水量データではないので，表データの部分を抽出します。

`tr_data`から降水量データだけを引っこ抜きます。

```python
prec = []
for row in range(len(tr_data)):
    tmp = tr_data[row].text 
    tmp = tmp.splitlines() #文字列を改行ごとに分割，リスト化
    prec.append(tmp)

print(prec)

#実行結果 ['', '2021/09/01', '0.0', '0.0', '0.0', '0.0', '0.0', '0.0', '0.0', '0.0', '0.0', '閉局', '閉局', '欠測', '0.0', '0.0', 0.0', '0.0', '0.0', '0.0', '0.0', '0.0', '0.0', '0.0', '1.0', '0.0']
```

これで，降水量データをリストに格納することができました。
`prec`はそれぞれの「日」と「降水量」を1つの要素とした，「月の日数の長さ」の二次元配列です。

扱いやすいように`pandas`のデータフレームに変換しておきます。
各要素の1成分目はなにもデータが入っていない空の成分なので，それを切り落とします。

```python
df = pd.DataFrame(prec)
df = df.drop([0], axis=1)
df.columns = range(df.shape[1])
```

最終的には，1列目を日時，2列目を降水量とするのが扱いやすいと思います。
そのため，各要素の日時を取得し，それに対応する降水量を配列に収納します。

```python
fixed_prec = [['Date Time', 'Precipitation (mm/h)']]

seq_time = [str(i) + ":00" for i in range(1, 25)]
for row in range(len(df)):
    for col in range(df.shape[1]-1):
        date = df.iat[row, 0]
        tmp_prec = df.iat[row, col+1].strip()
        if tmp_prec == '':
            fixed_prec.append([date + " " + seq_time[col], 'Scraping Failuer'])
        else:
            fixed_prec.append([date + " " + seq_time[col], tmp_prec])

print(fixed_prec)

#実行結果
[['Date Time', 'Precipitation (mm/h)'],
 ['2002/01/01 1:00', '0.0'],
 ['2002/01/01 2:00', '0.0'],
 ['2002/01/01 3:00', '0.0'],
 ['2002/01/01 4:00', '0.0'],
 ['2002/01/01 5:00', '0.0'],
 ['2002/01/01 6:00', '0.0'],
 ['2002/01/01 7:00', '0.0'],
 ['2002/01/01 8:00', '0.0'],
...]
```

このように，スクレイピングができました。

後はcsvファイル等に格納して完了です。

<!--
## スクレイピング時の注意

スクレイピングする際は，ある程度時間間隔を開けておきましょう。
サーバに負荷をかけたとして，[実際に書類送検された例](https://ja.wikipedia.org/wiki/%E5%B2%A1%E5%B4%8E%E5%B8%82%E7%AB%8B%E4%B8%AD%E5%A4%AE%E5%9B%B3%E6%9B%B8%E9%A4%A8%E4%BA%8B%E4%BB%B6)もあります。

> 参考記事
> https://docs.pyq.jp/column/crawler.html
-->

## コードまとめ
これらのコードをまとめた，1地点で複数年をスクレイピングするコードが下記のとおりです。

```Water_Information_System_scraping.py
from bs4 import BeautifulSoup 
import csv
import time
from urllib import request
import pandas as pd

place = '日向山'
ID = '103081283313060'
start_year = 2002
end_year = 2003

begin_date = []
for year in range(start_year, end_year + 1):
    for month in range(1, 13):
        if month < 10:
            begin_date.append(str(year) + '0' + str(month) + '01')
        else:
            begin_date.append(str(year) + str(month) + '01')

end_date = []
for year in range(start_year, end_year + 1):
    for month in range(1, 13):
        if month < 10:
            end_date.append(str(year) + '1231')
        else:
            end_date.append(str(year) + '1231')

fixed_prec = [['Date Time', 'Precipitation (mm/h)']]
start_time = time.time()

for n in range(len(begin_date)):
    print('Scraping ' + str(begin_date[n]) + '...')
    
    base_url = "http://www1.river.go.jp/cgi-bin/DspRainData.exe?KIND=2&ID=%s&BGNDATE=%s&ENDDATE=%s&KAWABOU=NO"
    url = base_url % (ID, begin_date[n], end_date[n])
    
    response = request.urlopen(url)
    soup = BeautifulSoup(response, features='lxml')
    response.close()
    
    #trタグは全ての表データ
    tr_data = soup.find_all('tr')
    
    #不要な行を削除
    tr_data = tr_data[4:len(tr_data)-1]
    
    prec = []
    for row in range(len(tr_data)):
        tmp = tr_data[row].text
        tmp = tmp.splitlines() #文字列を改行ごとに分割，リスト化
        prec.append(tmp)
    
    df = pd.DataFrame(prec)
    df = df.drop([0], axis=1)
    df.columns = range(df.shape[1])
    
    seq_time = [str(i) + ":00" for i in range(1, 25)]
    for row in range(len(df)):
        for col in range(df.shape[1]-1):
            date = df.iat[row, 0]
            tmp_prec = df.iat[row, col+1].strip()
            if tmp_prec == '':
                fixed_prec.append([date + " " + seq_time[col], 'Scraping Failuer'])
            else:
                fixed_prec.append([date + " " + seq_time[col], tmp_prec])
    time.sleep(1)

with open(str(start_year) + str(end_year) + place + '_precipitation_WIS.csv', 'w') as file:
    writer = csv.writer(file, lineterminator='\n')
    writer.writerows(fixed_prec)

end_time = time.time()
print('Caluculation time:{}'.format((end_time - start_time)/60) + ' min')
```

### おわりに
きれいなコードを書くための改善点を教えてください。