CSV形式のデータをJSON形式(KeyValuePairの配列)に変換する

# 背景

- お仕事の関係で Elastic Stack の勉強をしている
- ELK のバージョンは 5.4
- Logstash 5.4 では一応 CSV 形式のデータは扱える (CSV Filter)
    - ただし、ヘッダは削除し、別途指定する必要あり
    - 5.5 でヘッダを自動取り込みするオプションが追加
- わざわざヘッダを削除して、LS の conf ファイルでゴニョゴニョするのは面倒で手間
- LS で Filtering する前に JSON 形式にしておけば楽かも？

# データ

気象庁が公開している最高気温を扱ってみる。
[最新の気象データ - 最高気温 （気象庁）](http://www.data.jma.go.jp/obd/stats/data/mdrr/tem_rct/alltable/mxtemsadext00_rct.csv)

## データの仕様

- CSV 形式 (カンマ区切り)
- Shift_JIS
- CRLF
- 1行目：ヘッダ
- 2行目以降：データ
    - データ部の詳細は [こちら](http://www.data.jma.go.jp/obd/stats/data/mdrr/docs/csv_dl_format_mxtem.html)

# 活用技術
## Azure Functions

Functions の詳細については割愛。

- HttpTrigger - C#
    - GET Request した時に JSON 形式のデータを返す
    - JavaScriptでもよかったのだが、いろいろ難あり C# へ変更（後述）

## Azure Function Tools for Visual Studio 2017

Azure Portal でコード書けるけど Intellisence 効かないし、SyntaxErrorとかも出てこないから、やっぱりIDEで書きたいところ。

Azure Function Tools を入れるために VS2017 ~~のプレビュー版~~をインストール。
~~現時点ではプレビュー版でないとこやつをインストールできないので注意。~~
(2018/04/11 UPDATE) v15.3くらいからリリース版でもちゃんと動くので、プレビュー版の表記を消しました。

# ソースコード

```C#:GetHighestTemperature.cs
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace FunctionApp001
{
    public static class Function1
    {
        [FunctionName("GetHighestTemperature")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            var url = "http://www.data.jma.go.jp/obd/stats/data/mdrr/tem_rct/alltable/mxtemsadext00_rct.csv";
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";

            var response = (HttpWebResponse)request.GetResponse();
            log.Info($"Get Request: {url}");
            log.Info($"Response Status: {response.StatusCode.ToString()}");
            if (response.StatusCode == HttpStatusCode.OK)
            {
                using (var sr = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding("shift_jis")))
                {
                    var csv = sr.ReadToEnd().Replace("\r", "").Split('\n')
                        .Where(line => line.Length > 0)
                        .Select(line => line.Split(',').ToList())
                        .ToList();
                    var headers = csv[0];
                    log.Info("Completed.");
                    return req.CreateResponse(HttpStatusCode.OK, csv.Skip(1).Select(row => Enumerable.Zip(headers, row, Tuple.Create).ToDictionary(obj => obj.Item1, obj => obj.Item2)).ToArray());
                }
            }
            else
            {
                log.Error("Get request failed.");
                return req.CreateResponse(HttpStatusCode.BadRequest, "Get request failed.");
            }
        }
    }
}
```

(2018/04/11 UPDATE) テンプレートリテラルを`string.Format()`から`$""`の形式に変更しました。短く書けるようになりました！

あんまり特別なことをしているわけではないかと。

Azure Portal では下記のとおり。
もろもろ既定で using されてる。

```C#:GetHighestTemperatureFunction
using System.Net;
using System.Text;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    var url = "http://www.data.jma.go.jp/obd/stats/data/mdrr/tem_rct/alltable/mxtemsadext00_rct.csv";
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = "GET";

    var response = (HttpWebResponse)request.GetResponse();
    log.Info($"Get Request: {url}");
    log.Info($"Response Status: {response.StatusCode.ToString()}");
    if (response.StatusCode == HttpStatusCode.OK)
    {
        using (var sr = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding("shift_jis")))
        {
            var csv = sr.ReadToEnd().Replace("\r", "").Split('\n')
                .Where(line => line.Length > 0)
                .Select(line => line.Split(',').ToList())
                .ToList();
            var headers = csv[0];
            log.Info("Completed.");
            return req.CreateResponse(HttpStatusCode.OK, csv.Skip(1).Select(row => Enumerable.Zip(headers, row, Tuple.Create).ToDictionary(obj => obj.Item1, obj => obj.Item2)).ToArray());
        }
    }
    else
    {
        log.Error("Get request failed.");
        return req.CreateResponse(HttpStatusCode.BadRequest, "Get request failed.");
    }
}
```

メインである `Run` メソッドの中身は同じ。
インデントが少ない分見やすい・・・。

## 簡単に解説

```C#
using (var sr = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding("shift_jis")))
{
...
}
```

利用しているデータが厄介なことに SJIS なので、 `new StreamReader()` でちゃんと文字エンコードを指定しましょう。

```C#
var csv = sr.ReadToEnd().Replace("\r", "").Split('\n')
    .Where(line => line.Length > 0)
    .Select(line => line.Split(',').ToList())
    .ToList();
```

CSV データを `List<List<string>>` 形式に変換している部分。
1. `StreamReader` で一気に全部読みこんで、改行コードの処理。`Split('\n')` で String 配列に。1要素に行が丸ごと入ってる。
2. `Where()` でデータがある行に絞り込む。最終行の空データを排除する目的。
3. `Select()` で各行をカンマで `Split` 。
4. `ToList()` で `List<List<string>>` の完成。

```C#
var headers = csv[0];
```

CSV ヘッダは1行目なので、それだけ取り出しておく。
このあと KVP 配列にする際の Key となる大事な部分。

```C#
csv.Skip(1).Select(row => Enumerable.Zip(headers, row, Tuple.Create).ToDictionary(obj => obj.Item1, obj => obj.Item2)).ToArray()
```

`return` 句に書いている肝心な部分。
もうちょっとスマートな書き方がありそうな気がする。コメントにて改善案募集中。

1. `Skip(1)` でヘッダを除外。つまりデータ部の各行に対して以下を実行。
2. `Select()` 句の中身
    1. `Zip()` でヘッダの各要素とデータの各要素をタプル化。
    2. `ToDictionary()` で KVP を作成。
3. `ToArray()`で KVP 配列の完成。

## 実行結果

```JSON
[
  {
    "観測所番号":"11001",
    "都道府県":"北海道宗谷地方",
    "地点":"宗谷岬",
    "国際地点番号":"",
    ...
    "統計開始年":"1978"
  },
  ...
]    
```

# JavaScript で書かなかった理由

JS だともっと簡単に書けるのだが断念した。
その理由が "文字化け" 。

ローカル開発環境の Node.js ではうまいこといったのに、なぜか Functions では謎な結果となった。

ソースは残していないが、以下のような感じだったかと。
これを Function 風に変えたはず。

```Javascript
request.get('http://www.data.jma.go.jp/obd/stats/data/mdrr/tem_rct/alltable/mxtemsadext00_rct.csv')
    .pipe(iconv.decodeStream('shift_jis'))
    .pipe(csv.parse({ columns: true }, (err, output) => res.send(output)));
```

何がどうおかしかったのか、というと、上述の実行結果でいう、
「観測所番号」とか「都道府県」とか、つまり Key が謎な文字コードでエンコードされてしまった。
「北海道宗谷地方」とか「宗谷岬」とか、つまり Value は正しく日本語で出力されていた。

Key 側だけ文字化けする、という謎の状況。

個人的な意見だが、おそらく Azure Functions 側の問題ではないかな、と。
原因不明。

結論：SJISデータはめんどくちゃい。