正規表現で文字列をカッコやタグで囲む方法【Python】re.finditer()を使って複数のキーワードに異なる装飾・置換をおこなうコード例

正規表現を使って、キーワードごとに異なるタグで囲む(マークアップする)方法です。

HTML形式などで、最終的に自分で見る用の文章を出すときに、単語を色分けしておいたらとても読みやすくなりました。そのときの Python コード例を紹介します。

やり方です。re.finditer() 関数を使いました。

  1. 複数のキーワードを、正規表現でまとめる。
  2. re.finditer(正規表現, テキスト) を、for文 にかける。
  3. マッチオブジェクトから、キーワードを取り出して、色分けする。

公式マニュアル
re.finditer(pattern, string, flags=0)
※ 読み方ですが、自分は『ファインド アイティーアー』って読んでいます。YouTube で finditer を検索したら、そう読んでた方がいたので、マネしました。その前は、『ファインド イテレーター』って読んでいました。わかれば何でもいいと思います。

色分けをするときは、『マッチオブジェクトの m.start(), m.end(), m[0]』と、『スライス表記 [:]』を使います。そして、『キーワードの加工』と『加工後の文字列の連結』をします。

公式マニュアル
マッチオブジェクト
スライス表記 (slicing)

以上です。

ざっくりコード例の抜粋です。

キーワードに応じて色を変えているのが、colors[i[m[0].lower()]] のところです(後述)。

def decorate_text_list(colors, keywords, text):
    #############
    # (中略)
    #############
    # 正規表現でヒットしたキーワードをひとつずつ囲んでいく
    for m in re.finditer(pattern, text, flags=re.IGNORECASE):
        # (1/5) 前方部分をリストに追加する
        texts.append(text[s:m.start()])

        # (2/5) キーワードを囲む(装飾する) & リストに追加する
        texts.append(
            '<span style="background:%s">%s</span>' % (
                colors[i[m[0].lower()]], m[0]))

        # (3/5) m.end()を次の開始位置に設定する
        s = m.end()
    else:
        # (4/5) 最後に残った部分を追加
        texts.append(text[s:])

    # (5/5) リストの要素を連結して完成
    return ''.join(texts)

キーワードの加工』と『加工後のテキスト連結』は、ほかにも色々やり方があると思います。

ほかの方法のコード例と速度比較も載せました。

<span>タグや色の名前の代わりに、カッコ類を使うようなコードに書きかえれば、キーワードを「」、『』、【】などで囲むこともできます

スポンサーリンク

正規表現で文字列を囲む方法

一例として、キーワードを色分けをする方法を紹介します。狙った文字列をHTMLの<span>タグで囲んで、色分けします。

以降で、文字列を囲む『手順』⇒『コード例の全体』⇒『速さ比較』を紹介していきます。

解説が長いですが、全部見なくても、やり方はなんとなくわかると思います。

なので、 必要なところだけ拾い読みしていくのがおすすめです。

手順

色の名前のリストを作る

キーワードごとに色分けするために、リストで、色の名前を用意します。タプルで作ってもOKです。

# 色分けの定義例 [0]orange [1]skyblue [2]lightgreen
colors = ['orange', 'skyblue', 'lightgreen']

そして、キーワードと要素番号の辞書 i を作ります(あとのほうで解説しています)。

  1. 正規表現にマッチしたら、
  2. そのキーワード m[0] に対応する要素番号 i[m[0]] を取得して、
  3. 色のリストにアクセスする colors[i[m[0]]]

実際には、m[0].lower() でキーワードを小文字化してアクセスします。これで、大文字小文字を区別せずに色分けすることができました。

公式マニュアル
str.lower()

colors[i[m[0].lower()]]

キーワードを正規表現にまとめる

囲みたい、装飾したいキーワードのリストを作ります。

次に、正規表現の縦棒 '|' で区切って、ひとつの正規表現パターンにまとめます。

区切り文字を使って連結するときは、ジョインメソッド '|'.join() を使います。

公式マニュアル
str.join(iterable)

# 装飾したいキーワードの例
keywords = ['aa', 'bb', 'cc', 'dd']

