GoogleColaboratoryで気象庁の過去気象データをスクレイピングしてみた。

【2020/12/7 追記】初版から約2年経った今でも見られてるらしいので文章を整理したりしました。
# 1.はじめに
## 1.1 前置き
学校のとある調査で気象庁のデータが大量に必要になりました。[気象庁ではここでscvファイルでダウンロードする](http://www.data.jma.go.jp/gmd/risk/obsdl/index.php)ことができます。しかし、それには**容量制限**があるので一都市ずつファイルを合成するなど人間の労力が必要になります。2,3都市ならまだしも40都市以上必要なので今回は**スクレイピング**することにしました。

## 1.2 今回目指すもの

```
札幌, 室蘭, 函館, 青森, 秋田, 盛岡, 山形, 仙台, 福島, 新潟, 金沢, 富山, 長野, 宇都宮, 福井, 前橋, 熊谷, 水戸, 岐阜, 名古屋, 甲府, 銚子, 津, 静岡, 横浜, 松江, 鳥取, 京都, 彦根, 広島, 岡山, 神戸, 和歌山, 奈良, 松山, 高松, 高知, 徳島, 下関, 福岡, 佐賀, 大分, 長崎, 熊本, 鹿児島, 宮崎
```
以上46都市の

```
年月日, 陸の平均気圧(hPa), 海の平均気圧(hPa), 降水量(mm), 平均気温(℃), 平均湿度(%), 平均風速(m/s), 日照時間(h)
```
を```csv形式```でスクレイピングします。

# 2.サイトの解析
## 2.1 注意事項
まずは、対象にするサイトや組織から**APIが提供されているか？**ということを調べましょう。TwitterなどはAPIが提供されており簡単に好きなデータを取ることができます。この場合スクレイピングは**不必要**ということです。気象庁では当然のようにAPIはありません。一般人が作ったAPIはたくさんありますが(Githubにあると思う)、必要なデータが果たしてとれるのかということを吟味しなければなりません。
もう一つ。それは、サイトがしっかりスクレイピングできる構造になっているかどうかです。僕自身、スクレイピングをするのは初めてでしたので、適しているかどうかをはっきり区別できるわけではありませんが。(区別の仕方は次項で)

## 2.2 サイトの解析
今回は気象庁のデータサイトを例にします。データが**見れる(表示されている)**サイトにアクセスしてください。([気象庁ではここ](http://www.data.jma.go.jp/obd/stats/etrn/index.php?prec_no=&block_no=&year=&month=&day=&view=))
やることは二つあります。

### 2.2.1 やること①:URLの解析
試しに札幌の2017年の1月の日ごとのデータを見てみましょう。URLは以下の通りです。

```text:
http://www.data.jma.go.jp/obd/stats/etrn/view/daily_s1.php?prec_no=14&block_no=47412&year=2017&month=1&day=&view=
```
URLの中の```?```より前の部分はサーバーへアクセスるための住所ですが、```?```より後ろは**Query**と言われサイトに主に文字を渡すことができます。つまり、気象庁のサーバーは自らにアクセスしてきたURLからQueryを取り出しサイトを表示しているのです。
よって、後ろのQueryを編集すればほかのサイトが取れるはずです。Queryは```key=value```の形になっており、`&`で区切って複数付けることができます。検証してみたところ都市は```prec_no```(都道府県の番号)と```block_no```(都市？エリア？の番号)の二つで管理され省略は**不可**でした。年月日などは```year```(年)、```month```(月)と```day```(日にち)の三つで管理され日にち以外の二つは**省略不可**でした。最後にサイトのテンプレートです。このサイトは複数のプリセットが用意されており、必要なデータだけを表示することができます。これは```view```で表され、省略すると```s1```が与えられています。```s1```のViewは一番多くのデータを表示していたので今回はこれを使います。下の画像は順に`s1`と`a1`の表示の様子です。
<img width="1128" alt="2018-11-24 (5).png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/46751464-5919-29ba-827d-ba2b2735e67c.png">

<img width="1128" alt="2018-11-24 (6).png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/0f559f82-2dcf-4cc9-c6a7-222693303004.png">

## 2.2.2 やること②:Webページの解析
試しに札幌の2017年の1月の日ごとのデータを見てみましょう。
http://www.data.jma.go.jp/obd/stats/etrn/view/daily_s1.php?prec_no=14&block_no=47412&year=2017&month=1&day=&view=s1
次に、アクセスしているブラウザで開発者モードにしてください。サイトのソースコードが見えるやつです。HTMLを見ると最初は折りたたまれていると思いますが三角を押すことで展開できます。ソースコードにカーソルを合わせるとその該当部分が変色します。
<img width="1128" alt="2018-11-24.png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/2f62c8e8-73bc-c219-4114-c795b790a1d7.png">
これを見て真ん中の表が光ったら展開、真ん中の表が光ったら展開と繰り返していくと...

<img width="1128" alt="2018-11-24 (1).png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/34fb29fd-a47d-f941-48c2-f8b2a19e2628.png">

表のみが光るようになり、タグを見てみると```table```となっています。これはHTML語で「表」という意味になります。

<img width="1128" alt="2018-11-24 (2).png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/da193be3-c8c0-1277-dd4e-66b132322835.png">

さらに中に入ると横一列のデータがタグで囲まれているというおぞましい光景がみえます。

<img width="1128" alt="2018-11-24 (3).png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/4148f711-39c9-7174-2a1d-95d1bac9cc5b.png">

カーソルを移動すると表示されている表の列も移動します。**非常に楽しい**です。
<img width="1128" alt="2018-11-24 (4).png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/f9290318-a02b-1c4c-1e48-8b3734601587.png">

さて、```table```内の```tr```タグが表の一列をなしているのがわかります。classは```"mtx"```だそうです。matrixの略ですね。最初の四列は横軸なので不必要です。ちなみに```tr```タグの中は下のように```td```タグになっています。
<img width="451" alt="2018-11-24 (7).png" src="https://qiita-image-store.s3.amazonaws.com/0/263233/d59aa4b2-f3e1-ca9e-6e5e-3b1b0f63c46d.png">
さて、仕組みが分かったところで実装しましょう。

# 4.実装
まずは、都市名、prec_no、block_noを調べ上げて```list```にしましょう。以下の通りです。

```python:

 place_codeA = [14, 21, 23, 31, 32, 33, 35, 34, 36, 54, 56, 55, 48, 41, 57, 42, 43, 40, 52, 51, 49, 45, 53, 50, 46, 68, 69, 61, 60, 67, 66, 63, 65, 64, 73, 72, 74, 71, 81, 82, 85, 83, 84, 86, 88, 87]
 place_codeB = [47412, 47423, 47430, 47575, 47582, 47584, 47588, 47590, 47595, 47604, 47605, 47607, 47610, 47615, 47616, 47624, 47626, 47629, 47632, 47636, 47638, 47648, 47651, 47656, 47670, 47741, 47746, 47759, 47761, 47765, 47768, 47770, 47777, 47780, 47887, 47891, 47893, 47895, 47762, 47807, 47813, 47815, 47817, 47819, 47827, 47830]
 place_name = ["札幌", "室蘭", "函館", "青森", "秋田", "盛岡", "山形", "仙台", "福島", "新潟", "金沢", "富山", "長野", "宇都宮", "福井", "前橋", "熊谷", "水戸", "岐阜", "名古屋", "甲府", "銚子", "津", "静岡", "横浜", "松江", "鳥取", "京都", "彦根", "広島", "岡山", "神戸", "和歌山", "奈良", "松山", "高松", "高知", "徳島", "下関", "福岡", "佐賀", "大分", "長崎", "熊本", "鹿児島", "宮崎"]   
```

インデックス番号で等しくなっています。例えば札幌は```place_name```のなかの0番ですから、prec_codeはplace_codeAの0番。また、place_codeBの0番です。まぁ、順序が同じってことです。追加したい都市を後ろに追加すればいいだけです。(prec_noとblock_noはサイトにアクセスすればURLに書いてあります。ちゃんと追加してくださいね。)

実装に移りましょう。

```python:main.py
#さっきの
place_codeA = [14, 21, 23, 31, 32, 33, 35, 34, 36, 54, 56, 55, 48, 41, 57, 42, 43, 40, 52, 51, 49, 45, 53, 50, 46, 68, 69, 61, 60, 67, 66, 63, 65, 64, 73, 72, 74, 71, 81, 82, 85, 83, 84, 86, 88, 87]
place_codeB = [47412, 47423, 47430, 47575, 47582, 47584, 47588, 47590, 47595, 47604, 47605, 47607, 47610, 47615, 47616, 47624, 47626, 47629, 47632, 47636, 47638, 47648, 47651, 47656, 47670, 47741, 47746, 47759, 47761, 47765, 47768, 47770, 47777, 47780, 47887, 47891, 47893, 47895, 47762, 47807, 47813, 47815, 47817, 47819, 47827, 47830]
place_name = ["札幌", "室蘭", "函館", "青森", "秋田", "盛岡", "山形", "仙台", "福島", "新潟", "金沢", "富山", "長野", "宇都宮", "福井", "前橋", "熊谷", "水戸", "岐阜", "名古屋", "甲府", "銚子", "津", "静岡", "横浜", "松江", "鳥取", "京都", "彦根", "広島", "岡山", "神戸", "和歌山", "奈良", "松山", "高松", "高知", "徳島", "下関", "福岡", "佐賀", "大分", "長崎", "熊本", "鹿児島", "宮崎"]   

import requests
from bs4 import BeautifulSoup #ダウンロードしてなかったらpipでできるからやってね。
import csv

# URLで年と月ごとの設定ができるので%sで指定した英数字を埋め込めるようにします。
base_url = "http://www.data.jma.go.jp/obd/stats/etrn/view/daily_s1.php?prec_no=%s&block_no=%s&year=%s&month=%s&day=1&view=p1"

#取ったデータをfloat型に変えるやつ。(データが取れなかったとき気象庁は"/"を埋め込んでいるから0に変える)
def str2float(str):
  try:
    return float(str)
  except:
    return 0.0


if __name__ == "__main__":
  #都市を網羅します
  for place in place_name:
    #最終的にデータを集めるリスト (下に書いてある初期値は一行目。つまり、ヘッダー。)
    All_list = [['年月日', '陸の平均気圧(hPa)', '海の平均気圧(hPa)', '降水量(mm)', '平均気温(℃)', '平均湿度(%)', '平均風速(m/s)', '日照時間(h)']]
    print(place)
    index = place_name.index(place)
    # for文で2007年~2017年までの11回。
    for year in range(2007,2018):
      print(year)
      # その年の1月~12月の12回を網羅する。
      for month in range(1,13):
        #2つの都市コードと年と月を当てはめる。
        r = requests.get(base_url%(place_codeA[index], place_codeB[index], year, month))
        r.encoding = r.apparent_encoding

        # まずはサイトごとスクレイピング
        soup = BeautifulSoup(r.text)
        # findAllで条件に一致するものをすべて抜き出します。
        # 今回の条件はtrタグでclassがmtxになってるものです。
        rows = soup.findAll('tr',class_='mtx')

        # 表の最初の1~4行目はカラム情報なのでスライスする。(indexだから初めは0だよ)
        # 【追記】2020/3/11 申し訳ございません。間違えてました。
        rows = rows[4:]

        # 1日〜最終日までの１行を網羅し、取得します。
        for row in rows:
          # 今度はtrのなかのtdをすべて抜き出します
          data = row.findAll('td')

          #１行の中には様々なデータがあるので全部取り出す。
          # ★ポイント
          rowData = [] #初期化
          rowData.append(str(year) + "/" + str(month) + "/" + str(data[0].string))
          rowData.append(str2float(data[1].string))
          rowData.append(str2float(data[2].string))
          rowData.append(str2float(data[3].string))
          rowData.append(str2float(data[6].string))
          rowData.append(str2float(data[9].string))
          rowData.append(str2float(data[11].string))
          rowData.append(str2float(data[16].string))

          #次の行にデータを追加
          All_list.append(rowData)
          
    #都市ごとにデータをファイルを新しく生成して書き出す。(csvファイル形式。名前は都市名)
    with open(place + '.csv', 'w') as file:
      writer = csv.writer(file, lineterminator='\n')
      writer.writerows(All_list)
```

コメントを雑ではありますが書いておいたので、半分はそれで理解を。
## ★ポイントの解説
BeautifulSoupは名前こそ面白いものの、HTML語のデータをわかりやすい形で抜き出します。そのデータをインデックスでとればいいのです。あとは左からn番目のtdを取得して配列に入れてあげます。

## GoogleColaboratoryでのデータのダウンロード
GoogleColaboratoryではファイルを生成しても仮想サーバー上に生成されるだけです。仮想サーバー上にあるデータをダウンロードしましょう。

```python:
#さっきの(セルを新たに作ってやれば簡単)
place_codeA = [14, 21, 23, 31, 32, 33, 35, 34, 36, 54, 56, 55, 48, 41, 57, 42, 43, 40, 52, 51, 49, 45, 53, 50, 46, 68, 69, 61, 60, 67, 66, 63, 65, 64, 73, 72, 74, 71, 81, 82, 85, 83, 84, 86, 88, 87]
place_codeB = [47412, 47423, 47430, 47575, 47582, 47584, 47588, 47590, 47595, 47604, 47605, 47607, 47610, 47615, 47616, 47624, 47626, 47629, 47632, 47636, 47638, 47648, 47651, 47656, 47670, 47741, 47746, 47759, 47761, 47765, 47768, 47770, 47777, 47780, 47887, 47891, 47893, 47895, 47762, 47807, 47813, 47815, 47817, 47819, 47827, 47830]
place_name = ["札幌", "室蘭", "函館", "青森", "秋田", "盛岡", "山形", "仙台", "福島", "新潟", "金沢", "富山", "長野", "宇都宮", "福井", "前橋", "熊谷", "水戸", "岐阜", "名古屋", "甲府", "銚子", "津", "静岡", "横浜", "松江", "鳥取", "京都", "彦根", "広島", "岡山", "神戸", "和歌山", "奈良", "松山", "高松", "高知", "徳島", "下関", "福岡", "佐賀", "大分", "長崎", "熊本", "鹿児島", "宮崎"]   

from google.colab import files

for place in place_name:
  files.download(place + '.csv')
```
途中で「このサイトからの連続のファイルダウンロードを許可しますか？」みたいのが出てくるので許可を押してください。これでローカルにダウンロードできたはずです。

# 4.まとめ
こんかいは特定のサイトがスクレイピングできるかどうかを判断しそのサイトの規則性を見出して連続でスクレイピングしてみました。急いで書き上げたので至らなかったところもあるかと思います。ぜひ、質問してください。
Twitter: https://twitter.com/CyberHacnoshuke (フォローしてくださいお願いします。)
Github: https://github.com/CyberHacnoshuke