読み込み中...

C++のregexec関数を使った正規表現の活用例9選

C++のregexec関数を使用したコーディング例のイメージ C++
この記事は約23分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

この記事はC++でのプログラミングにおいて、特に正規表現を使いこなすための重要な関数であるregexec関数に焦点を当てています。

正規表現は、テキストデータの検索や置換、データバリデーションなど、多岐にわたる場面で活用されます。

この技術をマスターすることは、効率的なプログラミングを実現する上で非常に役立ちます。

初心者から中級者まで、具体的なサンプルコードと共に、この関数の基本的な使い方から応用までを段階的に解説していきます。

●regexec関数とは

regexec関数は、C++で利用可能なPOSIX互換正規表現ライブラリの一部で、特定のパターンに基づいてテキストストリング内を検索するために使用されます。

この関数を利用することで、複雑なテキスト処理をプログラム的に扱うことが可能になり、ソフトウェア開発の多くの面でその力を発揮します。

○regexec関数の基本概念

regexec関数を理解する前に、正規表現自体の基本から始めましょう。

正規表現は、テキストに含まれる文字列のパターンを記述するための強力な言語です。

これを利用することで、一致する文字列を「検索」、「置換」、あるいは「抽出」することができます。

C++におけるregexec関数は、この正規表現を使って特定のパターンと文字列が一致するかをチェックするメカニズムを提供します。

○regexec関数のシグネチャと引数の詳細

regexec関数のプロトタイプは下記のようになります。

int regexec(const regex_t *preg, const char *string, size_t nmatch, regmatch_t pmatch[], int eflags);

ここで、各引数の役割は下記の通りです。

  • preg -> コンパイルされた正規表現へのポインタ
  • string -> 検索対象のテキスト
  • nmatch -> pmatch配列のサイズ(マッチング情報を格納するための配列)
  • pmatch -> マッチング結果の詳細を格納する配列
  • eflags -> 実行時フラグ、マッチングの挙動を細かく制御するために使用されます

この関数の戻り値は、マッチが見つかった場合は0、見つからなかった場合はREG_NOMATCH、その他エラーが発生した場合はエラーコードを返します。

各エラーコードは、特定の問題を示しており、デバッグの際に役立ちます。

●regexec関数の使い方

regexec関数を実際に使ってみることで、その力をより深く理解できます。

まずは基本的な文字列マッチングから始め、徐々に複雑なパターンマッチングへとステップアップしていきます。

具体的な使用例とともに、それぞれのステップで必要なコードの書き方と、得られる結果の解釈方法を見ていきましょう。

○サンプルコード1:基本的な正規表現のマッチング

最初の例として、あるパターンに一致する単語をテキストから探す基本的な使い方を紹介します。

例えば、「cat」という単語が入力テキスト中に存在するかどうかを確認するシンプルなケースです。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    int ret;
    char msgbuf[100];

    // 正規表現をコンパイル
    ret = regcomp(&regex, "cat", 0);
    if (ret) {
        fprintf(stderr, "正規表現のコンパイルに失敗しました\n");
        return 1;
    }

    // テキストを検索
    ret = regexec(&regex, "a cat and a dog", 0, NULL, 0);
    if (!ret) {
        printf("パターンが見つかりました。\n");
    } else if (ret == REG_NOMATCH) {
        printf("パターンが見つかりませんでした。\n");
    } else {
        regerror(ret, &regex, msgbuf, sizeof(msgbuf));
        fprintf(stderr, "正規表現の検索エラー: %s\n", msgbuf);
    }

    // 使用が終わった正規表現を解放
    regfree(&regex);
    return 0;
}

このコードでは、regcompで正規表現をコンパイルし、regexecでテキストに対して実際にマッチングを行っています。

マッチングの結果は、戻り値をチェックすることで得られます。

○サンプルコード2:マッチング結果の取得と表示

次に、マッチングした具体的な文字列の位置を取得する方法を見ていきます。

