JavaScript Tips | 文字列操作:文字列からHTMLタグを除去する

JavaScript JavaScript
スポンサーリンク
スポンサーリンク

業務・実務で使える JavaScript ユーティリティ:文字列からHTMLタグを除去する方法

JavaScriptでHTMLを含む文字列から本文だけを取り出したい場合は、HTMLを解析してからテキストを取得する方法が基本です。ブラウザーでは、DOMParser と textContent を組み合わせて実装できます。

たとえば、<p>こんにちは。<strong>お知らせ</strong>です。</p> から、こんにちは。お知らせです。 という文字列を取り出す処理です。記事の検索用データを作る、リッチテキストから概要を作る、HTML形式のデータをプレーンテキストへ変換する、といった用途で役立ちます。

ただし、HTMLタグの除去と、危険なHTMLを安全にする処理は別物です。また、単純にタグを取り除くと、段落や改行が失われたり、スクリプトの本文まで残ったりする場合があります。基本動作と制約を理解したうえで、用途に合った関数を設計することが重要です。

最初に整理する:タグ除去・テキスト抽出・サニタイズの違い

「HTMLタグを除去する」という表現には、複数の目的が混ざりやすいため、最初に整理しておきます。

  • タグ除去は、<p> や <strong> といったマークアップを取り除くことです。
  • テキスト抽出は、HTMLを解析し、必要な文章や文字列を取り出すことです。文字参照の解釈や、不要な要素の除外も関係します。
  • サニタイズは、HTMLとして利用するために、危険な要素や属性などを適切なルールで除去・制限することです。

この記事で扱うのは、主にHTMLをプレーンテキストへ変換する処理です。書式付きのHTMLを安全に残したい場合は、専用のサニタイザーを使う設計が必要になります。

ステップ1:入力と期待する出力を決める

コードを書く前に、どのようなHTMLを受け取り、どのような文字列を返すかを決めます。特に、タグの境界、改行、画像、スクリプトなどの扱いが重要です。

  • <p>商品説明</p> は、商品説明 にします。
  • <strong>重要</strong> は、強調の書式をなくして 重要 にします。
  • <br> を、何も残さず除去するか、改行へ変換するかを決めます。
  • <script> や <style> は、中の文字列も除外するかを決めます。
  • 画像の alt 属性やリンク先URLを、出力へ含めるかを決めます。
  • null や数値など、文字列以外を受け取った場合の扱いを決めます。

たとえば、検索用データなら改行を空白にまとめたい場合があります。一方、メール本文や一覧プレビューなら、段落の区切りをある程度残したほうが読みやすくなります。万能な変換結果を目指すのではなく、使い道に合わせて仕様を選びます。

ステップ2:DOMParserでHTMLを解析する

DOMとはHTMLを構造として扱う仕組み

HTMLは、単なる文字の並びではなく、要素が入れ子になった構造です。たとえば、段落の中に強調要素がある場合、ブラウザーは親子関係を持つデータとして扱います。この構造がDOMです。

DOMParser を使うと、HTML文字列を文書オブジェクトへ変換できます。parseFromString() の第2引数へ "text/html" を指定すると、HTMLとして解析されます。生成される文書は、現在表示しているページとは別の文書です。

// 解析するHTML文字列
const html = "<p>こんにちは。<strong>重要なお知らせ</strong>です。</p>";

// HTMLを解析するためのオブジェクトを作る
const parser = new DOMParser();

// 文字列をHTMLとして解析する
const doc = parser.parseFromString(html, "text/html");

// body内のテキストを取得する
const text = doc.body.textContent ?? "";

console.log(text);
// "こんにちは。重要なお知らせです。"
JavaScript

doc.body は、解析した文書の本文部分です。textContent を使うことで、その中にあるテキストをまとめて取得できます。?? "" は、取得結果が null または undefined だった場合に空文字を使う書き方です。

文字参照もHTMLのルールで解釈される

HTMLには、&amp; や &lt; のように、特定の文字を表す記法があります。HTMLとして解析すると、それぞれ対応する文字へ変換されます。

// HTML内の文字参照も解釈される
const html = "<p>A & B:<重要></p>";
const doc = new DOMParser().parseFromString(html, "text/html");

console.log(doc.body.textContent);
// "A & B:<重要>"
JavaScript

タグらしい見た目の文字が結果に残る場合でも、それが本文として記述された文字なら正常です。タグ除去は、山括弧のある文字を無条件に消す処理ではありません。

また、&nbsp; は通常の半角スペースではなく、改行しない空白文字へ変わります。比較や検索のために通常のスペースへ統一したい場合は、後処理として明示的に行います。

ステップ3:不要な要素を内容ごと取り除く

