【Python】文章から測定値と単位を取り出す:符号・小数・指数表記に対応する

PythonのTopに戻る

数値変換の前に、認める表記を決める

文章の中から-1.2e-3 Vを見つけるには、float()へ文章全体を渡すのではなく、数値部分と単位を先に切り出す。符号、小数点、指数部にはそれぞれ省略できる条件がある。単に数字とピリオドを並べて許すと、1.2.3のような文字列の一部分を正しい値として拾うことがあるので、数値の構造を式として組み立てる。

以下は半角表記に限定した小さな抽出器である。数値は整数、1.、.5、1.5を認め、末尾にeまたはEと符号付き整数の指数を付けられる。単位はmV、V、Aの3種類。文章中では値全体が空白または文字列端に接するものだけを抽出する。この限定により、単位付き値と識別子の区別を明確にする。

数値・単位・境界を別々に読む

example.py

import math
import re

NUMBER = r"[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?"
MEASUREMENT = re.compile(
    rf"(?<!\S)(?P<number>{NUMBER})[ \t]*(?P<unit>mV|V|A)(?!\S)"
)

def extract(text):
    found, rejected = [], []
    for match in MEASUREMENT.finditer(text):
        value = float(match.group("number"))
        if not math.isfinite(value):
            rejected.append((match.group(0), "non-finite"))
            continue
        found.append((value, match.group("unit"), match.span()))
    return found, rejected

text = "values: -1.2e-3 V +.5 A 2.mV bad: 1.2.3 V 1,000 V 1e V 1e999 V"
found, rejected = extract(text)
assert [(v, u) for v, u, _ in found] == [(-0.0012, "V"), (0.5, "A"), (2.0, "mV")]
assert rejected == [("1e999 V", "non-finite")]
assert extract("12 V 1 kV 1Vx")[0] == []
for value, unit, span in found:
    print(repr(text[slice(*span)]), value, unit)
print("rejected:", rejected)

実行結果

'-1.2e-3 V' -0.0012 V
'+.5 A' 0.5 A
'2.mV' 2.0 mV
rejected: [('1e999 V', 'non-finite')]

小数部と指数部の役割

最初の[+-]?が省略できる符号である。仮数部は「整数部が必須で小数部が任意」か「整数部がなく小数点の後に数字が必須」の二択にした。そのため1.と.5は通るが、ピリオドだけは通らない。指数部は全体が任意であり、eだけを置くことは認めない。指数を付けるなら、その後に少なくとも1桁の数字が必要になる。

単位は長いmVを先に並べている。ここでは単位の換算をせず、元の単位を値と一緒に返す。mVとVを同じ列として集計するなら、係数を定義して統一した単位へ変換する工程が必要である。数値だけのリストにしてしまうと、元の大きさを復元できなくなるので、単位を捨てない。

float("1e999")はこの環境では無限大になるため、変換に成功したことと有限の測定値であることを同一視しない。math.isfinite()で確認し、除外した値を別に残した。さらに装置の仕様による上限・下限も必要ならここで調べる。非常に小さい指数は0.0へ丸められる場合があるので、微小値の厳密な桁を保ちたい用途にはDecimalなども検討する。

取り出せない文字列も仕様に含める

1.2.3 Vや1,000 Vを完全な測定値として認めないだけでなく、末尾の3 Vや000 Vを拾わないように左の空白境界を付けた。右も空白境界なので、値の後ろに読点や括弧が付く文章はそのままでは対象外になる。句読点を許可するには境界を変更する必要があり、変更後に誤抽出が増えていないかを確認する。

見つからなかった部分はこの関数のrejectedに入らない。rejectedに入るのは、構造として一致したが非有限値になったものだけである。文章全体から抜けなく数値を読む必要があるなら、区切りや行の構造を決めたうえで各項目へfullmatch()を適用し、不一致を報告する方式が向く。抽出器だけで入力全体の正しさを保証しない。

全角数字、Unicodeのマイナス記号、μなどの接頭辞、桁区切り、複合単位はこの例では対応していない。追加する場合は元の規則へ無造作に.*を足すのではなく、対象表記を列挙して正常例と拒否例を増やす。抽出した範囲spanも残しておくと、元の文章へ戻って目視確認できる。

同じ値でも元の表記を残す

1 V、1.0 V、1e0 Vはfloatではいずれも同じ値になる。元の有効数字や記載方法を後から比較したい場合は、数値への変換結果だけでなくmatch.group("number")も保存する。例はspanを残しているため、元のtextがあれば表記を取り出せるが、元の文章を破棄するなら文字列も記録に含める。値・単位・元の表記の三つを保つことで、計算と入力の監査を別々に行える。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:測定ログを列に分ける:名前付きグループで正規表現を読む / 一致した内容で置換を変える:re.subに関数を渡す

PythonのTopに戻る