Back

テキストでDOM要素を検索する方法

テキストでDOM要素を検索する方法

DOM APIにはgetElementByText()は存在しません。要素が何であるかではなく、何を表示しているかに基づいて要素を特定する必要がある場合は、その機能を自分で構築する必要があります。これは思っている以上に頻繁に発生します — 自動化スクリプト、UIテスト、動的コンテンツの解析など — そして適切なアプローチは、どの程度の柔軟性が必要かによって異なります。

重要なポイント

  • DOM APIにはテキストコンテンツで要素を選択する組み込みメソッドはありませんが、3つのネイティブアプローチでそのギャップを埋めることができます:querySelectorAllでのフィルタリング、TreeWalkerでのトラバース、XPathでのクエリです。
  • TreeWalkerは、大きなNodeListを事前に収集することなく、あらゆる要素タイプにわたってDOM全体のテキスト検索を行うための最も汎用性の高いネイティブオプションです。
  • テキストマッチングにはinnerTextよりもtextContentを優先してください — より高速で、レイアウトの再計算をトリガーせず、要素の可視性に関係なく一貫して動作します。
  • 余分な空白、ネストされた子孫テキスト、スクリプト実行時に存在しない可能性がある動的に挿入されたコンテンツなど、よくある落とし穴に注意してください。

なぜquerySelectorはテキストコンテンツでDOMをクエリできないのか

querySelector()querySelectorAll()はCSSセレクタのみを受け入れます。CSSには:has()疑似クラスや属性セレクタがありますが、要素のテキストコンテンツをマッチングするための標準的なCSSセレクタは存在しません。div:text("Submit")のようなセレクタは仕様に存在しないのです。

そのため、3つの実用的なアプローチが残されています:候補セットをフィルタリングする、ネイティブAPIでDOMをトラバースする、またはXPathを使用する方法です。

方法1: テキストで候補セットをフィルタリング

最もシンプルなアプローチは、タグまたはクラスで要素をクエリし、その後テキストでフィルタリングすることです。これは要素タイプが事前にわかっている場合にうまく機能します。

function findByText(tag, text) {
  return [...document.querySelectorAll(tag)].filter(el =>
    el.textContent.trim() === text
  )
}

// 使用例
const buttons = findByText('button', 'Submit')

これは読みやすく、候補セットが小さい場合は高速です。弱点は、一度に1つの要素タイプしか検索できないことです。'*'ですべての要素を検索することは可能ですが、大きなDOMでは遅くなります。

方法2: TreeWalkerでDOMをトラバース

TreeWalkerは、ノードを効率的に走査できる組み込みのDOM APIです。すべてのモダンブラウザで十分にサポートされており、完全なNodeListを事前に収集するオーバーヘッドを回避できます。

function findElementsByText(root, text) {
  const walker = document.createTreeWalker(
    root,
    NodeFilter.SHOW_ELEMENT,
    {
      acceptNode(node) {
        return node.textContent.trim() === text
          ? NodeFilter.FILTER_ACCEPT
          : NodeFilter.FILTER_SKIP
      }
    }
  )

  const results = []
  while (walker.nextNode()) results.push(walker.currentNode)
  return results
}

// 使用例
const matches = findElementsByText(document.body, 'TV')

これはツリー全体にわたってあらゆる要素タイプを検索します — タグ固有のアプローチでは提供できない汎用的なソリューションです。また、最初のマッチのみが必要な場合は、早期終了もサポートしています。

FILTER_SKIPFILTER_REJECTの違いについて: ここでFILTER_SKIPを使用すると、ウォーカーはマッチしないノードの子要素にも降りていきます。代わりにFILTER_REJECTを使用すると、ウォーカーはそのノードとそのサブツリー全体をスキップします。テキスト検索の場合、親のtextContentがマッチしなくても、より深い子孫がマッチする可能性があるため、ほとんどの場合FILTER_SKIPが適切です。

方法3: DOMでのXPathテキスト検索

より表現力豊かなマッチングのために、document.evaluate()はテキストベースのクエリを含むXPath式をサポートしています。これは複雑なパターンに対する最も強力なオプションです。

function findByXPath(expression, context = document) {
  const result = document.evaluate(
    expression,
    context,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
  )

  return Array.from({ length: result.snapshotLength }, (_, i) =>
    result.snapshotItem(i)
  )
}

