🗜️

JavaScript の 正規表現replace でネストしたdivを平坦化(flatten)する, 小要素のないdivを消す

2020/12/23に公開3

もっといい正規表現でのやり方があったら教えてください。
npm パッケージとか使えばスッとできそうなんですが
ブックマークレットにCDNから読み込みたくなかっただけです。

TL;DR

  • ネストしたdivを平坦化(flatten)
    • targetString.replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>") を何度か繰り返す
  • 小要素のないdivを消す
    • targetString.replace(/<div>\s*<div>([^>]*)<\/div>\s*<\/div>/gs, "<div>$1</div>") を何度か繰り返す

なぜやったのか

ブックマークレットを作っていて

  • ネストしたdivを平坦化(flatten)
  • 小要素のないdivを消す

をreplaceでどうにかしたかったので、やりました。

どうやってそれぞれの正規表現に行き着いたか

正規表現チェッカーを使いながらいい感じのを考えました笑
オススメの正規表現チェッカーはこの2つです。

ネストしたdivを平坦化(flatten)

まず平坦化と言う表現が合ってるのかわからないのですが、
例を挙げると

こういうHTMLを

<div><div><div><div>foo</div></div></div></div>
<div>
  <div>
    <div><div>bar</div></div>
  </div>
</div>
<div><div>baz</div></div>

こうしたかったという話です。

<div>foo</div>
<div>bar</div>
<div>baz</div>

最終的に行き着いたのは
targetString.replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>") を何度か繰り返す
という方法で、コードとしてはこうしました。汚ねぇ……😂

targetString
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")
  .replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>");

他にも

targetString
  .replace(/<div>\s*<div>\s*<div>(.*)<\/div>\s*<\/div>\s*<\/div>/gs, "<div>$1</div>");

こんな感じで複数ネストを一気に単純 div 囲いに変換もできるのですが
この正規表現だとネスト数分のパターン用意しないといけないので、
何度か繰り返すことでまぁ大体大丈夫だろってところまでネストをなくすことにしました。


2020/12/23 追記
@catnose さん からコメントいただきまして、

replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>") の部分ですが、これだとネストされているときに閉じタグ(</div>)がずれてマッチされてしまうことがあるようです。
なので
text.replace(/<div>\s*<div>([^>]*)<\/div>\s*<\/div>/gs, "<div>$1</div>")

とのことです。
while を使った方法も提示してくださっているのでコメント欄も合わせてみてみてください


replace の replace(/../gs,'') この部分の s ですが
dotAll という ES2018 以降のオプションです。Can i use .. をみる限り大体のブラウザで既に対応されてそう。

s ("dotAll") フラグが true にセットされている場合は、改行文字にもマッチします。

平坦化(flatten) という表現は
多次元配列を単次元配列に変換する平坦化(flatten)とちょっと似てるなと思ったからです……。

小要素のないdivを消す

<div></div>
<div><div></div></div>
<div><div><div></div></div></div>

こういうのを消し去りたかった。
行き着いた正規表現は
targetString.replace(/<div>\s*<\/div>/g, "") を何度か繰り返す
で、コードとしてはこうしました。これも汚ねぇ……😂

targetString
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "")
  .replace(/<div>\s*<\/div>/g, "");

これもネストしている分まとめて複数div分を置換できるのですが
愚直にやった方がシンプルな気がしたのでこうしてます。

もっといいやり方があったら教えてください〜😂

Discussion

catnosecatnose

たしかに簡単そうで「あれ、どうやるんだ…?」ってなりますね。「ネストしたdivを平坦化」の方に取り組んでみました。

あまり綺麗とは言えない & HTML構造に間違いがあると無限ループの可能性があるので怖いですが…。

let text = "ネストされてるdiv";
while (/<div>\s*<div>/.test(text)) {
    text = text.replace(/<div>\s*<div>([^>]*)<\/div>\s*<\/div>/gs, "<div>$1</div>");
}
return text; // フラットになったdiv

replace(/<div>\s*<div>(.*)<\/div>\s*<\/div>/gs, "<div>$1</div>")の部分ですが、これだとネストされているときに閉じタグ(</div>)がずれてマッチされてしまうことがあるようです。なので

text.replace(/<div>\s*<div>([^>]*)<\/div>\s*<\/div>/gs, "<div>$1</div>")

のようにすると良いと思います!

mpywmpyw

PCRE ならもっとエレガントにできるのでは…
と思ったけど,再帰フェーズに入ったときの後方参照拾えなかったのでダメでしたw
供養します

<?php

$html = <<<EOD
<div><div><div><div>foo</div></div></div></div>
<div>
  <div>
    <div><div>bar</div></div>
  </div>
</div>
<div><div>baz</div></div>
EOD;

echo preg_replace(
    '@<div>\s*(?:(?R)|(.+?))\s*</div>@',
    '<div>$1</div>',
    $html
);

/*
<div></div> ←残念ながら中身は空になります
<div></div>
<div></div>
*/