# 正規表現のパターンを作る
pattern = r'(%s)' % '|'.join(keywords)

変数 pattern の中身は、以下のようになります。

'(aa|bb|cc|dd)'

キーワードリストは、上記のように直接リストに書いてもいいですし、CSV や JSON を読み込んで作ったリストを使ったりもします。

キーワードと要素番号の辞書を作る

色のリストの範囲内で、キーワードに要素番号を割り当てます。

# キーワードをキーにして、色のリストの要素番号辞書を作る。
# (アルファベットは .lower() で小文字に統一)
i = {k.lower(): n % len(colors) for (n, k) in enumerate(keywords)}

辞書のカッコ{}の中身は、内包表記(ないほうひょうき)という書き方です。

公式マニュアル
リスト、集合、辞書の表示 ⇒ 内包表記 (comprehension)

変数 i の中身は、以下のようになります。

{'aa': 0, 'bb': 1, 'cc': 2, 'dd': 0}

n % len(colors) のパーセント記号は『剰余(じょうよ)』です。連番 n を len(colors) で割ったときの余りのことです。

これで、キーワードがたくさんあっても、色のリストの範囲内で繰り返し同じ番号を振ることができました。上の例でいうと、キーワード 'dd' に、再び 'orange' の 0 が入っています。

名前や関数の説明です。

キーワードに応じて色を変える方法

マッチしたキーワードm[0] ⇒ 要素番号辞書 i ⇒ 色の名前リスト の流れで、色分けする時の色を変えていきます。

colors[i[m[0].lower()]]

実行例です。

'Aa'にマッチした!
m[0].lower() ⇒ 'aa'
i[m[0].lower()] ⇒ i['aa'] ⇒ 0
colors[i[m[0].lower()]] ⇒ colors[0] ⇒ 'orange'

このようにして、キーワードに対応した色を取得しています。

re.finditer() 関数を使う ⇒ リストで連結する(方法1)

装飾後のテキストをリストに入れて、最後に ''.join() で連結する方法です。

正規表現でマッチしたところを取得していくには、re.finditer() 関数(ファインドアイティーアー関数)を使います。

公式マニュアル
re.finditer(pattern, string, flags=0)

re.finditer() を for文 にかけると、マッチオブジェクトが返ります。

マッチオブジェクトには、開始位置 m.start()、終了位置 m.end()、マッチした文字列 m[0] が入っています。これらを使って、キーワードを加工したり、加工後の文字列を連結したりします。

『スライス表記 [:]』と『開始位置 m.start() & 終了位置 m.end()』を組み合わせたところ、加工後の文字列をうまくつなげることができました。

def decorate_text_list(colors, keywords, text):
    """正規表現を使ってテキストを装飾する関数
    (装飾後のテキストをリストに入れて、最後に ''.join() で連結する方法)"""

    # キーワードをキーにして、色のリストの要素番号辞書を作る。
    # (アルファベットは .lower() で小文字に統一)
    i = {k.lower(): n % len(colors) for (n, k) in enumerate(keywords)}

    # 正規表現のパターンを作る
    pattern = r'(%s)' % '|'.join(keywords)

    # 結果を入れるリスト
    texts = []

    # テキストの開始位置を設定する変数
    # (最初はゼロから始めて、m.end()で更新していく)
    s = 0
    
    # 正規表現でヒットしたキーワードをひとつずつ囲んでいく
    # (re.IGNORECASE で大文字小文字を無視してマッチさせる)
    for m in re.finditer(pattern, text, flags=re.IGNORECASE):
        # (1/5) 前方部分をリストに追加する
        texts.append(text[s:m.start()])

        # (2/5) キーワードを囲む(装飾する) & リストに追加する
        # (要素番号辞書に渡すキーは .lower() で小文字化して渡す)
        texts.append(
            '<span style="background:%s">%s</span>' % (
                colors[i[m[0].lower()]], m[0]))

        # 参考1 .format() を使う方法
        # texts.append(
        #     '<span style="background:{color}">{keyword}</span>'.format(
        #         color=colors[i[m[0].lower()]], keyword=m[0]))
        
        # 参考2 フォーマット済み文字列リテラル(f文字列) を使う方法
        # texts.append(
        # f'<span style="background:{colors[i[m[0].lower()]]}">{m[0]}</span>')

        # (3/5) m.end()を次の開始位置に設定する
        s = m.end()
    else:
        # (4/5) 最後に残った部分を追加
        texts.append(text[s:])

    # (5/5) リストの要素を連結して完成
    return ''.join(texts)

