JavaScriptでページからテキストコンテンツを抽出する関数の実装方法

## はじめに

このシリーズ記事では、JavaScriptを使用して特定のDOMノードおよびその子孫からテキストコンテンツを抽出する方法を説明します。この記事はその第1部で、関数の使い方とコーディング方法に焦点を当てます。第2部では、このコードをChromeプラグインで使用する方法を説明します。

第2部の記事をご覧になりたい方は、こちらをチェックしてください → [Chromeプラグインの作成：ページからテキストコンテンツを抽出する](https://qiita.com/figueiredoluiz/items/493f7dc0507c6749ca60)。

## 背景

ウェブページからテキストコンテンツを抽出することは、データスクレイピングや自動化など、多くのユースケースで役立ちます。この記事では、基本的なJavaScriptとDOMの知識を前提としています。

## 実装方法

### ノードの選択

```jsx
var node = "body > div.content";
```

指定されたCSSセレクタを使用して特定のノードを選択します。このノードを正しく選択することが、正確なテキスト抽出にとって重要です。

**Chrome DevToolsを使用して要素セレクタを取得する方法**

1. Chromeブラウザを開き、対象のウェブページにアクセスします。
2. `F12`キーを押すか、右クリックして「検証」を選択し、Chrome DevToolsを開きます。
3. 「要素」タブを選択します。
4. ページ上の目的の要素を右クリックし、「Copy」>「Copy selector」を選択します。
5. これで、選択した要素のCSSセレクタがクリップボードにコピーされます。このセレクタを使用して特定のノードを選択できます。


### 再帰的にテキストコンテンツを取得する関数

```jsx
function getTextContent(element) {
  let textContent = "";
  
  // If element is a string selector, convert it to a DOM element
  if (typeof element === 'string') {
    element = document.querySelector(element);
  }
  
  if (element) {
    let children = element.childNodes;
    children.forEach(function(child) {
      if (child.nodeType === Node.TEXT_NODE) {
        textContent += child.textContent.trim() + "\n";
      } else if (child.nodeType === Node.ELEMENT_NODE) {
        textContent += getTextContent(child); // Pass the child element directly
      }
    });
  }
  
  return textContent;
}
```

この関数は、指定されたDOMノードからテキストコンテンツを抽出します。では、コードの各部分とその理由を詳しく説明します。

#### 1. 関数定義とノードの取得
    
```jsx
function getTextContent(element) {
  let textContent = "";
  
  if (typeof element === 'string') {
    element = document.querySelector(element);
  }
```

- まず、`element`という引数を取る`getTextContent`関数を定義します。この引数は、テキストコンテンツを抽出するノードのCSSセレクタまたはDOMノードです。
- `document.querySelector(element)`を使用して、指定されたセレクタに一致する最初のDOMノードを取得します。

#### 2. 初期化
    
```jsx
  let textContent = "";
```
取得したテキストコンテンツを格納するための変数`textContent`を空の文字列で初期化します。

#### 3. ノードの存在チェック
    
```jsx
  if (element) {
```

指定されたノードが存在するかをチェックします。存在しない場合、関数は空の文字列を返します。

#### 4. 子ノードの取得とループ

```jsx
    let children = element.childNodes;
    children.forEach(function(child) {
```

- `childNodes`プロパティを使用して、現在のノードのすべての子ノードを取得します。
- `forEach`ループを使用して各子ノードを処理します。

#### 5. テキストノードの処理

    
```jsx
      if (child.nodeType === Node.TEXT_NODE) {
        textContent += child.textContent.trim() + "\n";
```
    
子ノードがテキストノード（`Node.TEXT_NODE`）である場合、そのテキストコンテンツを`trim()`メソッドで前後の空白を除去してから、`textContent`に追加します。ここで`"\n"`を追加することで各行の区切りを明確にします。

#### 6. 要素ノードの再帰処理
    
```jsx
      } else if (child.nodeType === Node.ELEMENT_NODE) {
        textContent += getTextContent(child); // Pass the child element directly
      }
```
    
子ノードが要素ノード（`Node.ELEMENT_NODE`）である場合、再帰的に`getTextContent`関数を呼び出し、その結果を`textContent`に追加します。この再帰呼び出しにより、すべての子孫ノードを探索し、テキストコンテンツを抽出できます。

#### 7. テキストコンテンツの返却
    
```jsx
    });
  }
  return textContent;
}
```
    
すべての子ノードを処理した後、`textContent`を返します。

このアプローチでは、再帰を使用することで、ネストされた要素内のテキストも含めてすべてのテキストコンテンツを簡単に抽出できます。

### テキストコンテンツの取得

```jsx
var text = getTextContent(node);
```

この関数を使用して、選択したノードからテキストコンテンツを取得します。

### テキストコンテンツの出力

```jsx
console.log(text);
```

抽出したテキストコンテンツをコンソールに出力します。

### コードのテスト
コードをテストするためには、Chrome DevToolsのコンソールを使用します。以下の手順に従ってください。

1. Chromeブラウザを開き、対象のウェブページにアクセスします。
2. `F12`キーを押すか、右クリックして「検証」を選択し、Chrome DevToolsを開きます。
3. 「コンソール」タブを選択します。
4. 上記のJavaScriptコードをコンソールに貼り付けて、Enterキーを押します。
5. コンソールにテキストコンテンツが表示されます。

## 使用例

この関数は、ウェブページからテキストデータを収集する際に便利です。例えば、ニュースサイトの記事内容を取得する場合に使用できます。

## 結論

この記事では、JavaScriptを使用してDOMノードからテキストコンテンツを抽出する方法を説明しました。この関数は、データスクレイピングや自動化など、多くのユースケースで役立ちます。次回の記事では、このコードをChromeプラグインで使用する方法について説明します。

## 参考文献

- [MDN Web Docs - Document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
- [MDN Web Docs - Node.childNodes](https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes)
- [MDN Web Docs - Node.TEXT_NODE](https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#node_type_constants)

## 別のアプローチ

コメントでいただいたフィードバックに基づき、異なるアプローチも試してみました。それぞれの方法には利点がありますので、ユースケースに応じて選択してください。

### NodeIteratorを使用する方法

```jsx
function getTextContents(element) {
  if (typeof element === 'string') {
    element = document.querySelector(element);
  }
  const textContents = [];
  const iter = document.createNodeIterator(element, NodeFilter.SHOW_TEXT);
  let text;
  while ((text = iter.nextNode()) !== null) {
    textContents.push(text.textContent.trim());
  }
  return textContents.join("\n");
}
```

**NodeIteratorを使用する方法:**
この方法では、createNodeIteratorを使用して指定された要素内のすべてのテキストノードを取得します。各テキストノードの内容を配列に追加し、最終的に配列の要素を改行で結合して返します。

参照： [MDN Web Docs - document.createNodeIterator](https://developer.mozilla.org/ja/docs/Web/API/Document/createNodeIterator)

### XPathを使用する方法

```jsx
function getTextContents(element) {
  if (typeof element === 'string') {
    element = document.querySelector(element);
  }
  const result = document.evaluate(
    ".//text()",
    element,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  );
  const textContents = [];
  for (let i = 0; i < result.snapshotLength; i++) {
    textContents.push(result.snapshotItem(i).textContent.trim());
  }
  return textContents.join("\n");
}
```
**XPathを使用する方法:**
この方法では、document.evaluateを使用してXPathクエリによりすべてのテキストノードを取得します。取得した各テキストノードの内容を配列に追加し、最終的に配列の要素を改行で結合して返します。
参照： [MDN Web Docs - document.evaluate](https://developer.mozilla.org/ja/docs/Web/API/Document/evaluate)

---

このように、複数の方法でテキストコンテンツを抽出することができます。用途に応じて最適な方法を選択してください。

**備考:**
これらの関数は、テキストコンテンツをクリップボードへコピーする目的で使用される予定です。そのため、抽出したテキストは改行を用いて結合しています。これにより、複数行にまたがるテキストの取り扱いが容易になります。