ちくわ君と学ぶ正規表現

森の研究所で、ちくわ君は大量のテキストファイルに囲まれて困った顔をしていた。机の上には赤い木の実が散らばっており、いつも以上に集中を要する作業に取り組んでいることがうかがえた。

「ちくわさん、おはようございます!」はんぺん君が元気よく研究所に飛び込んできた。「今日はなんだか大変そうですね」

「おはよう、はんぺん君」ちくわ君は疲れた表情で振り返った。「実は、大量のログファイルから特定のパターンを抜き出す作業をしているんだけれど、手作業だと時間がかかりすぎてね」

「特定のパターンですか?」はんぺん君は興味深そうに尋ねた。

「そういえば、君にはまだ『正規表現』について教えていなかったね」ちくわ君は木の実を一つ口に含んだ。「これは文字列のパターンマッチングに使う、とても強力なツールなんだ」

正規表現の基本概念

「正規表現は、文字列のパターンを表現するための特別な記号体系なんだ」ちくわ君はホワイトボードに向かった。「例えば、『メールアドレスを見つけたい』『電話番号を抽出したい』『特定の形式の日付を探したい』といった時に使うんだよ」

「パターンですか...」はんぺん君は首をかしげた。

「簡単な例から始めよう」ちくわ君はPythonを起動した。「まずは基本的なマッチングから実際に試してみよう」

import re

# 基本的なパターンマッチング
text = "今日はcat、昨日はcut、明日はcotの話です"
pattern = r'c.t'  # cとtの間に任意の1文字
matches = re.findall(pattern, text)
print("マッチした文字列:", matches)
# 出力: ['cat', 'cut', 'cot']

「おお!」はんぺん君は目を輝かせた。「その『.』が『何でも一文字』という意味なんですね」

基本的なメタ文字

「その通り!正規表現にはいろんな特殊文字があるんだ」ちくわ君は実例を示しながら説明した。

# 量詞の使用例
text = "a ab abb abbb abbbb"

# * : 0回以上の繰り返し
pattern_star = r'ab*'
print("ab*:", re.findall(pattern_star, text))
# 出力: ['a', 'ab', 'abb', 'abbb', 'abbbb']

# + : 1回以上の繰り返し
pattern_plus = r'ab+'
print("ab+:", re.findall(pattern_plus, text))
# 出力: ['ab', 'abb', 'abbb', 'abbbb']

# ? : 0回または1回
pattern_question = r'ab?'
print("ab?:", re.findall(pattern_question, text))
# 出力: ['a', 'ab', 'ab', 'ab', 'ab']

はんぺん君は必死にメモを取った。「『*』は0回以上、『+』は1回以上...覚えました!」

文字クラスと範囲指定

# 文字クラスの例
text = "電話: 03-1234-5678, ZIP: 123-4567, ID: ABC-123"

# 数字のみ
numbers = re.findall(r'\d+', text)
print("数字:", numbers)
# 出力: ['03', '1234', '5678', '123', '4567', '123']

# 英字のみ
letters = re.findall(r'[A-Za-z]+', text)
print("英字:", letters)
# 出力: ['ZIP', 'ID', 'ABC']

# カスタム文字クラス
phone_parts = re.findall(r'[0-9]{2,4}', text)
print("2-4桁の数字:", phone_parts)
# 出力: ['03', '1234', '5678', '123', '4567', '123']

実践:電話番号の抽出

「実際の例で練習してみよう」ちくわ君は新しいファイルを開いた。「日本の電話番号を抽出する正規表現を作ってみよう」

# 電話番号抽出の例
def extract_phone_numbers(text):
    # 日本の電話番号パターン
    patterns = {
        '固定電話': r'0\d{1,4}-\d{1,4}-\d{4}',
        '携帯電話': r'0[789]0-\d{4}-\d{4}',
        'フリーダイアル': r'0120-\d{3}-\d{3}'
    }
    
    results = {}
    for phone_type, pattern in patterns.items():
        matches = re.findall(pattern, text)
        if matches:
            results[phone_type] = matches
    
    return results

# テストデータ
contact_info = """
お問い合わせ先:
本社: 03-1234-5678
携帯: 080-9876-5432
サポート: 0120-123-456
大阪支社: 06-9876-5432
"""

phone_results = extract_phone_numbers(contact_info)
for phone_type, numbers in phone_results.items():
    print(f"{phone_type}: {numbers}")
# 出力: 固定電話: ['03-1234-5678', '06-9876-5432']
#       携帯電話: ['080-9876-5432']
#       フリーダイアル: ['0120-123-456']

