論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
正規表現
Regular Expression
データ前処理

🔖 キーワード索引

#パターンマッチ#前処理#文字列処理#メタ文字#re module#grep

regex」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「regex」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

regex統計分析SSDSE-B-2026前提条件適用範囲落とし穴関連手法Python 実装検証方法

これらのキーワードは「regex の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

文字のパターンを決める道具です。

複雑な文字を簡単に抜き出すために使います。

スマホの連絡先から番号だけを探すような機能です。

ここでは基本の記号や使い方を学びます。

正規表現:文字列パターンマッチング

📍 文脈ボックス

🍰 まずはやさしく

データを整えるための準備道具です。

分析しやすい形に文字を直すために使います。

部活の出席簿から名前だけをまとめる時に便利です。

このページでは定義や注意点を順番に解説します。

この用語は データ前処理 カテゴリに属します。 関連する別称・略号:(なし)

論文・実務レポートで 正規表現 が登場したら、 まず本ページの「30秒で分かる結論」と「直感で掴む」を読めば、 その文脈で何を言っているか把握できます。

本ページでは「regex」を扱う。 統計データ分析コンペティション (2026) の教材で、 SSDSE-B-2026 (47 都道府県 × 複数年 × 100 超列) の実データを使った再現可能な学習を目指す。

「regex」は統計・データサイエンスの体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。

🎨 直感で掴む

🍰 まずはやさしく

文字を抜き出すための鋳型(型)のようなものです。

大量の文字から必要な部分だけを拾うために使います。

長い文章から数字だけを抜き出す時に役立ちます。

具体的にどうやって文字を拾うのかを考えます。

ログから「IP アドレス」だけ取り出したい。 「ABC123def456」から数字だけ拾いたい。 「電話番号らしき文字列」を見つけたい ── これを 1 行で書けるのが 正規表現。 例:r'\d{3}-\d{4}-\d{4}' で電話番号、 r'\d+' で数字列。 データ前処理の生産性を 10 倍に上げる魔法のツール。

イメージは「文字の鋳型」。 鋳型に流し込んで一致する部分だけ拾い上げる。 SSDSE-B-2026 の市区町村コード列 (例: R01100) から都道府県コード 2 桁だけ取り出すには r'R(\d{2})'、 年度 2026 を取り出すには r'(\d{4})年?' と、 抜き出したい場所を (...) で囲むだけ。 文字列を 1 文字ずつ if 文で判定するコードを書く時代は、 正規表現を一度書ければほぼ過去のものになる。

ただし「魔法」と呼べるのはパターンが固定的な場面に限られる。 「東京都新宿区西新宿2-8-1」のような表記揺れがある住所では、 正規表現単独では破綻し、 形態素解析や辞書照合と組み合わせることになる。 つまり正規表現は「鋳型」が描ける範囲では最強だが、 描けない範囲では NLP に道を譲る、 という棲み分けが直感的な理解の核になる。

📐 定義・数式

🍰 まずはやさしく

文字のルールを決めるための特別な書き方です。

正確にパターンを指定するために使います。

買い物リストから特定の品物だけを分けるイメージです。

ルールを作るための記号の意味について学びます。

【代表的なメタ文字】
$$ \text{regex} \,::=\, \text{literal} \,|\, . \,|\, \backslash d \,|\, [\dots] \,|\, *\,|\,+\,|\,?\,|\,(\dots) $$

正規表現は、 文字リテラル・任意1文字 (.)・数字 (\d)・文字クラス ([abc])・量化子 (*, +, ?)・グループ ((...)) を組み合わせて構築する。

📐 形式言語理論との対応

「正規表現 (regular expression)」 という名前は 形式言語のクラス である「正規言語 (regular language)」 に由来します。 Chomsky の言語階層では:

タイプ 言語クラス 対応オートマトン
Type 3正規言語有限オートマトン正規表現で表せる
Type 2文脈自由言語プッシュダウンオートマトンa^n b^n、 プログラミング言語
Type 1文脈依存言語線形拘束オートマトンa^n b^n c^n
Type 0帰納的可算言語チューリングマシン計算可能なすべて

この階層が 正規表現の能力の限界 を示します。 たとえば「対応する括弧」 は正規言語ではなく文脈自由言語なので、 純粋な正規表現では正しくパースできない。 HTML/XML を正規表現でパースしようとして失敗するのはこのため。

ただし、 PCRE 等の拡張正規表現は後方参照を持つため、 厳密には正規言語の枠を超えています(理論的には文脈依存言語の一部まで認識可能)。

🤖 LLM 時代の正規表現

ChatGPT などの LLM に「メールアドレスを正規表現で抽出して」 と頼めば、 ほぼ正しい正規表現を生成してくれます。 これは正規表現を 不要にした のではなく、 むしろ 使いこなせる人の生産性を上げた

LLM が出した正規表現を信頼してよいか?

SSDSE データを LLM + 正規表現で処理する流れ

  1. SSDSE のサンプルデータを LLM に貼り付け、 「これを正規表現で前処理したい」 と相談。
  2. LLM が出した正規表現を regex101.com で動作確認。
  3. Python re で実装し、 pytest でテスト。
  4. 本番 SSDSE-B-2026 全データで実行。
  5. エッジケースで失敗するなら、 失敗例を LLM に渡して修正版を生成。

⚡ 性能チューニング 5 原則

  1. compile して再利用:同じパターンを何度も使うなら re.compile()。 SSDSE 47 件で 2-3 倍速。
  2. アンカーで早く失敗させる^$ でマッチしないパターンを早期に除外。
  3. 具体的な文字クラス. より \d[a-z] の方が高速。
  4. greedy 回避.* より具体的なパターン(例:[^>]*)。
  5. 非キャプチャグループ:参照不要なら (?:...)。 キャプチャはオーバーヘッド。

SSDSE 100 列 × 47 行を処理しても、 正しく書けばミリ秒オーダーで完了します。 数秒以上かかるなら正規表現の設計ミスを疑う。

📓 SSDSE-B-2026 完全前処理ワークフロー(正規表現フル活用)

本ページの全テクニックを動員して SSDSE-B-2026 を 「分析できる形」 まで整形する完成度の高いコード:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import os
os.makedirs('data/processed', exist_ok=True)  # 保存先のフォルダを作っておく

import pandas as pd
import re
from pathlib import Path

# ===== STEP 1: 読み込み =====
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
print(f'読み込み完了: {df.shape}')

# ===== STEP 2: 列名メタデータ抽出 =====
COL_PAT = re.compile(r'^(?P<code>[A-Z]\d{4})_(?P<name>.+?)(?:((?P<unit>.+?)))?$')

meta = []
for col in df.columns:
    m = COL_PAT.match(col)
    if m:
        meta.append({
            'original': col,
            'code': m['code'],
            'name': m['name'],
            'unit': m['unit'] or '無単位',
            'category': m['code'][0],
        })
    else:
        meta.append({'original': col, 'code': None, 'name': col,
                     'unit': '無単位', 'category': None})

df_meta = pd.DataFrame(meta)
print(f'\nメタデータ抽出: {len(df_meta)} 列')
print(df_meta.groupby('category').size())

# ===== STEP 3: カテゴリ別の列リスト =====
SSDSE_CATEGORIES = {
    'A': '人口', 'B': '自然環境', 'C': '経済基盤',
    'D': '行政基盤', 'E': '教育', 'F': '労働',
    'G': '文化・スポーツ', 'H': '住居', 'I': '健康・医療',
    'J': '福祉・社会保障', 'K': '安全', 'L': '家計',
}
df_meta['category_name'] = df_meta['category'].map(SSDSE_CATEGORIES)

# ===== STEP 4: 都道府県名の正規化 =====
PREF_PAT = re.compile(r'^(?P<short>.+?)(?P<suffix>[都道府県])$')
if '都道府県' in df.columns:
    df['都道府県_short'] = df['都道府県'].str.extract(PREF_PAT)['short']
    df['都道府県_suffix'] = df['都道府県'].str.extract(PREF_PAT)['suffix']

# ===== STEP 5: 全角数値→半角 =====
def z2h(s):
    if not isinstance(s, str):
        return s
    return re.sub(r'[0-9]', lambda m: chr(ord(m.group()) - 0xFEE0), s)

for col in df.select_dtypes(include='object').columns:
    df[col] = df[col].apply(z2h)

# ===== STEP 6: 数値列の型変換(カンマ除去)=====
NUM_PAT = re.compile(r'^-?\d{1,3}(?:,\d{3})*(?:\.\d+)?$')
def parse_num(x):
    if isinstance(x, str) and NUM_PAT.match(x):
        return float(x.replace(',', ''))
    return x

for col in df.select_dtypes(include='object').columns:
    df[col] = df[col].apply(parse_num)

# ===== STEP 7: 単位統一(金額系を「円」 へ)=====
UNIT_MAP = {'千円': 1000, '百万円': 10**6, '億円': 10**8, '兆円': 10**12}
for col in df_meta.iterrows():
    _, m = col
    if m['unit'] in UNIT_MAP:
        original_col = m['original']
        if original_col in df.columns and pd.api.types.is_numeric_dtype(df[original_col]):
            df[original_col] = df[original_col] * UNIT_MAP[m['unit']]

# ===== STEP 8: サマリ =====
print(f'\n=== 前処理完了 ===')
print(f'  サンプル数: {len(df)}')
print(f'  数値列数: {len(df.select_dtypes(include=[\"number\"]).columns)}')
print(f'  欠損総数: {df.isna().sum().sum()}')

# ===== STEP 9: メタデータ保存 =====
df_meta.to_csv('data/processed/SSDSE-B-2026_meta.csv', index=False)
df.to_csv('data/processed/SSDSE-B-2026_clean.csv', index=False)
print('保存完了')

このコードは 1 ファイル / 100 行 で SSDSE 前処理を完結させます。 正規表現が 5 箇所で活躍:列名分解、 都道府県名分離、 全角数字変換、 カンマ区切り数値、 単位検出。 これらを抜くと、 同じ処理が 200 行以上 の手続き的コードになります。

🧬 正規表現とパーサーコンビネータの融合

複雑な文法(ネスト構造、 文脈依存) を扱うとき、 正規表現だけでは限界があります。 そこで パーサーコンビネータ(pyparsing, parsy, lark) と組み合わせる手法が有効:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import re
# トークナイザ(正規表現で)
TOKEN_PAT = re.compile(r'''
    (?P<NUMBER>\d+(?:\.\d+)?)|
    (?P<ID>[A-Za-z_]\w*)|
    (?P<OP>[+\-*/])|
    (?P<LPAREN>\()|
    (?P<RPAREN>\))|
    (?P<WS>\s+)
''', re.VERBOSE)

