먹고 기도하고 코딩하라

웹 소스 코드 보며 beautifulsoup4 문법 익히기 본문

개발일지

웹 소스 코드 보며 beautifulsoup4 문법 익히기

사과먹는사람 2020. 3. 3. 19:08
728x90
728x90

 

이 포스팅은 beautifulsoup4 에서 가장 잘 쓰이고 간단하게 쓸 수 있는 핵심 문법들을 정리하기 위해 쓰였습니다.

여기서는 웹페이지의 소스 코드에서 우리가 원하는 부분을 골라내는 방법과 beautifulsoup4 로 원하는 정보만 쏙 뽑아내는 것에 주안점을 두겠습니다.

주의하실 점은 동적 웹페이지에서는 이 방법이 먹히지 않을 수 있다는 것입니다. 브라우저 엔진으로 스크립트를 해석해야 하는 경우 selenium 과 웹 드라이버를 설치해야 할 수 있습니다. 이 방법에 대해서는 저의 다른 포스팅에서 더 자세하게 다루고 있으니 참고하시기 바랍니다. 기본적으로 정적 웹페이지를 크롤링하는 방법을 다룹니다.

 


시작해보겠습니다. 일단 저는 우리 학교 소스 코드에서 날짜와 요일, 그리고 학생식과 교직원식을 뽑아내는 것을 목표로 하겠습니다. beautifulsoup4 와 lxml 파서는 이미 설치했다는 가정 하에 진짜 beautifulsoup4 문법만 다룹니다. beautifulsoup4 및 lxml 파서 설치는 ubuntu 환경에서 다음과 같이 설치하면 됩니다. (이 때 python은 3.5 이상을 씁니다)

 

$ sudo apt-get install python3-bs4
$ pip3 install BeautifulSoup4
$ pip3 install lxml

 

참고로 저희 학교 학식 페이지는 이렇게 생겼습니다. 테이블만 딱 찾아서 보면 되는 거죠.

 

 

내가 원하는 부분을 찾아내려면 구글 크롬 브라우저가 필요합니다. 크롬 브라우저의 개발자 도구 때문인데요. 개발자 도구는 브라우저 우측 상단의 점 세 개 메뉴에서 도구 더보기-개발자 도구로 볼 수 있습니다.

개발자 도구를 열면 우측에 사이드바가 뜨면서 소스 코드와 스타일, 마진과 패딩 등 웹페이지 구조를 볼 수 있습니다. 우리는 이 중에서 각 태그가 웹페이지의 어떤 부분을 나타내는지를 살펴볼 예정입니다.

어떤 태그에는 마우스를 포커싱하면 웹페이지 일부분이 파랗게 뜰 것입니다. 파랗게 뜨는 그 부분이 그 태그가 가리키는 부분입니다. 표를 찾으려면 일단 문서 전체에서 점차점차 하위 태그로 좁혀 들어가야겠죠. 직접 해봅시다.

 

 

찾고 찾고 내려가다보니 class가 "table-responsive"인 div 태그까지 내려가서 겨우 찾았습니다. 이 부분만 제가 복사해서 HTML 문서를 만들겠습니다. 물론 이 웹페이지 자체를 path로 받을 수도 있지만 동적 웹페이지이기 때문에 날것 그대로의 소스 코드가 긁힐 수 있기 때문에 그냥 브라우저 엔진에서 잘 해석해서 내놓은 소스 코드를 복사했습니다. 웹페이지를 path로 넣었을 때 결과가 제대로 나오지 않을 수 있다는 데 주의해주세요.

테이블 부분의 소스 코드는 다음과 같습니다.

 