textContentはスクリプトやスタイルの本文も含む

textContent は、ノードとその子孫にあるテキストを取得します。<script> や <style> の内容も対象になるため、そのままではプログラムやCSSが文章に混ざる場合があります。また、CSSで非表示になっている要素のテキストも取得します。

記事本文などを取り出す場合は、不要な要素を先に削除してからテキストを取得すると、目的に近い結果になります。

// 本文に混ぜたくない要素を含むHTML
const html = `
  <p>商品のお知らせです。</p>
  <script>const internalValue = 123;</script>
  <style>p { color: red; }</style>
`;

const doc = new DOMParser().parseFromString(html, "text/html");

// script、style、templateを要素ごと削除する
doc.querySelectorAll("script, style, template").forEach(element => {
  element.remove();
});

// 残った本文を取得し、前後の余分な空白を削除する
const text = (doc.body.textContent ?? "").trim();

console.log(text);
// "商品のお知らせです。"
JavaScript

querySelectorAll() は、条件に一致する要素をまとめて取得します。remove() は、その要素と中身を文書から取り除きます。表示中のページではなく、解析して作った文書を操作しています。

なお、template の内容は通常の子要素とは別の仕組みで保持されるため、通常の body.textContent では取得されません。この例では、抽出対象にしない方針をコードでも明示しています。

ステップ4:改行や段落の扱いを決める

textContentは見た目の改行を自動では再現しない

HTMLの段落や改行は、通常のテキストノードとは異なります。したがって、<br> や段落の境界が、そのまま改行文字になるわけではありません。

// HTML上では改行や段落があっても、その境界は自動で改行にならない
const html = "<p>1行目<br>2行目</p><p>次の段落</p>";
const doc = new DOMParser().parseFromString(html, "text/html");

console.log(doc.body.textContent);
// "1行目2行目次の段落"
JavaScript

このままでは、別々の文章や項目がつながってしまうことがあります。読みやすさを残したい場合は、テキストを取得する前に、必要な場所へ改行文字を追加します。

brを改行文字に置き換える

// br要素を改行文字に置き換える
const html = "<p>東京都<br>千代田区</p>";
const doc = new DOMParser().parseFromString(html, "text/html");

doc.body.querySelectorAll("br").forEach(element => {
  // 要素を文字列で置き換えると、テキストとして挿入される
  element.replaceWith("\n");
});

console.log(doc.body.textContent);
// "東京都\n千代田区"
JavaScript

段落の区切りも残したい場合は、p や div などの末尾へ改行を追加します。ただし、要素の入れ子によって改行が増えるため、最後に連続する改行を整理します。

HTMLの見た目を完全に再現する変換ではない

この方法は、指定したタグを基準に区切りを付ける簡易的な変換です。CSSの display、疑似要素、複雑な表組みなどまでは再現しません。

innerText は表示状態を考慮するプロパティですが、別文書で解析した要素から取得すれば、画面と同じ結果になるとは限りません。テキスト抽出のために外部HTMLを表示中のページへ挿入する方法も避けます。

実務向けテンプレート:HTMLをプレーンテキストへ変換する

ここまでをまとめた、ブラウザー向けの関数が次の htmlToText() です。通常は単純なテキスト抽出を行い、必要な場合だけ改行の補助処理を有効にします。

  • 入力は文字列に限定します。
  • 文字列以外は TypeError にします。
  • script、style、template を除外します。
  • 画像の代替テキストや、リンク先URLは追加しません。
  • オプションで、改行と一部の段落境界を残します。
  • 結果の前後の空白は削除します。
/**
 * HTML文字列から本文のテキストを取り出します。
 *
 * ブラウザー向けの簡易変換です。
 * HTMLのサニタイザーとしては使用しないでください。
 *
 * @param {string} html 変換するHTML文字列
 * @param {boolean} preserveLineBreaks 改行の補助処理を有効にするか
 * @returns {string} 抽出したテキスト
 * @throws {TypeError} 引数の型が不正な場合
 */
