背景
仕事ではPerlを使ってテキスト処理を行うことが多い。
Pythonは、RaspberryPiのio処理などでツール的にしか使ってこなかった。
なので、プログラミング言語としてPythonがどのような設計となっているかを知るために、試しにテキスト処理のスクリプトを作成してみることとした。
※ 自分の実装に対してリファクタリングをして頂いたソースコードをまとめ・TODOの後に紹介しています。
ぜひbefore → afterとして参考にして頂ければと思います。
できたもの
以下のようなログファイルを1行ずつ処理して、いい感じに出力する。
date Thu Apr 11 04:41:25 pm 2013
base hex timestamps absolute
internal events logged
// version 8.0.0
Begin Triggerblock Thu Apr 11 04:41:25 pm 2013
0.000000 Start of measurement
0.001316 CAN 1 Status:chip status error active
0.001399 1 1F3 Rx d 3 00 10 00 Length = 146000 BitCount = 77 ID = 499
0.002763 1 1E5 Rx d 8 4C 00 21 10 00 00 00 B9 Length = 228000 BitCount = 118 ID = 485
0.003009 1 710 Rx d 8 00 5F 00 00 00 00 13 BE Length = 238000 BitCount = 123 ID = 1808
0.003175 1 C7 Rx d 4 00 38 26 9B Length = 158000 BitCount = 83 ID = 199
0.003349 1 1CC Rx d 4 00 00 00 00 Length = 165883 BitCount = 87 ID = 460
0.003586 1 F9 Rx d 8 00 DA 40 33 D0 63 FF 1C Length = 228000 BitCount = 118 ID = 249
0.003738 1 1CF Rx d 3 00 00 05 Length = 144000 BitCount = 76 ID = 463
0.003976 1 711 Rx d 8 00 23 00 7E FF EB FC 6F Length = 230000 BitCount = 119 ID = 1809
0.004148 1 1D0 Rx d 4 00 00 00 00 Length = 164000 BitCount = 86 ID = 464
0.004382 1 C1 Rx d 8 30 14 F6 08 32 B4 F7 70 Length = 226000 BitCount = 117 ID = 193
0.004615 1 C5 Rx d 8 31 27 F8 44 32 B0 F8 5C Length = 224121 BitCount = 116 ID = 197
0.004825 1 BE Rx d 6 00 00 4D 00 00 00 Length = 202242 BitCount = 105 ID = 190
0.005051 1 D1 Rx d 7 80 00 BF FE 00 FE 00 Length = 218121 BitCount = 113 ID = 209
0.005292 1 C9 Rx d 8 80 2C 5A 60 00 00 18 00 Length = 232242 BitCount = 120 ID = 201
0.005538 1 1C8 Rx d 8 80 00 00 00 FF FE 3F FE Length = 238121 BitCount = 123 ID = 456
0.005774 1 18E Rx d 8 00 00 00 84 78 46 08 45 Length = 228242 BitCount = 118 ID = 398
# 必要なフィールドのみを出力
$python canlogfilter.py log.txt 0.001399 1 1F3 Rx 3 00 10 00
0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.003175 1 0C7 Rx 4 00 38 26 9B
0.003349 1 1CC Rx 4 00 00 00 00
0.003586 1 0F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.003738 1 1CF Rx 3 00 00 05
0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.004148 1 1D0 Rx 4 00 00 00 00
0.004382 1 0C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.004615 1 0C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.004825 1 0BE Rx 6 00 00 4D 00 00 00
0.005051 1 0D1 Rx 7 80 00 BF FE 00 FE 00
0.005292 1 0C9 Rx 8 80 2C 5A 60 00 00 18 00
0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
# 差分時間を追加して出力
$python canlogfilter.py log.txt -d(-dオプション) 0.001399 0.001399 1 1F3 Rx 3 00 10 00
0.001364 0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.000246 0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.000166 0.003175 1 0C7 Rx 4 00 38 26 9B
0.000174 0.003349 1 1CC Rx 4 00 00 00 00
0.000237 0.003586 1 0F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.000152 0.003738 1 1CF Rx 3 00 00 05
0.000238 0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.000172 0.004148 1 1D0 Rx 4 00 00 00 00
0.000234 0.004382 1 0C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.000233 0.004615 1 0C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.000210 0.004825 1 0BE Rx 6 00 00 4D 00 00 00
0.000226 0.005051 1 0D1 Rx 7 80 00 BF FE 00 FE 00
0.000241 0.005292 1 0C9 Rx 8 80 2C 5A 60 00 00 18 00
0.000246 0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
0.000236 0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
# 特定のフィールド値に従いレコードを絞り込みして出力(-uオプション)
$python canlogfilter.py log.txt -u 710 0C9 18E
0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.005292 1 0C9 Rx 8 80 2C 5A 60 00 00 18 00
0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
# 特定のフィールド値に従いレコードを削除して出力(-oオプション)
$python canlogfilter.py log.txt -o 710 0C9 18E
0.001399 1 1F3 Rx 3 00 10 00
0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.003175 1 0C7 Rx 4 00 38 26 9B
0.003349 1 1CC Rx 4 00 00 00 00
0.003586 1 0F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.003738 1 1CF Rx 3 00 00 05
0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.004148 1 1D0 Rx 4 00 00 00 00
0.004382 1 0C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.004615 1 0C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.004825 1 0BE Rx 6 00 00 4D 00 00 00
0.005051 1 0D1 Rx 7 80 00 BF FE 00 FE 00
0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
# オプション同士の組み合わせ(-u, -d)
$python canlogfilter.py log.txt -u 710 0C9 18E -d
0.003009 0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.002283 0.005292 1 0C9 Rx 8 80 2C 5A 60 00 00 18 00
0.000482 0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
ソースコード
import re
import argparse
class Record:
def __init__(self):
self.crtime = 0.00000
self.ch = 1
self.hexid = 0x000
self.dir = "Rx"
self.stat = "d"
self.dlc = 0
self.data = []
self.length = 0
self.bitcount = 0
self.decid = 0
def main():
parser = argparse.ArgumentParser(description = 'CanlogFilter')
parser.add_argument('inputFile', help = 'Input file path')
parser.add_argument('--difftime', '-d', action = 'store_const', const = True, default = False, help = 'Print with difftime')
parser.add_argument('--pickup', '-u', nargs = '*', help = 'pick up records')
parser.add_argument('--dropoff', '-o', nargs = '*', help = 'drop off records')
args = parser.parse_args()
canlog = []
canlog = parse(args.inputFile)
if args.pickup != None and args.dropoff != None:
print "--pickup and --dropoff, both provide"
return -1
elif args.pickup != None:
canlog = pick_log(canlog, map(lambda x:int(x, 16), args.pickup))
elif args.dropoff != None:
canlog = drop_log(canlog, map(lambda x:int(x, 16), args.dropoff))
if args.difftime == True:
printlog_with_diff_time(canlog)
else:
printlog(canlog)
def parse(filename):
canlog = []
for line in open(filename, 'r'):
fields = line.split()
if re.match("1|2", fields[1]):
rec = Record()
rec.crtime = float(fields[0])
rec.ch = int(fields[1], 10)
rec.hexid = int(fields[2], 16)
rec.dir = fields[3]
rec.stat = fields[4]
rec.dlc = int(fields[5], 10)
rec.data = map(lambda x:int(x, 16), fields[6:rec.dlc+6])
rec.length = int(fields[rec.dlc+8], 10)
rec.bitcount = int(fields[rec.dlc+11], 10)
rec.decid = int(fields[rec.dlc+14], 10)
canlog.append(rec)
return canlog
def pick_log(canlog, ids):
ret = []
for rec in canlog:
if rec.hexid in ids:
ret.append(rec)
return ret
def drop_log(canlog, ids):
ret = []
for rec in canlog:
if not rec.hexid in ids:
ret.append(rec)
return ret
def printlog(canlog):
for rec in canlog:
print '%f %d %03X %s %d' % (rec.crtime, rec.ch, rec.hexid, rec.dir, rec.dlc),
for byte in rec.data:
print '%02X' % byte,
print
def printlog_with_diff_time(canlog):
prevtime = 0
difftime = 0
for rec in canlog:
difftime = rec.crtime - prevtime
print '%f %f %d %03X %s %d' % (difftime, rec.crtime, rec.ch, rec.hexid, rec.dir, rec.dlc),
for byte in rec.data:
print '%02X' % byte,
print
prevtime = rec.crtime
if __name__ == '__main__' : main()
- 使用したライブラリ、関数など
- 引数処理
- 標準の
argparse
ライブラリを使用- 詳細に設定できる分、使い方が複雑な印象
- 標準の
- ファイル操作
-
open()
を使用- これだけで行ごとの処理をかけるのはとても書きやすく、わかりやすい
-
- その他
-
lambda
-
map()
との組み合わせはPythonでは定番な様子 - もっと上手く使えるシチュエーションを考えてみたい
-
-
- 引数処理
標準的なものだけで、十分な使い勝手の良さを得られた印象。
もっと書き込んでいる人は違うものを使うのかもしれないけど。
まとめ・TODO
- まとめ
- とても早く、PerlやRuby以上に簡単に書けるのは気持ちが良い
- TODO
- レコードのオブジェクト化の為だけにクラスを使っているのが中途半端な感じ
- クラスメソッドが組めるようなクラスの定義を考えてみよう
- レコードのオブジェクト化の為だけにクラスを使っているのが中途半端な感じ
TODOに対してコメントより頂いたソースコード
shiracamus様より
TODO でクラス定義の見直しを考えておられるようですが、私なりに実装してみました。
見直しの参考になれば幸いです。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import argparse
def hexint(x):
return int(x, 16)
class Record:
@staticmethod
def create(line):
fields = line.split()
if len(fields) < 2 or fields[1] not in ('1', '2'):
return None
record = Record()
record.crtime = float(fields[0])
record.ch = int(fields[1])
record.hexid = hexint(fields[2])
record.dir = fields[3]
record.stat = fields[4]
record.dlc = int(fields[5])
record.data = map(hexint, fields[6:record.dlc + 6])
record.length = int(fields[record.dlc + 8])
record.bitcount = int(fields[record.dlc + 11])
record.decid = int(fields[record.dlc + 14])
return record
def __str__(self):
return ('{crtime} {ch} {hexid:03X} {dir} {dlc}'.format(**vars(self))
+ ' '.join('%02X' % byte for byte in self.data))
class Canlog:
def __init__(self, records):
self.records = list(records)
@staticmethod
def create(lines):
return Canlog(record
for record in map(Record.create, lines)
if record != None)
def pickup(this, ids):
return Canlog(record
for record in this.records
if record.hexid in ids)
def dropoff(canlog, ids):
return Canlog(record
for record in self.records
if record.hexid not in ids)
def print_without_diff_time(self):
for record in self.records:
print record
def print_with_diff_time(self):
prevtime = 0
for record in self.records:
difftime = record.crtime - prevtime
print difftime, record
prevtime = record.crtime
def main():
parser = argparse.ArgumentParser(description='CanlogFilter')
parser.add_argument('inputFile', help='Input file path')
parser.add_argument('--difftime', '-d', action='store_const', const=True, default=False, help='Print with difftime')
parser.add_argument('--pickup', '-u', nargs='*', help='pick up records')
parser.add_argument('--dropoff', '-o', nargs='*', help='drop off records')
args = parser.parse_args()
if args.pickup != None and args.dropoff != None:
print "--pickup and --dropoff, both provide"
return -1
with open(args.inputFile) as lines:
canlog = Canlog.create(lines)
if args.pickup != None:
canlog = canlog.pickup(map(hexint, args.pickup))
elif args.dropoff != None:
canlog = canlog.dropoff(map(hexint, args.dropoff))
if args.difftime == True:
canlog.print_with_diff_time()
else:
canlog.print_without_diff_time()
if __name__ == '__main__':
main()
TODOに対してクラスの組み方の例を示して頂きました。
またそれだけでなく、細かなインデントや、ちょっとしたコーディング作法としても、とても参考になりました。
knoguchi様より
Pythonを長いこと使っている者です。Pythonの機能てんこ盛りで若干書き直してみました。
オーバーエンジニアリング気味のコードですが、ご参考まで。
https://gist.github.com/knoguchi/4fc486a0cc39c1fd256d2fb6f619ee98
argparseで排他オプションをグループ化した。ifでチェックする必要がなくなった。
16進数の引数の型をargparseで変換するようにした。
Recordのデフォルト値をキーワード引数でセットするようにした。
クラスメソッドでRecordオブジェクトを作るようにした
dlcはdataの長さなので、プロパティに変更した
リスト渡しの関数をジェネレーターで置き換えた。大量のログを処理するとき、メモリの消費量が抑えられ> る。
fieldsの処理をnamedtupleで置き換えた。名前でアクセスできるようになったので、ログにフィールド > が追加になっても添え字を書き換える必要がない。
forループを使ったフィルタをfilterで置き換えた。
二箇所で行われていたレコードの内容の表示をRecordのstrメソッドに移動した。
ファイルはwithコンテキストマネージャを使って、処理完了後に自動で閉じるようにした。
if x != Noneはif xと書ける。if x == Trueも同じ。None, Trueを明示的にチェックするにはisを> 使う。
追記: @shiracamus さんの投稿に気づかずにコメントしたので、だいぶ被ってしまいました。
import re
import argparse
from collections import namedtuple
class Record:
def __init__(self, crtime=0.00000, ch=1, hexid=0x000, dir="Rx", stat="d", data=None, length=0, bitcount=0, decid=0):
self.crtime = crtime
self.ch = ch
self.hexid = hexid
self.dir = dir
self.stat = stat
self.data = data or []
self.length = length
self.bitcount = bitcount
self.decid = decid
@property
def dlc(self):
return len(self.data)
@classmethod
def parse_from_file(cls, input_file):
"""
The log format is fixed header fields, variable length data, ordered key-value pairs
header fields: crtime, ch, hexid, dir, stat, dlc
variable data: byte * dlc
"""
HEADER_TYPES = (
('crtime', float),
('ch', int),
('hexid', lambda s: int(s, 16)),
('dir', str),
('stat', str),
('dlc', int),
)
HEADER_LENGTH = len(HEADER_TYPES)
Header = namedtuple("Header", [field for field, _ in HEADER_TYPES])
for line in input_file:
fields = line.split()
if not re.match("1|2", fields[1]):
# ignore non-data rows
continue
# extract header
header_values = fields[:HEADER_LENGTH]
header_values = [func(value) for (field, func), value in zip(HEADER_TYPES, header_values)]
header = Header(*header_values)
# extract data
data = map(lambda x: int(x, 16), fields[HEADER_LENGTH:][:header.dlc])
# extract trailer
length = fields[-7]
bitcount = fields[-4]
decid = fields[-1]
yield cls(
crtime=header.crtime,
ch=header.ch,
hexid=header.hexid,
dir=header.dir,
stat=header.stat,
data=data,
length=length,
bitcount=bitcount,
decid=decid
)
def __str__(self):
return '%f %d %03X %s %d %s' % (
self.crtime, self.ch, self.hexid, self.dir, self.dlc,
' '.join(["%02X" % byte for byte in self.data])
)
def main():
parser = argparse.ArgumentParser(description='CanlogFilter')
parser.add_argument('inputFile', help='Input file path')
parser.add_argument('--difftime', '-d', action='store_const', const=True, default=False, help='Print with difftime')
group = parser.add_mutually_exclusive_group()
group.add_argument('--pickup', '-u', nargs='*', type=lambda x: int(x, 16), help='pick up records')
group.add_argument('--dropoff', '-o', nargs='*', type=lambda x: int(x, 16), help='drop off records')
args = parser.parse_args()
with open(args.inputFile) as input_file:
canlog = Record.parse_from_file(input_file)
if args.pickup:
canlog = pick_log(canlog, args.pickup)
elif args.dropoff:
canlog = drop_log(canlog, args.dropoff)
if args.difftime:
printlog_with_diff_time(canlog)
else:
printlog(canlog)
def pick_log(canlog, ids):
return filter(lambda rec: rec.hexid in ids, canlog)
def drop_log(canlog, ids):
return filter(lambda rec: rec.hexid not in ids, canlog)
def printlog(canlog):
for rec in canlog:
print rec
def printlog_with_diff_time(canlog):
prevtime = 0
for rec in canlog:
difftime = rec.crtime - prevtime
print '%f %s' % (difftime, rec)
prevtime = rec.crtime
if __name__ == '__main__': main()
リファクタして頂いた箇所は、コメント頂いた通り。
argparseの使い方も直して頂いています。
これは、頂いたどちらのソースにも共通していますが、私がfor line in open(filename) ~
のようなPerl的な文で書いているのをwith ~ as ~
という文で書いているのは正直な所初見で、Pythonにはまだ知らない構文があるんだな、とPythonらしく書けるようになりたいなと思うに至りました。
テキスト処理一つとっても、色々な人に使い込まれているなりの書き方がPythonにはあるな、と思いました。
もっと色々書いてみよう。