この例では、入力テキスト中で正規表現に一致する部分の開始位置と終了位置を表示しています。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    int ret;
    regmatch_t pmatch[1];
    char msgbuf[100];

    // 正規表現をコンパイル
    ret = regcomp(&regex, "cat", 0);
    if (ret) {
        fprintf(stderr, "正規表現のコンパイルに失敗しました\n");
        return 1;
    }

    // テキストを検索
    ret = regexec(&regex, "he has a cat", 1, pmatch, 0);
    if (!ret) {
        printf("パターンが見つかりました。位置: %d - %d\n", pmatch[0].rm_so, pmatch[0].rm_eo);
    } else if (ret == REG_NOMATCH) {
        printf("パターンが見つかりませんでした。\n");
    } else {
        regerror(ret, &regex, msgbuf, sizeof(msgbuf));
        fprintf(stderr, "正規表現の検索エラー: %s\n", msgbuf);
    }

    // 使用が終わった正規表現を解放
    regfree(&regex);
    return 0;
}

このコードは、pmatch配列を使用してマッチングした部分文字列の位置情報を取得しています。

このように、regexecはパターンマッチングの結果だけでなく、その詳細も提供するため、非常に強力です。

○サンプルコード3:複数のマッチングパターンの処理

複数の異なるパターンを同時にチェックしたい場合の処理方法を紹介します。

この例では、複数の単語が与えられたテキスト内に存在するかどうかを確認しています。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    int ret;
    char msgbuf[100];
    char *words[] = {"cat", "dog", "bird", NULL};
    char *text = "he has a cat and a dog";

    for (int i = 0; words[i] != NULL; i++) {
        // 正規表現をコンパイル
        ret = regcomp(&regex, words[i], 0);
        if (ret) {
            fprintf(stderr, "正規表現のコンパイルに失敗: %s\n", words[i]);
            continue;
        }

        // テキストを検索
        ret = regexec(&regex, text, 0, NULL, 0);
        if (!ret) {
            printf("%s が見つかりました。\n", words[i]);
        } else if (ret == REG_NOMATCH) {
            printf("%s は見つかりませんでした。\n", words[i]);
        } else {
            regerror(ret, &regex, msgbuf, sizeof(msgbuf));
            fprintf(stderr, "正規表現の検索エラー: %s\n", msgbuf);
        }

        // 使用が終わった正規表現を解放
        regfree(&regex);
    }
    return 0;
}

この例では、複数の検索語を配列で管理し、それぞれに対して繰り返し正規表現のマッチングを行っています。

これにより、複数のパターンを効率的に処理することが可能です。

○サンプルコード4:大規模なテキストデータの処理

大量のデータに対する正規表現マッチングを効率的に行う方法を見ていきます。

特に、大きなファイルやストリームからのデータ処理に適しています。

#include <stdio.h>
#include <stdlib.h>
#include <regex.h>

#define BUFFER_SIZE 1024

int main() {
    regex_t regex;
    char buffer[BUFFER_SIZE];
    FILE *fp;
    int ret;

    // 正規表現をコンパイル
    if (regcomp(&regex, "\\bword\\b", REG_EXTENDED)) {
        fprintf(stderr, "正規表現のコンパイルに失敗しました\n");
        return 1;
    }

    // ファイルを開く
    fp = fopen("largefile.txt", "r");
    if (!fp) {
        perror("ファイルを開けません");
        regfree(&regex);
        return 1;
    }

    // ファイルを一行ずつ読み込みながら検索
    while (fgets(buffer, BUFFER_SIZE, fp)) {
        if (!regexec(&regex, buffer, 0, NULL, 0)) {
            printf("マッチが見つかりました: %s", buffer);
        }
    }

    // ファイルを閉じる
    fclose(fp);

    // 使用が終わった正規表現を解放
    regfree(&regex);
    return 0;
}

このコードは、ファイルからテキストを読み込みながら、逐次的に正規表現による検索を行っています。

大規模なデータも効率的に処理することが可能です。

○サンプルコード5:エラーハンドリングと例外処理

正規表現のマッチング処理中に発生する可能性のあるエラーを適切に処理する方法について説明します。