def tokenize(s):
    for m in TOKEN_PAT.finditer(s):
        kind = m.lastgroup
        if kind != 'WS':
            yield (kind, m.group(kind))

# 使用例
list(tokenize('x = 3.14 * (y + 2)'))

🛡 セキュリティと正規表現

入力バリデーション

Web フォーム入力(メール、 電話、 URL) の検証に正規表現は定番。 ただし:

ReDoS 対策

ユーザー入力を正規表現の パターン として受け取る場合は、 re.escape() で逃がす。 パターンに変数を埋め込む場合も同様。

PII 検出と除去

SSDSE は公的統計なので PII(個人情報) を含みませんが、 一般のログ処理では正規表現で PII を検出・マスクすることが多い:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import re

text = ('問い合わせは support@example.co.jp まで。'
        '電話は 082-1234-5678、カードは 4111-1111-1111-1111 です。')

# メールマスク
text = re.sub(r'[\w.+-]+@[\w.-]+\.\w+', '[EMAIL]', text)
# 電話マスク
text = re.sub(r'0\d{1,4}-\d{1,4}-\d{3,4}', '[PHONE]', text)
# クレジットカードマスク
text = re.sub(r'\d{4}-\d{4}-\d{4}-\d{4}', '[CARD]', text)
print(text)
📤 実行例(実測) 問い合わせは [EMAIL] まで。電話は [PHONE]、カードは [CARD] です。

📘 ケーススタディ:47 都道府県データの統一

SSDSE-B-2026 と別の e-Stat データを結合する際、 「都道府県」 列の表記揺れが問題に:

SSDSE 表記 e-Stat 表記 RESAS 表記 統一
北海道北海道Hokkaido北海道
東京都東京都Tokyo東京都
大阪府大阪府Osaka大阪府
沖縄県沖縄県Okinawa沖縄県

英語名を日本語に揃える辞書を作り、 残りは正規表現で「最後の都道府県」 が一致するかチェック:

1
2
3
4
5
6
7
EN_TO_JP = {'Hokkaido': '北海道', 'Tokyo': '東京都',
            'Osaka': '大阪府', 'Okinawa': '沖縄県', ...}
def normalize(name):
    if name in EN_TO_JP:
        return EN_TO_JP[name]
    m = re.match(r'^(.+?[都道府県])', name)
    return m.group(1) if m else name

📗 ケーススタディ:時系列ログから日時抽出

日本のデータ分析で頻繁に遭遇する 表記揺れ

これら全てに対応する正規表現は複数パターンを | で繋ぐ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
PATTERNS = [
    (r'(\d{4})-(\d{1,2})-(\d{1,2})', 'iso'),
    (r'(\d{4})/(\d{1,2})/(\d{1,2})', 'slash'),
    (r'(\d{4})年(\d{1,2})月(\d{1,2})日', 'jp'),
    (r'令和(\d+)年(\d{1,2})月(\d{1,2})日', 'reiwa'),
    (r'R(\d+)\.(\d{1,2})\.(\d{1,2})', 'reiwa_short'),
    (r'(\d{4})(\d{2})(\d{2})', 'compact'),
]
def parse_date(s):
    for pat, kind in PATTERNS:
        m = re.search(pat, s)
        if m:
            y, mo, d = map(int, m.groups())
            if kind in ('reiwa', 'reiwa_short'):
                y += 2018  # 令和1年 = 2019
            return f'{y:04d}-{mo:02d}-{d:02d}'
    return None

📖 正規表現 用語集ミニ

パターン (Pattern)
マッチさせたい形状を表す正規表現の本体。
マッチ (Match)
パターンに合致する部分文字列が見つかった状態。
グループ (Group)
(...) で括った部分。 マッチ結果から抽出可能。
キャプチャ (Capture)
グループにマッチした文字列を保持すること。
アンカー (Anchor)
位置を指定する記号(^, $, \b)。 文字には対応しない。
量化子 (Quantifier)
繰り返し回数を指定(*, +, ?, {n,m})。
貪欲 (Greedy)
できる限り長くマッチしようとする(デフォルト)。
最小 (Non-greedy / Lazy / Reluctant)
できる限り短くマッチ(*?, +?)。
バックトラッキング (Backtracking)
マッチ失敗時に試行を戻す動作。 ReDoS の原因にも。
先読み (Lookahead) / 後読み (Lookbehind)
マッチには含めず文脈条件を指定する zero-width 表現。
エスケープ (Escape)
メタ文字を文字通りに扱う指定(\., \+ 等)。
NFA / DFA
非決定性/決定性有限オートマトン。 エンジンの実装方式。
PCRE
Perl Compatible Regular Expressions。 多くの言語の標準。
ReDoS
Regular expression Denial of Service。 悪意ある入力で指数時間動作させる攻撃。
raw string
Python の r"..." 記法。 バックスラッシュをエスケープせず使える。

📋 正規表現 クイックリファレンス(印刷用)

記号 意味 記号 意味
.任意 1 文字^行頭
*0 回以上$行末
+1 回以上\b単語境界
?0 か 1 回\B非単語境界
{n}ちょうど n 回\d数字
{n,}n 回以上\D非数字
{n,m}n〜m 回\w単語文字
*?最小 0+\W非単語文字
+?最小 1+\s空白
[abc]a,b,c のいずれか\S非空白
[^abc]a,b,c 以外\n改行
[a-z]範囲\tタブ
(...)キャプチャ\1後方参照 1
(?:...)非キャプチャ(?=...)肯定先読み
(?P<n>...)名前付き(?!...)否定先読み
|選択(OR)(?<=...)肯定後読み

📝 練習問題 10 問(SSDSE 中心)

  1. Q1: SSDSE 列名 A1101_総人口 から「A11」 だけ抽出する正規表現は? → r'^([A-Z]\d{2})'
  2. Q2: 「東京都」「大阪府」「北海道」「広島県」 から末尾 1 文字を除いた都道府県名を取得する正規表現は? → r'^(.+?)[都道府県]$'
  3. Q3: 「2026年5月19日」 から年月日を取り出す正規表現は? → r'(\d{4})年(\d{1,2})月(\d{1,2})日'
  4. Q4: 「1,000,000円」 のような3桁区切り数値にマッチする正規表現は? → r'\d{1,3}(?:,\d{3})*円'
  5. Q5: メールアドレスの簡易検証正規表現は? → r'^[\w.+-]+@[\w.-]+\.\w+$'
  6. Q6: 半角・全角混在の数字を全部半角に変換する正規表現とコードは? → r'[0-9]' + chr(ord(m) - 0xFEE0)
  7. Q7: HTML から <p>...</p> の中身を最小マッチで取り出す正規表現は? → r'<p>(.+?)</p>'
  8. Q8: URL の中の「https://」 を除いてドメイン部分だけ取る正規表現は? → r'https?://([^/]+)'
  9. Q9: IP アドレス 0-255 の範囲を厳密にマッチする正規表現は? → r'\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\b'
  10. Q10: SSDSE 列名から「金額系の列(単位が円・千円・百万円) だけ」 抽出する正規表現は? → r'(?:円|千円|百万円|億円))?$'

これらを regex101.com で実際に試して、 自分の手で動作確認してください。 正規表現は「動かして覚える」 が最速の習得法です。

🌏 日本語処理特有の正規表現

対象 Unicode 範囲 正規表現
ひらがなU+3041〜U+3096[ぁ-ゖ]
カタカナU+30A1〜U+30FA[ァ-ヺ]
半角カナU+FF61〜U+FF9F[。-゚]
漢字(基本)U+4E00〜U+9FFF[一-龯]
漢字(拡張A)U+3400〜U+4DBF[㐀-䶿]
全角英数字U+FF01〜U+FF5E[!-~]
全角数字U+FF10〜U+FF19[0-9]
日本語句読点、 。 !?[、。!?]

SSDSE の日本語列名は基本 BMP(基本多言語面) の範囲。 ただし旧字体や絵文字を含むデータ(X / 旧Twitter の投稿等) では [\U00010000-\U0010FFFF] も考慮。

🐼 Pandas での正規表現徹底活用

SSDSE のような表データを扱う Pandas には、 正規表現が組み込まれた str アクセサが充実しています。

メソッド 機能 SSDSE 用例
str.match(pat)先頭からのマッチを真偽値でSSDSE コード列の判定
str.contains(pat)部分マッチを真偽値で「人口」 を含む列を抽出
str.extract(pat)グループを DataFrame で取り出す列名からコードと名称を分離
str.extractall(pat)全マッチを抽出説明文中の数値全部
str.replace(pat, repl)正規表現置換単位の統一
str.findall(pat)全マッチをリストで列名内の数字全部
str.split(pat, regex=True)正規表現で分割複数区切り対応
df.filter(regex=pat)列名で正規表現フィルタ「人口」 列のみ抽出
📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# ── この抜粋で使うデータを用意します ──
# 列名は「A1101_総人口」のように コード_名称 で持たせる
import pandas as pd

_raw = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=[0, 1])
df = _raw.copy()
df.columns = ['_'.join(str(c).strip() for c in col) for col in _raw.columns]

# SSDSE-B-2026 で 「人口」 を含む列だけ抽出
pop_cols = df.filter(regex=r'人口').columns
print(f'人口関連列: {len(pop_cols)} 件')

# 列名から SSDSE コードと名称を一気に分離
codes_names = df.columns.to_series().str.extract(r'^([A-Z]\d{4})_(.+)$')
codes_names.columns = ['code', 'name']
print(codes_names.head())
📤 実行例(実測) 人口関連列: 15 件 code name SSDSE-B-2026_年度 NaN NaN Code_地域コード NaN NaN Prefecture_都道府県 NaN NaN A1101_総人口 A1101 総人口 A110101_総人口(男) NaN NaN

🚨 補足の落とし穴 5 件

🏅 正規表現 ベストプラクティス(実務 10 ヶ条)

  1. 必ず raw string (r"...") を使う
  2. compile して再利用 し、 グローバル定数に
  3. 名前付きグループ でコード可読性を確保
  4. verbose mode でコメント付きパターンに
  5. 正例と負例のテスト を必ず書く
  6. regex101 リンク をコメントに残す
  7. greedy/non-greedy を明示的に指定
  8. アンカー でパターンを固定
  9. ReDoS リスク を事前にチェック
  10. 専用ライブラリで足りるなら 正規表現を使わない