メールアドレスの検証

「今度はメールアドレスを扱ってみよう」ちくわ君は新しい例題を出した。

# メールアドレスの抽出と検証
def validate_email(email):
    # 基本的なメールアドレスパターン
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

def extract_emails(text):
    # テキストからメールアドレスを抽出
    pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
    return re.findall(pattern, text)

# テスト
email_text = """
連絡先一覧:
営業部: sales@company.com
技術部: tech@company.co.jp
無効なアドレス: invalid@
サポート: support@example.org
"""

emails = extract_emails(email_text)
print("抽出されたメール:")
for email in emails:
    status = "有効" if validate_email(email) else "無効"
    print(f"  {email} - {status}")
# 出力: sales@company.com - 有効
#       tech@company.co.jp - 有効
#       support@example.org - 有効

グループ化とデータ抽出

「正規表現の強力な機能の一つが『グループ化』なんだ」ちくわ君は新しい概念を紹介した。

# グループ化による詳細抽出
def parse_log_entry(log_line):
    # ログエントリのパターン(グループ化使用)
    pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) \[(\w+)\] (.*)'
    match = re.match(pattern, log_line)
    
    if match:
        date, time, level, message = match.groups()
        return {
            'date': date,
            'time': time,
            'level': level,
            'message': message
        }
    return None

# ログデータの解析
log_entries = [
    "2024-06-17 10:30:15 [INFO] ユーザーログイン成功",
    "2024-06-17 10:35:22 [ERROR] データベース接続失敗",
    "2024-06-17 10:40:33 [WARN] メモリ使用量が80%を超過"
]

print("ログ解析結果:")
for entry in log_entries:
    parsed = parse_log_entry(entry)
    if parsed:
        print(f"日時: {parsed['date']} {parsed['time']}")
        print(f"レベル: {parsed['level']}")
        print(f"メッセージ: {parsed['message']}")
        print("-" * 30)

名前付きグループの活用

# 名前付きグループでより読みやすく
def parse_product_info(text):
    pattern = r'商品名:(?P[^、]+)、価格:(?P\d+)円、在庫:(?P\d+)個'
    matches = re.finditer(pattern, text)
    
    products = []
    for match in matches:
        product = {
            'name': match.group('name'),
            'price': int(match.group('price')),
            'stock': int(match.group('stock'))
        }
        products.append(product)
    
    return products

# 商品情報の解析
product_text = """
商品名:ノートPC、価格:89800円、在庫:15個
商品名:マウス、価格:2500円、在庫:50個
商品名:キーボード、価格:5800円、在庫:25個
"""

products = parse_product_info(product_text)
for product in products:
    print(f"{product['name']}: {product['price']:,}円 (在庫{product['stock']}個)")
# 出力: ノートPC: 89,800円 (在庫15個)
#       マウス: 2,500円 (在庫50個)
#       キーボード: 5,800円 (在庫25個)

文字列の置換と整形

# re.sub()を使った文字列置換
def format_phone_numbers(text):
    # (XXX) XXX-XXXX 形式を XXX-XXX-XXXX に変換
    pattern = r'\((\d{3})\)\s*(\d{3})-(\d{4})'
    replacement = r'\1-\2-\3'
    return re.sub(pattern, replacement, text)

def mask_sensitive_data(text):
    # クレジットカード番号をマスク
    pattern = r'\b(\d{4})-(\d{4})-(\d{4})-(\d{4})\b'
    replacement = r'\1-****-****-\4'
    return re.sub(pattern, replacement, text)

# テスト
original = "連絡先: (090) 123-4567, カード: 1234-5678-9012-3456"
formatted = format_phone_numbers(original)
masked = mask_sensitive_data(formatted)

print("元データ:", original)
print("整形後:", formatted)
print("マスク後:", masked)
# 出力: 元データ: 連絡先: (090) 123-4567, カード: 1234-5678-9012-3456
#       整形後: 連絡先: 090-123-4567, カード: 1234-5678-9012-3456
#       マスク後: 連絡先: 090-123-4567, カード: 1234-****-****-3456

先読み・後読みアサーション

「上級者向けのテクニックも紹介しておこう」ちくわ君は発展的な内容に移った。

# 先読みアサーションの実用例
def extract_secure_passwords(passwords):
    # 強いパスワードの条件:8文字以上、大文字・小文字・数字・記号を含む
    strong_pattern = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$'
    
    strong_passwords = []
    for password in passwords:
        if re.match(strong_pattern, password):
            strong_passwords.append(password)
    
    return strong_passwords

