列番号ではなく項目名で取り出す
測定ログの行を日時・装置ID・測定値に分けたいとき、区切るだけでは入力が正しい構造か分からない。正規表現で行全体を確認し、必要な部分に名前を付けて取り出すと、列の役割をコードに残せる。ここでは「日時 空白 装置ID 空白 数値 V」という固定形式を対象にする。形式違いの行は捨てず、元の行番号と理由を別のリストに残す。
仕様は、日時が秒まで、装置IDが大文字英字2文字と数字2文字、値が符号付き整数または小数、単位がVである。指数表記や単位の自動換算は含めない。空白は半角スペースとタブだけを認める。このように入力仕様を先に狭めれば、読み取れなかった行の原因を調べやすい。
行の構造と値の意味を分けて確認する
example.py
import re
from datetime import datetime
PATTERN = re.compile(
r"(?P<time>[0-9]{4}-[0-9]{2}-[0-9]{2} "
r"[0-9]{2}:[0-9]{2}:[0-9]{2})[ \t]+"
r"(?P<device>[A-Z]{2}[0-9]{2})[ \t]+"
r"(?P<value>[+-]?[0-9]+(?:\.[0-9]+)?)[ \t]+V"
)
lines = [
"2026-10-01 09:00:00 AB01 -1.25 V\n",
"2026-02-30 09:00:01 AB01 2 V\n",
"2026-10-01 09:00:02 AB02 3.5 mV\n",
"2026-10-01 09:00:03 AB02 +2 V\n",
]
records, rejected = [], []
for line_number, raw in enumerate(lines, start=1):
line = raw.rstrip("\r\n")
match = PATTERN.fullmatch(line)
if match is None:
rejected.append((line_number, "format", line))
continue
fields = match.groupdict()
try:
timestamp = datetime.strptime(fields["time"], "%Y-%m-%d %H:%M:%S")
value = float(fields["value"])
except ValueError:
rejected.append((line_number, "value", line))
continue
records.append({"time": timestamp, "device": fields["device"], "value_V": value})
assert [r["value_V"] for r in records] == [-1.25, 2.0]
assert [(n, reason) for n, reason, _ in rejected] == [(2, "value"), (3, "format")]
for row in records:
print(row["time"].isoformat(), row["device"], row["value_V"])
print("rejected:", [(n, reason) for n, reason, _ in rejected])
実行結果
2026-10-01T09:00:00 AB01 -1.25
2026-10-01T09:00:03 AB02 2.0
rejected: [(2, 'value'), (3, 'format')]
名前付きグループを読む
(?P<time>…)のtimeがグループ名である。groupdict()は、グループ名をキー、一致した文字列を値にした辞書を返す。日時の括弧を追加しても、「3番目のグループは測定値だったか」と数え直さずに済む。繰り返しのためだけの括弧には(?:…)を使い、取り出す項目だけに名前を付けている。
fullmatch()は行全体との一致を要求する。search()にすると、前後に壊れた文字がある行から正常そうな一部分だけを拾うことがある。行末の改行だけをrstrip("\r\n")で除去し、その他の空白は勝手に消していない。入力の誤りを検出したい処理では、strip()による自動修正を先に行わない方がよい場合がある。
正規表現で分かるのは表記の構造までである。2月30日も数字の並びとしては一致するが、datetime.strptime()で実在しない日付と分かる。そこで構造違いをformat、変換できない値をvalueとして分けた。日時・装置ID・測定値を別々に検索して後から組み合わせると、別の行の情報が混ざるので、必ず一行を一つの記録として扱う。
実際のログへ適用するときの注意
文字列からfloatへの変換に成功しても、装置の測定範囲内とは限らない。必要なら変換後に下限・上限を確認し、非有限値の可否も決める。時刻にはタイムゾーン情報がないため、この例のdatetimeはタイムゾーンなしである。装置間の時刻比較に使うなら、ログを出した場所の時刻規約を別途確定させる。
大量のログではrecordsやrejectedに全件をためるとメモリを使う。正常行をCSVへ、不正行を別ファイルへ逐次出力すれば、ここで示した判定を保ったまま行単位処理へ移せる。エラー件数だけでなく元の行番号を残すと、元データとの照合ができる。実ログに識別情報が含まれる場合は、不正行の保存先にも同じアクセス管理が必要になる。
入力仕様が変わったときは、正常行の追加だけでなく「通してはいけない行」もテストへ追加する。単位違い、末尾の余計な文字、実在しない日付を含めておけば、正規表現を緩めた変更が取り込み条件を広げていないか確認できる。
取り込み後の確認方法
実行結果では4行のうち2行が記録になり、2行が拒否される。件数の合計が入力行数と一致することを確認すると、分岐の途中で行を落としたミスを見つけやすい。空行やコメント行を除外する運用に変えるなら、その件数も別に数える。装置IDごとに件数を集計し、ある装置だけ突然0件になっていないか調べると、入力形式の変更にも気づける。形式変更を吸収する処理と、異常を見つける処理のどちらを優先するかを決めておこう。
実行環境と関連情報
掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。
関連:IDの形式を厳密にチェックする:fullmatchとASCII文字の指定 / 文章から測定値と単位を取り出す:符号・小数・指数表記に対応する / 巨大なテキストを丸ごと読まない:行単位で抽出・集計する
- Python公式ドキュメント(2026年10月2日参照)
