MAGAZINE

ルーターマガジン

クローリング/スクレイピング

HTMLパーサーでテキストノードを取得すると改行が大量に入ってしまう問題と解決策

2023.03.14
Pocket

ルーターエンジニアのsasaokaです。

スクレイピングをする際、あるノードの子ノードのテキストを全て取得したい場面がときどきあります。今回はそのときに起きる問題と解決策を、RubyのNokogiri、PythonのBeautifulSoupの例を挙げて紹介します。

問題点

サンプルとして次のHTMLを用いて、tableタグの中のテキストを取得してみます。

<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<title>タイトル</title>
	</head>
	<body>
		<table>
			<tr>
				<th>ヘッダー1</th>
				<th>ヘッダー2</th>
			</tr>
			<tr>
				<td>  値1

				</td>
				<td>値2</td>
			</tr>
		</table>
	</body>
</html>

Nokogiri

RubyのNokogiriを使う場合は次のようなコードになります。traverseメソッドを使うことで、あるノードの子孫のノードに再帰的にアクセスすることができます。

require 'nokogiri'

html = File.open('sample.html').read

text_arr = []
doc = Nokogiri::HTML.parse(html)
doc.at_css('table').traverse do |node|
  text_arr.push(node.text) if node.name == 'text'
end

pp text_arr

ヘッダー1、ヘッダー2、値1、値2の4つのみ出力して欲しいのですが、このコードを実行すると次の結果が返ってきます。

["\n" + "\t\t",
 "\n" + "\t\t\t\t",
 "ヘッダー1",
 "\n" + "\t\t\t\t",
 "ヘッダー2",
 "\n" + "\t\t\t",
 "\n" + "\t\t\t",
 "\n" + "\t\t\t\t",
 "  値1\n" + "\n" + "\t\t\t\t",
 "\n" + "\t\t\t\t",
 "値2",
 "\n" + "\t\t\t",
 "\n" + "\t\t"]

ブラウザではHTMLをレンダリングするときにソースコード上の改行は空白は無視されますが、これらもテキストノードとして扱われてしまっているため、改行やタブを取得してしまっています。

次に改行を取り除いたHTML:

<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8"><title>タイトル</title></head><body><table><tr><th>ヘッダー1</th><th>ヘッダー2</th></tr><tr><td>  値1</td><td>値2</td></tr></table></body></html>

を用意して、上のRubyコードを実行してみると次のようになります。

["ヘッダー1", "ヘッダー2", "  値1", "値2"]
"値1"の前のスペースは残っていますが、期待通り4つのテキストのみ抽出できています。

BeautifulSoup

次にPythonのBeautifulSoupを用いて同様の実験をしました。コードは以下です。

import bs4

html = open('sample.html', 'r').read()

soup = bs4.BeautifulSoup(html, "html.parser")
text_list = []
for node in soup.find('table').descendants:
    if isinstance(node, bs4.NavigableString):
        text_list.append(node.string)

print(text_list)
改行ありのHTMLからは

['\n', '\n', 'ヘッダー1', '\n', 'ヘッダー2', '\n', '\n', '\n', '  値1\n\n\t\t\t\t', '\n', '値2', '\n', '\n']

と13個の値が取り出されました。一方改行なしのHTMLからは

['ヘッダー1', 'ヘッダー2', '  値1', '値2']

とNokogiriと同様に4つのテキストのみ取り出されました。

解決策

不要な改行やタブを取得しないように、テキスト前後の改行やタブ、空白をstripメソッドで除いた後、それが空かどうかを判定して、空でないときに配列に入れれば良いです。

次の例はNokogiriで、あるノードの子ノードのテキストの配列を返す関数です。

def extract_child_node_text_arr(parent)
  text_arr = []
  parent.traverse do |child|
    if child.name == 'text'
      text = child.text.strip
      text_arr.push(text) unless text.empty?
    end
  end
  text_arr
end

BeautifulSoupでは次のようになります。

def extract_child_node_text_list(parent):
    text_list = []
    for child in parent.descendants:
        if isinstance(child, bs4.NavigableString):
            text = child.string.strip()
            if text: text_list.append(text)
    return text_list
これらの関数を用いると、改行ありのHTMLからも改行なしのHTMLからと同じように
['ヘッダー1', 'ヘッダー2', '値1', '値2']
の4つのテキストを抽出することができます。
Pocket

CONTACT

お問い合わせ・ご依頼はこちらから