キーワードの大文字小文字を厳密に区別したいときは、コード中の str.lower() と re.IGNORECASE を削除します。

for文 の else節 ですが、for文の中身が break せずに完遂したときだけ、実行されます。for文に空のリストや空のタプルを渡した時も、for文 の else節 は実行されました。上記の例では、べつに else節 を使わなくてもいい気もします。

公式マニュアル
for 文
break 文と continue 文とループの else 節

リストの中身の連結には、ジョインメソッド ''.join() を使います。空文字列 '' のジョインメソッドなので、単純にひとつの文字列へと連結されます。

公式マニュアル
str.join(iterable)

装飾後のテキストをリストに入れて、最後に ''.join() で連結する方法は、巨大なテキストでも高速に処理できるメリットがありました(速さ比較で紹介します)。

文字列を組み立てる方法(3種類)

変数を使って文字列を組み立てる方法(書式化する方法)ですが、3種類ほどありました。

新しい Python を使えるなら、接頭辞をつけて f''F'' で始める フォーマット済み文字列リテラルがおすすめです。文字列の中に変数名を直接書けるので、非常に便利でした。普段からよく使っています。

しかしながら、状況によっては %''.format() を使う方法が便利だったりしました。なので、必要に応じて使い分ける感じでいいのかもしれません。それぞれに特有のメリットがありました。

re.finditer() 関数を使う ⇒ +で連結する(方法2)

装飾後のテキストを + で連結して再代入していく方法です。

全体のテキストが短いときは、こちらの方法が高速でした。ですが、1000文字や1万文字になってくると、非常に遅くなりました(速さ比較で紹介します)。

処理の流れですが、最初にマッチオブジェクトを全部取得してしまいます。

そのあとで、マッチオブジェクトを逆順にたどって、うしろからテキストを装飾していきます。

正順で前方から装飾していったところ、文字列が増えた分だけ、m.start() と m.end() の位置からずれてしまいました。逆順にたどることで、『これから処理するところの位置』が保存できました。

def decorate_text_adder(colors, keywords, text):
    """正規表現を使ってテキストを装飾する関数
    (装飾後のテキストを + で連結して再代入していく方法)"""

    # キーワードをキーにして、色のリストの要素番号辞書を作る。
    # (アルファベットは .lower() で小文字に統一)
    i = {k.lower(): n % len(colors) for (n, k) in enumerate(keywords)}

    # 正規表現のパターンを作る
    pattern = r'(%s)' % '|'.join(keywords)

    # マッチオブジェクトを一旦リストに追加する
    match_objects = []
    for m in re.finditer(pattern, text, flags=re.IGNORECASE):
        match_objects.append(m)

    # リストの順番を逆順にして、後ろから置換していく。
    for m in reversed(match_objects):
        text = text[:m.start()] + \
            '<span style="background:%s">%s</span>' % (
                colors[i[m[0].lower()]], m[0]) + \
            text[m.end():]
    return text

行末の 円記号(バックスラッシュ)は、コードを複数行に分けるためのものです。このように、カッコが無いところでコードを改行するときは、円記号(バックスラッシュ)をつけるとうまく動きました。

公式マニュアル
字句解析 明示的な行継続

スポンサーリンク

コード例の全体

正規表現で文字列を囲むコード例の全体です。

指定したキーワードをタグで囲んで、色分けします。

処理の流れです。

  1. メイン関数の中で、『decorate_text_list()』と『decorate_text_adder()』の2種類の関数を実行する。
  2. 2つの結果が一致することを確認する(同じになるはず)。
  3. Pythonファイルと同じ場所に、『元のテキスト』と『装飾後のテキスト』の2つをファイルに保存する。