<!DOCTYPE html>
	<head>
	<title>학식 홈페이지</title>
	<meta charset="utf-8">
	</head>
	<body>
		<div class="table-responsive">
			<table id="schedule-table" class="tbl menu-table text-center" summary="구분기준으로 요일별 식단을 알려드립니다.">
				<caption class="sr-only">식단안내</caption>
				<colgroup>
					<col>
					<col style="width:17%;">
					<col style="width:17%;">
					<col style="width:17%;">
					<col style="width:17%;">
					<col style="width:17%;">
				</colgroup>
				<thead>
					<tr>
						<th scope="col">구분</th>
						<th scope="col">월요일<br>03월 02일</th>
						<th scope="col">화요일<br>03월 03일</th>
						<th scope="col">수요일<br>03월 04일</th>
						<th scope="col">목요일<br>03월 05일</th>
						<th scope="col">금요일<br>03월 06일</th>
					</tr>
				</thead>
				<tbody>
					<tr>
						<th scope="row">학생식당<br>
							<span class="dietTime" id="dietTime_01"></span>
						</th>
						<td>월요일 학식</td>
						<td>화요일 학식</td>
						<td>수요일 학식</td>
						<td>목요일 학식</td>
						<td>금요일 학식</td>
					</tr>
					<tr>
						<th scope="row">교직원식당<br>
							<span class="dietTime" id="dietTime_02"></span>
						</th>
						<td>월요일 교직원식</td>
						<td>화요일 교직원식</td>
						<td>수요일 교직원식</td>
						<td>목요일 교직원식</td>
						<td>금요일 교직원식</td>
					</tr>
					<tr id="dietNote">
						<th scope="row">비고</th>
						<td colspan="5" id="dietNoteContent">&nbsp;</td>
					</tr>
				</tbody>
			</table>
		</div>
	</body>
</html>

 

일단 table의 id가 "schedule-table"인 것을 확인합시다. 그리고 thead의 th 태그들이 구분부터 월~금까지 요일과 날짜가 나오죠. tbody에서는 학식은 0번째 tr의 td들이고 교직원식은 1번째 tr의 td들입니다. 감이 오시나요?

이 파일을 test_page.html 이라고 저장해둡시다. /home/ubuntu/test_page.html 이 위치입니다.

이제 파이썬 코드를 짜며 하나하나 봅시다.

 

$ vi test.py

# test.py
from bs4 import BeautifulSoup
import os
import sys

html = '/home/ubuntu/test_page.html'

soup = BeautifulSoup(html, 'lxml')
target_table = soup.find(id='schedule-table')

info_str = target_table.find_all("th")
for i in range(len(info_str)):
    info_str[i] = info_str[i].get_text('\n')+'\r\r'

meal_str = target_table.find_all("td")
for i in range(len(meal_str)):
    meal_str[i] = meal_str[i].get_text('\n')+'\r\r'

if os.path.exists('week_meal.txt'):
    os.remove('week_meal.txt')
if os.path.exists('week_info.txt'):
    os.remove('week_info.txt')
        
meal_fp = open('week_meal.txt', 'w', encoding='utf-8')
meal_fp.writelines(meal_str)
meal_fp.close()
info_fp = open('week_info.txt', 'w', encoding='utf-8')
info_fp.writelines(info_str)
info_fp.close()

 

이제부터 beautifulsoup4 문법 초간단 설명을 하겠습니다. 이 정도만 알고 python3 기초 문법 정도만 알아도 웬만한 텍스트 파싱에는 문제가 없습니다.

 

  • BeautifulSoup(string(경로), parser) : string 경로의 HTML 문서(string 타입)를 parser 로 읽어서 bs4.BeautifulSoup 객체를 return 합니다. 

  • bs4.BeautifulSoup.find(argv) : 매개변수로 주어진 것을 토대로 탐색해서 제일 처음 찾는 것을 bs4.element.Tag 객체로 return 합니다.

  • bs4.element.Tag.find_all(argv) : 매개변수로 주어진 것을 토대로 탐색해서 해당하는 모든 것을 bs4.element.ResultSet 객체로 return 합니다.

    • 태그로 검색하기 : find('p') -> 문서 처음부터 끝까지 탐색하다가 제일 먼저 찾는 <p> 태그와 그 내용을 반환합니다. find_all('p')은 문서에 나타나는 <p> 태그와 그 내용 전부를 set으로 만들어 전부 반환합니다. 보통 태그만으로 찾을 때는 결과가 정말 많을 가능성이 높기 때문에 id나 클래스와 함께 검색합니다.

    • id로 검색하기 : find(id='schedule-table') -> 문서 처음부터 끝까지 탐색하다가 어떤 태그든지 제일 먼저 찾는 id='schedule-table'인 태그와 그 내용을 반환합니다.

    • 태그+클래스로 검색하기 : find('p', 'my_class') -> 문서 처음부터 끝까지 탐색하다가 제일 먼저 찾는 <p> 태그이면서 'my_class'를 class로 갖는 태그와 그 내용을 반환합니다.

    • 태그+태그 속성으로 검색하기 : find('p', {'align':'center'}) -> 문서 처음부터 끝까지 탐색하다가 제일 먼저 찾는 <p> 태그이면서 align 속성이 center 인 태그와 그 내용을 반환합니다. (사실 align 속성은 이제 css에서 하지만 예시로 든 것일 뿐입니다). 태그+클래스나 태그+id도 이 방법으로 탐색 가능합니다.

  • bs4.element.Tag.select(argv1 argv2 argv3) : argv1, 2, 3 순으로 중첩된 모든 argv3 태그들을 찾아 bs4.element.ResultSet 객체로 return 합니다. 매개변수는 얼마든지 줄 수 있으며 가장 마지막의 하위 태그 내용들을 담습니다. find_all(argv3)과 동작 논리가 비슷하지만 argv1부터 3까지 순서대로 중첩된 태그에서 찾는다는 것이 차이점입니다.

 

 