エラーハンドリングは、堅牢なアプリケーションを開発する上で重要な要素です。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    int ret;
    char msgbuf[100];

    // 正規表現をコンパイル
    ret = regcomp(&regex, "[a-z]+", REG_EXTENDED);
    if (ret) {
        regerror(ret, &regex, msgbuf, sizeof(msgbuf));
        fprintf(stderr, "正規表現のコンパイルエラー: %s\n", msgbuf);
        return 1;
    }

    // テキストを検索
    ret = regexec(&regex, "123", 0, NULL, 0);
    if (ret == REG_NOMATCH) {
        printf("パターンが見つかりませんでした。\n");
    } else if (ret) {
        regerror(ret, &regex, msgbuf, sizeof(msgbuf));
        fprintf(stderr, "正規表現の検索エラー: %s\n", msgbuf);
    } else {
        printf("パターンが見つかりました。\n");
    }

    // 使用が終わった正規表現を解放
    regfree(&regex);
    return 0;
}

このコードでは、エラーが発生した場合にregerror関数を使用して、エラーメッセージを取得し、適切に処理しています。

このようなエラーハンドリングを行うことで、プログラムの信頼性を高めることができます。

●よくあるエラーと対処法

C++におけるregexec関数を使用する際に遭遇する可能性がある一般的なエラーには様々なものがあります。

それぞれのエラーに対して効果的な対処法を知ることは、プログラムをより強固にし、開発効率を向上させるために不可欠です。

主なエラーとその解決策を説明します。

○正規表現が正しくない場合

まず、正規表現が正しくない場合、regcomp()関数からエラーが返されます。

この関数は正規表現をコンパイルする際に使われ、エラーがあるとそれを指摘します。

エラーメッセージはregerror()関数を使用して取得でき、どのような問題が発生しているかを具体的に知ることができます。

regex_t regex;
char msgbuf[100];
int ret = regcomp(&regex, "[Z-a]", 0); // 不正な範囲指定
if (ret) {
    regerror(ret, &regex, msgbuf, sizeof(msgbuf));
    fprintf(stderr, "正規表現のコンパイルエラー: %s\n", msgbuf);
}
regfree(&regex);

このコードは、正規表現に誤った範囲指定がされている場合のエラーメッセージを表示します。

適切な範囲を指定することでこの問題は解決されます。

次に、実行時のエラーとしてREG_NOMATCHがあります。

これは指定された正規表現が対象のテキストに一致しなかった場合に返されるエラーコードです。

この場合の対処法は、正規表現を見直すか、対象のテキストが期待する内容を含んでいるかを確認することです。

さらに、実行時にregexec()関数が予期せず大量のリソースを消費することがあります。

これは特に「キャタストロフィック・バックトラッキング」と呼ばれる問題で、複雑すぎる正規表現や、繰り返しを多用する正規表現に起因します。

この問題を避けるためには、正規表現を単純化するか、非貪欲な量指定子を利用することが推奨されます。

// 非貪欲なマッチングへの変更例
regex_t regex;
char msgbuf[100];
int ret = regcomp(&regex, ".*?Z", 0); // 'Z' の前の任意の最短マッチ
if (ret) {
    regerror(ret, &regex, msgbuf, sizeof(msgbuf));
    fprintf(stderr, "正規表現のコンパイルエラー: %s\n", msgbuf);
}
regfree(&regex);

このコードは、任意の文字列の後に続く文字 ‘Z’ に最短でマッチするように正規表現を変更する方法を表しています。

○エラーコードの解読方法

C++のregexec関数を使用する際、エラーコードの正確な解読はトラブルシューティングに非常に重要です。

regerror()関数を使用して、エラーコードに対応する具体的な説明を取得することができます。

これにより、何が問題なのかを正確に把握し、適切な修正を行うことが可能になります。

○頻出するマッチングエラーとその解決策

正規表現で最も頻繁に遭遇する問題の一つが、意図しない文字列にマッチしてしまうことです。

このような問題は、正規表現が適切に設計されていないことが原因で発生します。

たとえば、.(ドット)があらゆる単一文字にマッチするため、より具体的な文字クラスを使用するか、文字列の境界を明示することで解決できます。

