1
PyCon mini Sapporo 2015
山田 聡 @denzowill
2
注意
機械学習とかでてきません
全編泥臭いテキスト処理の話です
だいぶ入門者向けです
3
お前だれよ
山田 聡@denzowill
東京でDBのサポートやってます@株式会社アシスト
(PostgreSQLとかOracleとか)
Python歴1年ちょっと
社内で便利屋的な扱い
最近似顔絵が
似てなくなって来ました。。。
4
だいたいこんな感じで生きてます
5
話すこと
Oracleの稼動統計レポート(Statspack)を
JSONに変換した時の話です
非構造化データを構造化した時の
苦労話です
6
これを
Snapshot Snap Id Snap Time Sessions Curs/Sess
~~~~~~~~ ---------- ------------------ -------- ---------
Begin Snap: 32987 27-5月 -15 09:25:01 477 21.8
End Snap: 32988 27-5月 -15 10:25:01 472 22.7
Elapsed: 60.00 (mins) Av Act Sess: 4.0
DB time: 237.82 (mins) DB CPU: 105.44 (mins)
7
こうしたかった
{
"Elapsed": 60,
"Av_Act_Sess": 4,
"DB_time": 237,
"DB_CPU": 105.44,
"Begin_Snap": {
"Snap_Id": 32987,
"Snap_Time": "27-5月 -15 09:25:01",
"Sessions": 477,
"Curs_Sess": 21.8
},
"End_Snap": {
"Snap_Id": 32988,
"Snap_Time": "27-5月 -15 10:25:01",
"Sessions": 472,
"Curs_Sess": 22.7
}
}
8
構造化された
データは便利
9
構造化すると
加工が楽
プログラマチックに処理できる
いろいろ連携の夢広がりんぐ
10
構造化したい?
11
構造化したい!
12
生
フ
ァ
イ
ル
を
観
察
規
則
性
の
発
見
実
装
構造化するには
結
果
の
チ
ェ
ッ
ク
13
生
フ
ァ
イ
ル
を
観
察
規
則
性
の
発
見
実
装
ファイルをしっかりみる
結
果
の
チ
ェ
ッ
ク
14
みるところ
15
単一フォーマット?
→1ファイルに複数のフォーマットがあるか
固定長?特定文字区切り?
→行や列を区切るルールを発見
変なデータがまじることは?
→###等不正なデータの有無
16
Avg %Total
%Tim Total Wait wait Waits Call
Event Waits out Time (s) (ms) /txn Time
---------------------------- ------------ ---- ---------- ------ -------- ------
buffer busy waits 12 0 6 499 12.0 37.6
Disk file operations I/O 77 0 3 39 77.0 18.7
control file sequential read 920 0 3 3 920.0 17.7
rdbms ipc reply 25 0 1 45 25.0 7.1
db file sequential read 51 0 1 15 51.0 4.7
control file parallel write 85 0 0 3 85.0 1.6
固定長データ、列の区切り目を
取得すればよさそう。
データは文字列と数値。
素直なデータ
17
CPU Elapsd Old
Buffer Gets Executions Gets per Exec %Total Time (s) Time (s) Hash Value
--------------- ------------ -------------- ------ -------- --------- ----------
10,074 67 150.4 31.6 0.11 0.08 335360792
select file#, block#, blocks from seg$ where type# = 3 and ts# =
:1
3,808 685 5.6 12.0 0.02 0.01 2482976222
select intcol#,nvl(pos#,0),col#,nvl(spare1,0) from ccol$ where c
on#=:1
1,294 1 1,294.0 4.1 0.09 0.38 2522684317
Module: SQL*Plus
BEGIN statspack.snap; END;
少しクセのあるデータ
基本固定長データなのは同じ
だけど空行区切りの、複数行構成
数字行を開始ともみなせる
でもModule:の行があったりなかったり…
18
生
フ
ァ
イ
ル
を
観
察
規
則
性
の
発
見
実
装
ある程度規則が見つかったら実装
結
果
の
チ
ェ
ッ
ク
19
大まかにまずは区切る
→セクション単位に分割してから
フォーマット毎にクラスをつくる
→変更多発なので影響を局所化
共通ロジックをベースクラスへ
→子クラス作成中に思ったら親に移す
実装するときに考えていること
20
実際につくったら...
21
セクション1
セクション2
セクション3
区切りa
区切りb
セクション1
セクション2
セクション3
区切り文字で分割
22
セクション1
フォーマットA
セクション2
フォーマットB
セクション3
フォーマットC
セクション名とフォーマットをマッピング
ディクショナリ的な何か
セクション1:フォーマットA
セクション2:フォーマットB
セクション3:フォーマットC
23
セクション1
フォーマットA
セクション2
フォーマットB
セクション3
フォーマットC
フォーマットに対応するパーサを割り当て
フォーマットA用
パース処理
フォーマットC用
パース処理
フォーマットB用
パース処理
どことなくモジュールの
独立性が保たれてる(きがする)
24
パーサ間の関係
フォーマットA用
パース処理
フォーマットC用
パース処理
フォーマットB用
パース処理
ベースパーサ
結構綺麗な
フォーマットの
パーサ
ゆるい継承関係
後が怖くてセクションとパーサがほぼ1:1
25
になりました。
26
パーサどうなった?
27
class ParserBase(object):
def __init__(self, lines=None):
# unicodeで取得したセクションの各行
self.lines = lines
# データの整形
def reformat(self):
# 子クラスでの実装をする
raise Exception("Plz Implement")
# 実際の解析処理
def parse_main(self):
# 子クラスでの実装をする
raise Exception("Plz Implement")
reformat/parse_mainを
継承先で実装していく
基底クラス
28
後は継承して
ひたすらre
29
Avg %Total
%Tim Total Wait wait Waits Call
Event Waits out Time (s) (ms) /txn Time
---------------------------- ------------ ---- ---------- ------ -------- ------
buffer busy waits 12 0 6 499 12.0 37.6
Disk file operations I/O 77 0 3 39 77.0 18.7
control file sequential read 920 0 3 3 920.0 17.7
rdbms ipc reply 25 0 1 45 25.0 7.1
db file sequential read 51 0 1 15 51.0 4.7
control file parallel write 85 0 0 3 85.0 1.6
固定長データ、列の区切り目を
取得すればよさそう。
データは文字列と数値。
素直なデータ
30
def parse_main(self):
:
for line in self.lines:
# ---- --- 的なのが出たら取得開始
if re.search(sep_str, line):
val_flg = True
# ---- --- を[0,4,8..]的に変換
sep_posit_list = self.get_splited_position(line)
continue
# ---- --- 的なのがでるまで無視
if not val_flg:
continue
# データ部分を[0,4,8..]的な位置で分割しながら格納
line_row_list.append(self.split_str_by_posit(line, sep_posit_list))
31
CPU Elapsd Old
Buffer Gets Executions Gets per Exec %Total Time (s) Time (s) Hash Value
--------------- ------------ -------------- ------ -------- --------- ----------
10,074 67 150.4 31.6 0.11 0.08 335360792
select file#, block#, blocks from seg$ where type# = 3 and ts# =
:1
3,808 685 5.6 12.0 0.02 0.01 2482976222
select intcol#,nvl(pos#,0),col#,nvl(spare1,0) from ccol$ where c
on#=:1
1,294 1 1,294.0 4.1 0.09 0.38 2522684317
Module: SQL*Plus
BEGIN statspack.snap; END;
少しクセのあるデータ
基本固定長データなのは同じ
だけど空行区切りの、複数行構成
数字行を開始ともみなせる
でもModule:の行があったりなかったり…
32
def parse_main(self):
:
for line in self.lines:
:
# カンマ、空白、小数点を除いた後、数値だけの行か
# データのブロックの開始判定
if self.is_only_int_line(line):
# SQL文の行ではないのでフラグを初期化
sql_l_flg = False
:
# 文字列バッファの初期化
sql_str = u""
buf_str = u""
# 数値データは、固定長なので通常通り区切って格納
row_list.append(self.split_str_by_posit(line, sep_posit_list))
# モジュール名の行を取得
elif re.search(u"Module:s.+", line):
row_list[-1].append(line.strip()[8:])
# 以降の行はSQL文
sql_l_flg = True
elif sql_l_flg:
sql_str += line.strip()
else:
buf_str += line.strip()
33
大体こんな感じで
微調整しながら
作りました
34
生
フ
ァ
イ
ル
を
観
察
規
則
性
の
発
見
実
装
実装を終えたらテスト
結
果
の
チ
ェ
ッ
ク
35
Unittest
→JUnitライク、標準モジュール
nose
→もうちょっと高度に
doctest
→Docstringに書いた内容でテストされる
Pythonでのテスト
36
Unittest
→これくらいでちょうどいいかとおもった
nose
→そこまでしなくてもいっか
doctest
→今回は引数とかがでかいのでDocstringには書きづらい
Pythonでのテストを検討した
37
import unittest
import StatspackParser
import sys
# とりあえず対象のレポートを渡したかった
FILE_NAME= sys.argv[1]
class LogicTest(unittest.TestCase):
def setUp(self):
self.file_name = FILE_NAME
def test_parse(self):
sp = StatspackParser(self.file_name)
parsed_data = sp.do_parse()
# テストファイルの該当セクション最後のSQLをチェック
self.assertEqual(parsed_data["SQL_ordered_by_Gets"][-1]["SQL_TEXT"], "select * from emp")
# その他もろもろ
:
if __name__ == '__main__':
# unittest自体の引数ではないので消す
del sys.argv[1]
unittest.main()
38
いろいろみつかった
orz
39
40
→崩れてた
41
Foreground Wait Events DB/Inst: ORCL/ORCL1 Snaps: 32987-32988
-> Only events with Total Wait Time (s) >= .001 are shown
-> ordered by Total Wait Time desc, Waits desc (idle events last)
Avg %Total
%Tim Total Wait wait Waits Call
Event Waits out Time (s) (ms) /txn Time
---------------------------- ------------ ---- ---------- ------ -------- ------
db file sequential read 281,434 0 5,788 21 0.4 37.5
direct path read 14,550 0 1,005 69 0.0 6.5
enq: TX - index contention 168 0 627 3735 0.0 4.1
:
:
Foreground Wait Events DB/Inst: ORCL/ORCL1 Snaps: 32987-32988
-> Only events with Total Wait Time (s) >= .001 are shown
-> ordered by Total Wait Time desc, Waits desc (idle events last)
Avg %Total
%Tim Total Wait wait Waits Call
Event Waits out Time (s) (ms) /txn Time
---------------------------- ------------ ---- ---------- ------ -------- ------
KJC: Wait for msg sends to c 776 0 0 0 0.0 .0
なんかヘッダが何回もでる
42
# 繰り返しのヘッダを取り除く
def remove_duplicate_header_and_info(self):
# ヘッダ行を取得
head = self.guess_header_block()
# 文字列として再整形
total_header_string = u"n".join(head)
# 構成行から一括して削除
# 属性としては1行1要素のリストで持ってたので再度文字列として取得する
line_string = self.get_line_string()
headerless_string = line_string.replace(total_header_string, u"")
# 再度ヘッダを頭にだけ付け直して再設定
self.set_line_string(u"n".join(head) + u"n" + headerless_string)
取り除く前処理追加
43
Begin Snap: 1 20-8月 -15 06:59:18
→割と普通
Begin Snap: 1 08-Aug-15 15:33:11
→英語表記
Begin Snap: 1 20-8譛-15 10:33:18
→なんか化けてる(届いた時点で)
Begin Snap: 1 14-7? -15 23:00:01
→もはや化けてるとかのレベルじゃない
日付ばらばら
44
def parse_date(self, date_string):
# 見つけたフォーマットを全部いれておく
formats = [
u"%d-%m月 %H:%M:%S",
u"%d-%m月 %H:%M",
u"%d-%m月-%y %H:%M", # like 24-12月-14 07:35
:
u"%d-%m? %H:%M", # like 24-12月-14 07:35
u"%d-%m? %H:%M:%S", # like 24-12月-14 07:35
]
# パース出来るまで頑張る
for format_pat in formats:
try:
# unicodeでできないのでstrにする
ret = datetime.datetime.strptime(date_string.encode(self.encode),
format_pat.encode(self.encode))
break
except ValueError:
pass
else:
# 全滅なら投げといて、後でフォーマットを追加する
raise NoMatchDateFormatException(date_string.encode(self.encode))
return ret
手当たりしだい試すことにした
45
Buffer wait Statistics DB/Inst: ORCL/ORCL1 Snaps: 32987-32988
-> ordered by wait time desc, waits desc
Class Waits
------------------------------------------------------------------ -----------
Total Wait Time (s) Avg Time (ms)
------------------- -------------
data block 7,134
75 10
undo header 5
0 0
2nd level bmb 2
0 0
-------------------------------------------------------------
環境によっては折り返されてる
46
折り返しを戻す前処理をいれた
が。
47
崩れ方がまちまちで
統一処理にしづらい
48
処理失敗時パーサを切り替える事にした
retry_flag = True
while retry_flag:
try:
# 解析したディクショナリを取得する
parser_inst.reformat()
result_dict = parser_inst.parse_main()
if result_dict:
if len(result_dict.values()[0]) > 0:
return_dict["PARSED_DATA"][result_dict.keys()[0]] = result_
# 処理は成功しているがデータがとれていない
else:
raise NoValidDataException(parser_inst)
# ここまできたらリトライしない
retry_flag = False
# バージョン差異でパースエラーになる可能性はある
except (IndexError, NoMatchDateFormatException, NoValidDataException):
try:
# 予備のパーサを取得してリトライ
parser_inst = parser_inst.get_sub_parser_inst()
except NoSubParserException as e:
# 予備のパーサが出尽くした
retry_flag = False
49
やってみると
崩れっぷりを事前定義する力技
以外と各セクション3パターン以内
実用レベル範囲内で動いた
50
生
フ
ァ
イ
ル
を
観
察
規
則
性
の
発
見
実
装
いったりきたりで何とか動いた
結
果
の
チ
ェ
ッ
ク
51
生
フ
ァ
イ
ル
を
観
察
規
則
性
の
発
見
実
装
結
果
の
チ
ェ
ッ
ク
まとめ
52
元データ
社内での利用方法
解析処理 Dict
JSON
csv
PostgreSQLの
JSONBにぶちこむ
顧客資料のベースに
json.dumps
D3.js等で可視化
53
まとめ
解析対象のルールをしっかり判断
データの崩れをどうするか検討
ときには力技での対応
54
ご清聴
ありがとうございました

PythonでテキストをJSONにした話(PyCon mini sapporo 2015)