54
1 PyCon mini Sapporo 2015 山田 聡 @denzowill

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

Embed Size (px)

Citation preview

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

1

PyCon mini Sapporo 2015山田 聡 @denzowill

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

2

注意

機械学習とかでてきません

全編泥臭いテキスト処理の話です

だいぶ入門者向けです

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

3

お前だれよ

山田 聡@denzowill

東京でDBのサポートやってます@株式会社アシスト(PostgreSQLとかOracleとか)

Python歴1年ちょっと

社内で便利屋的な扱い最近似顔絵が

似てなくなって来ました。。。

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

4

だいたいこんな感じで生きてます

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

5

話すこと

Oracleの稼動統計レポート(Statspack)をJSONに変換した時の話です

非構造化データを構造化した時の苦労話です

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

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)

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

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 }}

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

8

構造化されたデータは便利

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

9

構造化すると

加工が楽

プログラマチックに処理できる

いろいろ連携の夢広がりんぐ

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

10

構造化したい?

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

11

構造化したい!

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

12

生ファイルを観察

規則性の発見

実装

構造化するには

結果のチェック

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

13

生ファイルを観察

規則性の発見

実装

ファイルをしっかりみる

結果のチェック

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

14

みるところ

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

15

単一フォーマット?→1ファイルに複数のフォーマットがあるか

固定長?特定文字区切り?→行や列を区切るルールを発見

変なデータがまじることは?→###等不正なデータの有無

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

16

Avg %Total %Tim Total Wait wait Waits CallEvent Waits out Time (s) (ms) /txn Time---------------------------- ------------ ---- ---------- ------ -------- ------buffer busy waits 12 0 6 499 12.0 37.6Disk file operations I/O 77 0 3 39 77.0 18.7control file sequential read 920 0 3 3 920.0 17.7rdbms ipc reply 25 0 1 45 25.0 7.1db file sequential read 51 0 1 15 51.0 4.7control file parallel write 85 0 0 3 85.0 1.6

固定長データ、列の区切り目を取得すればよさそう。

データは文字列と数値。

素直なデータ

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

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 335360792select file#, block#, blocks from seg$ where type# = 3 and ts# = :1

3,808 685 5.6 12.0 0.02 0.01 2482976222select intcol#,nvl(pos#,0),col#,nvl(spare1,0) from ccol$ where con#=:1

1,294 1 1,294.0 4.1 0.09 0.38 2522684317Module: SQL*PlusBEGIN statspack.snap; END;

少しクセのあるデータ

基本固定長データなのは同じだけど空行区切りの、複数行構成

数字行を開始ともみなせるでもModule:の行があったりなかったり…

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

18

生ファイルを観察

規則性の発見

実装

ある程度規則が見つかったら実装

結果のチェック

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

19

大まかにまずは区切る→セクション単位に分割してから

フォーマット毎にクラスをつくる→変更多発なので影響を局所化

共通ロジックをベースクラスへ→子クラス作成中に思ったら親に移す

実装するときに考えていること

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

20

実際につくったら...

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

21

セクション1

セクション2

セクション3

区切りa

区切りb

セクション1

セクション2

セクション3

区切り文字で分割

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

22

セクション1フォーマットA

セクション2フォーマットB

セクション3フォーマットC

セクション名とフォーマットをマッピング

ディクショナリ的な何かセクション1:フォーマットAセクション2:フォーマットBセクション3:フォーマットC

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

23

セクション1フォーマットA

セクション2フォーマットB

セクション3フォーマットC

フォーマットに対応するパーサを割り当て

フォーマットA用パース処理

フォーマットC用パース処理

フォーマットB用パース処理

どことなくモジュールの独立性が保たれてる(きがする)

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

24

パーサ間の関係

フォーマットA用パース処理

フォーマットC用パース処理

フォーマットB用パース処理

ベースパーサ

結構綺麗なフォーマットの

パーサ

ゆるい継承関係

後が怖くてセクションとパーサがほぼ1:1

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

25

になりました。

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

26

パーサどうなった?

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

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を継承先で実装していく

基底クラス

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

28

後は継承してひたすらre

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

29

Avg %Total %Tim Total Wait wait Waits CallEvent Waits out Time (s) (ms) /txn Time---------------------------- ------------ ---- ---------- ------ -------- ------buffer busy waits 12 0 6 499 12.0 37.6Disk file operations I/O 77 0 3 39 77.0 18.7control file sequential read 920 0 3 3 920.0 17.7rdbms ipc reply 25 0 1 45 25.0 7.1db file sequential read 51 0 1 15 51.0 4.7control file parallel write 85 0 0 3 85.0 1.6

固定長データ、列の区切り目を取得すればよさそう。

データは文字列と数値。

素直なデータ

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

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))

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

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 335360792select file#, block#, blocks from seg$ where type# = 3 and ts# = :1

3,808 685 5.6 12.0 0.02 0.01 2482976222select intcol#,nvl(pos#,0),col#,nvl(spare1,0) from ccol$ where con#=:1

1,294 1 1,294.0 4.1 0.09 0.38 2522684317Module: SQL*PlusBEGIN statspack.snap; END;

少しクセのあるデータ

基本固定長データなのは同じだけど空行区切りの、複数行構成

数字行を開始ともみなせるでもModule:の行があったりなかったり…

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

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()

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

33

大体こんな感じで微調整しながら作りました

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

34

生ファイルを観察

規則性の発見

実装

実装を終えたらテスト

結果のチェック

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

35

Unittest→JUnitライク、標準モジュール

nose→もうちょっと高度に

doctest→Docstringに書いた内容でテストされる

Pythonでのテスト

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

36

Unittest→これくらいでちょうどいいかとおもった

nose→そこまでしなくてもいっか

doctest→今回は引数とかがでかいのでDocstringには書きづらい

Pythonでのテストを検討した

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

37

import unittestimport StatspackParserimport 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()

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

38

いろいろみつかったorz

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

39

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

40

→崩れてた

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

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 CallEvent Waits out Time (s) (ms) /txn Time---------------------------- ------------ ---- ---------- ------ -------- ------db file sequential read 281,434 0 5,788 21 0.4 37.5direct path read 14,550 0 1,005 69 0.0 6.5enq: 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 CallEvent Waits out Time (s) (ms) /txn Time---------------------------- ------------ ---- ---------- ------ -------- ------KJC: Wait for msg sends to c 776 0 0 0 0.0 .0

なんかヘッダが何回もでる

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

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)

取り除く前処理追加

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

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→もはや化けてるとかのレベルじゃない

日付ばらばら

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

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

手当たりしだい試すことにした

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

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 10undo header 5 0 02nd level bmb 2 0 0 -------------------------------------------------------------

環境によっては折り返されてる

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

46

折り返しを戻す前処理をいれた

が。

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

47

崩れ方がまちまちで統一処理にしづらい

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

48

処理失敗時パーサを切り替える事にしたretry_flag = Truewhile 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

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

49

やってみると

崩れっぷりを事前定義する力技

以外と各セクション3パターン以内

実用レベル範囲内で動いた

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

50

生ファイルを観察

規則性の発見

実装

いったりきたりで何とか動いた

結果のチェック

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

51

生ファイルを観察

規則性の発見

実装

結果のチェック

まとめ

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

52

元データ

社内での利用方法

解析処理 Dict

JSON csv

PostgreSQLのJSONBにぶちこむ

顧客資料のベースに

json.dumps

D3.js等で可視化

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

53

まとめ

解析対象のルールをしっかり判断

データの崩れをどうするか検討

ときには力技での対応

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

54

ご清聴ありがとうございました