regex_t regex;
char msgbuf[100];
int ret = regcomp(&regex, "^\\d{3}-\\d{2}-\\d{4}$", 0); // 米国の社会保障番号形式
if (ret) {
    regerror(ret, &regex, msgbuf, sizeof(msgbuf));
    fprintf(stderr, "正規表現のコンパイルエラー: %s\n", msgbuf);
}
regfree(&regex);

このコードは、社会保障番号の形式に正確にマッチするための正規表現を表しており、意図しないマッチングを防ぐために具体的なパターンを定義しています。

●regexec関数の応用例

regexec関数の応用範囲は広く、多くのプログラミングシナリオでその力を発揮します。

ここでは、いくつかの実際の応用例を通じて、どのようにしてこの強力な関数が日常のコーディング作業を支援するかを解説します。

○サンプルコード6:ファイルからのデータ抽出

大量のテキストデータから特定の情報を効率的に抽出することは、データ分析や情報処理の現場で頻繁に求められる作業です。

下記のコードは、ログファイルからエラーメッセージを検索し、その一部を抽出する方法を表しています。

#include <regex.h>
#include <stdio.h>

int main() {
    FILE *fp;
    char line[1024];
    regex_t regex;
    regmatch_t matches[1];

    // 正規表現のコンパイル
    if (regcomp(&regex, "ERROR: (.+)", REG_EXTENDED)) {
        fprintf(stderr, "正規表現のコンパイルに失敗しました。\n");
        return 1;
    }

    fp = fopen("system.log", "r");
    if (fp == NULL) {
        perror("ファイルを開けません");
        regfree(&regex);
        return 1;
    }

    // ファイルを行ごとに読み込み
    while (fgets(line, sizeof(line), fp)) {
        if (!regexec(&regex, line, 1, matches, 0)) {
            printf("発見したエラー: %.*s\n", matches[0].rm_eo - matches[0].rm_so, line + matches[0].rm_so);
        }
    }

    fclose(fp);
    regfree(&regex);
    return 0;
}

このプログラムは、エラーメッセージを含む行を特定し、エラーの内容を抽出して表示しています。

○サンプルコード7:ログファイルの解析

サーバーやアプリケーションのログファイルを解析する際、特定のパターンの発生を追跡することは、システムの正常性を監視する上で重要です。

下記の例は、特定のステータスコードを含むログエントリを検出する方法を表しています。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    char *logEntry = "10.0.0.1 - status [200] OK";
    int ret;

    // 正規表現のコンパイル
    ret = regcomp(&regex, "\\[404\\]", REG_EXTENDED);
    if (ret) {
        char error_message[100];
        regerror(ret, &regex, error_message, sizeof(error_message));
        fprintf(stderr, "コンパイルエラー: %s\n", error_message);
        return 1;
    }

    // ログエントリの検索
    if (!regexec(&regex, logEntry, 0, NULL, 0)) {
        printf("404エラーが見つかりました。\n");
    } else {
        printf("404エラーはありません。\n");
    }

    regfree(&regex);
    return 0;
}

このスクリプトは、404エラーがログに存在するかどうかをチェックします。

異なるステータスコードや条件で同様の検索を行うことが可能です。

○サンプルコード8:ウェブページからの情報抽出

Webスクレイピングは、ウェブページから有用な情報を抽出する一般的な方法です。

正規表現を利用してHTMLから特定のデータを抽出する例を紹介します。

#include <regex.h>
#include <stdio.h>

int main() {
    const char *html = "<title>Example Domain</title>";
    regex_t regex;
    regmatch_t pmatch[2];

    // 正規表現のコンパイル
    regcomp(&regex, "<title>([^<]+)</title>", REG_EXTENDED);

    // HTMLからタイトルタグの内容を抽出
    if (!regexec(&regex, html, 2, pmatch, 0)) {
        printf("ページタイトル: %.*s\n", pmatch[1].rm_eo - pmatch[1].rm_so, html + pmatch[1].rm_so);
    }

    regfree(&regex);
    return 0;
}

このコードは、HTML内の<title>タグの中身を抽出します。

この技術を応用して、より複雑なウェブページからのデータ抽出も可能です。

○サンプルコード9:ユーザー入力のバリデーション