""" main_decorate.py
正規表現でキーワードを色分けするコード例
re.finditer()を使ってキーワードをタグで囲む(装飾する)"""

import re
import os

def main():
    """メイン関数"""

    # 色分けの定義例 [0]orange [1]skyblue [2]lightgreen
    colors = ['orange', 'skyblue', 'lightgreen']

    # 装飾したいキーワードの例
    keywords = ['aa', 'bb', 'cc', 'dd']

    # テキストの例
    src_text = '<html><body><p>'
    src_text += '\naa\nbb\ncc\ndd\nee\nff\ngg\nAa\nBb\n'
    src_text += '</p></body></html>'

    # 正規表現を使ってテキストを装飾する(2通りの方法を試す)
    text_list = decorate_text_list(colors, keywords, src_text)
    text_adder = decorate_text_adder(colors, keywords, src_text)

    # デバッグ情報
    if text_list == text_adder:
        print('結果は一致しました text_list == text_adder')
    else:
        print('不一致でした text_list != text_adder')

    # Pythonスクリプトと同じフォルダに 元のhtml を保存する
    src_file = os.path.join(os.path.dirname(__file__), 'src.html')
    with open(src_file, 'w', encoding='utf-8') as f:
        f.write(src_text)

    # Pythonスクリプトと同じフォルダに 結果のhtml を保存する
    dst_file = os.path.join(os.path.dirname(__file__), 'dst.html')
    with open(dst_file, 'w', encoding='utf-8') as f:
        f.write(text_list)
    return


def decorate_text_list(colors, keywords, text):
    """正規表現を使ってテキストを装飾する関数
    (装飾後のテキストをリストに入れて、最後に ''.join() で連結する方法)"""

    # キーワードをキーにして、色のリストの要素番号辞書を作る。
    # (アルファベットは .lower() で小文字に統一)
    i = {k.lower(): n % len(colors) for (n, k) in enumerate(keywords)}

    # 正規表現のパターンを作る
    pattern = r'(%s)' % '|'.join(keywords)

    # 結果を入れるリスト
    texts = []

    # テキストの開始位置を設定する変数
    # (最初はゼロから始めて、m.end()で更新していく)
    s = 0
    
    # 正規表現でヒットしたキーワードをひとつずつ囲んでいく
    # (re.IGNORECASE で大文字小文字を無視してマッチさせる)
    for m in re.finditer(pattern, text, flags=re.IGNORECASE):
        # (1/5) 前方部分をリストに追加する
        texts.append(text[s:m.start()])

        # (2/5) キーワードを囲む(装飾する) & リストに追加する
        # (要素番号辞書に渡すキーは .lower() で小文字化して渡す)
        texts.append(
            '<span style="background:%s">%s</span>' % (
                colors[i[m[0].lower()]], m[0]))

        # (3/5) m.end()を次の開始位置に設定する
        s = m.end()
    else:
        # (4/5) 最後に残った部分を追加
        texts.append(text[s:])

    # (5/5) リストの要素を連結して完成
    return ''.join(texts)


def decorate_text_adder(colors, keywords, text):
    """正規表現を使ってテキストを装飾する関数
    (装飾後のテキストを + で連結して再代入していく方法)"""

    # キーワードをキーにして、色のリストの要素番号辞書を作る。
    # (アルファベットは .lower() で小文字に統一)
    i = {k.lower(): n % len(colors) for (n, k) in enumerate(keywords)}

    # 正規表現のパターンを作る
    pattern = r'(%s)' % '|'.join(keywords)

    # マッチオブジェクトを一旦リストに追加する
    match_objects = []
    for m in re.finditer(pattern, text, flags=re.IGNORECASE):
        match_objects.append(m)

    # リストの順番を逆順にして、後ろから置換していく。
    for m in reversed(match_objects):
        text = text[:m.start()] + \
            '<span style="background:%s">%s</span>' % (
                colors[i[m[0].lower()]], m[0]) + \
            text[m.end():]
    return text


if __name__ == '__main__':
    main()

実行結果

『元のテキスト』と『装飾後のテキスト』を、ブラウザで開いてキャプチャしました。以下の画像は、キャプチャした画像を縦に並べたものです。