// "Submit"というテキストを含む任意の要素を検索
const els = findByXPath('//*[contains(text(), "Submit")]')

DOMでのXPathテキスト検索は、部分マッチと複雑な条件をきれいに処理します。トレードオフは可読性です — XPath構文はほとんどのフロントエンド開発者にとって馴染みがありません。

1つ覚えておくべきことは、contains(text(), "Submit")は要素の直接のテキストノードに対してのみマッチするということです。“Submit”が子要素の内部にある場合、この式は親にマッチしません。すべての子孫テキストを検索するには、代わりにcontains(., "Submit")を使用してください。ここで.は子孫を含む要素全体の文字列値を参照します。

textContent vs. innerText: マッチングにどちらを使用すべきか

両方のプロパティはテキストを返しますが、動作が異なります:

プロパティ返す内容レイアウトをトリガーするか?
textContent非表示要素を含む生のDOMテキストいいえ
innerTextレンダリングされたテキストのみはい(リフロー)

テキストマッチングにはtextContentを使用してください。より高速で、レイアウトの再計算をトリガーせず、可視性に関係なくすべての要素で一貫して動作します。

テキストでDOM要素を検索する際のよくある落とし穴

空白: textContentにはHTMLフォーマットからの空白が含まれます。比較する前に常に.trim()を使用してください。

ネストされたテキスト: 要素のtextContentにはすべての子孫テキストが含まれます。<div><span>TV</span></div>divtextContent"TV"です。どの要素レベルをターゲットにしているかを明確にしてください。

動的コンテンツ: ページ読み込み後に挿入されたテキストは、スクリプト実行時には存在しません。MutationObserverを使用するか、コンテンツの存在が確認された後に検索を実行してください。

適切なアプローチの選択

  • 既知の要素タイプ、シンプルなマッチquerySelectorAllでフィルタリング
  • 任意の要素タイプ、DOM全体の検索TreeWalker
  • 部分マッチまたは複雑なパターンdocument.evaluate()経由のXPath

テストコンテキストで作業している場合、Testing LibraryのようなツールはgetByText()を組み込みで提供しています — 知っておく価値がありますが、上記のネイティブ技術は非テストスクリプトにとって依然として不可欠です。

まとめ

テキストベースのDOM検索は標準APIのギャップですが、これら3つのアプローチはすべての実用的なケースをカバーします。要素タイプがわかっている場合は、迅速でターゲットを絞った検索のためにquerySelectorAllとフィルタを使用してください。特定のタグにコミットせずにDOM全体のトラバースが必要な場合はTreeWalkerを使用してください。マッチングロジックが部分テキストや複雑な条件を要求する場合はXPathを使用してください。どの方法を選択する場合でも、innerTextよりもtextContentを優先し、比較前に空白をトリミングし、誤ったマッチを避けるためにネストされた子孫テキストを考慮してください。

よくある質問

いいえ。querySelectorとquerySelectorAllはCSSセレクタのみを受け入れ、CSSにはテキストコンテンツをマッチングするセレクタがありません。querySelectorAllの結果セットをフィルタリングする、TreeWalkerでDOMを走査する、またはdocument.evaluateでXPath式を実行するなど、JavaScriptベースのアプローチを使用して、要素が含む内容で要素を特定する必要があります。

FILTER_SKIPはTreeWalkerに現在のノードをスキップするが、その子要素は訪問するように指示します。FILTER_REJECTはノードとそのサブツリー全体をスキップします。テキストベースの検索の場合、親がマッチしなくても、より深い子孫がマッチする可能性があるため、通常はFILTER_SKIPが適切な選択です。

いいえ。contains(text(), value)という式は要素の直接のテキストノードのみをチェックします。ターゲット文字列がネストされた子要素の内部にある場合は、代わりにcontains(., value)を使用してください。ドット演算子は、すべての子孫テキストを含む要素の完全な文字列値を参照します。

textContentはレイアウトのリフローをトリガーしないため、より高速です。CSSの可視性に関係なくDOMサブツリー内のすべてのテキストを返すため、一貫性があり予測可能です。innerTextはレンダリングされたテキストのみを返し、ブラウザにレイアウトの再計算を強制するため、マッチング目的には不要なオーバーヘッドが追加されます。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay