「regex」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「regex」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。
これらのキーワードは「regex の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。
🍰 まずはやさしく
文字のパターンを決める道具です。
複雑な文字を簡単に抜き出すために使います。
スマホの連絡先から番号だけを探すような機能です。
ここでは基本の記号や使い方を学びます。
正規表現:文字列パターンマッチング
. 任意1文字、 * 0回以上、 + 1回以上、 ? 0/1回、 [] 文字クラス。re.search/match/findall/sub。 grep, sed, awk でも使う。🍰 まずはやさしく
データを整えるための準備道具です。
分析しやすい形に文字を直すために使います。
部活の出席簿から名前だけをまとめる時に便利です。
このページでは定義や注意点を順番に解説します。
この用語は データ前処理 カテゴリに属します。 関連する別称・略号:(なし)。
論文・実務レポートで 正規表現 が登場したら、 まず本ページの「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 に道を譲る、 という棲み分けが直感的な理解の核になる。
🍰 まずはやさしく
文字のルールを決めるための特別な書き方です。
正確にパターンを指定するために使います。
買い物リストから特定の品物だけを分けるイメージです。
ルールを作るための記号の意味について学びます。
正規表現は、 文字リテラル・任意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 等の拡張正規表現は後方参照を持つため、 厳密には正規言語の枠を超えています(理論的には文脈依存言語の一部まで認識可能)。
ChatGPT などの LLM に「メールアドレスを正規表現で抽出して」 と頼めば、 ほぼ正しい正規表現を生成してくれます。 これは正規表現を 不要にした のではなく、 むしろ 使いこなせる人の生産性を上げた。
regex101.com で動作確認。re で実装し、 pytest でテスト。re.compile()。 SSDSE 47 件で 2-3 倍速。^ や $ でマッチしないパターンを早期に除外。. より \d や [a-z] の方が高速。.* より具体的なパターン(例:[^>]*)。(?:...)。 キャプチャはオーバーヘッド。SSDSE 100 列 × 47 行を処理しても、 正しく書けばミリ秒オーダーで完了します。 数秒以上かかるなら正規表現の設計ミスを疑う。
本ページの全テクニックを動員して 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) の検証に正規表現は定番。 ただし:
urllib.parse) を使う。ユーザー入力を正規表現の パターン として受け取る場合は、 re.escape() で逃がす。 パターンに変数を埋め込む場合も同様。
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) |
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 |
日本のデータ分析で頻繁に遭遇する 表記揺れ:
2026-05-19 ISO 86012026/05/192026年5月19日令和8年5月19日R8.5.1920260519 連結May 19, 2026これら全てに対応する正規表現は複数パターンを | で繋ぐ:
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 |
(...) で括った部分。 マッチ結果から抽出可能。^, $, \b)。 文字には対応しない。*, +, ?, {n,m})。*?, +?)。\., \+ 等)。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) | (?<=...) | 肯定後読み |
A1101_総人口 から「A11」 だけ抽出する正規表現は? → r'^([A-Z]\d{2})'r'^(.+?)[都道府県]$'r'(\d{4})年(\d{1,2})月(\d{1,2})日'r'\d{1,3}(?:,\d{3})*円'r'^[\w.+-]+@[\w.-]+\.\w+$'r'[0-9]' + chr(ord(m) - 0xFEE0)<p>...</p> の中身を最小マッチで取り出す正規表現は? → r'<p>(.+?)</p>'https://」 を除いてドメイン部分だけ取る正規表現は? → r'https?://([^/]+)'r'\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\b'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] も考慮。
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) | 列名で正規表現フィルタ | 「人口」 列のみ抽出 |
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()) |
re.compile。"""...""" や Heredoc で \n を含む文字列をマッチさせるとき、 ^$ の挙動は re.MULTILINE 次第。str.replace の regex デフォルトが False に。 明示指定推奨。[...] 内では . や + はリテラル扱いなのでエスケープ不要。r"...") を使うLLM がパターン抽出を高精度でこなせる時代、 正規表現は不要になるのでしょうか? 答えは No。 むしろ 使いこなせる人の価値が上がっています。 LLM はメガバイト単位のテキストを処理するのに高コスト、 正規表現は数ミリ秒。 SSDSE-B-2026 のような 明確な構造を持つデータ の前処理では正規表現が圧勝。
逆に「あいまいな自然言語からの意味抽出」 は LLM の独壇場。 両者を 適材適所 で組み合わせるのが現代のデータエンジニアリングです。
ここでは 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 の生 CSV を読み込み、 正規表現 で列名のゆらぎ(全角空白・年度括弧・単位末尾)を統一する。 統一後に 記述統計 と可視化を行えるよう、 整然データ(tidy data)形式に変形する。
ここで正規表現の出番になる箇所は 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])) |
📤 実行例:
💬 結果の読み方: 2 段ヘッダを繋いだ直後は A110101_総人口(男) のようにコードと日本語名が同居していますが、 名前付きグループ (?P<code>[^_]+)_(?P<label>[^(]+)(?:((?P<sub>[^)]+)))? で 3 つに割ると 総人口_男 という短い名前に統一できます。 重複した列名が 0 件なので、 この正規化で列が潰れていないことも確認できました。 地域コードは 564 行を県単位に畳むと 47 件になり、 その全部が ^R\d{5}$ にマッチします(R01000 のように県レベルは下 3 桁が 000)。 ここでポイントなのは、 564 行のまま数えると「有効コード数 564」となって検証が素通りしてしまうこと。 縦持ちデータでは「何を 1 件と数えるか」を先に決めてから正規表現で検証する必要があります。 ここまで済めば、 あとは pandas の通常操作で十分扱えます。
クレンジング済みのデータで 総人口 (2020) と総人口 (2023) の関係を散布図にしました。 完全に直線上に並ぶことから、 4 年間の人口変化は 規模に比例する小さなドリフト であり、 順序関係は保存されていることが分かります。 相関係数 0.999、 回帰 の傾きは 0.992。

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

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

