Py) 크롤링 - BS(table-01)

Py) 크롤링 - BS(table-01)

크롤링한 소스코드를 정제하는 BeatifulSoup를 활용하여 웹페이지의 표를 정제하는 방법을 알아본다.


개요

웹페이지의 소스코드를 정제하는 경우 표(table)의 데이터를 추출해야 하는 경우가 많다. 이때 BeautifulSoup 라이브러리(이하 bs)로 생성한 객체에서 표의 데이터를 추출하는 방법을 알아본다.

표의 이해

.select()를 활용해서 데이터를 추출하고자 하는 표를 뽑는데 성공했다면 그 다음 단계는 해당 표의 구조를 이해하고 데이터를 추출하는 것이다. 표는 다음과 같은 구조로 이루어져 있다.
HTML Table 기본구조

특수한 경우가 아니라면 대부분의 표는 <table> 태그로 감싸져있다. <table> 태그는 하위 태그인 <tr> 태그의 집합이라고 할 수 있다. “tr”은 “table row”를 의미하며 표 제목 뿐만아니라 데이터가 있는 행 하나를 의미한다. 그리고 <tr> 태그의 하위 태그는 <th> 태그와 <td> 태그가 있다. <th> 태그는 표의 제목(table header)을 의미하며 <td> 태그는 표의 데이터(table data)를 의미한다.

즉, 상기 예시 표의 가장 위에 있는 행인 표제목의 원소인 {Name, Age, City}의 경우 <th> 태그로 감싸져 있고, 그 아래에 있는 데이터의 원소인 {John Doe, 28, New York}의 경우 <td> 태그로 감싸져 있다.

물론 표의 복잡도에 따라 여러가지 추가태그가 붙을 수 있겠지만 가장 기본적인 형태의 표는 상기와 같다고 이해하면 되겠다.

예시

다음과 같이 라이브러리와 샘플 코드를 준비한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import pandas as pd
from bs4 import BeautifulSoup as bs

source_code = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sample Table</title>
</head>
<body>
<table border="1">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>John Doe</td>
<td>28</td>
<td>New York</td>
</tr>
<tr>
<td>Jane Smith</td>
<td>34</td>
<td>Los Angeles</td>
</tr>
<tr>
<td>Emily Johnson</td>
<td>25</td>
<td>Chicago</td>
</tr>
</tbody>
</table>
</body>
</html>
"""

bs_source = bs(source_code)

상기 코드를 html 파일로 저장하고 열어서 보면 다음과 같은 모양의 표를 볼 수 있다.
처리 대상 표

일단 표 부분만 추출하여 별도의 객체로 저장해보자.

1
bs_table = bs_source.select_one("table")

그리고 <tr> 태그만 추출해보면 첫 번째 <tr>태그의 하위 태그는 <th>이고 나머지는 <td> 태그임을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bs_table_tr = bs_table.select("tr")
bs_table_tr
## [<tr>
## <th>Name</th>
## <th>Age</th>
## <th>City</th>
## </tr>,
## <tr>
## <td>John Doe</td>
## <td>28</td>
## <td>New York</td>
## </tr>,
## <tr>
## <td>Jane Smith</td>
## <td>34</td>
## <td>Los Angeles</td>
## </tr>,
## <tr>
## <td>Emily Johnson</td>
## <td>25</td>
## <td>Chicago</td>
## </tr>]

각 행의 데이터를 추출하기 위해 먼저 첫 번째 행부터 시작해보자.

1
2
bs_table_tr[0].select("th")
## [<th>Name</th>, <th>Age</th>, <th>City</th>]

상기 결과를 보면 각 태그의 원소가 <th>태그로 감싸져있는데 해당 태그의 텍스트를 추출하기 위해서 .text 어트리뷰트를 사용할 수 있고 한 번에 처리하기 위해 다음과 같이 리스트 내포(list comprehension)를 사용할 수 있다.

1
2
3
ls_table_header = [e.text for e in bs_table_tr[0].select("th")]
ls_table_header
## ['Name', 'Age', 'City']

다음으로 데이터 행을 추출해보자. 추출 대상이 되는 <tr>태그와 그 하위 태그인 <td>태그를 추출하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bs_table_tr[1:]
## [<tr>
## <td>John Doe</td>
## <td>28</td>
## <td>New York</td>
## </tr>,
## <tr>
## <td>Jane Smith</td>
## <td>34</td>
## <td>Los Angeles</td>
## </tr>,
## <tr>
## <td>Emily Johnson</td>
## <td>25</td>
## <td>Chicago</td>
## </tr>]

텍스트를 추출하기 위해 한 단계씩 진행해보자. 첫 번째 데이터 행의 첫 번째 텍스트를 뽑기 위해 [0].select("td")를 사용하는 것을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bs_table_tr[1:][0]
## <tr>
## <td>John Doe</td>
## <td>28</td>
## <td>New York</td>
## </tr>

bs_table_tr[1:][0].select("td")
## [<td>John Doe</td>, <td>28</td>, <td>New York</td>]

bs_table_tr[1:][0].select("td")[0]
## <td>John Doe</td>

bs_table_tr[1:][0].select("td")[0].text
## 'John Doe'

상기 내용을 이중 반복문을 사용하여 한 번에 처리해보자. 바깥 반복문은 개별 행을 추출하고 내부 반복문은 각 행의 데이터를 추출한다.

1
2
3
4
5
6
7
8
9
10
11
12
ls_table_data = []
for row in bs_table_tr[1:]:
ls_table_row = []
for data in row.select("td"):
ls_table_row = ls_table_row + [data.text]

ls_table_data = ls_table_data + [ls_table_row]

ls_table_data
## [['John Doe', '28', 'New York'],
## ['Jane Smith', '34', 'Los Angeles'],
## ['Emily Johnson', '25', 'Chicago']]

보다 간결하게 하려면 다음과 같이 리스트 내포을 2중으로 사용하여 해결할 수 있다. 하지만 코드 작성 난이도는 많이 올라가는 편이니 리스트 내포를 충분히 이해하고 있지 않다면 실수를 방지하기 위해 반복문을 사용하는 것을 권장한다.

1
2
3
4
5
ls_table_data = [[e.text for e in r.select("td")] for r in bs_table_tr[1:]]
ls_table_data
## [['John Doe', '28', 'New York'],
## ['Jane Smith', '34', 'Los Angeles'],
## ['Emily Johnson', '25', 'Chicago']]

앞에서 생성한 “ls_table_header”와 “ls_table_data”를 사용하여 DataFrame을 생성하면 다음과 같다.

1
2
df_table = pd.DataFrame(ls_table_data, columns = ls_table_header)
df_table
Name Age City
0 John Doe 28 New York
1 Jane Smith 34 Los Angeles
2 Emily Johnson 25 Chicago
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×