function htmlToText(html, preserveLineBreaks = false) {
  // 意図しない自動文字列化を防ぐ
  if (typeof html !== "string") {
    throw new TypeError("html は文字列で指定してください");
  }

  // "false" などの文字列を誤って指定することを防ぐ
  if (typeof preserveLineBreaks !== "boolean") {
    throw new TypeError("preserveLineBreaks は boolean で指定してください");
  }

  // 空文字は、そのまま空文字として返す
  if (html === "") {
    return "";
  }

  // 表示中のページとは別の文書として解析する
  const doc = new DOMParser().parseFromString(html, "text/html");

  // 本文に含めない要素を内容ごと削除する
  doc.querySelectorAll("script, style, template").forEach(element => {
    element.remove();
  });

  if (preserveLineBreaks) {
    // brを改行文字へ変換する
    doc.body.querySelectorAll("br").forEach(element => {
      element.replaceWith("\n");
    });

    // 一部の段落・見出し・リスト項目の末尾へ改行を追加する
    const blockSelector =
      "p, div, section, article, header, footer, blockquote, " +
      "h1, h2, h3, h4, h5, h6, li, pre, tr";

    doc.body.querySelectorAll(blockSelector).forEach(element => {
      element.append("\n");
    });
  }

  // タグではなく、テキストだけを取得する
  let text = doc.body.textContent ?? "";

  if (preserveLineBreaks) {
    // 過剰な空行を簡易的に整理する
    text = text
      .replace(/\r\n?/g, "\n")
      .replace(/\n[ \t]+/g, "\n")
      .replace(/[ \t]+\n/g, "\n")
      .replace(/\n{3,}/g, "\n\n");
  }

  // この関数では、結果の前後の空白を削除する
  return text.trim();
}

// 基本的な抽出
console.log(
  htmlToText("<p>こんにちは。<strong>お知らせ</strong>です。</p>")
);
// "こんにちは。お知らせです。"

// 改行の補助処理を有効にする
console.log(
  htmlToText("<p>1行目<br>2行目</p><p>次の段落</p>", true)
);
// "1行目\n2行目\n次の段落"

// 不要な要素の中身を除外する
console.log(
  htmlToText("<p>本文</p><script>const value = 1;</script>")
);
// "本文"
JavaScript

このテンプレートの適用範囲

この関数は、一般的な本文HTMLを簡易的にテキスト化するための出発点です。完全なHTMLからテキストへの変換器ではありません。

たとえば、表のセル間にはタブを追加していないため、セルの内容がつながります。また、改行の整理は pre 内の空白や空行にも適用されるため、ソースコードの整形を厳密に保存する用途には向きません。元のHTMLに含まれるインデントも、一部がテキストとして残る場合があります。

表、コード、画像の代替テキスト、リンク先URLなどを正確に扱う必要がある場合は、DOMをたどって要素ごとに変換ルールを適用するか、用途に合うHTML-to-textライブラリーを検討します。

正規表現だけでタグを消す方法を基本にしない理由

単純なHTMLなら動いて見える

次のような正規表現は、短い例ではタグを取り除けるように見えます。

// 単純な置換例ですが、汎用的なHTML処理には使わないでください
const html = "<p>こんにちは</p>";
const text = html.replace(/<[^>]*>/g, "");

console.log(text); // "こんにちは"
JavaScript

しかし、この式はHTMLを理解しているわけではありません。単に、< から次の > までを削除しています。

属性値の中にある記号などで崩れる

HTMLでは、属性値の中に > が含まれる場合もあります。単純な正規表現では、そこでタグが終わったと誤認します。

// 属性値に > を含むHTML
const html = '<p title="1 > 0">判定結果</p>';

// この式では、属性値の途中までしか除去できない
const text = html.replace(/<[^>]*>/g, "");

console.log(text);
// ' 0">判定結果'
JavaScript

さらに、コメント、文字参照、不完全なHTML、スクリプトやスタイルの内容なども考慮が必要です。正規表現を少しずつ複雑にするより、HTMLパーサーへ任せるほうが、処理の目的に合っています。

入力形式が厳密に限定された内部データに対し、限定的な置換を行うことまで否定する必要はありません。ただし、外部から受け取る任意のHTMLへ流用しないことが重要です。

情報セキュリティ上の重要な注意点

DOMParserを使えば完全に無害になるわけではない

DOMParser がHTMLとして解析した別文書では、スクリプトは実行されない状態になります。ただし、画像やiframeなどが外部リソースを取得する可能性があります。また、解析したノードを表示中のDOMへ挿入すると、危険な属性や内容が有効になる可能性があります。

解析後に画像を削除すれば、解析時の通信まで確実に防げるとは限りません。外部通信を絶対に発生させたくない処理では、通信機能を持たないパーサーや、適切に隔離した処理環境を検討します。

この記事のコードでは、解析したDOMノードを画面へ挿入せず、取り出した文字列だけを利用します。それでも、解析時の副作用を含めて用途に合うかを確認する必要があります。

抽出したテキストはtextContentで表示する

HTMLから取り出した結果でも、文字参照の解釈により、山括弧を含む文字列になることがあります。その結果を innerHTML へ渡すと、再びHTMLとして解釈されます。

// HTMLとしてではなく、文字として表示するための要素
const output = document.createElement("div");

// 変換結果を文字列として設定する
output.textContent = htmlToText(
  "<p>記法の例:<strong></p>"
);

