似たような文字列を含んだデータをまとめる

説明しにくいんだけど、日付とデータが一行に入ってるデータファイルがあるとき、データの方だけ合計を取って、日付の共通部分は残しておきたい、みたいなことを考えた。

つまり、

#date time count time
2013/03/12 08:23:34 12 0.21
2013/03/15 08:40:20 13 0.22
2013/03/19 12:23:34 22 0.21
2013/03/23 00:41:44 12 0.20

みたいなデータを食べさせると

2013/03/XX XX:XXXXX 59 0.84

みたいな出力をしてほしい。

pythonでそういうスクリプトを書いた。各データについてまず整数として、次に浮動小数点としてパースを試みて、どっちもだめなら文字列と見なして、それらの共通部分を作る。

python2.1から入っているシーケンスの比較を行う組み込みライブラリである difflib を使った。

sum_rows.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# sum_rows.py

import difflib

def try_convert_into_number(s):
    """try convert s into integer. if could not try convert into float. then give up (return None)"""
    try:
        return int(s)
    except ValueError:
        try:
            return float(s)
        except ValueError:
            return None

def make_diff_to_XXX(s1,s2):
    """compare two strings s1 and s2 then return string marked with 'X' where s1 differs from s2"""
    s = difflib.SequenceMatcher(a=s1, b=s2)
    str=''
    back = 0
    for block in s.get_matching_blocks():
        # print "match at a[%d] and b[%d] of length %d" % block
        if block[0] != back:
            str = str + 'X' * (block[0]-back)
        str = str + s1[block[0]:block[0]+block[2]]
        back = block[0]+block[2]
    return str

# make_diff_to_XXX("This is a pen", "This is a pan")

def nonempty_lines(stream):
    """yield lines that is neither comment (begins by #) nor empty"""
    comment_char = '#'
    for l in stream:
        if l == "" or l[0] == comment_char: continue
        l = l.rstrip()
        yield l

def sum_rows(stream):
    """sum numbers in each columns from lines in stream.
    raises error when a column contains both number and string."""
    hist = []
    max_cols=0
    for l in nonempty_lines(stream):
        words = l.split()
        max_cols = max(max_cols, len(words))
        if(max_cols>len(hist)):
            hist.extend([None]*(max_cols-len(hist)))
        for i, w in enumerate(words):
            num = try_convert_into_number(w)
            if num != None:
                hist[i] = (hist[i] or 0) + num
            elif not hist[i]:
                hist[i] = w
            # comment-out 2 lines below for large file
            else:
                hist[i] = make_diff_to_XXX(hist[i], w)
    return hist

import sys
if __name__ == "__main__":
    if len(sys.argv) <= 1:
        print "usage: {0} file('-' for stdin)".format(sys.argv[0])
        exit(1)
    fin = sys.stdin if (sys.argv[1] == '-') else open(sys.argv[1])
    hist = sum_rows(fin)
    for i in range(len(hist)):
        print hist[i],
    print

僕は

$ sum_rows datafile

とか

$ grep ^2014/05/ datafile | sum_rows -

みたいな感じで使っている。