このコードでやること: 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})") |
📤 実行例:
💬 5 パターン × 3 ケースの 15 件すべてが期待通り。 失敗ケース(rで始まる、 大文字小文字違反、 アンカー不足)を 明示的に追加 するのが、 後戻りを防ぐコツです。
| 罠 | 症状 | 対策 |
|---|---|---|
| 全角空白 | 「北海道」と「 北海道 」が別カテゴリ扱い | [ \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 に変換し集計から除外 |
^R\d{5}$ を当てて 47 件マッチしたか が残っていないか(any( in c for c in df.columns))df.applymap(lambda x: bool(re.search(r'[^\d\.\-]', str(x)))))^(-|―|…|NA|X|\*|c)$ でカバーされているかR0?(\d+)0?0? の罠に注意)(a+)+, (.*)* を使っていないかre.compile 時の re.VERBOSE | re.UNICODE を指定しているかケース 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 の前処理で頻出する 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 |
📤 使用例:
💬 4 つの関数を組み合わせるだけで、 SSDSE-B-2026 の 列名整形 → 単位除去 → 欠損変換 → 年度抽出 が一気通貫で行えます。 同じパターンを毎回書くと typo で挙動が変わるので、 定数として 1 か所に集約 しておくのが鉄則。
| 観点 | 関数化のメリット | 関数化しない場合のデメリット |
|---|---|---|
| 再現性 | 同じ前処理が常に同じ結果を返す | notebook ごとに微妙に違う処理 |
| テスト | 単体テストが書ける | 手元で目視確認のみ |
| 読みやすさ | 処理意図が関数名で読める | 正規表現が直書きで難読 |
| 保守性 | 変更は 1 箇所のみ | 複数箇所に同じ正規表現が散在 |
| パフォーマンス | re.compile 1 回で高速 | 呼び出し毎に re-compile |
| 習得コスト | 関数名で学習可能 | 正規表現本体を毎回読み解く |
あるデータ分析チームが、 SSDSE-B-2026 から都道府県別の 合計特殊出生率 と 総人口 の関係を可視化しようとしました。 ところが、 散布図を描いたら点が 92 個 出てきました。 47 都道府県のはずなのに、 倍近い数。 原因を 1 つずつ追っていくと、 すべて文字列のクレンジング不足が原因でした。 ここではその物語を、 正規表現の使い方とともに辿ります。
最初に発見されたのは、 '北海道' と ' 北海道' (先頭に半角空白)が groupby のキーとして別物扱いされている問題でした。 pandas の str.strip() は半角空白だけを除去するため、 全角空白 ' ' が残るケースが見落とされます。 修正は df['都道府県'].str.replace(r'[ \s]+', '', regex=True)。 これで 92 個のうち 18 個の重複が一気に解消されました。
別ファイルから結合した部分で「東京」「東京都」「Tokyo」「TOKYO」 が混在。 文字列マッチでは絶望的ですが、 正規表現なら ^(?:東京都?|Tokyo)$ でまとめてマッチできます。 さらに大文字小文字を無視するなら re.IGNORECASE または (?i)。 修正後の都道府県ユニーク数は 92 → 60 件まで減少。
原典側で「(沖縄)」のように 括弧つき の記述があったため、 別キーになっていました。 正規表現 ^[((]?(?P<name>.+?)(?:県|府|都|道)?[))]?$ で本体名を取り出し、 末尾の「県/府/都/道」を取り去ってから正規化。 これで 60 → 50 件。
都道府県コードに「R01100」(全角)と「R01100」(半角)が混在していました。 unicodedata.normalize('NFKC', s) で一括正規化したあと ^R\d{5}$ で再検証。 47 件に揃った瞬間、 チームから歓声が上がりました。 正規化と正規表現は 常にセットで考える べきです。
最後に、 合計特殊出生率カラムが「1.27(推計)」「1.32 (確報)」のように括弧で注釈が付いていました。 ^(?P<rate>\d+\.\d+)\s*[((](?P<flag>.+?)[))]\s*$ で本体と注釈を分離。 推計値と確報値で平均が 0.04 ずれていたため、 データ品質 の指標として注釈フラグを別カラムに残しました。
5 段階のクレンジングを経て、 47 都道府県の 合計特殊出生率 × 総人口 の散布図がようやく完成。 沖縄が右下(人口少・出生率高)に、 東京が左上(人口多・出生率低)に位置するという、 教科書通りの傾向が明瞭に出ました。 もし正規表現を 使わずに目視で都道府県名を統一 していたら、 1 人 1 日の作業になっていたでしょう。
| 場面 | 問題 | 使った正規表現 | ユニーク数 | 学び |
|---|---|---|---|---|
| 1 | 全角空白の混入 | [ \s]+ | 92 → 74 | str.strip() 不十分 |
| 2 | 表記ゆらぎ | ^(?:東京都?|Tokyo)$ | 74 → 60 | 英字併記の罠 |
| 3 | 括弧装飾 | [((]?...[))]? | 60 → 50 | 全角・半角の両方 |
| 4 | 全角英数字 | NFKC + ^R\d{5}$ | 50 → 47 | 正規化が先 |
| 5 | 注釈混入 | (\d+\.\d+)\s*[((](.+?)[))] | 分離成功 | 注釈は別カラムに |
| 6 | 可視化完了 | ― | 47 | 前処理は分析の半分 |
この物語の教訓は明確です: 正規表現は「文字列を機械的に正しく揃える」最強の武器。 ただし 万能ではなく、 データ品質 の理解、 文字コード の知識、 そして テスト による検証と組み合わせて初めて真価を発揮します。
よく使う正規表現を 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 |
| 3 | URL 抽出 | 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 リストにまとめ、 すべて一気に検証する。 テスト を後から増やしやすい形を意識した。
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)} 件') |
📤 実行例:
💬 結果の読み方: 全パターンが少なくとも 1 件マッチしていますが、 件数を数えると「拾いすぎ」がはっきり見えます。 「日本語」は 6 件 ──「広島市」「令和5年」「集計」「東京都」「コード」「平均」を拾っており、 [ぁ-んァ-ヶ一-龯々]+ は漢字・ひらがな・カタカナを全部含むので当然の結果です。 「英数字」に至っては 10 件すべて、 つまり [A-Za-z0-9]+ はほぼ何にでも当たります(「〒730-0011 広島市」も数字を含むため該当)。 いちばん危ないのが「郵便番号」の 2 件で、 \d{3}-\d{4} は「730-0011」だけでなく電話番号「082-251-1111」の後半にも当たってしまう。 アンカー ^...$ や単語境界 \b を付けていないことによる典型的な誤検出です。 「マッチした/しなかった」ではなく件数と中身を目視することで、 このように正規表現を強める判断材料が得られます。
.* は 貪欲(できるだけ長くマッチ)、 .*? は 非貪欲(できるだけ短くマッチ)。 HTML <a>...</a> を抜くとき、 貪欲だと最初の <a> から最後の </a> まで一括で食う。 非貪欲なら 1 つずつ。 SSDSE 注釈付き値「12.3 (推計)」を抜くときも非貪欲 \((.+?)\) が安全。
完全一致を要求するとき。 都道府県コード R01100 を検証したいなら ^R\d{5}$。 アンカーを忘れると「prefR01100xyz」も True を返すので、 ID 検証では必ず付ける。
丸括弧 (...) で囲んだ部分が group(1), group(2), ... として取り出せる。 名前付きにするなら (?P<name>...)。 取り出す必要のないグルーピングは (?:...) で非キャプチャに。
search はどこかにあれば True、 match は 先頭 から一致、 fullmatch は 全体 一致。 都道府県コード検証なら fullmatch を使うとアンカー ^...$ を書かずに済む。
普通の文字列だと '\d' が「\d」ではなく「未定義のエスケープ」扱い。 r'\d' なら そのまま正規表現エンジンに渡る。 Python 3.12 以降は警告が出る。
re.IGNORECASE, re.MULTILINE, re.DOTALL, re.VERBOSE を | で結合。 パターン内で (?im)... と書く方法もある。 SSDSE のように改行が混じる長文では re.DOTALL 必須。
\n または \r\n。 ドット . は re.DOTALL がない限り改行を含まない。 [\s\S] で「あらゆる文字」を表現する裏技も常用される。
原則 NG。 ダブルクォート内のカンマ、 エスケープなど CSV の文法を 正規表現で完全に表現するのは不可能 に近い。 必ず pandas.read_csv() や csv モジュールを使う。 正規表現はあくまで セル内の文字列処理 に限定する。
同じく 原則 NG。 Stack Overflow の有名な答え にもあるように、 入れ子の HTML は 正規言語ではない ので正規表現では完全には扱えない。 BeautifulSoup を使うべき。
バックトラックの爆発 がほぼ全て。 (a+)+, (.*)*, (a|a)+ のようなパターンは ReDoS 確定。 対策は (1) 重複ループを取り除く、 (2) 可能な限り 非貪欲 や 独占的量詞 を使う、 (3) re2 モジュール (バックトラックしない) を使う、 (4) timeout 機能のある regex モジュールへ切替。
数式に出てくる記号の意味を 1 つずつ確認しましょう。
.\d\w*+()正規表現 \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 アドレスを抽出する典型例。
r'\d{1,3}(?:\.\d{1,3}){3}' で IPv4 を表現。re.compile で繰り返し使用する場合は事前コンパイル。findall で全マッチを取り出す。SSDSE-B-2026 の列名は A1101_総人口、 A2101_15歳未満人口、 B1101_出生数 のような形式。 正規表現でコードと名称を一気に分離:
| 入力列名 | 正規表現 | コード (group 1) | 名称 (group 2) |
|---|---|---|---|
A1101_総人口 | ^([A-Z]\d{4})_(.+)$ | A1101 | 総人口 |
A2101_15歳未満人口 | ^([A-Z]\d{4})_(.+)$ | A2101 | 15歳未満人口 |
B1101_出生数 | ^([A-Z]\d{4})_(.+)$ | B1101 | 出生数 |
E2101_完全失業率 | ^([A-Z]\d{4})_(.+)$ | E2101 | 完全失業率 |
I5102_1人当たり医療費 | ^([A-Z]\d{4})_(.+)$ | I5102 | 1人当たり医療費 |
この 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) |
| 目的 | 正規表現 |
|---|---|
| 日本の郵便番号 | 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 文字列で電話番号正規表現の一致を計算する。
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)}") |
💬 手計算 (Step 2) 3/5 と 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'] |
SSDSE データ読み込み → 列名分離 → 都道府県コード抽出 → 数値の正規化 まで一気に:
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()) |
このコードで 100 行以上の SSDSE データを 1 分以内に整形できます。 正規表現なしでは数時間の手作業になります。
下のライブ正規表現テスターで、 パターンを入力すると対象テキストのマッチ箇所がリアルタイムでハイライトされます。 対象テキストは架空のデータクレンジング風サンプル(電話番号・日付・金額・メールが混在した問い合わせログ)で、 自由に書き換え可能。 プリセットボタンで代表パターンを一発投入でき、 ( ) のキャプチャ結果、 replace による置換結果も同時に確認できます。 マッチの位置は下のマッチマップ (SVG) に表示され、 クリック/ドラッグ/スワイプで注目するマッチを切り替えられます。 処理は JavaScript の RegExp で正確に実行し、 不正なパターンはエラー表示します。
( ) の中身(注目中のマッチ)プリセット「数字列 \d+」を押すと、 電話番号・日付・金額の数字部分がすべて緑に光ります。 これが正規表現の本質で、 「数字が 1 個以上続く場所」という鋳型 (パターン) を 3 文字で宣言しただけで、 テキスト中の該当箇所が全部拾えます。 次に「日付」「電話番号」を押すと、 鋳型を具体的にするほどマッチが絞り込まれる様子が分かります。 \d{4}(数字ちょうど 4 個)のように量化子で回数を指定し、 [-/.] のように文字クラスで「この中のどれか 1 文字」と書く ── この 2 つの組み合わせだけで、 実務のパターンの大半は書けます。
\d{4}.\d{2}.\d{2}) を押すと、 ドット区切りの日付だけでなく 2026-04-01 までマッチします。 . は「任意の 1 文字」だからです。 「\. でエスケープ」を押すと 2026.04.03 だけに絞られます。 リテラルの . + * ? ( ) [ ] は必ず \ でエスケープ。<A101> と <B202> と <C303> が丸ごと 1 個のマッチになります (.* は最長一致)。 「非貪欲 <.*?>」に切り替えると、 タグ 3 個が別々にマッチします。 マッチ件数が 1 → 3 に変わるのをマッチマップで確認してください。090-… なので、 \d+ ではマッチしません(JavaScript でも Python の re のデフォルトでも \d は原則半角)。 プリセット「全角数字 [0-9]+」で初めて拾えます。 日本語の実データでは、 正規表現を書く前に unicodedata.normalize('NFKC', s) で全角→半角に正規化するのが定石です(データクレンジング参照)。プリセット「グループ( )+置換」を押すと、 日付が ( ) で年・月・日の 3 グループに分解され、 下のキャプチャ表に $1 $2 $3 の中身が表示されます。 置換欄の $1年$2月$3日 により表記が統一される ── これが pandas の df[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 のコード A1101 を A\d+ で拾ったら A11010 まで巻き込んだ ── など、 silent failure が支配的です。 以下は実務で頻発する 4 パターン。
.* は最長マッチ。 .*? で最短化することが多い。. や + をリテラルとして使うときは \. \+。\w は環境で日本語マッチが変わる。 re.UNICODE を意識。(a+)+b のようなネストした量化子は、 マッチしない入力で指数時間。 必ず 所有量化 や非backtrack 設計を。.* はファイル全体を飲み込みかねない。 必要なら .*? や [^>]* を。. は任意 1 文字。 リテラル「.」 を表すには \.。\w は Python 3 で日本語を含むが、 一部の言語(Go, JavaScript) では英数字のみ。^ と $ はデフォルトで文字列の先頭・末尾のみ。 各行の先頭・末尾にしたいなら re.MULTILINE フラグ。. は通常改行を含まない。 改行を含めたいなら re.DOTALL。re.IGNORECASE。r"..."。 "\d" のような書き方は警告。re.escape で逃がす。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 アプリで正規表現を使うときは要注意。
| タスク | 正規表現 | 他手法 | 推奨 |
|---|---|---|---|
| CSV / TSV パース | エッジケース多 | csv, pandas | 専用ライブラリ |
| HTML パース | 壊れやすい | BeautifulSoup, lxml | 専用ライブラリ |
| JSON | 不可能 | json モジュール | 専用 |
| 単純なパターン抽出 | 最強 | str メソッド | 正規表現 |
| 大量テキスト検索 | 遅い場合あり | grep, ripgrep, ag | CLI ツール |
| 自然言語の意味抽出 | 不向き | spaCy, GiNZA, LLM | NLP ライブラリ |
| プログラミング言語パース | 原理的に不可能 | ASTパーサー | 専用パーサー |
1956 年、 数学者 Stephen Kleene が「regular set(正則集合)」 という概念を発表したのが起源。 Kleene 閉包(*) は彼の名前にちなみます。 これは 有限オートマトン で受理できる言語のクラスです。
1968 年、 Ken Thompson(後の Unix 開発者) が QED エディタに正規表現エンジンを実装。 これが grep("global regular expression print") として独立。 「grep」 はその後の Unix 文化の象徴に。
1980 年代後半、 Henry Spencer の regex ライブラリと 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 件のテストをすべて通過') |
正規表現を pytest で守ることで、 リファクタしても挙動が変わらないことを保証できます。 SSDSE データの列名パース処理にも適用すべき手法。
.* アイコン)。re.DEBUG フラグでパース過程を可視化。. * + ? [ ])を覚える。 regex101 で 20 個練習。\d, \w, \s, {n})。 SSDSE 列名で実践。(...), (?:...), (?P<name>...))。 抽出パターン作成。^, $, \b)と先読み・後読み。 URL/メール のバリデーション。複雑なパターンでは (?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 |
(?=...) 肯定先読み、 (?!...) 否定先読み、 (?<=...) 肯定後読み、 (?<!...) 否定後読み。 マッチには含めず 文脈条件を指定。
1 2 3 4 | # 「円」 が後ろにある数字だけ抽出 re.findall(r'\d+(?=円)', '1000円 と 200ドル') # ['1000'] # 「$」 が前にある数字 re.findall(r'(?<=\$)\d+', '$1000 と 200円') # ['1000'] |
\1, \2 で前のグループにマッチした文字列を再利用。 HTML タグの対応チェックや繰り返し検出に有用。
1 2 | # 重複単語検出 re.findall(r'\b(\w+)\s+\1\b', 'the the cat sat') # ['the'] |
(?(1)yes|no) で「グループ 1 がマッチしたか」 で異なるパターンを使う。 PCRE/Python 標準で利用可能。
(?>...) で「バックトラッキングしない」 領域を定義。 catastrophic backtracking 対策。 Python 標準 re はサポートしないので regex ライブラリ。
\p{L} 文字、 \p{N} 数字、 \p{Han} 漢字。 regex ライブラリで使える。 多言語処理で強力。
(?i) で大文字小文字無視、 (?m) でマルチライン、 (?s) でドット改行マッチ。 パターン内で局所的に適用可能。
re.VERBOSE で空白とコメントを無視。 複雑なパターンが 読みやすく なる。
1 2 3 4 5 6 7 | pat = re.compile(r""" ^ # 先頭 (?P<code>[A-Z]\d{4}) # SSDSE コード _ # 区切り (?P<name>.+) # 変数名 $ # 末尾 """, re.VERBOSE) |
何度も使うパターンは re.compile() で事前コンパイル。 数倍の高速化が期待できます。 SSDSE 47 都道府県を処理するときは特に効果大。
finditer はメモリ効率的(イテレータ)、 sub は関数を渡せる(置換ロジック動的)。 大量データ処理の常套手段。
論文や業務レポートで「正規表現で前処理した」 と書くとき、 含めるべき情報:
| 項目 | 記述例 |
|---|---|
| 使用したパターン | r'^([A-Z]\d{4})_(.+)$' (SSDSE 列名分離) |
| エンジン・バージョン | Python 3.11 標準 re モジュール |
| フラグ | re.UNICODE (デフォルト)、 re.IGNORECASE (使用時) |
| マッチ件数 | 対象 100 列中 98 列がマッチ、 2 列が non-match |
| 非マッチの扱い | 対象から除外、 別ログに記録 |
| エッジケース | 列名に「-」 を含むケースは別パターンで対応 |
| テスト | pytest で 20 ケース全パス(リポジトリ参照) |
r"..." を使っているか^ $ でパターンを固定したか(a+)+ など) がないかre.compile したかSSDSE-B-2026 の列名は A1101_総人口 形式。 これを「総人口_A1101」 のように名称優先 に並び替えたい:
1 2 3 | df.columns = df.columns.str.replace( r'^([A-Z]\d{4})_(.+)$', r'\2_\1', regex=True ) |
「東京都」 → 「東京」、 「北海道」 → 「北海」、 「大阪府」 → 「大阪」、 「広島県」 → 「広島」:
1 2 3 | df['pref_short'] = df['都道府県'].str.replace( r'[都道府県]$', '', regex=True ) |
ただし「北海道」 を「北海」 に略すと意味が変わるので注意。 文脈に応じて。
列名「県民所得(千円)」 から単位を抽出し、 値を 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) |
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 ) |
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()) |
<.*> は HTML 全体を 1 つのタグと判定。 非貪欲 <.*?> もしくは文字クラス <[^>]*> を使う。. は既定で \n を含まない。 re.DOTALL または (?s) を必ず付ける。\d{5} だけでは「ABC12345XYZ」も部分マッチ。 完全一致なら ^\d{5}$。(?:...) で非キャプチャに。 速度と可読性が向上。r'\d+' を使わず '\\d+' と書くとエスケープが二重で読みにくい。\w がデフォで非日本語のみマッチ。 re.UNICODE または regex モジュールの \p{Hiragana} を使う。\b は ASCII 単語境界のみ判定。 日本語では機能しない、 (?<!\S) 等の lookaround で代替。(a+)+b のような重複ループはバックトラック爆発を引き起こす。 ReDoS 解説を必読。これらの罠を避けるには、 (1) raw string + 名前付きグループ + コメント (re.VERBOSE) の三点セットを常用すること。 また テスト駆動 で「失敗ケースを先に書く」習慣を付けると、 想定外のマッチを早期に潰せます。
正規表現は 文字列処理の万能ツール です。 SSDSE-B-2026 のような公的統計データの前処理から、 ログ解析、 入力バリデーション、 LLM 出力のパースまで、 用途は無限。 ただし 万能ではない ── HTML/CSV/JSON のような構造化データには専用ライブラリを、 自然言語の意味抽出には NLP を使うこと。
本ページのチートシート・実用パターン集・テスト駆動例を傍らに置いて、 SSDSE データを実際に動かしてみてください。 データ前処理、 ログデータ、 テーブル、 自然言語処理 も合わせて学ぶと、 データエンジニアリングの全体像が掴めます。
正規表現を中心に、 上下流の文字列処理・形式言語論・データクレンジング・pandas との接続関係を 6 つのノードに整理した。 SSDSE-B-2026 の市区町村コード抽出など、 実務でどの位置に当たるかを俯瞰できる。
正規表現を中心に、 上方向は「前提となる文字列・文字コードの知識」、 右上は「並列に使われる glob や fnmatch などの簡易パターン」、 右下は「発展領域である形式言語論・DFA/NFA・チョムスキー階層」、 下は「応用先のデータクレンジング・ログ解析・入力バリデーション」、 左下は「対比として CSV/JSON 用の専用パーサ」、 左上は「pandas .str.contains/extract との統合」を示している。 SSDSE-B-2026 で郵便番号や年度表記を統一する作業は、 主に「応用」と「統合」の交点に位置する。
このマップから読み取れる重要な含意は、 正規表現は形式言語論という理論的基盤の上に乗り、 その下流で pandas や ETL パイプラインに組み込まれる という構造である。 すなわち単独で動く道具ではなく、 上下流の手法と組み合わせて初めて SSDSE のような実データを安全に扱える。 とくに ReDoS や Unicode 周辺は「発展」ノードから降りてくる注意点として位置づけられる。
「正規表現」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
パターンが固定的な情報(電話番号・郵便番号・ログ書式)を 確実に 取り出す前処理段で「正規表現」が機能し、 後段の統計・機械学習の品質を底上げする。
「正規表現」を実際の課題に当てはめるとき、 対象テキストの構造と精度要件で判定する。
r'\d{3}-\d{4}'。 「東京都〜区」のように揺らぎ大 → 形態素解析 や N-gram へre.findall、 入力フォームの妥当性確認 → re.fullmatch、 全角を半角に統一 → re.sub(a+)+ のような nested quantifier を避け、 re2 や データクレンジング 専用ライブラリで線形時間保証を取るSSDSE-B-2026 で「市区町村コード R01100」「年度 2026」のような固定書式は正規表現が最短。 自由記述コラムを扱う場合は pandas の .str.extract でグループ抽出するか、 NLP 手法に切り替える。
正規表現を「賢い検索」と思うと足をすくわれる。 正確には 文字の並び(形)だけを照合する検問所 であり、 その文字列が 実在するか・大きいか・正しいか は一切判断しない。 パターンに合格することは「書式が整っている」ことの証明であって、 「中身が妥当」であることの証明ではない。 この一線を引けるかどうかが、regex を使えるかどうかの分水嶺になる。
SSDSE-B-2026 のコード列 Code は R01000 から R47000 まで47件。 ^R\d{5}$ は47件すべてに合格するが、 同じパターンは実在しない R99000 や R00000 も平然と通してしまう。 「形が合う」と「その番号が存在する」は別の話だ、というのが直感の核心。
① 「\d{5} に合格 = 妥当なコード」ではない。 実データで確認すると、コード列は次の通り全件が形式チェックを通る(実測):
書式検証(regex)と存在検証(マスタ照合)を混同しないこと。 ^R\d{5}$ を通した後で、code in valid_code_set のように 集合照合を別途 かけて初めて「実在するコード」と言える。
② 接尾辞の一括除去は北海道を壊す。 都道府県名から接尾辞を落とすとき、 re.sub(r'[都道府県]$', '', name) と一括指定したくなる。 しかし実データの語尾分布は偏っている(実測):
県$ だけを剥がすと4件(北海道・東京都・京都府・大阪府)が接尾辞を残して不統一になり、 [都道府県]$ を一括で剥がすと 北海道が「北海」に化ける。 どちらも罠。 安全策は「県のみ除去し、残り4件はホワイトリストで明示変換する」か、 そもそも接尾辞を削らず正式名称のまま突き合わせること。 正規表現は語尾の1文字が「除いてよい飾り」か「地名の一部」かを区別できない。
③ 貪欲マッチはコードでも牙をむく。 コード R01000 から「先頭以降・末尾の0まで」を掴もうと R(.*)0 と書くと、 貪欲な .* は 最後の 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 の実測値。合成データは使用していない。)