이제 다른 건 위의 설명으로 끼워 맞춰서 보실 수 있을 겁니다.

중간에 for 문은 find_all의 결과인 ResultSet을 대상으로 돌리는 루프인데요. find 로 찾은 건 단일 Tag라 루프 돌리는 게 불가능하지만 ResultSet은 한 개 이상의 Tag 원소를 갖는 리스트이기 때문에 루프를 돌릴 수 있습니다.

매일매일의 학식이 보고 싶다면 위와 같이 target_table 에서 "td"로 된 태그들의 텍스트만 모두 긁어모으면 됩니다. 문제는 Tag를 그대로 쓸 수 없다는 건데 내용물 텍스트와 텍스트를 감싸는 <td> 태그가 함께 출력되기 때문이죠. 이 때 텍스트만 긁어모을 수 있게 해주는 게 바로 .get_text() 입니다.

근데 그냥 .get_text()를 해주면 가끔 문제가 생깁니다. 코드를 다음과 같이 일부 수정해서 실행해 보겠습니다.

 

info_str = target_table.find_all("th")
for i in range(len(info_str)):
    info_str[i] = info_str[i].get_text()+'\r\r'

 

get_text() 안의 '\n'을 없앴습니다. week_info.txt 는 어떻게 되어 있을까요?

 

 

별로 좋아보이지 않네요. 물론 한 <td> 안에 요일과 날짜가 함께 들어있긴 하지만 소스 코드에는 <br> 태그가 있어 웹페이지에서 나뉘어져 보이는데 여기서는 합쳐져서 보입니다. 개행이 있어야 깔끔하게 볼 수 있겠죠? <br> 태그를 개행인 '\n'으로 바꿔줄 수 있는 방법이 .get_text()의 인자에 '\n'을 넣어주는 것입니다. 그런 다음 저는 캐리지 리턴을 두 번 더 넣어주는 방식으로 요소 사이를 구분했습니다.

캐리지 리턴(Carriage Return, '\r')과 라인 피드(Line Feed, '\n')의 차이는 캐리지 리턴은 해당 줄 맨 앞으로 가는 거고 라인 피드는 현 위치에서 바로 아래로 이동하는 것입니다.

윈도우는 그냥 CR, LF 조합으로 줄 바꿈을 정의하지만 Unix나 Linux 계열은 LF 만으로 줄바꿈을 정의하기 때문에 CR, LF 차이가 분명합니다. 리눅스 vi 편집기에서는 CR이 '^M'으로 표시되는데 저는 이걸 원소 사이를 구분하는 것으로 썼기 때문에 어느 것이 리스트 몇 번째 원소에서 나온 것인지 확실히 구분하기에 좋아서 써봤습니다. 쓰는 게 필수는 아닙니다만 나중에 저는 텍스트 파일을 불러와 다시 요일별로 나눌 때 CR을 기준으로 split했기에 상황에 따라 유용하게 쓸 수 있다는 점을 알아두시는 게 좋겠습니다.

 

여튼 for 루프 밑은 뻔합니다. 같은 파일이 있으면 지워주고 새로 만드는 겁니다. 파일 열기와 쓰기 닫기 등은 파이썬에서 다루는 내용이므로 여기서는 자세히 설명하지 않습니다. 

 

아주 기초적인 문법만 다뤘으므로 다른 페이지에서도 똑같이 실험해보시기 바랍니다.

멜론 TOP 100 차트를 크롤링하는 튜토리얼이 있는데 다음에 python3 문법과 함께 꼼꼼히 다뤄보도록 하겠습니다. ㅎㅎ

읽어주셔서 감사합니다.

 

 

728x90
반응형
Comments