ウェブアプリケーションやフォームでのユーザー入力を検証することは、セキュリティとデータ整合性を保つ上で不可欠です。

下記の例では、ユーザーが入力したメールアドレスが適切なフォーマットを持っているかを確認しています。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    char *email = "user@example.com";
    int ret;

    // メールアドレスの正規表現パターンのコンパイル
    ret = regcomp(&regex, "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$", REG_EXTENDED);
    if (ret) {
        char error_message[100];
        regerror(ret, &regex, error_message, sizeof(error_message));
        fprintf(stderr, "正規表現のコンパイルエラー: %s\n", error_message);
        return 1;
    }

    // メールアドレスの検証
    if (!regexec(&regex, email, 0, NULL, 0)) {
        printf("有効なメールアドレスです。\n");
    } else {
        printf("無効なメールアドレスです。\n");
    }

    regfree(&regex);
    return 0;
}

このスニペットは、指定されたメールアドレスが正しいフォーマットに従っているかを確認します。

この手法は、さまざまな種類の入力検証に応用可能です。

●エンジニアなら知っておくべき豆知識

C++におけるプログラミング作業をより効率的に行うためには、いくつかの重要なテクニックや考慮すべき点があります。

特に、正規表現の使用や関数のパフォーマンスについての知識は、日々の開発活動において非常に役立ちます。

○regexec関数のパフォーマンス最適化

正規表現の実行パフォーマンスを最適化することは、特に大規模なテキスト処理を行う際に重要です。

例えば、正規表現の中で非常によく使われる「.*」というパターンは、できるだけ使用を控え、より具体的な表現に置き換えることが推奨されます。

これにより、無駄なバックトラッキングを避け、処理速度を向上させることができます。

下記のコード例では、より効率的な正規表現を用いてテキストから特定のデータを抽出しています。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    int ret;
    regmatch_t pmatch[1];
    char *str = "Example: This is a sample text.";

    // 効率的な正規表現のコンパイル
    ret = regcomp(&regex, "Example: ([^\\.]*)\\.", REG_EXTENDED);
    if (ret) {
        fprintf(stderr, "正規表現のコンパイルに失敗しました\n");
        return 1;
    }

    // テキストを検索
    ret = regexec(&regex, str, 1, pmatch, 0);
    if (!ret) {
        printf("抽出したテキスト: '%.*s'\n", pmatch[0].rm_eo - pmatch[0].rm_so, str + pmatch[0].rm_so);
    } else if (ret == REG_NOMATCH) {
        printf("パターンが見つかりませんでした。\n");
    } else {
        char msgbuf[100];
        regerror(ret, &regex, msgbuf, sizeof(msgbuf));
        fprintf(stderr, "正規表現エラー: %s\n", msgbuf);
    }

    regfree(&regex);
    return 0;
}

この例では、「Example:」という文字列に続く文をピリオドまで抽出しています。

このように具体的な条件を指定することで、処理の効率を大幅に改善することができます。

○POSIX正規表現と他の正規表現ライブラリの比較

POSIX正規表現と他のライブラリ(例えばPCRE、Perlの正規表現など)との間には、いくつかの違いが存在します。

POSIX正規表現は、互換性とポータビリティが高い一方で、PCREなどのライブラリに比べて機能が限定されている場合があります。

例えば、後読みアサーションや非貪欲マッチングなど、PCREには存在するがPOSIXではサポートされていない機能もあります。

開発者は、使用する正規表現ライブラリを選択する際に、必要とする機能とパフォーマンス要件を考慮する必要があります。

多くの場合、標準のライブラリで十分な機能を提供していますが、複雑な正規表現が必要な場合はPCREなどの専用ライブラリの使用を検討することも有効です。

正規表現の選択は、プロジェクトの要件に適応させることが重要です。

まとめ

この記事では、C++におけるregexec関数の基本から応用までを詳しく解説しました。

特に、POSIX正規表現と他のライブラリとの比較を通じて、どのような状況でどのライブラリを選択すべきかを紹介しました。

今後もこの知識を活用し、より洗練されたコードの書き方を追求しましょう。