# パスワード強度チェック
test_passwords = [
    "password",      # 弱い
    "Password123",   # まあまあ
    "P@ssw0rd123",   # 強い
    "Str0ng!Pass",   # 強い
    "weak"           # 弱い
]

strong_passwords = extract_secure_passwords(test_passwords)
print("強いパスワード:")
for pwd in strong_passwords:
    print(f"  {pwd}")
# 出力: P@ssw0rd123, Str0ng!Pass

実習:総合的なテキスト分析

「最後に、今まで学んだことを使って総合的な分析をしてみよう」ちくわ君は総合演習を提案した。

# 総合的なテキスト分析システム
class TextAnalyzer:
    def __init__(self):
        self.patterns = {
            'emails': r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
            'phones': r'0\d{1,4}-\d{1,4}-\d{4}',
            'urls': r'https?://[^\s]+',
            'dates': r'\d{4}-\d{2}-\d{2}',
            'prices': r'¥[\d,]+|[\d,]+円'
        }
    
    def analyze(self, text):
        results = {}
        for category, pattern in self.patterns.items():
            matches = re.findall(pattern, text)
            results[category] = matches
        
        # 統計情報
        results['stats'] = {
            'total_chars': len(text),
            'total_words': len(re.findall(r'\S+', text)),
            'total_lines': len(text.split('\n'))
        }
        
        return results

# 分析実行
sample_text = """
お問い合わせ先:
メール: contact@company.com
電話: 03-1234-5678
ウェブサイト: https://www.company.com

商品情報:
発売日: 2024-06-17
価格: ¥89,800
詳細: https://www.company.com/product
"""

analyzer = TextAnalyzer()
analysis = analyzer.analyze(sample_text)

print("=== テキスト分析結果 ===")
for category, items in analysis.items():
    if category != 'stats':
        print(f"{category}: {items}")

print(f"\n=== 統計情報 ===")
stats = analysis['stats']
print(f"文字数: {stats['total_chars']}")
print(f"単語数: {stats['total_words']}")
print(f"行数: {stats['total_lines']}")

# 出力例:
# emails: ['contact@company.com']
# phones: ['03-1234-5678']
# urls: ['https://www.company.com', 'https://www.company.com/product']
# dates: ['2024-06-17']
# prices: ['¥89,800']

パフォーマンスと注意点

「正規表現を使う時の注意点もあるんだ」ちくわ君は木の実を噛みながら説明した。

# パフォーマンスの考慮
import time

def performance_test():
    text = "a" * 10000 + "b"
    
    # 効率的でないパターン(時間がかかる)
    bad_pattern = r'a*a*b'
    
    # 効率的なパターン
    good_pattern = r'a+b'
    
    # 計測
    start = time.time()
    re.search(good_pattern, text)
    good_time = time.time() - start
    
    print(f"効率的なパターン: {good_time:.6f}秒")
    
    # コンパイル済み正規表現の使用
    compiled_pattern = re.compile(good_pattern)
    
    start = time.time()
    for _ in range(1000):
        compiled_pattern.search(text)
    compiled_time = time.time() - start
    
    print(f"コンパイル済み(1000回): {compiled_time:.6f}秒")

performance_test()

まとめ

夕方になった頃、はんぺん君は満足そうに立ち上がった。「今日は正規表現について本当にたくさん学べました!文字列の処理がこんなに柔軟にできるなんて驚きです」

「正規表現は最初は難しく感じるかもしれないけれど、一度覚えてしまえば、テキスト処理の強力な武器になるよ」ちくわ君は微笑んだ。「プログラミングだけでなく、日常的なテキスト編集でも役立つからね」

「でも、すべてを正規表現で解決しようとしないことも大切だ。複雑すぎるパターンなら、プログラムで段階的に処理した方が読みやすいこともあるからね」

「家に帰ったら、早速Pythonで正規表現を試してみます!」はんぺん君は意気込んだ。

「それは素晴らしい!最初は簡単なパターンから始めて、徐々に複雑なものに挑戦していこう。正規表現は実践で覚えるのが一番だからね」

はんぺん君が帰った後、ちくわ君は元のログファイル解析に戻った。正規表現を使って、あっという間に必要な情報を抽出できた。手作業では何時間もかかる作業が、数分で完了したのだ。

森の研究所には、今日も効率的なプログラミングの知恵が満ちていた。

教育 正規表現 Python テキスト処理