【Python3】表のスクレイピング(罫線の位置も考慮)


はじめに

サイト,またはhtmlファイルにある表をpandasのDataFrameで取得したいとき,わざわざ頑張ってスクリプトを書かなくてもpandasを使うとめちゃめちゃ便利です.


required

pip install pandas beautifulsoup4 requests lxml


綺麗な表のスクレイピング

今回はwikipediaのPythonを対象としますが,普通のサイトでしたら表は綺麗なはずです.(ここでいう'綺麗'の意味は後述)

from bs4 import BeautifulSoup

import requests
import pandas as pd

url = 'https://en.wikipedia.org/wiki/Python_(programming_language)'
r = requests.get(url)
soup = BeautifulSoup(r.content, 'lxml') # lxml or html.parser

# テーブルを全てデータフレーム化
df_list = [pd.read_html(str(table))[0] for table in soup.find_all('table')]

print('表の数: %d' % len(df_list)) # 表の数: 8

df_list[2]

[out]

Type
mutable
Description
Syntax example

0
bool
immutable
Boolean value
TrueFalse

1
bytearray
mutable
Sequence of bytes
bytearray(b'Some ASCII')bytearray(b"Some ASCII...

2
bytes
immutable
Sequence of bytes
b'Some ASCII'b"Some ASCII"bytes([119, 105, 107...

3
complex
immutable
Complex number with real and imaginary parts
3+2.7j

4
dict
mutable
Associative array (or dictionary) of key and v...
{'key1': 1.0, 3: False}

5
ellipsisa
immutable
An ellipsis placeholder to be used as an index...
...Ellipsis

6
float
immutable
Floating point number, system-defined precision
3.1415927

7
frozenset
immutable
Unordered set, contains no duplicates; can con...
frozenset([4.0, 'string', True])

8
int
immutable
Integer of unlimited magnitude[82]
42

9
list
mutable
List, can contain mixed types
[4.0, 'string', True]

10
NoneTypea
immutable
An object representing the absence of a value.
None

11
NotImplementedTypea
immutable
A placeholder that can be returned from overlo...
NotImplemented

12
set
mutable
Unordered set, contains no duplicates; can con...
{4.0, 'string', True}

13
str
immutable
A character string: sequence of Unicode codepo...
'Wikipedia'"Wikipedia""""Spanningmultiplelines"""

14
tuple
immutable
Can contain mixed types
(4.0, 'string', True)

めちゃめちゃ簡単に表のパースができます


参考

そもそものhtmlをpd.read_html()してデータフレームのリストを作ることも可能ですが,一旦find_all('table')してからその文字列をpd.read_html()した方が処理時間が圧倒的に早かったです.


綺麗ではない表のスクレイピング

pdfなどを変換ツールを用いて無理やりhtmlに変換した場合,そのままpd.read_html()するとおかしな挙動になります


html =\

"""
<html>
<body>
<table style="border-collapse:collapse" cellspacing="0">
<tbody>
<tr style="height:13pt">
<td style="width:22pt;border-top-style:solid;border-top-width:1pt;border-left-style:solid;border-left-width:1pt;border-bottom-style:solid;border-bottom-width:1pt">
<p class="s5" style="text-indent: 0pt;line-height: 12pt;text-align: center;">特</p>
</td>
<td style="width:52pt;border-top-style:solid;border-top-width:1pt;border-bottom-style:solid;border-bottom-width:1pt">
<p class="s5" style="padding-left: 6pt;text-indent: 0pt;line-height: 12pt;text-align: left;">定 資</p>
</td>
<td style="width:62pt;border-top-style:solid;border-top-width:1pt;border-bottom-style:solid;border-bottom-width:1pt">
<p class="s5" style="text-indent: 0pt;line-height: 12pt;text-align: left;">産 の 種</p>
</td>
<td style="width:22pt;border-top-style:solid;border-top-width:1pt;border-bottom-style:solid;border-bottom-width:1pt;border-right-style:solid;border-right-width:1pt">
<p class="s5" style="padding-right: 5pt;text-indent: 0pt;line-height: 12pt;text-align: right;">類</p>
</td>
<td style="width:261pt;border-top-style:solid;border-top-width:1pt;border-left-style:solid;border-left-width:1pt;border-bottom-style:solid;border-bottom-width:1pt;border-right-style:solid;border-right-width:1pt">
<p class="s5" style="padding-left: 5pt;text-indent: 0pt;line-height: 12pt;text-align: left;">不動産信託受益権</p>
</td>
</tr>
<tr style="height:13pt">
<td style="width:22pt;border-top-style:solid;border-top-width:1pt;border-left-style:solid;border-left-width:1pt;border-bottom-style:solid;border-bottom-width:1pt">
<p class="s5" style="text-indent: 0pt;line-height: 12pt;text-align: center;">取</p>
</td>
<td style="width:52pt;border-top-style:solid;border-top-width:1pt;border-bottom-style:solid;border-bottom-width:1pt">
<p class="s5" style="padding-left: 6pt;text-indent: 0pt;line-height: 12pt;text-align: left;">得 予</p>
</td>
<td style="width:62pt;border-top-style:solid;border-top-width:1pt;border-bottom-style:solid;border-bottom-width:1pt">
<p class="s5" style="text-indent: 0pt;line-height: 12pt;text-align: left;">定 年 月</p>
</td>
<td style="width:22pt;border-top-style:solid;border-top-width:1pt;border-bottom-style:solid;border-bottom-width:1pt;border-right-style:solid;border-right-width:1pt">
<p class="s5" style="padding-right: 5pt;text-indent: 0pt;line-height: 12pt;text-align: right;">日</p>
</td>
<td style="width:261pt;border-top-style:solid;border-top-width:1pt;border-left-style:solid;border-left-width:1pt;border-bottom-style:solid;border-bottom-width:1pt;border-right-style:solid;border-right-width:1pt">
<p class="s5" style="padding-left: 5pt;text-indent: 0pt;line-height: 12pt;text-align: left;">平成 <span class="s6">** </span>年 <span class="s6">** </span>月 <span class="s6">** </span>日(予定)</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
"""

soup = BeautifulSoup(html, "lxml") # lxml or html.parser
# テーブルを全てデータフレーム化
df_list = [pd.read_html(str(table))[0] for table in soup.find_all('table')]

print('表の数: %d' % len(df_list)) # 表の数: 1

df_list[0]

[out]

0
1
2
3
4

0

定 資
産 の 種

不動産信託受益権

1

得 予
定 年 月

平成 ** 年 ** 月 ** 日(予定)

変換ツールでhtmlに変換するとこのような文字列分割のバグ(?)がよく起こります.

ただ元のhtmlをブラウザでみてみるとちゃんと罫線が正しい位置にあることを確認できるかと思います.(リンクは,dropboxにログインしていないとただしく見えないと思います)

htmlをもう一度よくみてみると.

<td style="width:22pt;border-top-style:solid;border-top-width:1pt;border-bottom-style:solid;border-bottom-width:1pt;border-right-style:solid;border-right-width:1pt">

のように,'border-hogehoge-style'という文字列が入っています.

このhogehogeがtopだったら上の罫線あり,leftだったら左の罫線ありということになります.


バグ対応

これを考慮して,


  • tdのstyleに'border-left-style'が入ってなかったら,左のtdに文字列をマージしてhtmlを書き換える

というロジックでスクリプトを書くと以下になります

import bs4

from bs4 import BeautifulSoup
import pandas as pd

def correct_table(table: bs4.element.Tag):
"""
【破壊関数】
引数のtableタグに左側罫線のないセルがあった場合にのみ動く
* あった場合は無理やりその左のセルに中の文字列をくっつける
"""

while(1):
break_flg = 0
for i, row in enumerate(table.find_all("tr")):
for j, cell in enumerate(row.find_all(["td", "th"])):
if j > 0:
""" 各行一番左のセルは確実に左側罫線があるのでそれ以外を考える """
# index取得
current_cell_index = table.find_all().index(cell)
one_left_cell_index = table.find_all().index(row.find_all(["td", "th"])[j - 1]) if j!=0 else None
# style取得(もしstyleアトリビュートがなければ'border-left-style'というstyleであるとする)
current_cell_style = cell.get("style") or "border-left-style"
# colspan取得
current_cell_colspan = int(cell.get("colspan")) if cell.get("colspan") else 1
one_left_cell_colspan = int(table.find_all()[one_left_cell_index].get("colspan")) if table.find_all()[one_left_cell_index].get("colspan") else 1
# セルの左側に罫線があるかどうかの判定
left_ruled_line_exist = "border-left-style" in current_cell_style

if not left_ruled_line_exist:
""" セルの左側に罫線がない場合の処理 """
one_left_cell_value = table.find_all()[one_left_cell_index].text
table.find_all()[one_left_cell_index]["colspan"] = current_cell_colspan + one_left_cell_colspan
table.find_all()[one_left_cell_index].string =\
table.find_all()[one_left_cell_index].text + table.find_all()[current_cell_index].text
cell.extract() #いまのセルを削除する
break_flg = 1
break
if break_flg == 0:
""" セルの左側に罫線がないセルが一つもなくなったらwhile文からbreakする """
break
def read_table_from_soup(soup: BeautifulSoup) -> list:
""" soupから表をパースしてデータフレームのリストを返す """
[correct_table(table) for table in soup.find_all('table')]
df_list = [pd.read_html(str(table))[0] for table in soup.find_all('table')]
return df_list

if __name__ == '__main__':
# html変数は先ほどと同じものを使う
soup = BeautifulSoup(html, "lxml") # lxml or html.parser
df_list = read_table_from_soup(soup)
print('表の数: %d' % len(df_list)) # 表の数: 1

df_list[0]

[out]

0
1
2
3
4

0
特 定 資 産 の 種 類
特 定 資 産 の 種 類
特 定 資 産 の 種 類
特 定 資 産 の 種 類
不動産信託受益権

1
取 得 予 定 年 月 日
取 得 予 定 年 月 日
取 得 予 定 年 月 日
取 得 予 定 年 月 日
平成 ** 年 ** 月 ** 日(予定)

このように,罫線の位置で判別してpandas.DataFrameにパースできます


おわりに

おそらくもっと賢い書き方があると思うのでコメントいただければ修正します。

また宣伝で恐縮ですがestieというオフィスの不動産テックやっています.

オフィス不動産のデータベース構築,分析も行なっているのでご興味あればお願いします!

estie

estie corporate