pythonでテキストをjsonにした話(pycon mini sapporo 2015)

Post on 12-Jan-2017

927 Views

Category:

Technology

3 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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

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

データは文字列と数値。

素直なデータ

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:の行があったりなかったり…

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

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

データは文字列と数値。

素直なデータ

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 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:の行があったりなかったり…

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

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

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

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

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

46

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

が。

47

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

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

49

やってみると

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

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

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

50

生ファイルを観察

規則性の発見

実装

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

結果のチェック

51

生ファイルを観察

規則性の発見

実装

結果のチェック

まとめ

52

元データ

社内での利用方法

解析処理 Dict

JSON csv

PostgreSQLのJSONBにぶちこむ

顧客資料のベースに

json.dumps

D3.js等で可視化

53

まとめ

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

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

ときには力技での対応

54

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

top related