コマンドプロンプト

コマンドプロンプトの表示です。

意図したとおり、2つの方法で同じ装飾結果が得られていました。

結果は一致しました text_list == text_adder

元のテキスト

src.html の内容です。

<html><body><p>
aa
bb
cc
dd
ee
ff
gg
Aa
Bb
</p></body></html>

 装飾後のテキスト

dst.html の内容です。

意図したとおり、正規表現にマッチした文字列が、キーワードに応じた色のタグで囲まれています

また、大文字と小文字の区別なく、Aa と Bb にも反応して、タグで囲まれています。

<html><body><p>
<span style="background:orange">aa</span>
<span style="background:skyblue">bb</span>
<span style="background:lightgreen">cc</span>
<span style="background:orange">dd</span>
ee
ff
gg
<span style="background:orange">Aa</span>
<span style="background:skyblue">Bb</span>
</p></body></html>

コード例の解説は以上です。

スポンサーリンク

速さ比較

正規表現で加工したあとの文字列の連結方法で、『リストで連結する方法』と『演算子 + で連結する方法』の2つを紹介しました。

どちらが速いのか?

timeit() で、関数の実行にかかった時間を計りました。

結論ですが、『リストで連結する方法』が速くて優秀でした。

一方で、『演算子 + で連結する方法』ですが、短いテキストの時は高速でした。しかし、テキストが長くなると、目に見えて遅くなりました。

この方法は、コードの中で『小さな文字列を連結する時にこそ、役に立つ』ということがわかりました。

時間の計測コード

timeit() で時間を計ったときのコードです。

""" main_timeit.py
(timeitで時間を計測する)
正規表現でキーワードを色分けするコード例
re.finditer()を使ってキーワードをタグで囲む"""

import re
import os
from timeit import timeit as ti
from main_decorate import decorate_text_list
from main_decorate import decorate_text_adder

def main():
    """メイン関数"""

    # 色分けの定義例 [0]orange [1]skyblue [2]lightgreen
    colors = ['orange', 'skyblue', 'lightgreen']

    # キーワード例
    keywords = ['aa', 'bb', 'cc', 'dd']


    # 【関数の実行にかかる時間を計る】
    # 1. 短いテキストの場合
    t = '<html><body><p>'
    t += '\naa\nbb\ncc\ndd\nee\nff\ngg\nAa\nBb\n'
    t += '</p></body></html>'
    print('len(t): %d 文字' % len(t))

    g = globals()
    g.update(locals())
    t_list = ti('decorate_text_list(colors, keywords, t)', number=1, globals=g)
    t_adder = ti('decorate_text_adder(colors, keywords, t)', number=1, globals=g)
    print('decorate_text_list : %f 秒' % t_list)
    print('decorate_text_adder: %f 秒' % t_adder)

    print()

    # 【関数の実行にかかる時間を計る】
    # 2. とても長いテキストの場合
    t = '<html><body><p>'
    t += '\naa\nbb\ncc\ndd\nee\nff\ngg\nAa\nBb\n' * 3000
    t += '</p></body></html>'
    print('len(t): %d 文字' % len(t))

    g = globals()
    g.update(locals())
    t_list = ti('decorate_text_list(colors, keywords, t)', number=1, globals=g)
    t_adder = ti('decorate_text_adder(colors, keywords, t)', number=1, globals=g)
    print('decorate_text_list : %f 秒' % t_list)
    print('decorate_text_adder: %f 秒' % t_adder)
    return


if __name__ == '__main__':
    main()

実行結果

1回の関数実行にかかった時間です。

テキストの文字数が増えたときに、『リストで連結する方法 decorate_text_list()』のほうが、圧倒的に短い時間で完了しています。

『演算子 + で連結する方法 decorate_text_adder()』は、長いテキストには不向きのようでした。

この結果を見て、通常はリストを使って連結する方法で良いと思いました。

len(t): 61 文字
decorate_text_list : 0.000367 秒
decorate_text_adder: 0.000047 秒

len(t): 84033 文字
decorate_text_list : 0.031993 秒
decorate_text_adder: 1.615881 秒
タイトルとURLをコピーしました