【Python】一致した内容で置換を変える:re.subに関数を渡す

PythonのTopに戻る

置換文字列をマッチごとに計算する

本文に出てくる250 mVを0.250 Vへ換算したい場合、固定文字列によるreplace()では値ごとの計算ができない。re.sub()の置換先に関数を渡すと、一致した数値を受け取り、計算後の文字列を返せる。本文全体を作り直さず、条件に一致した箇所だけを変えるのが便利な点である。

以下では読みやすさを優先し、整数または小数とmVを対象とし、数値とmVの間には半角スペースまたはタブ、左右には文字列端または空白を認める。指数表記、桁区切りカンマ、括弧や句読点に隣接する表記は対象外とする。文中の一部分を誤って換算しないよう、左右の境界も仕様に含める。より多様な測定値を扱うときは、先に数値表記の規則を拡張する。

Decimalで値を換算して置換する

example.py

import re
from decimal import Decimal

PATTERN = re.compile(
    r"(?<!\S)(?P<number>[+-]?[0-9]+(?:\.[0-9]+)?)"
    r"[ \t]+mV(?!\S)"
)

def to_volts(match):
    volts = Decimal(match.group("number")) / Decimal("1000")
    return f"{volts:.3f} V"

text = "a= 250 mV b= -12.5 mV keep= 2 V id= X20mV"
converted, count = PATTERN.subn(to_volts, text)
assert converted == "a= 0.250 V b= -0.012 V keep= 2 V id= X20mV"
assert count == 2
assert PATTERN.sub(to_volts, "1e3 mV") == "1e3 mV"
assert PATTERN.sub(to_volts, "1,000 mV") == "1,000 mV"
assert PATTERN.sub(to_volts, converted) == converted
print(converted)
print("replacements:", count)

実行結果

a= 0.250 V b= -0.012 V keep= 2 V id= X20mV
replacements: 2

置換関数の受け取り方と返し方

to_volts()へ渡されるのは元の文字列全体ではなく、その箇所のMatchオブジェクトである。group("number")で取り出した数値は文字列なので、Decimalへ変換してから1000で割る。戻り値は必ず文字列にする。関数自体を渡すため、PATTERN.subn(to_volts, text)と書き、to_volts()のようにその場で呼び出さない。

subn()は置換後の文字列と置換回数の組を返す。sub()でも同じ関数が使えるが、実データの一括変換では件数を記録できるsubn()が役立つ。「100件ほど変わるはずなのに0件だった」と気づけば、単位表記や空白の仕様が入力と違う可能性を調べられる。0件を正常とみなすかは用途次第である。

この例は小数点以下3桁で表示するため、-12.5 mVは厳密な換算値-0.0125 Vから丸められる。Decimalの既定の丸め方はちょうど中間なら偶数側を選ぶため、結果は-0.012 Vになる。表示桁数は測定精度を保証しない。丸めてよいか、必要なら何桁を保存するかを先に決め、解析用の値と見せる文字列を分ける方がよい。

部分一致による誤変換を防ぐ

左右の(?<!\S)と(?!\S)は、文字列端か空白に接する位置を要求する。これによりX20mVや1,000 mVの末尾だけを拾わない。ただし「250 mV,」のようなカンマ直前の値も対象から外れる。この除外はバグではなく、例で採用した境界規則によるものだ。句読点を認めるなら、認める記号を具体的に列挙してテストも増やす。

置換関数から返した文字列は、その同じsub()呼び出しの中で再び検索されるわけではない。ただし変換を何度も実行する運用なら、2回目にさらに値が変わらないか確認する。本例はmVだけをVへ変えるため、変換済み文にもう一度適用しても同じ結果になることをassertで確かめた。

本番の文章は元ファイルに直接上書きせず、別名で出力し、差分と置換件数を確認してから採用するとよい。置換関数に例外が起きた場合、その処理を正常な変換として扱わない。未知の単位を勝手に推測せず残すことと、読み取れなかった箇所を見つけられる検査を用意することが重要である。

換算値を表示する前の確認

対象外の部分が残ることも大事な結果である。例では2 VとX20mVが変化せず、指数表記と桁区切りの拒否もassertで確認している。実際の文章を試す前に、値が0、負、非常に小さい場合を追加して、丸めによる0.000 Vを許容できるか確かめる。単位の変換そのものが正しくても、桁を落としすぎると意味のある差が読者に伝わらなくなるため、表示形式は換算式と同じくらい重要になる。

実行環境と関連情報

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

関連:文章から測定値と単位を取り出す:符号・小数・指数表記に対応する / 検索語に記号があっても壊れない:re.escapeで複数語を検索する

PythonのTopに戻る