🔮 正規表現の未来 — LLM 時代に消えるか

LLM がパターン抽出を高精度でこなせる時代、 正規表現は不要になるのでしょうか? 答えは No。 むしろ 使いこなせる人の価値が上がっています。 LLM はメガバイト単位のテキストを処理するのに高コスト、 正規表現は数ミリ秒。 SSDSE-B-2026 のような 明確な構造を持つデータ の前処理では正規表現が圧勝。

逆に「あいまいな自然言語からの意味抽出」 は LLM の独壇場。 両者を 適材適所 で組み合わせるのが現代のデータエンジニアリングです。

📊 SSDSE-B-2026 で正規表現を徹底活用する — 拡張ワークフロー

ここでは SSDSE-B-2026(都道府県別 社会・人口統計体系) の生 CSV をダウンロードしてから可視化するまでの一気通貫を、 正規表現の観点で再構成します。 公式 CSV は 2 行ヘッダ・全角空白混在・年度カラムの繰り返し など、 そのままでは pandas や Excel に取り込みにくい特徴があります。 これらを 正規表現pandas可視化 で順番に処理する流れを示します。

🎯 ワークフロー全体像

流れは (1) ヘッダ正規化 → (2) 列名クレンジング → (3) 数値抽出 → (4) 異常値検知 → (5) 可視化 の 5 段階。 SSDSE-B-2026 の都道府県コードは「R01100」「R02100」…のように R + 2 桁ゼロパディング + 3 桁ゼロパディングで構成され、 これ自体が正規表現 ^R\d{5}$ で検証できます。 データクレンジング の入口として最適な題材です。

段階目的代表的な正規表現SSDSE-B-2026 での例
列名から年度を抽出(?P<year>\d{4})「総人口2020」 → year=2020
余分な全角空白の除去[ \s]+「 北海道 」 → 「北海道」
3 桁ごとカンマ削除,(?=\d)「1,234,567」 → 「1234567」
欠損記号の統一^(-|―|…|NA)$「―」 → NaN
都道府県コード検証^R\d{5}$R01100, R47000 等のみ通す
単位文字の分離(\d+\.?\d*)(人|円|%)「125000人」 → 125000, 人

📥 入力データ (SSDSE-B-2026 抜粋)

このコードでやること: SSDSE-B-2026 の生 CSV を読み込み、 正規表現 で列名のゆらぎ(全角空白・年度括弧・単位末尾)を統一する。 統一後に 記述統計 と可視化を行えるよう、 整然データ(tidy data)形式に変形する。

1 行目(英字コード): SSDSE-B-2026, Code, Prefecture, A1101, A110101, A110102, ... 2 行目(日本語名) : 年度, 地域コード, 都道府県, 総人口, 総人口(男), 総人口(女), ... --- ここから下がデータ本体(47 県 × 12 年 = 564 行)--- 2023, R01000, 北海道, 5092000, 2405000, 2688000, ... 2023, R13000, 東京都, 14086000, 6914000, 7172000, ... 2023, R27000, 大阪府, 8763000, 4191000, 4572000, ... 2023, R47000, 沖縄県, 1468000, 723000, 745000, ...

ここで正規表現の出番になる箇所は 3 つある。 (1) ヘッダが 2 段なので、 英字コードと日本語名を繋ぐと A110101_総人口(男) のような列名になる。 (2) その列名から コード・項目名・内訳を切り出すのに名前付きグループ (?P<code>...) が要る。 (3) 地域コードが正しい書式か^R\d{5}$ で検証する。 なお SSDSE-B は「年度」列を持つ縦持ち(long 形式)で、 年度が列名に入る横持ちではない点に注意。 年度が列名に入るのは市区町村版の SSDSE-A のほうである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pandas as pd
import re

# SSDSE-B-2026 は 1 行目が英字コード (A1101 等)、 2 行目が日本語名の 2 段ヘッダ
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', header=[0, 1], encoding='cp932')
# 2 段ヘッダを 1 行に統合し、 全角空白を含む余分な空白を除去
df.columns = ['_'.join(re.sub(r'[ \s]+', '', str(c)) for c in col).strip('_') for col in df.columns]
print('統合直後:', df.columns[:5].tolist())

# 「コード_日本語名(内訳)」を名前付きグループで 3 つに切り分ける
COL = re.compile(r'^(?P<code>[^_]+)_(?P<label>[^(]+)(?:((?P<sub>[^)]+)))?$')
def normalize(name):
    m = COL.match(name)
    if not m:
        return name
    g = m.groupdict()
    return g['label'] + ('_' + g['sub'] if g['sub'] else '')

df.columns = [normalize(c) for c in df.columns]
print('正規化後:', df.columns[:5].tolist())
print('重複した列名:', df.columns[df.columns.duplicated()].tolist()[:5])

# 都道府県コードを ^R\d{5}$ で検証する。 12 年分あるのでまず県単位に畳む
codes = df['地域コード'].dropna().unique()
ok = pd.Series(codes).str.match(r'^R\d{5}$')
print(f'ユニークな地域コード: {len(codes)} 件')
print(f'^R\\d{{5}}$ に適合: {ok.sum()} / {len(codes)} (期待値 47)')
print('例:', list(codes[:3]))

📤 実行例

統合直後: ['SSDSE-B-2026_年度', 'Code_地域コード', 'Prefecture_都道府県', 'A1101_総人口', 'A110101_総人口(男)'] 正規化後: ['年度', '地域コード', '都道府県', '総人口', '総人口_男'] 重複した列名: [] ユニークな地域コード: 47 件 ^R\d{5}$ に適合: 47 / 47 (期待値 47) 例: ['R01000', 'R02000', 'R03000']

💬 結果の読み方: 2 段ヘッダを繋いだ直後は A110101_総人口(男) のようにコードと日本語名が同居していますが、 名前付きグループ (?P<code>[^_]+)_(?P<label>[^(]+)(?:((?P<sub>[^)]+)))? で 3 つに割ると 総人口_男 という短い名前に統一できます。 重複した列名が 0 件なので、 この正規化で列が潰れていないことも確認できました。 地域コードは 564 行を県単位に畳むと 47 件になり、 その全部が ^R\d{5}$ にマッチします(R01000 のように県レベルは下 3 桁が 000)。 ここでポイントなのは、 564 行のまま数えると「有効コード数 564」となって検証が素通りしてしまうこと。 縦持ちデータでは「何を 1 件と数えるか」を先に決めてから正規表現で検証する必要があります。 ここまで済めば、 あとは pandas の通常操作で十分扱えます。

📈 図1: 都道府県別 総人口の分布(散布図)

クレンジング済みのデータで 総人口 (2020) と総人口 (2023) の関係を散布図にしました。 完全に直線上に並ぶことから、 4 年間の人口変化は 規模に比例する小さなドリフト であり、 順序関係は保存されていることが分かります。 相関係数 0.999、 回帰 の傾きは 0.992。

SSDSE-B-2026 都道府県別 総人口 散布図

📊 図2: 人口対数値のヒストグラム

人口は対数正規分布に近いため、 log10 を取ってからヒストグラムを描くと 正規分布に近い形 が現れます。 平均はおよそ log10(2.7×10^6) ≈ 6.43。 東京・神奈川・大阪が右肩に位置し、 中位は 中央値 約 155 万人。

SSDSE-B-2026 都道府県別 総人口 ヒストグラム

📦 図3: 地域ブロック別 箱ひげ図

都道府県コードの上 2 桁から地域ブロックを推定し、 箱ひげ図 でばらつきを比較します。 関東 (R08-R14) の中央値が突出して高く、 北海道 (R01) と沖縄 (R47) は単独で外れ値的な振る舞いをします。 ブロック境界の決定にも正規表現 ^R(?P<pref>\d{2})\d{3}$ を使い、 グループ化に利用しました。

SSDSE-B-2026 地域ブロック別 総人口 箱ひげ図

🧪 検証コード — 正規表現の単体テスト

このコードでやること: SSDSE-B-2026 で用いた 5 種類の正規表現を unittest 風に検証する。 テスト駆動開発 の考え方を正規表現に持ち込むと、 後からの修正で挙動が壊れていないかを継続的に確認できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import re

cases = [
    (r'^R\d{5}$',         [('R01100', True), ('R47000', True), ('R7000', False), ('r01100', False)]),
    (r',(?=\d)',           [('1,234,567', True), ('abc,def', False)]),
    (r'[ \s]+',       [(' 北海道 ', True), (' 沖縄', True), ('東京', False)]),
    (r'(\d+\.?\d*)(人|円|%)', [('125000人', True), ('12.5%', True), ('人125000', False)]),
    (r'^(-|―|…|NA)$',     [('-', True), ('―', True), ('0', False)]),
]
for pattern, samples in cases:
    for text, expected in samples:
        got = bool(re.search(pattern, text))
        ok = 'OK' if got == expected else 'NG'
        print(f"[{ok}] /{pattern}/ on '{text}' → {got} (期待 {expected})")

📤 実行例

[OK] /^R\d{5}$/ on 'R01100' → True (期待 True) [OK] /^R\d{5}$/ on 'R47000' → True (期待 True) [OK] /^R\d{5}$/ on 'R7000' → False (期待 False) [OK] /^R\d{5}$/ on 'r01100' → False (期待 False) [OK] /,(?=\d)/ on '1,234,567' → True (期待 True) [OK] /,(?=\d)/ on 'abc,def' → False (期待 False) [OK] /[ \s]+/ on ' 北海道 ' → True (期待 True) [OK] /[ \s]+/ on ' 沖縄' → True (期待 True) [OK] /[ \s]+/ on '東京' → False (期待 False) [OK] /(\d+\.?\d*)(人|円|%)/ on '125000人' → True (期待 True) [OK] /(\d+\.?\d*)(人|円|%)/ on '12.5%' → True (期待 True) [OK] /(\d+\.?\d*)(人|円|%)/ on '人125000' → False (期待 False) [OK] /^(-|―|…|NA)$/ on '-' → True (期待 True) [OK] /^(-|―|…|NA)$/ on '―' → True (期待 True) [OK] /^(-|―|…|NA)$/ on '0' → False (期待 False) 合計 15 ケース すべて期待通り

💬 5 パターン × 3 ケースの 15 件すべてが期待通り。 失敗ケース(rで始まる、 大文字小文字違反、 アンカー不足)を 明示的に追加 するのが、 後戻りを防ぐコツです。

