文字列操作正規表現

【JavaScript】 入れ子のhtmlタグを正規表現で取得する

更新日:2021/10/11

ブラウザなら入れ子となったタグをDOMで簡単に取得できます。
しかしNode.jsなどのブラウザ以外のJavaScriptや、わざわざDOMに展開するまでもない場合などは、正規表現などでテキストのまま処理したいケースがあります。

今回は、その方法を考えてみました。

 

入れ子を考えない場合

入れ子を考えずにhtmlのタグを取得する場合、次のような正規表現を使ったコードが考えられます。

タグを取得する単純な正規表現

IDで取得


const id = "abc";
const regex = new RegExp( `<div(?:\\s+?|\\s+?.+?\\s+?)id="${ id }".*?>.*?<\\/div.*?>`,"s");
console.log( regex.exec( htmlText ) );

classで取得


const className = "abc";
const regex = new RegExp( `<div(?:\\s+?|\\s+?.+?\\s+?)class=(?:"|".*?\\s+?)${ 
        className }(?:"|\\s+?.*?").*?>.*?<\\/div.*?>` , "sg");
let result;
while( (result = regex.exec( htmlText )) !== null ){
    console.log( result );
}

idはhtml中に一つ、classは複数存在するという前提で、二つのコードを作成しています。

両方に共通する最初の括弧、(?:\s+?|\s+?.+?\s+?)はスペースのみ、またはスペース+何らかの文字列+スペースに対応します。最初の?:は、内容をキャプチャしないという意味です。

クラス名のマッチングは、前後に他のクラス名が入ることを想定しています。
IDの前後にスペースが入る可能性がありますが、レアケースなのでここでは対応していません。

閉じタグの\s*?は、スペースが挿入されているケースを想定しています。

なお上のコードは、文字列をRegExpのコンストラクタに渡して、正規表現オブジェクトを作成しています。
そのためエスケープ文字(\)を二つ重ねています。

次のように正規表現リテラルで作成する場合は、重ねる必要がありません。

const regex = /<div(?:\s+?|\s+?.+?\s+?)id="abc".*?>.*?<\/div.*?>/s;

ただしリテラルではid名等を変更できないので、両方を適切に使い分ける必要があります。

ちなみに最近の僕は物忘れが激しいのか、3回に一回は間違えて、原因にたどり着くまで10分くらいかかります(汗

 

入れ子を考慮する

前述のコードは、次のような入れ子に対応できません。

<div id="abc">
  <div></div>
</div>

なぜなら、最初の閉じタグとマッチしてしまうからです。

<div id="abc">
  <div></div> ← これとマッチ
</div>

もしかしたら正規表現のみで実現できるかもしれませんが、僕には無理だったので、単純なコードで取得してみます。

入れ子タグを取得するコード


const getNestedTag =  (tagName,matchData) =>{

    if( matchData === null ) return null;
    const buff = [ matchData[0] ];

    let restData = matchData.input.substring( matchData.index + matchData[0].length );
    const regex = new RegExp(`<(\\/?)${tagName}.*?>`);
    let count=0;
    while(1){
        const match = restData.match( regex );
        if( match === null ) return null;
        const index = match.index + match[0].length;
        buff.push( restData.substring( 0 , index ) );

        if( match[1] === "/" ){
            if( count === 0 ) break;
            count --;
        }else{ count ++; }
        restData = restData.substring( index );
    }
    return  buff.join("");
};

考え方は単純です。

後に続くタグが <div>だったら、カウントを+1、</div>だったら-1します。
カウントが0のとき</div>が現れたら、それが閉じタグです。

この関数は、タグ名( "div"や"p" )と、スタートとなるタグの検索結果を引数として受け取ります。
次のように使用します。

使用例

IDで取得


const id = "abc";
const result = new RegExp( `<div(?:\\s+?|\\s+?.+?\\s+?)id="${ id }".*?>`,"s")
                .exec( htmlText );

console.log( getNestedTag("div" , result ))

classで取得


const className = "abc";
const regex = new RegExp( `<div(?:\\s+?|\\s+?.+?\\s+?)class=(?:"|".*?\\s+?)${
    className }(?:"|\\s+?.*?").*?>` , "sg");

let result;
while( (result = regex.exec( data )) !== null ){
    console.log( getNestedTag("div" , result ) );
}

更新日:2021/10/11

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。