// 必要な場所へ、表示用の要素だけを追加する
document.body.append(output);
JavaScript

この例で使っているのは、通常の表示用 div です。文字列の表示が目的なら、HTMLとして再解釈させないことが基本です。抽出結果を「安全なHTML」として扱わないようにします。

Trusted Typesを強制している環境では追加対応が必要

CSPでTrusted Typesを強制している環境では、通常の文字列を parseFromString() へ渡すと TypeError になる場合があります。その場合は、アプリケーションで定めた安全な入力ポリシーに合わせて設計する必要があります。

単に制約を回避するため、すべての入力を無条件で信頼するポリシーを追加してはいけません。今回の文字列型チェックだけのテンプレートは、Trusted Types対応を含む汎用実装ではない点にも注意します。

ブラウザーとNode.jsでは実行環境を分けて考える

この記事のテンプレートは、DOMParser を利用できるブラウザー環境を前提にしています。Node.jsなどのサーバー側では、ブラウザーと同じDOM APIが使えることを前提にしないようにします。

サーバー側で処理する場合は、HTMLパーサーやHTML-to-textライブラリーを利用し、次の項目を確認します。

  • 文字参照をどのように解釈するか。
  • スクリプトやスタイルの本文を除外できるか。
  • 段落、表、リスト、画像の変換ルールを指定できるか。
  • 解析時にスクリプト実行や外部通信を行わない設定になっているか。
  • 入力サイズの制限や、依存ライブラリーの更新管理ができているか。

ブラウザーで動くコードを、そのままサーバー側へコピーしても動くとは限りません。ユーティリティの説明には、対応する実行環境も明記します。

テストで正常系と境界条件を確認する

HTMLの変換は、短い正常例だけでは仕様を確認しきれません。入れ子、文字参照、改行、空文字、不要な要素などをまとめてテストします。

// [HTML文字列, 改行を補助するか, 期待値]
const testCases = [
  ["<p>こんにちは</p>", false, "こんにちは"],
  ["<p>A<strong>B</strong>C</p>", false, "ABC"],
  ["<p>A & B</p>", false, "A & B"],
  ["<p><重要></p>", false, "<重要>"],
  ["<p>A<br>B</p>", false, "AB"],
  ["<p>A<br>B</p>", true, "A\nB"],
  ["<p>A</p><p>B</p>", true, "A\nB"],
  [
    "<p>本文</p><script>const value = 1;</script>",
    false,
    "本文"
  ],
  ["<p>本文</p><style>p { color: red; }</style>", false, "本文"],
  ["<!-- コメント --><p>本文</p>", false, "本文"],
  ["<p></p>", false, ""],
  ["", false, ""],
  ["通常の文章", false, "通常の文章"]
];

// 実際の値と期待値を比較する
for (const [html, preserveLineBreaks, expected] of testCases) {
  const actual = htmlToText(html, preserveLineBreaks);

  console.log({
    html,
    preserveLineBreaks,
    expected,
    actual,
    pass: actual === expected
  });
}
JavaScript

文字列以外の入力についても、例外になることを確認します。

// 文字列以外の値を渡した場合のテスト
const invalidValues = [null, undefined, 123, {}, []];

for (const value of invalidValues) {
  try {
    // この関数では、文字列以外は受け付けない
    htmlToText(value);

    console.error("失敗:期待した例外が発生しませんでした");
  } catch (error) {
    console.log({
      value,
      pass: error instanceof TypeError
    });
  }
}
JavaScript

さらに実際の業務データを使い、表のセルがつながらないか、不要な空白が残らないか、必要な代替テキストが失われていないかを確認します。こうした確認から変換ルールを追加していくと、用途に合った実装になります。

まとめ

JavaScriptで文字列からHTMLタグを除去したい場合は、ブラウザーでは DOMParser でHTMLを解析し、textContent でテキストを取得する方法が基本です。ただし、文章として使いやすい結果にするには、不要な要素や改行の扱いまで決める必要があります。

  • HTMLタグ除去と、HTMLのサニタイズを区別します。
  • 正規表現だけで任意のHTMLを処理しようとしないようにします。
  • script や style の内容を、本文へ混ぜないようにします。
  • 改行、段落、表、画像などの扱いを用途に合わせて決めます。
  • 解析時の外部リソース取得や、実行環境の制約も考慮します。
  • 抽出結果はプレーンテキストとして扱い、表示時にHTMLへ戻さないようにします。

重要なのは、単にタグらしい文字を消すことではなく、「HTMLのどの情報を、どの形で残すか」を設計することです。入力条件と変換ルールを明示し、実データを含むテストで確認することで、業務で使いやすいユーティリティになります。

タイトルとURLをコピーしました