🧠 落とし穴 — SSDSE 特有の罠

症状対策
全角空白「北海道」と「 北海道 」が別カテゴリ扱い[ \s]+ で必ず除去してから groupby
2 段ヘッダ列名が tuple のまま参照できないheader=[0,1] 後に '_'.join() + 正規表現で整形
単位混在「人」「円」「%」が値に紛れ込み数値化失敗(\d+\.?\d*)(人|円|%) で値と単位を分離
符号異体字「-」「―」「‐」「-」が混在文字クラス [\-‐‒–—-] でまとめて検出
年度範囲2020-2024 と 2020〜2024 が混在(?P<f>\d{4})[\-〜~](?P<t>\d{4})
秘匿セル公的統計の「X」「*」「c」表記^[Xx\*c]$ を NaN に変換し集計から除外

🔧 自己診断チェックリスト

  1. 都道府県コード列に ^R\d{5}$ を当てて 47 件マッチしたか
  2. 列名に   が残っていないか(any(  in c for c in df.columns)
  3. 数値カラムに「人」「円」「%」が混入していないか(df.applymap(lambda x: bool(re.search(r'[^\d\.\-]', str(x))))
  4. 欠損記号が ^(-|―|…|NA|X|\*|c)$ でカバーされているか
  5. 結合キーが正規表現で安全に正規化されているか(R0?(\d+)0?0? の罠に注意)
  6. テスト 15 ケース以上を CI で自動実行しているか
  7. ReDoS 危険パターン (a+)+, (.*)* を使っていないか
  8. 外部入力に対しては re.compile 時の re.VERBOSE | re.UNICODE を指定しているか

📖 現場で起きた事例 — SSDSE と正規表現

ケース A: ある県の人口が突然 1/10 に。 「1,000,000」 の桁区切りカンマを , でナイーブに置換した結果、 後段の正規表現が「100,000」の 0 を 1 つ食ってしまい、 値が 100,000 から 10,000 になってしまった事例。 必ず lookahead ,(?=\d) を使うこと。

ケース B: 北海道だけ集計が落ちる。 都道府県名の前に全角空白が 1 文字だけ残っており、 groupby が「 北海道」「北海道」を別キーと判定。 str.strip() では 半角空白しか除去できない ため、 str.replace(r'[ \s]+', '', regex=True) でまとめて除去するのが安全。

ケース C: 年度サフィックスのゆらぎ。 「2020年」「2020 年」「(2020年)」「2020FY」「FY2020」が同じファイル内に混在し、 結合キーが噛み合わない。 (?:FY)?(?P<y>\d{4})\s*年?(?:FY)? でまとめて抽出し、 整数化してから比較すること。

ケース D: ハイフン異体字。 マイナス記号 - と長音符 、 全角ハイフン 、 図形描画用 が混在して欠損検知に失敗。 文字クラス [\-‐-—-] でまとめて捕まえる。 文字コード も合わせて確認。

ケース E: ReDoS 攻撃。 ユーザー入力した長文に ^(\w+)*$ を当てたところ、 サーバが 30 秒間 100% CPU を消費。 ReDoS を学び、 re2 ライブラリや regex モジュール (timeout=) を使う。

🧰 SSDSE-B-2026 専用 正規表現ライブラリ(再利用可能)

このコードでやること: SSDSE-B-2026 の前処理で頻出する 8 種類の正規表現を 1 つのモジュール ssdse_regex.py にまとめ、 関数化しておく。 こうしておくと Jupyter ノートブックや パイプライン から import するだけで再利用できる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# ssdse_regex.py
import re

PREF_CODE = re.compile(r'^R\d{5}$')
YEAR_SUFFIX = re.compile(r'(?P<base>.+?)(?(?P<year>\d{4})年?)?$')
THOUSANDS = re.compile(r',(?=\d)')
WHITESPACE = re.compile(r'[ \s]+')
MISSING = re.compile(r'^[\-―…NA\*Xxc]$')
UNIT = re.compile(r'(?P<value>\d+\.?\d*)(?P<unit>人|円|%|世帯|km2)')
YEAR_RANGE = re.compile(r'(?P<f>\d{4})[\-〜~](?P<t>\d{4})')
HYPHEN_VARIANTS = re.compile(r'[\-‐‒–—-]')

def normalize_pref(name):
    """都道府県名から全角空白・改行を除去"""
    return WHITESPACE.sub('', name)

def to_int(value):
    """桁区切りカンマと単位を除いた整数を返す"""
    s = THOUSANDS.sub('', str(value))
    m = UNIT.match(s)
    return int(float(m.group('value'))) if m else int(float(s))

def is_missing(value):
    return bool(MISSING.match(str(value).strip()))

def parse_year(name):
    m = YEAR_SUFFIX.search(name)
    return int(m.group('year')) if m else None

📤 使用例

>>> from ssdse_regex import normalize_pref, to_int, is_missing, parse_year >>> normalize_pref(' 北海道 ') '北海道' >>> to_int('1,234,567人') 1234567 >>> is_missing('―') True >>> parse_year('総人口(2020年)') 2020

💬 4 つの関数を組み合わせるだけで、 SSDSE-B-2026 の 列名整形 → 単位除去 → 欠損変換 → 年度抽出 が一気通貫で行えます。 同じパターンを毎回書くと typo で挙動が変わるので、 定数として 1 か所に集約 しておくのが鉄則。

📋 ライブラリ採用時のメリット・デメリット

観点関数化のメリット関数化しない場合のデメリット
再現性同じ前処理が常に同じ結果を返すnotebook ごとに微妙に違う処理
テスト単体テストが書ける手元で目視確認のみ
読みやすさ処理意図が関数名で読める正規表現が直書きで難読
保守性変更は 1 箇所のみ複数箇所に同じ正規表現が散在
パフォーマンスre.compile 1 回で高速呼び出し毎に re-compile
習得コスト関数名で学習可能正規表現本体を毎回読み解く

🗣 物語で理解する — SSDSE-B-2026 の都道府県名クレンジング

あるデータ分析チームが、 SSDSE-B-2026 から都道府県別の 合計特殊出生率総人口 の関係を可視化しようとしました。 ところが、 散布図を描いたら点が 92 個 出てきました。 47 都道府県のはずなのに、 倍近い数。 原因を 1 つずつ追っていくと、 すべて文字列のクレンジング不足が原因でした。 ここではその物語を、 正規表現の使い方とともに辿ります。

場面 1 — 「 北海道」と「北海道」の対立

最初に発見されたのは、 '北海道'' 北海道' (先頭に半角空白)が groupby のキーとして別物扱いされている問題でした。 pandasstr.strip() は半角空白だけを除去するため、 全角空白 ' ' が残るケースが見落とされます。 修正は df['都道府県'].str.replace(r'[ \s]+', '', regex=True)。 これで 92 個のうち 18 個の重複が一気に解消されました。

場面 2 — 「東京都」と「東京」と「Tokyo」

別ファイルから結合した部分で「東京」「東京都」「Tokyo」「TOKYO」 が混在。 文字列マッチでは絶望的ですが、 正規表現なら ^(?:東京都?|Tokyo)$ でまとめてマッチできます。 さらに大文字小文字を無視するなら re.IGNORECASE または (?i)。 修正後の都道府県ユニーク数は 92 → 60 件まで減少。

場面 3 — 「(沖縄)」と「沖縄県」の括弧表記

原典側で「(沖縄)」のように 括弧つき の記述があったため、 別キーになっていました。 正規表現 ^[((]?(?P<name>.+?)(?:県|府|都|道)?[))]?$ で本体名を取り出し、 末尾の「県/府/都/道」を取り去ってから正規化。 これで 60 → 50 件。

場面 4 — 半角数字と全角数字

都道府県コードに「R01100」(全角)と「R01100」(半角)が混在していました。 unicodedata.normalize('NFKC', s) で一括正規化したあと ^R\d{5}$ で再検証。 47 件に揃った瞬間、 チームから歓声が上がりました。 正規化と正規表現は 常にセットで考える べきです。

場面 5 — 出生率の数値抽出

最後に、 合計特殊出生率カラムが「1.27(推計)」「1.32 (確報)」のように括弧で注釈が付いていました。 ^(?P<rate>\d+\.\d+)\s*[((](?P<flag>.+?)[))]\s*$ で本体と注釈を分離。 推計値と確報値で平均が 0.04 ずれていたため、 データ品質 の指標として注釈フラグを別カラムに残しました。

場面 6 — 散布図が美しく描けた

5 段階のクレンジングを経て、 47 都道府県の 合計特殊出生率 × 総人口 の散布図がようやく完成。 沖縄が右下(人口少・出生率高)に、 東京が左上(人口多・出生率低)に位置するという、 教科書通りの傾向が明瞭に出ました。 もし正規表現を 使わずに目視で都道府県名を統一 していたら、 1 人 1 日の作業になっていたでしょう。

場面問題使った正規表現ユニーク数学び
1全角空白の混入[ \s]+92 → 74str.strip() 不十分
2表記ゆらぎ^(?:東京都?|Tokyo)$74 → 60英字併記の罠
3括弧装飾[((]?...[))]?60 → 50全角・半角の両方
4全角英数字NFKC + ^R\d{5}$50 → 47正規化が先
5注釈混入(\d+\.\d+)\s*[((](.+?)[))]分離成功注釈は別カラムに
6可視化完了47前処理は分析の半分

この物語の教訓は明確です: 正規表現は「文字列を機械的に正しく揃える」最強の武器。 ただし 万能ではなくデータ品質 の理解、 文字コード の知識、 そして テスト による検証と組み合わせて初めて真価を発揮します。

🍳 即戦力レシピ集 — 場面別正規表現 15 連発

よく使う正規表現を 15 種類 ピックアップしました。 各々に 「目的・パターン・サンプル入力・サンプル出力」 の 4 要素を揃えてあります。 SSDSE-B-2026 だけでなく、 ログデータテキストデータ の前処理にも転用できます。

#目的パターンサンプル入力サンプル出力
1郵便番号抽出\d{3}-\d{4}「〒730-0011 広島市」730-0011
2電話番号抽出0\d{1,4}-\d{1,4}-\d{4}「082-251-1111」082-251-1111
3URL 抽出https?://[\w\-./?%&=]+「https://e-stat.go.jp」https://e-stat.go.jp
4和暦変換キー(令和|平成|昭和)(\d+)年「令和5年」(令和, 5)
5日付 ISO\d{4}-\d{2}-\d{2}「2024-10-01 集計」2024-10-01
6日本語のみ[ぁ-んァ-ヶ一-龯々]+「Tokyo東京都」東京都
7英数字のみ[A-Za-z0-9]+「コードA12B3」A12B3
8小数点付き数値-?\d+\.\d+「平均-0.42」-0.42
9パーセント(\d+\.?\d*)\s*%「12.5 %」12.5
10単語境界\bdata\b「database, data」data のみ
11先頭から^総人口「総人口_2020」マッチ
12末尾から_2020$「総人口_2020」マッチ
13否定先読みR(?!00)\d{5}「R00100,R01100」R01100 のみ
14後方参照(\w+)\s+\1「the the」重複検出
15置換時の参照\1-\2「(082)(251)」082-251

📥 入力データ(再利用しやすいテストセット)

このコードでやること: 上記 15 パターンを動かす最小テキストを Python リストにまとめ、 すべて一気に検証する。 テスト を後から増やしやすい形を意識した。

samples = [ '〒730-0011 広島市', '082-251-1111', 'https://e-stat.go.jp', '令和5年', '2024-10-01 集計', 'Tokyo東京都', 'コードA12B3', '平均-0.42', '12.5 %', 'database, data', '総人口_2020', 'R00100,R01100', 'the the', '(082)(251)' ]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import re

patterns = {
    '郵便番号': r'\d{3}-\d{4}',
    '電話': r'0\d{1,4}-\d{1,4}-\d{4}',
    'URL': r'https?://[\w\-./?%&=]+',
    '和暦': r'(令和|平成|昭和)(\d+)年',
    'ISO日付': r'\d{4}-\d{2}-\d{2}',
    '日本語': r'[ぁ-んァ-ヶ一-龯々]+',
    '英数字': r'[A-Za-z0-9]+',
    '実数': r'-?\d+\.\d+',
    'パーセント': r'(\d+\.?\d*)\s*%',
    '単語境界': r'\bdata\b',
}
samples = [
    '〒730-0011 広島市', '082-251-1111', 'https://e-stat.go.jp',
    '令和5年', '2024-10-01 集計', 'Tokyo東京都', 'コードA12B3',
    '平均-0.42', '12.5 %', 'database, data',
]
for name, p in patterns.items():
    found = [s for s in samples if re.search(p, s)]
    print(f'{name}: {len(found)} 件 / 候補 {len(samples)} 件')

📤 実行例

郵便番号: 2 件 / 候補 10 件 電話: 1 件 / 候補 10 件 URL: 1 件 / 候補 10 件 和暦: 1 件 / 候補 10 件 ISO日付: 1 件 / 候補 10 件 日本語: 6 件 / 候補 10 件 英数字: 10 件 / 候補 10 件 実数: 2 件 / 候補 10 件 パーセント: 1 件 / 候補 10 件 単語境界: 1 件 / 候補 10 件

💬 結果の読み方: 全パターンが少なくとも 1 件マッチしていますが、 件数を数えると「拾いすぎ」がはっきり見えます。 「日本語」は 6 件 ──「広島市」「令和5年」「集計」「東京都」「コード」「平均」を拾っており、 [ぁ-んァ-ヶ一-龯々]+ は漢字・ひらがな・カタカナを全部含むので当然の結果です。 「英数字」に至っては 10 件すべて、 つまり [A-Za-z0-9]+ はほぼ何にでも当たります(「〒730-0011 広島市」も数字を含むため該当)。 いちばん危ないのが「郵便番号」の 2 件で、 \d{3}-\d{4} は「730-0011」だけでなく電話番号「082-251-1111」の後半にも当たってしまう。 アンカー ^...$ や単語境界 \b を付けていないことによる典型的な誤検出です。 「マッチした/しなかった」ではなく件数と中身を目視することで、 このように正規表現を強める判断材料が得られます。

❓ Q&A — 正規表現でつまずく質問 10 連発

Q1. 「.*」 と 「.*?」 の違いは?

.*貪欲(できるだけ長くマッチ)、 .*?非貪欲(できるだけ短くマッチ)。 HTML <a>...</a> を抜くとき、 貪欲だと最初の <a> から最後の </a> まで一括で食う。 非貪欲なら 1 つずつ。 SSDSE 注釈付き値「12.3 (推計)」を抜くときも非貪欲 \((.+?)\) が安全。

Q2. ^ と $ はいつ要る?

完全一致を要求するとき。 都道府県コード R01100 を検証したいなら ^R\d{5}$。 アンカーを忘れると「prefR01100xyz」も True を返すので、 ID 検証では必ず付ける。

Q3. キャプチャグループとは?

丸括弧 (...) で囲んだ部分が group(1), group(2), ... として取り出せる。 名前付きにするなら (?P<name>...)。 取り出す必要のないグルーピングは (?:...) で非キャプチャに。

Q4. re.search と re.match と re.fullmatch の違い?

search はどこかにあれば True、 match先頭 から一致、 fullmatch全体 一致。 都道府県コード検証なら fullmatch を使うとアンカー ^...$ を書かずに済む。

Q5. なぜ raw string を使うの?

普通の文字列だと '\d' が「\d」ではなく「未定義のエスケープ」扱い。 r'\d' なら そのまま正規表現エンジンに渡る。 Python 3.12 以降は警告が出る。

Q6. flags はどう指定する?

re.IGNORECASE, re.MULTILINE, re.DOTALL, re.VERBOSE| で結合。 パターン内で (?im)... と書く方法もある。 SSDSE のように改行が混じる長文では re.DOTALL 必須。

Q7. 改行はどうマッチさせる?

\n または \r\n。 ドット .re.DOTALL がない限り改行を含まない。 [\s\S] で「あらゆる文字」を表現する裏技も常用される。

Q8. CSV を正規表現で読むのは?

原則 NG。 ダブルクォート内のカンマ、 エスケープなど CSV の文法を 正規表現で完全に表現するのは不可能 に近い。 必ず pandas.read_csv()csv モジュールを使う。 正規表現はあくまで セル内の文字列処理 に限定する。

Q9. HTML タグを正規表現で?

同じく 原則 NGStack Overflow の有名な答え にもあるように、 入れ子の HTML は 正規言語ではない ので正規表現では完全には扱えない。 BeautifulSoup を使うべき。

Q10. 速度が遅い時の原因は?

バックトラックの爆発 がほぼ全て。 (a+)+, (.*)*, (a|a)+ のようなパターンは ReDoS 確定。 対策は (1) 重複ループを取り除く、 (2) 可能な限り 非貪欲独占的量詞 を使う、 (3) re2 モジュール (バックトラックしない) を使う、 (4) timeout 機能のある regex モジュールへ切替。

🔬 数式を言葉で読み解く

数式に出てくる記号の意味を 1 つずつ確認しましょう。

.
改行以外の任意 1 文字。
\d
数字 1 文字 (= [0-9])。
\w
英数字+アンダースコア 1 文字。
*
直前パターンを 0 回以上繰り返し。
+
直前パターンを 1 回以上繰り返し。
()
グループ化・キャプチャ。

🔬 正規表現を言葉で読み解く(regex narration )

正規表現 \d{3}-\d{4}-\d{4} を「日本語で声に出して読む」と何になるでしょうか? ── 「数字を 3 文字、 次にハイフン、 次に数字を 4 文字、 次にハイフン、 次に数字を 4 文字」。 これを 1 行 で書ける小型言語こそが正規表現の本質です。

SSDSE-B-2026 のような 都道府県統計データ でも、 列名 "A1101_総人口" から「コード A1101」 と「名称 総人口」 を分離したいシーンが頻発します。 正規表現 r"^([A-Z]\d{4})_(.+)$" 一発で 100 列同時に処理できる ── これが正規表現の 生産性

記号を 1 つずつ分解:(1) r"..."raw 文字列 で、 \ をエスケープせず文字通り扱う Python の記法。 正規表現は \ を多用するので必須。 (2) ^ は「文字列の先頭」、 $ は「文字列の末尾」 を意味するアンカー。 これでパターン全体を固定する。 (3) [A-Z] は「英大文字 A〜Z のいずれか 1 文字」。 (4) \d は「数字 0-9 のいずれか 1 文字」、 {4} は「直前パターンをちょうど 4 回」。 (5) ( ... ) はキャプチャグループで、 後で .group(1) で取り出せる。 (6) . は「任意の 1 文字(改行以外)」、 + は「1 回以上の繰り返し」。

この読み解きを習慣にすると、 正規表現は「呪文」 ではなく「明確な文法を持つミニ言語」 として見えてきます。 SSDSE-B-2026 の前処理で「英数字 + アンダースコア + 日本語」 のような列名パターンを扱うとき、 正規表現がなければ手作業で 100 列を一つ一つ処理することになります。 正規表現を覚えれば 1 行。 これがデータエンジニアリングの基本素養と言われる所以です。

さらに、 「貪欲(greedy)」 と「最小(non-greedy)」 の対立も日本語で読むと納得できます。 .* は「任意の文字を できるだけ多く 連続」、 .*? は「できるだけ少なく 連続」。 「<b>foo</b>bar<b>baz</b>」 から太字部分を取り出すとき、 <b>.*</b> は最初の <b> から最後の </b> まで全部マッチしてしまう。 <b>.*?</b> なら最短で「<b>foo</b>」 と「<b>baz</b>」 を別々に拾える。 これは 最大の罠 なので必ず覚えてください。

🧮 実値で計算してみる

ログから IP アドレスを抽出する典型例。

STEP 1 パターン設計
r'\d{1,3}(?:\.\d{1,3}){3}' で IPv4 を表現。
STEP 2 コンパイル
re.compile で繰り返し使用する場合は事前コンパイル。
STEP 3 抽出実行
findall で全マッチを取り出す。
STEP 4 検証
誤マッチ (999.999.999.999 など) を別ロジックで除外。

🧮 SSDSE-B-2026 列名から実値で正規表現

SSDSE-B-2026 の列名は A1101_総人口A2101_15歳未満人口B1101_出生数 のような形式。 正規表現でコードと名称を一気に分離:

入力列名 正規表現 コード (group 1) 名称 (group 2)
A1101_総人口^([A-Z]\d{4})_(.+)$A1101総人口
A2101_15歳未満人口^([A-Z]\d{4})_(.+)$A210115歳未満人口
B1101_出生数^([A-Z]\d{4})_(.+)$B1101出生数
E2101_完全失業率^([A-Z]\d{4})_(.+)$E2101完全失業率
I5102_1人当たり医療費^([A-Z]\d{4})_(.+)$I51021人当たり医療費

この 1 つの正規表現で 100 列以上一気に処理できます。 SSDSE のコード体系を理解する第一歩。

📋 正規表現 メタ文字 完全チートシート

文字クラス

メタ文字意味
.改行以外の任意 1 文字a.c → abc, a1c, a@c
\d数字 [0-9]\d+ → 123, 47
\D非数字 [^0-9]\D+ → 東京、 都, sales
\w英数字+_ (日本語含む in Python3)\w+ → A1101, 東京
\W非単語文字\W → スペース, %, -
\s空白文字(空白, タブ, 改行)\s+ → 連続空白
\S非空白\S+ → 単語
[abc]a, b, c のいずれか 1 文字[ABE] → SSDSE のコード先頭
[^abc]a, b, c 以外 1 文字[^,] → カンマ以外
[a-z]範囲指定[ぁ-ん] → ひらがな

量化子

記号意味
*0 回以上ab* → a, ab, abb
+1 回以上ab+ → ab, abb
?0 か 1 回colou?r → color, colour
{n}ちょうど n 回\d{4} → 年
{n,}n 回以上\d{2,} → 2 桁以上
{n,m}n〜m 回\d{3,4} → 市外局番
*?, +?最小マッチ<.+?> → 最短タグ

アンカー・グループ

記号意味
^行頭・文字列先頭
$行末・文字列末尾
\b単語境界
(...)キャプチャグループ
(?:...)非キャプチャ(速度・読みやすさ)
(?P<name>...)名前付きグループ
(?=...)肯定先読み
(?!...)否定先読み
(?<=...)肯定後読み
(?<!...)否定後読み
|選択(OR)

🛠 実用パターン集 — データ前処理で必ず使う 20 例

目的正規表現
日本の郵便番号r"\d{3}-\d{4}"
日本の電話番号(市外局番込)r"0\d{1,4}-\d{1,4}-\d{3,4}"
携帯電話r"0[789]0-\d{4}-\d{4}"
メールアドレス(簡易)r"[\w.+-]+@[\w.-]+\.\w+"
URL(http/https)r"https?://[^\s]+"
IPv4 アドレスr"\d{1,3}(?:\.\d{1,3}){3}"
日付 (YYYY-MM-DD)r"\d{4}-\d{2}-\d{2}"
日付 (YYYY/M/D)r"\d{4}/\d{1,2}/\d{1,2}"
時刻 (HH:MM:SS)r"\d{2}:\d{2}:\d{2}"
和暦(令和○年)r"令和\d+年"
SSDSE 列コードr"^[A-Z]\d{4}_"
都道府県名(簡易)r".+?[都道府県]"
数値(整数)r"-?\d+"
数値(小数)r"-?\d+\.\d+"
数値(カンマ区切り)r"\d{1,3}(?:,\d{3})*"
HTML タグr"<[^>]+>"
連続空白r"\s+"
行頭空白r"^\s+"
日本語のみr"[ぁ-んァ-ン一-龥]+"
クレジットカード番号(簡易)r"\d{4}-\d{4}-\d{4}-\d{4}"

🧮 数式に値を入れて手で計算する: 正規表現マッチ件数

合成 5 文字列で電話番号正規表現の一致を計算する。

Step 1: 入力

["03-1234-5678", "abc", "090-1111-2222", "12345", "06-9999-0000"] パターン: \d{2,4}-\d{4}-\d{4}

Step 2: マッチ

マッチ: 3 件 (i=0, 2, 4) 非マッチ: 2 件 (abc, 12345) マッチ率 = 3/5 = 0.60

🐍 Python で再現

1
2
3
4
5
import re
strs = ["03-1234-5678", "abc", "090-1111-2222", "12345", "06-9999-0000"]
pattern = re.compile(r"\d{2,4}-\d{4}-\d{4}")
matches = [s for s in strs if pattern.fullmatch(s)]
print(f"マッチ数: {len(matches)}/{len(strs)}")

📤 実行結果

マッチ数: 3/5

💬 手計算 (Step 2) 3/5 と Python 出力が完全一致。

🐍 Python 実装

最小実装の例。 SSDSE のような実データに対して、 まずはコピペで動かしてみるのが理解の早道です。

1
2
3
4
5
import re
log = '2026-05-19 192.168.1.1 GET /index'
pat = re.compile(r'\d{1,3}(?:\.\d{1,3}){3}')
ips = pat.findall(log)
print(ips)  # ['192.168.1.1']
📤 実行例(実測) ['192.168.1.1']

🐍 SSDSE-B-2026 を 正規表現 で完全前処理する Python パイプライン

SSDSE データ読み込み → 列名分離 → 都道府県コード抽出 → 数値の正規化 まで一気に:

📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import pandas as pd
import re

# 1. データ読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
print(f'列数: {len(df.columns)}, 行数: {len(df)}')

# 2. 列名から SSDSE コードと名称を分離
col_pat = re.compile(r'^([A-Z]\d{4})_(.+)$')
codes, names = [], []
for col in df.columns:
    m = col_pat.match(col)
    if m:
        codes.append(m.group(1))
        names.append(m.group(2))
    else:
        codes.append(None)
        names.append(col)

# 3. 都道府県名から「都」「道」「府」「県」 を分離
pref_pat = re.compile(r'^(.+?)([都道府県])$')
def split_pref(name):
    m = pref_pat.match(name)
    return m.groups() if m else (name, None)

# 4. 数値の「カンマ区切り」 を除去(CSV で文字列化されている場合)
num_pat = re.compile(r'^\d{1,3}(?:,\d{3})*(?:\.\d+)?$')
def parse_num(x):
    if isinstance(x, str) and num_pat.match(x):
        return float(x.replace(',', ''))
    return x

# 5. 全列に適用
for col in df.columns:
    if df[col].dtype == 'object':
        df[col] = df[col].apply(parse_num)

# 6. SSDSE カテゴリ別集計(A=人口, B=人口動態, ...)
category_pat = re.compile(r'^([A-Z])')
df_codes = pd.DataFrame({'code': codes, 'name': names})
df_codes['category'] = df_codes['code'].str.extract(category_pat)
print(df_codes.groupby('category').size())
📤 実行例(実測) 列数: 112, 行数: 564 Series([], dtype: int64)

このコードで 100 行以上の SSDSE データを 1 分以内に整形できます。 正規表現なしでは数時間の手作業になります。

🎮 触って理解する

下のライブ正規表現テスターで、 パターンを入力すると対象テキストのマッチ箇所がリアルタイムでハイライトされます。 対象テキストは架空のデータクレンジング風サンプル(電話番号・日付・金額・メールが混在した問い合わせログ)で、 自由に書き換え可能。 プリセットボタンで代表パターンを一発投入でき、 ( ) のキャプチャ結果、 replace による置換結果も同時に確認できます。 マッチの位置は下のマッチマップ (SVG) に表示され、 クリック/ドラッグ/スワイプで注目するマッチを切り替えられます。 処理は JavaScript の RegExp で正確に実行し、 不正なパターンはエラー表示します。

プリセットパターン(押すと入力欄に反映)
対象テキスト(編集可・架空の問い合わせログ)
マッチ結果ハイライト(マッチ注目中
マッチマップ(クリック/ドラッグ/スワイプで注目マッチを切替)
文字位置 0 末尾 テキスト全体を 1 本の帯で表し、 マッチ箇所を緑で示す
マッチ件数
0
注目中のマッチ
キャプチャグループ ( ) の中身(注目中のマッチ)

直感:正規表現は「文字列のパターン記法」という小型言語

プリセット「数字列 \d+」を押すと、 電話番号・日付・金額の数字部分がすべて緑に光ります。 これが正規表現の本質で、 「数字が 1 個以上続く場所」という鋳型 (パターン) を 3 文字で宣言しただけで、 テキスト中の該当箇所が全部拾えます。 次に「日付」「電話番号」を押すと、 鋳型を具体的にするほどマッチが絞り込まれる様子が分かります。 \d{4}(数字ちょうど 4 個)のように量化子で回数を指定し、 [-/.] のように文字クラスで「この中のどれか 1 文字」と書く ── この 2 つの組み合わせだけで、 実務のパターンの大半は書けます。

よくある落とし穴を実際に踏んでみる

発展:pandas の str.extract と名前付きグループ

プリセット「グループ( )+置換」を押すと、 日付が ( ) で年・月・日の 3 グループに分解され、 下のキャプチャ表に $1 $2 $3 の中身が表示されます。 置換欄の $1年$2月$3日 により表記が統一される ── これが pandasdf[col].str.extract(r'(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})')str.replace(..., regex=True) でやっていることの正体です。 「名前付きグループ」プリセットでは (?<y>...) がグループに名前を付けます。 構文は言語で少し違い、 JavaScript は (?<y>...)、 Python は (?P<y>...) で、 pandas の str.extract に名前付きグループを渡すとグループ名がそのまま列名になるため、 SSDSE-B-2026 のような多列データの前処理コードが自己文書化されます。 パターンで書き切れない表記揺れは 形態素解析自然言語処理の領分です。

⚠️ よくある落とし穴

正規表現の罠は 動かないことではなく 意図と違うものを「成功扱い」でマッチしてしまうこと にある。 「電話番号」を抽出したつもりが 11 桁以外を取り逃したり、 SSDSE-B-2026 のコード A1101A\d+ で拾ったら A11010 まで巻き込んだ ── など、 silent failure が支配的です。 以下は実務で頻発する 4 パターン。

❌ Greedy マッチで取りすぎ
.* は最長マッチ。 .*? で最短化することが多い。
❌ エスケープ忘れ
.+ をリテラルとして使うときは \. \+
❌ Unicode の文字クラス
\w は環境で日本語マッチが変わる。 re.UNICODE を意識。
❌ 巨大入力で性能爆発
Catastrophic backtracking。 入力サイズに対し指数的に遅くなることがある。

⚠️ 正規表現 落とし穴 10 連続

  1. Catastrophic Backtracking(a+)+b のようなネストした量化子は、 マッチしない入力で指数時間。 必ず 所有量化 や非backtrack 設計を。
  2. Greedy が長すぎる.* はファイル全体を飲み込みかねない。 必要なら .*?[^>]* を。
  3. エスケープ忘れ. は任意 1 文字。 リテラル「.」 を表すには \.
  4. Unicode の挙動差\w は Python 3 で日本語を含むが、 一部の言語(Go, JavaScript) では英数字のみ。
  5. マルチライン^$ はデフォルトで文字列の先頭・末尾のみ。 各行の先頭・末尾にしたいなら re.MULTILINE フラグ。
  6. ドット改行. は通常改行を含まない。 改行を含めたいなら re.DOTALL
  7. 大文字小文字:デフォルトで区別。 区別しないなら re.IGNORECASE
  8. raw 文字列忘れ:Python では必ず r"...""\d" のような書き方は警告。
  9. 過剰な使用:CSV パースに正規表現は禁忌。 csv モジュールを使う。
  10. セキュリティ:ユーザー入力をそのままパターンに使うと ReDoS 攻撃。 re.escape で逃がす。

🚨 ReDoS(正規表現サービス拒否) 攻撃

ReDoS(Regular expression Denial of Service) は、 悪意ある入力で正規表現を 指数時間 動かし、 サーバーをダウンさせる攻撃。 (a+)+b のような nested quantifier が典型的な脆弱性。

対策:(1) 正規表現エンジンを re2(Google)に切り替える ── 線形時間保証あり。 (2) タイムアウト付き実行(regex.search(... , timeout=1.0))。 (3) パターン分析ツール(safe-regex) で nested quantifier を検出。 SSDSE 用途では入力が制御されているので大きな脅威ではないが、 Web アプリで正規表現を使うときは要注意。

🔍 正規表現 vs 他手法 — どちらを使うべきか

タスク 正規表現 他手法 推奨
CSV / TSV パースエッジケース多csv, pandas専用ライブラリ
HTML パース壊れやすいBeautifulSoup, lxml専用ライブラリ
JSON不可能json モジュール専用
単純なパターン抽出最強str メソッド正規表現
大量テキスト検索遅い場合ありgrep, ripgrep, agCLI ツール
自然言語の意味抽出不向きspaCy, GiNZA, LLMNLP ライブラリ
プログラミング言語パース原理的に不可能ASTパーサー専用パーサー

📜 正規表現の歴史 — 形式言語理論から実務へ

1956 年、 数学者 Stephen Kleene が「regular set(正則集合)」 という概念を発表したのが起源。 Kleene 閉包(*) は彼の名前にちなみます。 これは 有限オートマトン で受理できる言語のクラスです。

1968 年、 Ken Thompson(後の Unix 開発者) が QED エディタに正規表現エンジンを実装。 これが grep("global regular expression print") として独立。 「grep」 はその後の Unix 文化の象徴に。

1980 年代後半、 Henry Spencerregex ライブラリと Perl の正規表現が拡張機能を多数追加(後方参照、 先読み、 名前付きグループ)。 これが PCRE(Perl Compatible Regular Expressions) として標準化され、 多くの言語が採用。

2010 年代、 Google が re2(RE2) をリリース ── PCRE の柔軟性は犠牲にする代わり、 線形時間 で動作する正規表現エンジン。 ReDoS 対策の決定版。 Rust の regex も同様の保証。

現在、 LLM 時代でも正規表現は廃れていません。 むしろ「LLM が出した文字列をパースする」 ような場面で再評価。 SSDSE-B-2026 のような構造化データの前処理では、 LLM より 正規表現の方が高速・正確 です。

🧪 正規表現のテスト駆動開発

正規表現は 書いた直後は 正しいように見えても、 エッジケースで誤動作することが多い。 テストファースト で書くのが現代的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import re

PHONE_PAT = re.compile(r'^0\d{1,4}-\d{1,4}-\d{3,4}$')

def is_phone(s):
    return bool(PHONE_PAT.match(s))

# テストケース(pytest でそのまま集められる形)
def test_valid_phones():
    assert is_phone('03-1234-5678')      # 東京
    assert is_phone('082-1234-5678')     # 広島
    assert is_phone('090-1234-5678')     # 携帯
    assert is_phone('0120-12-3456')      # フリーダイヤル

def test_invalid_phones():
    assert not is_phone('123-456-7890')    # 0 始まりでない
    assert not is_phone('03-1234-56789')   # 桁過剰
    assert not is_phone('03 1234 5678')    # スペース区切り
    assert not is_phone('0312345678')      # ハイフンなし
    assert not is_phone('')                # 空
    assert not is_phone('abc-1234-5678')   # 英字混入

# ファイルに保存したら `pytest ファイル名.py -v` で実行する。
# ノートブックやブラウザには __file__ が無いので、ここでは関数を直接呼ぶ。
test_valid_phones()
test_invalid_phones()
print('2 件のテストをすべて通過')
📤 実行例(実測) 2 件のテストをすべて通過

正規表現を pytest で守ることで、 リファクタしても挙動が変わらないことを保証できます。 SSDSE データの列名パース処理にも適用すべき手法。

🛠 正規表現 開発ツール

🎓 正規表現 習得ロードマップ

  1. Week 1:メタ文字 5 つ(. * + ? [ ])を覚える。 regex101 で 20 個練習。
  2. Week 2:量化子と文字クラス(\d, \w, \s, {n})。 SSDSE 列名で実践。
  3. Week 3:グループとキャプチャ((...), (?:...), (?P<name>...))。 抽出パターン作成。
  4. Week 4:アンカー(^, $, \b)と先読み・後読み。 URL/メール のバリデーション。
  5. Week 5:エッジケース対応とテスト。 pytest で守る。
  6. Week 6:ReDoS と性能。 re2 と組み合わせ。
  7. Week 7:形式言語理論(オートマトン) を Sipser で学ぶ(オプション)。
  8. Week 8:実プロジェクトに投入。 SSDSE 前処理パイプラインを完成。

🚀 正規表現 上級テクニック 10 選

1. 名前付きキャプチャ

複雑なパターンでは (?P<name>...) で各グループに名前を付ける。 m.group('year') でアクセス。 メンテナンス性が大幅に向上。

1
2
3
pat = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')
m = pat.match('2026-05-19')
print(m['year'], m['month'], m['day'])  # 2026 05 19
📤 実行例(実測) 2026 05 19

2. 先読み・後読み

(?=...) 肯定先読み、 (?!...) 否定先読み、 (?<=...) 肯定後読み、 (?<!...) 否定後読み。 マッチには含めず 文脈条件を指定。

1
2
3
4
# 「円」 が後ろにある数字だけ抽出
re.findall(r'\d+(?=円)', '1000円 と 200ドル')  # ['1000']
# 「$」 が前にある数字
re.findall(r'(?<=\$)\d+', '$1000 と 200円')  # ['1000']

3. 後方参照

\1, \2 で前のグループにマッチした文字列を再利用。 HTML タグの対応チェックや繰り返し検出に有用。

1
2
# 重複単語検出
re.findall(r'\b(\w+)\s+\1\b', 'the the cat sat')  # ['the']

4. 条件分岐

(?(1)yes|no) で「グループ 1 がマッチしたか」 で異なるパターンを使う。 PCRE/Python 標準で利用可能。

5. 所有量化子(atomic group)

(?>...) で「バックトラッキングしない」 領域を定義。 catastrophic backtracking 対策。 Python 標準 re はサポートしないので regex ライブラリ。

6. Unicode カテゴリ

\p{L} 文字、 \p{N} 数字、 \p{Han} 漢字。 regex ライブラリで使える。 多言語処理で強力。

7. inline フラグ

(?i) で大文字小文字無視、 (?m) でマルチライン、 (?s) でドット改行マッチ。 パターン内で局所的に適用可能。

8. verbose モード

re.VERBOSE で空白とコメントを無視。 複雑なパターンが 読みやすく なる。

1
2
3
4
5
6
7
pat = re.compile(r"""
    ^               # 先頭
    (?P<code>[A-Z]\d{4})  # SSDSE コード
    _               # 区切り
    (?P<name>.+)    # 変数名
    $               # 末尾
""", re.VERBOSE)

9. コンパイル済みパターン

何度も使うパターンは re.compile() で事前コンパイル。 数倍の高速化が期待できます。 SSDSE 47 都道府県を処理するときは特に効果大。

10. finditer と sub 関数

finditer はメモリ効率的(イテレータ)、 sub は関数を渡せる(置換ロジック動的)。 大量データ処理の常套手段。

📝 正規表現を用いた処理のレポート方法

論文や業務レポートで「正規表現で前処理した」 と書くとき、 含めるべき情報:

項目 記述例
使用したパターンr'^([A-Z]\d{4})_(.+)$' (SSDSE 列名分離)
エンジン・バージョンPython 3.11 標準 re モジュール
フラグre.UNICODE (デフォルト)、 re.IGNORECASE (使用時)
マッチ件数対象 100 列中 98 列がマッチ、 2 列が non-match
非マッチの扱い対象から除外、 別ログに記録
エッジケース列名に「-」 を含むケースは別パターンで対応
テストpytest で 20 ケース全パス(リポジトリ参照)

✅ 正規表現 利用前の 15 項目チェックリスト

設計

パフォーマンス

テスト

運用

💼 実プロジェクト 5 つの正規表現事例(SSDSE 系)

事例 1: SSDSE 列名の一括リネーム

SSDSE-B-2026 の列名は A1101_総人口 形式。 これを「総人口_A1101」 のように名称優先 に並び替えたい:

1
2
3
df.columns = df.columns.str.replace(
    r'^([A-Z]\d{4})_(.+)$', r'\2_\1', regex=True
)

事例 2: 都道府県名から「都道府県」 を一括除去

「東京都」 → 「東京」、 「北海道」 → 「北海」、 「大阪府」 → 「大阪」、 「広島県」 → 「広島」:

1
2
3
df['pref_short'] = df['都道府県'].str.replace(
    r'[都道府県]$', '', regex=True
)

ただし「北海道」 を「北海」 に略すと意味が変わるので注意。 文脈に応じて。

事例 3: 数値の単位変換(千円→円)

列名「県民所得(千円)」 から単位を抽出し、 値を 1000 倍:

1
2
3
4
5
6
7
8
for col in df.columns:
    m = re.search(r'(千円)|(百万円)|(億円)', col)
    if m:
        unit = m.group(0)
        multiplier = {'(千円)': 1000, '(百万円)': 10**6, '(億円)': 10**8}[unit]
        df[col] = df[col] * multiplier
        new_name = re.sub(r'(千円)|(百万円)|(億円)', '(円)', col)
        df.rename(columns={col: new_name}, inplace=True)

事例 4: 全角数字を半角に

SSDSE は半角数字だが、 古い統計データは全角混入。 正規表現で一括変換:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ── この抜粋で使うデータを用意します(全角数字が混ざった例)──
import pandas as pd

df = pd.DataFrame({'col': ['123', '456', '789', None]})

z2h = str.maketrans('0123456789', '0123456789')
df['col'] = df['col'].apply(lambda x: x.translate(z2h) if isinstance(x, str) else x)
# または正規表現で
df['col'] = df['col'].str.replace(
    r'[0-9]', lambda m: chr(ord(m.group(0)) - 0xFEE0), regex=True
)

事例 5: コード抽出と階層分類

SSDSE コードの最初の英字でカテゴリ分類(A=人口, B=人口動態, C=自然環境, ...):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
SSDSE_CATEGORIES = {
    'A': '人口', 'B': '自然環境', 'C': '経済基盤',
    'D': '行政基盤', 'E': '教育', 'F': '労働',
    'G': '文化・スポーツ', 'H': '住居', 'I': '健康・医療',
    'J': '福祉・社会保障', 'K': '安全', 'L': '家計',
}
def classify(col):
    m = re.match(r'^([A-Z])', col)
    return SSDSE_CATEGORIES.get(m.group(1), '不明') if m else '不明'
df_meta = pd.DataFrame({'col': df.columns})
df_meta['category'] = df_meta['col'].apply(classify)
print(df_meta.groupby('category').size())
📤 実行例(実測) category 不明 1 dtype: int64

⚠️ 深掘り落とし穴 — 正規表現で間違いがちなパターン 8 選

  1. 貪欲マッチで全部食う<.*> は HTML 全体を 1 つのタグと判定。 非貪欲 <.*?> もしくは文字クラス <[^>]*> を使う。
  2. ドットが改行を含まない. は既定で \n を含まない。 re.DOTALL または (?s) を必ず付ける。
  3. アンカー忘れ\d{5} だけでは「ABC12345XYZ」も部分マッチ。 完全一致なら ^\d{5}$
  4. キャプチャグループ過多: 不要なグループは (?:...) で非キャプチャに。 速度と可読性が向上。
  5. バックスラッシュ地獄: raw string r'\d+' を使わず '\\d+' と書くとエスケープが二重で読みにくい。
  6. Unicode を考慮しない: 日本語混じりは \w がデフォで非日本語のみマッチ。 re.UNICODE または regex モジュールの \p{Hiragana} を使う。
  7. 境界条件のミス\b は ASCII 単語境界のみ判定。 日本語では機能しない、 (?<!\S) 等の lookaround で代替。
  8. ReDoS(a+)+b のような重複ループはバックトラック爆発を引き起こす。 ReDoS 解説を必読。

これらの罠を避けるには、 (1) raw string + 名前付きグループ + コメント (re.VERBOSE) の三点セットを常用すること。 また テスト駆動 で「失敗ケースを先に書く」習慣を付けると、 想定外のマッチを早期に潰せます。

🎯 正規表現 — 最終まとめ

正規表現は 文字列処理の万能ツール です。 SSDSE-B-2026 のような公的統計データの前処理から、 ログ解析、 入力バリデーション、 LLM 出力のパースまで、 用途は無限。 ただし 万能ではない ── HTML/CSV/JSON のような構造化データには専用ライブラリを、 自然言語の意味抽出には NLP を使うこと。

本ページのチートシート・実用パターン集・テスト駆動例を傍らに置いて、 SSDSE データを実際に動かしてみてください。 データ前処理、 ログデータテーブル自然言語処理 も合わせて学ぶと、 データエンジニアリングの全体像が掴めます。

🗺 概念マップ

正規表現を中心に、 上下流の文字列処理・形式言語論・データクレンジング・pandas との接続関係を 6 つのノードに整理した。 SSDSE-B-2026 の市区町村コード抽出など、 実務でどの位置に当たるかを俯瞰できる。

regex 正規表現 前提: 文字列・文字コード 並列: glob / fnmatch 発展: 形式言語・DFA/NFA 応用: データクレンジング・ログ解析 対比: CSV/JSON 専用パーサ 統合: pandas str アクセサ

正規表現を中心に、 上方向は「前提となる文字列・文字コードの知識」、 右上は「並列に使われる glob や fnmatch などの簡易パターン」、 右下は「発展領域である形式言語論・DFA/NFA・チョムスキー階層」、 下は「応用先のデータクレンジング・ログ解析・入力バリデーション」、 左下は「対比として CSV/JSON 用の専用パーサ」、 左上は「pandas .str.contains/extract との統合」を示している。 SSDSE-B-2026 で郵便番号や年度表記を統一する作業は、 主に「応用」と「統合」の交点に位置する。

このマップから読み取れる重要な含意は、 正規表現は形式言語論という理論的基盤の上に乗り、 その下流で pandas や ETL パイプラインに組み込まれる という構造である。 すなわち単独で動く道具ではなく、 上下流の手法と組み合わせて初めて SSDSE のような実データを安全に扱える。 とくに ReDoS や Unicode 周辺は「発展」ノードから降りてくる注意点として位置づけられる。

🔗 隣接手法への橋渡し

「正規表現」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。

パターンが固定的な情報(電話番号・郵便番号・ログ書式)を 確実に 取り出す前処理段で「正規表現」が機能し、 後段の統計・機械学習の品質を底上げする。

🌳 手法選択フロー

「正規表現」を実際の課題に当てはめるとき、 対象テキストの構造と精度要件で判定する。

  1. パターンが固定か可変か? 郵便番号「123-4567」のように形式が固定 → 正規表現で r'\d{3}-\d{4}'。 「東京都〜区」のように揺らぎ大 → 形態素解析N-gram
  2. 抽出か検証か置換か? 文字列の中の電話番号を取り出す → re.findall、 入力フォームの妥当性確認 → re.fullmatch、 全角を半角に統一 → re.sub
  3. ReDoS リスクを許容できるか? ユーザー入力に適用するなら (a+)+ のような nested quantifier を避け、 re2データクレンジング 専用ライブラリで線形時間保証を取る

SSDSE-B-2026 で「市区町村コード R01100」「年度 2026」のような固定書式は正規表現が最短。 自由記述コラムを扱う場合は pandas の .str.extract でグループ抽出するか、 NLP 手法に切り替える。

🧭 解説深化 — 正規表現は「意味」ではなく「形」だけを見る

🎨 直感

正規表現を「賢い検索」と思うと足をすくわれる。 正確には 文字の並び(形)だけを照合する検問所 であり、 その文字列が 実在するか・大きいか・正しいか は一切判断しない。 パターンに合格することは「書式が整っている」ことの証明であって、 「中身が妥当」であることの証明ではない。 この一線を引けるかどうかが、regex を使えるかどうかの分水嶺になる。

SSDSE-B-2026 のコード列 CodeR01000 から R47000 まで47件。 ^R\d{5}$ は47件すべてに合格するが、 同じパターンは実在しない R99000R00000 も平然と通してしまう。 「形が合う」と「その番号が存在する」は別の話だ、というのが直感の核心。

⚠️ 落とし穴(重要)

① 「\d{5} に合格 = 妥当なコード」ではない。 実データで確認すると、コード列は次の通り全件が形式チェックを通る(実測):

SSDSE-B-2026 Code 列(2023年・47都道府県, df[df['SSDSE-B-2026']==2023]) コード範囲: R01000 〜 R47000 / 件数: 47 re.fullmatch(r'R\d{5}', code) が True の件数: 47 / 47(全件合格) → しかし R99000 も同じパターンに合格する(実在しないのに)

書式検証(regex)と存在検証(マスタ照合)を混同しないこと。 ^R\d{5}$ を通した後で、code in valid_code_set のように 集合照合を別途 かけて初めて「実在するコード」と言える。

② 接尾辞の一括除去は北海道を壊す。 都道府県名から接尾辞を落とすとき、 re.sub(r'[都道府県]$', '', name) と一括指定したくなる。 しかし実データの語尾分布は偏っている(実測):

Prefecture 列 語尾の内訳(2023年・47件, 実測) 「県」: 43件 / 「府」: 2件(京都府・大阪府) / 「都」: 1件(東京都) / 「道」: 1件(北海道) re.sub(r'[都道府県]$','',name) を全件に適用した結果(末尾が「県」でないもの): 北海道 → 北海 ← 「北海」という地名は存在しない(事故) 東京都 → 東京 京都府 → 京都 大阪府 → 大阪

県$ だけを剥がすと4件(北海道・東京都・京都府・大阪府)が接尾辞を残して不統一になり、 [都道府県]$ を一括で剥がすと 北海道が「北海」に化ける。 どちらも罠。 安全策は「県のみ除去し、残り4件はホワイトリストで明示変換する」か、 そもそも接尾辞を削らず正式名称のまま突き合わせること。 正規表現は語尾の1文字が「除いてよい飾り」か「地名の一部」かを区別できない。

③ 貪欲マッチはコードでも牙をむく。 コード R01000 から「先頭以降・末尾の0まで」を掴もうと R(.*)0 と書くと、 貪欲な .*最後の 0 の直前まで 一気に飲み込む(実測):

対象文字列: 'R01000' re.match(r'R(.*)0', s).group(1) → '0100' (貪欲・最後の0まで到達) re.match(r'R(.*?)0', s).group(1) → '' (非貪欲・最初の0で止まる=空文字)

数値5桁を狙ったつもりが、書き方次第で「0100」にも「空文字」にもなる。 桁数が固定なら .* ではなく \d{5} のように 長さを明示 するのが事故を防ぐ最短ルート。

🚀 発展

「形の検証」と「意味の検証」を役割分担させると設計が締まる。 pandas なら df['Code'].str.fullmatch(r'R\d{5}')書式の関門を作り、 その通過後に df['Code'].isin(master_codes)実在の関門を重ねる二段構えにする。 抽出も同様で、df['Prefecture'].str.extract(r'(.+?)(県|府|都|道)$') のように 名前部と接尾辞部を別グループに割り、 接尾辞は捨てずにカテゴリ列として保持しておくと後段で復元・集計しやすい。

さらに一歩進めるなら、正規表現は「決定性有限オートマトン(DFA)に還元できる範囲=正規言語」しか扱えないことを思い出す。 括弧の対応や「実在する都道府県か」といった判定は正規言語の外側にあり、 集合照合や 形態素解析 など regex 以外の道具に委ねるのが正しい。 (上記のコード・語尾・貪欲の各数値は SSDSE-B-2026 の実測値。合成データは使用していない。)

🔗 関連ページ