連載
» 2021年03月16日 05時00分 公開

[解決!Python]文字列から特定の文字で囲まれている部分を抽出するには(re.findall関数)解決!Python

文字列から特定の文字で囲まれている部分を抽出するには「開き文字」「囲まれている部分」「閉じ文字」というパターンに注目して正規表現を組み立てるとよい。

[かわさきしんじ,Deep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「解決!Python」のインデックス

連載目次

import re

s = 'this is *sample string* for _extracting substring_.'

# アスタリスクで囲まれている部分を抽出
p = r'\*.*\*'  # アスタリスクに囲まれている任意の文字
#p = r'\*[^*]*\*'  # アスタリスクに囲まれているアスタリスク以外の文字
r = re.findall(p, s)  # パターンに当てはまるものを全て抽出
print(r)  # ['*sample string*']

# アスタリスクが不要ならグループを使って抽出する部分を指定する
p = r'\*(.*)\*'  # アスタリスクに囲まれている任意の文字(アスタリスクを除く)
r = re.findall(p, s)
print(r)  # ['sample string']

# 貪欲マッチと非貪欲マッチ(最小マッチ)
s = 'this is *sample string* for *extracting substring*'
p = r'\*(.*)\*'  # 貪欲マッチ(上と同じ)
r = re.findall(p, s)
print(r)  # ['sample string* for *extracting substring']

p = r'\*(.*?)\*'  # 非貪欲マッチ(最小マッチ)
r = re.findall(p, s)
print(r)  # ['sample string', 'extracting substring']

p = r'\*([^*]*)\*'  # アスタリスクに囲まれたアスタリスク以外の文字(貪欲マッチ)
r = re.findall(p, s)
print(r)  # ['sample string', 'extracting substring']

# アスタリスクかアンダースコアで囲まれている部分を抽出
s = 'this is *sample string* for _extracting substring_.'
p = r'\*(.*?)\*|_(.*?)_'  # バーティカルバー「|」で2つの条件を列記
r = re.findall(p, s)
print(r)  # [('sample string', ''), ('', 'extracting substring')]

r = [item[0] + item[1] for item in re.findall(p, s)]
# r = [item[0] if item[0] else item[1] for item in re.findall(p, s)]
print(r)  # ['sample string', 'extracting substring']

p = r'[*_](.*?)[*_]'  # 大かっこ「[]」にアスタリスクとアンダースコアを指定
r = re.findall(p, s)
print(r)  # ['sample string', 'extracting substring']

# Markdown記法のリンクからテキストとURLを抽出する
s = '[Deep Insider](https://www.atmarkit.co.jp/ait/subtop/di/)は' + \
'[@IT](https://www.atmarkit.co.jp/ "@IT")のフォーラムの一つです。'

p = '\\[(.*?)\\]\\((.*?)\\s*(?:"(.*?)")?\\)'
r = re.findall(p, s)
print(r)
# 出力結果
# [('Deep Insider', 'https://www.atmarkit.co.jp/ait/subtop/di/', ''), ('@IT',
# 'https://www.atmarkit.co.jp/', '@IT')]


特定の文字や文字列に囲まれている部分を抽出する基本

 文字列から特定の文字(クオート文字や何らかの記号類)や文字列(タグなど)に囲まれている部分を抽出するには、「開き文字」+「囲まれている文字」+「閉じ文字」というパターンに注目する。今回は特定の文字に話題を絞って、その考え方や正規表現の書き方を紹介する。

 ここでは、Pythonの正規表現モジュールであるreが提供するre.findall関数を例に取る。なお、re.findall関数の基本的な使い方については「[解決!Python]re.findall関数と正規表現を使って文字列から部分文字列を抽出するには」を参照されたい。

 以下に簡単な例を示す。ここではMarkdown記法を用いて記述されたテキストから強調を意味するアスタリスク「*」で囲まれている部分を抽出したいものとしよう(以下の変数sには同じく強調を意味するアンダースコア「_」も含まれているが、これについては後の例で取り上げる)。

import re

s = 'this is *sample string* for _extracting substring_.'

p = r'\*.*\*'  # アスタリスクに囲まれている任意の文字
r = re.findall(p, s) 
print(r)  # ['*sample string*']


 ここでは「開き文字」と「閉じ文字」はもちろんアスタリスクだ。ただし、正規表現ではアスタリスクは「直前の正規表現の0文字以上の繰り返し」という意味を持つ特殊な文字となっているので、正規表現中にアスタリスクを含めるには「\*」のようにエスケープする必要がある。

 「囲まれている文字」は「0文字以上の任意の文字」と考えられる。この場合、正規表現は「.*」となる。あるいは「0文字以上のアスタリスク以外の文字」とも考えられる。こちらなら正規表現は「[^*]*」となる(大かっこ「[]」で文字種を指定する場合にはアスタリスクをエスケープする必要はない点に注意)。どちらを採用するかは好みといえるだろう。ここでは前者を採用する。もちろん、「囲まれている文字」について何らかの条件を付加するのであれば、そうした正規表現を書く必要がある(が、本稿では基本的には「.*」とする)。

 よって、「開き文字」「囲まれている文字」「閉じ文字」を表す文字列は「r'\*.*\*'」というraw文字列として表現できる。これをパターンとして、re.findall関数を呼び出すと上の例に示した通り、「['*sample string*']」というアスタリスクで囲まれている部分が無事に抽出できた。

 「囲まれている文字」を「0文字以上のアスタリスク以外の文字」と考えた場合の例も以下に示す。上と同様な文字列が抽出できている点に注目しよう。

p = r'\*[^*]*\*'  # アスタリスクに囲まれている任意の文字
r = re.findall(p, s)  # パターンに当てはまるものを全て抽出
print(r)  # ['*sample string*']


開き文字と閉じ文字は抽出したくない場合

 開き文字と閉じ文字は不要というときには、正規表現中でかっこ「()」を使ってグループを指定すればよい。

 以下に例を示す。

p = r'\*(.*)\*'  # アスタリスクに囲まれている任意の文字(アスタリスクを除く)
r = re.findall(p, s)
print(r)  # ['sample string']


 開き文字と閉じ文字である「\*」の内部をかっこで囲んでいることに注意しよう。これにより、「['sample string']」のようにアスタリスク以外の部分が抽出できた。

 後読みアサーションと先読みアサーションと呼ばれる機能を使う方法もあるが本稿では省略する。

貪欲マッチと非貪欲マッチ(最小マッチ)

 ここで、元の文字列が「'this is *sample string* for *extracting substring*'」と強調したい部分をどちらもアスタリスクで囲んであるものとしよう。この場合、先ほどのパターンではうまく文字列を抽出できない。

s = 'this is *sample string* for *extracting substring*'

p = r'\*(.*)\*'  # 貪欲マッチ(上と同じ)
r = re.findall(p, s)
print(r)  # ['sample string* for *extracting substring']


 「.*」という正規表現は「貪欲マッチ」といわれ、文字列でマッチする部分が可能な限り長くなるようになっている。そのため、「string」の直後にある「*」なども「0文字以上の任意の文字」の一つと見なして抽出する。これを回避して、「sample string」と「extracting substring」を抽出するには、「非貪欲マッチ」(または「最小マッチ」)が行われるように指定する。これを行うコードを以下に示す。

p = r'\*(.*?)\*'  # 非貪欲マッチ(最小マッチ)
r = re.findall(p, s)
print(r)  # ['sample string', 'extracting substring']


 非貪欲マッチを実行するには、対象の正規表現に続けてクエスチョンマーク「?」を記述すればよい。これにより、なるべく短い文字列にマッチするようになる。そのため、上のコードでは、「sample string」と「extracting substring」が別個に抽出できている。

 なお、「囲まれている文字」を「0文字以上のアスタリスク以外の文字」とした場合には、非貪欲マッチにしなくても問題ない。これはもちろん、アスタリスクが出てきた時点で「[^*]*」のマッチが終わるからだ(アスタリスクは閉じ文字の「\*」にマッチする)。

p = r'\*([^*]*)\*'
r = re.findall(p, s)
print(r)  # ['sample string', 'extracting substring']


アスタリスクかアンダースコアで囲まれている部分を抽出

 ここで元の文字列を先ほどと同じく「'this is *sample string* for _extracting substring_.'」として、ここからアスタリスクまたはアンダースコアで囲まれている部分を抽出する方法を考える。

 「開き文字」「閉じ文字」はアスタリスク(\*)とアンダースコア(_)と異なるが、「囲まれている文字」はこれまでに見た「(.*?)」が使えるだろう(非貪欲マッチ)。このパターンの記述方法は幾つかある。

 1つはアスタリスクで囲まれた文字というパターン「\*(.*?)\*」と、アンダースコアで囲まれた文字というパターン「_(.*?)_」をバーティカルバー「|」でつなぐ方法だ。これは、バーティカルバーで区切られた正規表現のいずれかにマッチすることを意味する。以下がそのコードだ。

Copyright© Digital Advantage Corp. All Rights Reserved.

編集部からのお知らせ

6月16日にフォーマット統一のため利用規約を変更します

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。