カテゴリー
Awkコマンドを使ってテキストからjsonを生成する実用例〜株価日足で利用する
※ 当ページには【広告/PR】を含む場合があります。
2020/06/11
2022/09/30
Awkを使った実践例を思いついた時に紹介するコーナーです。
今回は、株式チャートで利用される日足のテキスト生データをJSON形式へパースさせてみる手順を模索しました。
実験用のテキスト
今回はコンマやスペース切りされていない未整形のテキストからjsonファイルに落とし込む手順を模索しました。
たとえば以下のように縦一列に、
日付、始値、高値、低値、終値、売買高
なお空白行を1つのデータセットの区切りとして利用します。
2020-04-22
19109.18
19137.95
18858.25
19137.95
1247290000
2020-04-21
19479.83
19529.06
19193.22
19280.78
1280090000
2020-04-20
19689.85
19784.38
19611.79
19669.12
1065420000
2020-04-17
19575.85
19922.07
19554.70
19897.26
1409050000
2020-04-16
19311.30
19362.17
19154.41
19290.20
1298590000
とりあえず5日分の日足データを用意しておきます。
これをテキストファイルとして適当なフォルダへ
input.dat
$ cat << EOF > input.dat
2020-04-22
19109.18
19137.95
18858.25
19137.95
1247290000
2020-04-21
19479.83
19529.06
19193.22
19280.78
1280090000
2020-04-20
19689.85
19784.38
19611.79
19669.12
1065420000
2020-04-17
19575.85
19922.07
19554.70
19897.26
1409050000
2020-04-16
19311.30
19362.17
19154.41
19290.20
1298590000
EOF
目標のjsonファイル
先んじて結果からみてもらうと早いので、このテキストデータから抽出した目的のjsonファイルは、
[
{
"date": "2020-04-22",
"open": 19109.18,
"high": 19137.95,
"low": 18858.25,
"close": 19137.95,
"volume": 1247290000
},
{
"date": "2020-04-21",
"open": 19479.83,
"high": 19529.06,
"low": 19193.22,
"close": 19280.78,
"volume": 1280090000
},
{
"date": "2020-04-20",
"open": 19689.85,
"high": 19784.38,
"low": 19611.79,
"close": 19669.12,
"volume": 1065420000
},
{
"date": "2020-04-17",
"open": 19575.85,
"high": 19922.07,
"low": 19554.7,
"close": 19897.26,
"volume": 1409050000
},
{
"date": "2020-04-16",
"open": 19311.3,
"high": 19362.17,
"low": 19154.41,
"close": 19290.2,
"volume": 1298590000
}
]
とまぁこんな感じです。
結果だけみるとAWK一つでこんなパワフルな変換ができる...というのが中々すごいコマンドです。
それでは具体的なAWKを使ったスクリプトの具体的な実装を以下で考えていきましょう。
BEGINブロックとENDブロック
AWKも
SEDはストリームエディタであるのに対し、AWKは一種のスクリプト言語ですのでより複雑な処理ができる分、プログラミングをする必要があります。
AWKの処理の流れでの定石パターンとしてまず頭に入れておきたいのが、
BEGIN{...前処理...}{...テキストの中身の処理...}END{...後処理...}
まずはテキストの中身の処理はおいといて、テキストの前処理のスクリプトを記述する
BEGIN
END
$ cat input.dat | awk '
BEGIN{
conv_json = "[";
}
{
print NR "行目" $0
}
END{
conv_json = conv_json "]";
print conv_json
}
'
これを実行すると以下のような出力になります。
1行目
2行目2020-04-22
3行目19109.18
4行目19137.95
5行目18858.25
6行目19137.95
7行目1247290000
8行目
9行目2020-04-21
10行目19479.83
11行目19529.06
12行目19193.22
13行目19280.78
14行目1280090000
15行目
16行目2020-04-20
17行目19689.85
18行目19784.38
19行目19611.79
20行目19669.12
21行目1065420000
22行目
23行目2020-04-17
24行目19575.85
25行目19922.07
26行目19554.70
27行目19897.26
28行目1409050000
29行目
30行目2020-04-16
31行目19311.30
32行目19362.17
33行目19154.41
34行目19290.20
35行目1298590000
36行目
[]
まだこのスクリプトはテキスト処理としては何もせずに、空の配列
[]
今回のスクリプトは最終的に配列でJSONファイルを出したいので、BEGINで結果の文字列を
[
]
というようなスクリプトに仕上げる必要があります。
NRと$0
上でさらっと使いましたがAWKで最もよく使うスクリプト変数がいくつかあります。
NR
行のカウントは1から始まります。
また
$0
ちなみに現在の行を読み込んだときのn列目の要素を操作したい場合には、
$1, $2, ..., $n, ...
個別の列要素のアクセスには1から始まるので、全体を返す意味の0と区別して、使うときには注意したいところです。
今回は各行で1つの列要素しかないので
$0
csvのようなコンマ切りのファイルでは
$1, $2, ...
テキスト部分の処理
それではテキストの中身から日足をJSONオブジェクト形式の文字列に変換していくスクリプトを追加します。
$ cat input.dat | awk '
BEGIN{
conv_json = "[";
}
/[0-9]{4}-[0-9]{2}-[0-9]{2}/{
print "日付は" NR "行目" $0
}
/^[0-9\.]+$/{
print "指標値は" NR "行目" $0
}
NF == 0{
print "空行は" NR "行目" $0
}
END{
conv_json = conv_json "]";
print conv_json
}
'
これを実行すると、
空行は1行目
日付は2行目2020-04-22
指標値は3行目19109.18
指標値は4行目19137.95
指標値は5行目18858.25
指標値は6行目19137.95
指標値は7行目1247290000
空行は8行目
日付は9行目2020-04-21
指標値は10行目19479.83
指標値は11行目19529.06
指標値は12行目19193.22
指標値は13行目19280.78
指標値は14行目1280090000
空行は15行目
日付は16行目2020-04-20
指標値は17行目19689.85
指標値は18行目19784.38
指標値は19行目19611.79
指標値は20行目19669.12
指標値は21行目1065420000
空行は22行目
日付は23行目2020-04-17
指標値は24行目19575.85
指標値は25行目19922.07
指標値は26行目19554.70
指標値は27行目19897.26
指標値は28行目1409050000
空行は29行目
日付は30行目2020-04-16
指標値は31行目19311.30
指標値は32行目19362.17
指標値は33行目19154.41
指標値は34行目19290.20
指標値は35行目1298590000
空行は36行目
[]
というような出力を得ます。
先ほどと違うのは、テキスト処理のスクリプト部分
{}
<行を限定する条件>{}
/[0-9]{4}-[0-9]{2}-[0-9]{2}/
/[0-9]+/
NF == 0
{}
条件を省略することの意味
逆説的に考えると、
<行を限定する条件>
上記でも実際に
BEGIN{...前処理...}{...テキストの中身の処理...}END{...後処理...}
真ん中の条件なしのテキストの中身処理部分はすべての行数を順次処理しているのが分かると思います。
正規表現で条件を指定する
AWKの便利なところは条件文に正規表現が利用できて、その正規表現にマッチした行だけに限定した処理を実行できるところです。
例えば
/[0-9]{4}-[0-9]{2}-[0-9]{2}/
2020-04-19
余談ですが、AWKで使える正規表現は単純なものなので、簡単なパターンだけに留めておいた方が無難です。
また、
GNU版AWK
BSD版AWK
一例を挙げると、数字文字を表す
\d
GNU版AWK
BSD版AWK
AWKスクリプトを違うバージョンでも併用したい場合には、
\d
[0-9]
こういったプログラミング的方言の違いやAWK独特の正規表現規約もあり、AWKではあまり複雑な正規表現は利用すべきではないと思います。
AWKでの正規表現をより極めたい方に、以下のサイトなどが参考になるかと思います。
ちなみに
/^[0-9\.]+$/
この場合、文字の先頭と末尾を表す
^
$
^...$
個人的にはより複雑な正規表現を行いたい場合には、SEDで予め適切な前処理を行っておくことをおすすめします。
特定の条件で指定する
前述の
NR
NF
つまり
NF == 0
空行
テキストからJSONオブジェクトを抽出
ここからはとりあえず深いことは考えずに、テキスト部分からJSONオブジェクトを一つ一つ抜き出して、配列に再構成していきます。
ちなみにAWKスクリプト内部でもコメントアウトとして
#
$ cat input.dat | awk '
BEGIN{
s_pos = 0; #初期値がFalseやゼロの場合、明示な変数の初期化はなくても良い
conv_json = "[";
}
/[0-9]{4}-[0-9]{2}-[0-9]{2}/{
s_pos = NR; #日付の行に来たらオブジェクトの最初の行数として記録
if(NR == s_pos) {
date = "\"date\":\"" $0 "\""
}
}
/^[0-9\.]+$/{
if(NR == s_pos+1) {
open = "\"open\":" $0
}
if(NR == s_pos+2) {
high = "\"high\":" $0
}
if(NR == s_pos+3) {
low = "\"low\":" $0
}
if(NR == s_pos+4) {
closed = "\"close\":" $0
}
if(NR == s_pos+5) {
volume = "\"volume\":" $0
}
}
NF == 0{
if(NR == s_pos+6) {
#空行を読み込んだら記録した変数を元にJSONオブジェクトを作成
content = "{" date "," open "," high "," low "," closed "," volume "}";
#配列の末尾にオブジェクトを追加
conv_json = conv_json content ","
}
}
END{
sub(/,$/, "", conv_json); # 末尾の余分な","を排除
conv_json = conv_json "]";
print conv_json
}
'
これを実行した生の出力は以下になります。
[{"date":"2020-04-22","open":19109.18,"high":19137.95,"low":18858.25,"close":19137.95,"volume":1247290000},{"date":"2020-04-21","open":19479.83,"high":19529.06,"low":19193.22,"close":19280.78,"volume":1280090000},{"date":"2020-04-20","open":19689.85,"high":19784.38,"low":19611.79,"close":19669.12,"volume":1065420000},{"date":"2020-04-17","open":19575.85,"high":19922.07,"low":19554.70,"close":19897.26,"volume":1409050000},{"date":"2020-04-16","open":19311.30,"high":19362.17,"low":19154.41,"close":19290.20,"volume":1298590000}]
if文を使う
AWKでも
if(条件) {...} else if(条件) {...} else {...}
Shellの中で動くからといってもShell特有のifの文法とは違い、c言語寄りな感じに書けて結構柔軟性があります。
if(条件){...}
if(条件) {...} else {...}
また
&&
||
組み込み関数sub()
AWKスクリプト内で文字列の置換を行う際には、
sub()
用法としては
sub(<置換文字(正規表現で最初にマッチする文字)>, <置換後の文字>, <入力文字列>)
返り値は無くそのまま置換後の結果が入力文字列に上書きされます。
今回のケースでは、JSONオブジェクトの最後の要素に余分なコンマ
,
$ echo '{},{},{},' | awk '{sub(/,$/,"",$0);print $0}'
{},{},{}
一部のデータが壊れている場合の対応
テキストデータがいつも100%正しいとは限りません。
時にはタイポなどが潜んでいて、日付や数値のフォーマットが崩れていたりして、読み込みが不可の場合があります。
ちょっと分かりにくいですが上のスクリプトにはバグが存在しています。
日付に間違いがあればその日足インスタンスの生成を自然とスキップしてくれますが、日付フォーマットは正常で、その他の数値に誤植があった場合に(更新されずに残った)前回の値がそのまま次のインスタンスに入力されてしまう問題です。
そんな不慮のデータをそのまま読み込んでしまうので、各行の検証を追加してもうちょっとだけ安全な処理に改良してみます。
$ cat input.dat | awk '
BEGIN{
s_pos = 0;
conv_json = "[";
}
/[0-9]{4}-[0-9]{2}-[0-9]{2}/{
s_pos = NR;
if(NR == s_pos) {
date = "\"date\":\"" $0 "\""
}
#各値の初期化(異常があった場合-1が返る)
open = "\"open\":-1"
high = "\"high\":-1"
low = "\"low\":-1"
closed = "\"close\":-1"
volume = "\"volume\":-1"
}
/^[0-9\.]+$/{
if(NR == s_pos+1) {
open = "\"open\":" $0
}
if(NR == s_pos+2) {
high = "\"high\":" $0
}
if(NR == s_pos+3) {
low = "\"low\":" $0
}
if(NR == s_pos+4) {
closed = "\"close\":" $0
}
if(NR == s_pos+5) {
volume = "\"volume\":" $0
}
}
NF == 0{
if(NR == s_pos+6) {
content = "{" date "," open "," high "," low "," closed "," volume "}";
conv_json = conv_json content ","
}
}
END{
sub(/,$/, "", conv_json);
conv_json = conv_json "]";
print conv_json
}
'
こうすることで数値に異常があった場合には、"-1"が反映され、データに誤りのあった箇所が検証できるようになりました。
最終的なスクリプトとして完成させる
以上を組み合わせると、スクリプトの全体は以下のようになります。
#!/bin/bash
DATA_DIR=./path/to/datafile/
cat ${DATA_DIR}input.dat | awk '
BEGIN{
s_pos = 0;
conv_json = "[";
}
/[0-9]{4}-[0-9]{2}-[0-9]{2}/{
s_pos = NR;
if(NR == s_pos) {
date = "\"date\":\"" $0 "\""
}
open = "\"open\":-1"
high = "\"high\":-1"
low = "\"low\":-1"
closed = "\"close\":-1"
volume = "\"volume\":-1"
}
/^[0-9\.]+$/{
if(NR == s_pos+1) {
open = "\"open\":" $0
}
if(NR == s_pos+2) {
high = "\"high\":" $0
}
if(NR == s_pos+3) {
low = "\"low\":" $0
}
if(NR == s_pos+4) {
closed = "\"close\":" $0
}
if(NR == s_pos+5) {
volume = "\"volume\":" $0
}
}
NF == 0{
if(NR == s_pos+6) {
content = "{" date "," open "," high "," low "," closed "," volume "}";
conv_json = conv_json content ","
}
}
END{
sub(/,$/, "", conv_json);
conv_json = conv_json "]";
print conv_json
}
' > ${DATA_DIR}output.json
入力ファイルと出力ファイルなどを作業フォルダ
./path/to/datafile/
まとめ
Awkコマンドを使うとパターンの定かなデータの形成が非常に楽になります。
正規表現での文字操作はAWK単体では完璧な高機能とは言えませんので、Sedでテキストを前処理した後で、Awkへデータを食わせるやり方が好ましいと思います。
個人的なビッグデータに対応したテキストマイニング用のアプリ開発のお供に欠かせない最強ツールとして、Curl、Sed、Awk、jqの4つがコマンドはやはり押さえておきたいところです。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー