HTML을 분석해 주는 BeautifulSoup
BeautifulSoup 라이브러리
지난 실습에서 requests 모듈을 이용해 HTTP 요청을 보내고, 응답을 받았다. 그런데 res.body의 결과를 확인했을 때, 긴 텍스트 형식이라서 분석하기 힘들었다. 이를 해결하기 위해 HTML 코드를 분석해 주는 HTML Parser를 사용할 수 있는데, 가장 유명한 것이 BeautifulSoup4이다.
BeautifulSoup4 설치
%pip install bs4
requests 모듈로 데이터 받기
# www.example.com 사이트를 요청한 후 응답 받아보기
import requests
res = requests.get("https://www.example.com")
res.text
bs4 선언 및 사용 - BeautifulSoup, prettify
from bs4 import BeautifulSoup
# 첫번째 인자로는 response의 body를 텍스트로 전달
# 두번째 인자로는 "html"로 분석한다는 것을 명시
soup = BeautifulSoup(res.text, "html.parser")
# 객체 soup의 .prettify()를 활용하면 분석된 HTML을 보기 편하게 반환
print(soup.prettify())
# result
# <!DOCTYPE html>
# <html>
# <head>
# <title>
# Example Domain
# </title>
# <meta charset="utf-8"/>
# <meta content="text/html; charset=utf-8" http-equiv="Content-type"/>
# <meta content="width=device-width, initial-scale=1" name="viewport"/>
# <style type="text/css">
# ...
# </body>
# </html>
bs4 사용 - title, head, body 등 태그, find, find_all
# title 가져오기
soup.title
# result : <title>Example Domain</title>
# <h1> 태그로 감싸진 요소 하나 찾기
soup.find("h1")
# result : <h1>Example Domain</h1>
# <p> 태그로 감싸진 요소 모두 찾기
soup.find_all("p")
# result
# [<p>This domain is for use in illustrative examples in documents. You may use this
# domain in literature without prior coordination or asking for permission.</p>,
# <p><a href="https://www.iana.org/domains/example">More information...</a></p>]
bs4 사용 - name, text
h = soup.find("h1")
# 태그 이름 가져오기
h.name
# result : h1
# 태그 내용 가져오기
h.text
# result : 'Example Domain'
원하는 요소 가져오기 1 - 책 이름 모으기
http://books.toscrape.com/catalogue/category/books/travel_2/index.html << 실습을 위한 웹페이지
책들의 제목은 h3 태그로 감싸져 있다는 것을 개발자 도구를 확인하여 알 수 있었다. 이 부분을 활용하여 책들의 제목을 스크래핑하는 실습을 진행한다.
라이브러리 불러오기 및 requests, BeautifulSoup 객체 선언
from bs4 import BeautifulSoup
import requests
res = requests.get("http://books.toscrape.com/catalogue/category/books/travel_2/index.html")
soup = BeautifulSoup(res.text, "html.parser")
모든 h3 태그의 요소 가져오기
h3 태그의 요소에는 책 제목 뿐만 아니라 다른 요소도 존재하기 어떻게 책 제목만을 추출할지 생각해보아야 한다.
h3_results = soup.find_all("h3")
h3_results[0]
# result : <h3><a href="../../../its-only-the-himalayas_981/index.html" title="It's Only the Himalayas">It's Only the Himalayas</a></h3>
원하는 책 제목(title)만 추출
a.text를 사용할 경우 제목이 길 경우 ...으로 생략되는 경우가 생긴다. 그러나 title 속성에는 모든 이름이 적혀 있으므로 해당 정보를 참고하도록 한다.
for book in h3_results:
print(book.a.text)
# result
# It's Only the Himalayas
# Full Moon over Noahâs ...
# See America: A Celebration ...
# Vagabonding: An Uncommon Guide ...
# Under the Tuscan Sun
# A Summer In Europe
# The Great Railway Bazaar
# A Year in Provence ...
# The Road to Little ...
# Neither Here nor There: ...
# 1,000 Places to See ...
태그의 속성은 딕셔너리 형태로 확인이 가능하다.
for book in h3_results:
print(book.a["title"])
# result
# It's Only the Himalayas
# Full Moon over Noahâs Ark: An Odyssey to Mount Ararat and Beyond
# See America: A Celebration of Our National Parks & Treasured Sites
# Vagabonding: An Uncommon Guide to the Art of Long-Term World Travel
# Under the Tuscan Sun
# A Summer In Europe
# The Great Railway Bazaar
# A Year in Provence (Provence #1)
# The Road to Little Dribbling: Adventures of an American in Britain (Notes From a Small Island #2)
# Neither Here nor There: Travels in Europe
# 1,000 Places to See Before You Die
HTML의 Locator로 원하는 요소 찾기
https://programmers.co.kr/pages/data_engineering << 실습을 위한 웹페이지
태그는 자신의 이름 뿐만 아니라 고유한 속성 또한 가질 수 있다. 이 중에서 id와 class는 Locator로서 특정 태그를 지칭하는 데 사용된다.
- tagname : 태그의 이름
- id : 하나의 고유 태그를 가리키는 라벨
- class : 유사한 여러 태그를 묶는 라벨
<p>This element has only tagname</p>
<p id="target">This element has tagname and id</p>
<p class="targets">This element has tagname and class</p>
id와 class를 이용한 스크래핑
찾고자 하는 요소는 아래 사진과 같이 div 태그로 감싸져 있다. class는 span12, id는 results이다. 또한 제목의 class는 page-header이다. 이를 활용하여 실습을 진행한다.
라이브러리 불러오기 및 requests, BeautifulSoup 객체 선언
from bs4 import BeautifulSoup
import requests
res = requests.get("https://programmers.co.kr/pages/data_engineering")
soup = BeautifulSoup(res.text, "html.parser")
id가 "results"인 div 태그 찾기
soup.find("div", id="results")
class가 "page-header"인 div 태그 찾기
class는 따로 'class='을 붙이지 않아도 된다.
find_result = soup.find("div", "page-header")
find_result
# result
# <div class="page-header">
# <h1>
# Example web scraping website
# <small></small>
# </h1>
# </div>
text를 깔끔하게 가져오기
h1 태그의 text만을 가져오고, strip() 함수를 활용해서 white space를 모두 제거한 결과이다.
find_result.h1.text.strip()
# result : 'Example web scraping website'
원하는 요소 가져오기 2 - hashcode 질문 모으기
https://qna.programmers.co.kr/ << 실습을 위한 웹페이지
찾고자 하는 질문 부분은 li 태그의 question-list-item 클래스를 갖고 있고, 내부에 div 태그의 question 클래스, div 태그의 top 클래스를 갖고 있다. 해당 부분을 활용하여 질문의 목록을 스크래핑하는 실습을 진행한다.
라이브러리 불러오기 및 requests, BeautifulSoup 객체 선언
user_agent를 header에 넣어 요청을 진행한다.
from bs4 import BeautifulSoup
import requests
user_agent = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"}
res = requests.get("https://qna.programmers.co.kr/", user_agent)
soup = BeautifulSoup(res.text, "html.parser")
질문을 추출하는 코드 작성
questions = soup.find_all("li", "question-list-item")
for q in questions:
print(q.find("div", "question").find("div", "top").h4.text)
페이지네이션(pagination)
일반적으로 모든 내용을 한 번에 보여주지 않고 아래 사진처럼 일정 부분을 잘라 페이지 별로 나누어 놓는다. 그래서 모든 내용을 확인하기 위해서는 페이지 별로 스크래핑을 진행해야 한다. 그러나 페이지의 링크를 확인해 보면, 다른 페이지로 이동했을 때 변하는 것은 https://qna.programmers.co.kr/?page=3 << 맨 뒤의 .?page={i}이다. 따라서 이 부분을 활용하면 여러 페이지의 질문을 적절히 확인할 수 있다. 그러나 과도한 요청을 방지하기 위해 0.5초마다 요청을 보낸다.
import time
ser_agent = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"}
for i in range(1, 6):
res = requests.get("https://qna.programmers.co.kr/?page={}".format(i), user_agent)
soup = BeautifulSoup(res.text, "html.parser")
questions = soup.find_all("li", "question-list-item")
for q in questions:
print(q.find("div", "question").find("div", "top").h4.text)
time.sleep(0.5)
동적 웹 페이지와의 만남
정적 웹 사이트 vs 동적 웹 사이트
웹 페이지는 어떻게 생성되냐에 따라 크게 두 가지로 구분된다. HTML 내용이 고정된 정적(static) 웹 사이트와 HTML 내용이 변하는 동적(dynamic) 웹 사이트이다.
정적 웹 사이트
정적 웹 사이트는 HTML 문서가 완전하게 응답한다. 따라서 requests 등을 통해 요청을 했을 때, 응답을 파싱해도 아무런 문제가 없다.
동적 웹 사이트
동적 웹 사이트는 응답 후 HTML이 렌더링이 될 때까지의 지연시간이 존재한다. 따라서 정적 웹 사이트처럼 바로 파싱을 하면 안 될 수도 있고, HTML의 구조가 바뀔 수도 있다.
동적 웹사이트의 동작 방식
웹 브라우저에선 JavaScript라는 프로그래밍 언어가 동작한다. 이는 비동기 처리를 통해 응답 이후에 필요한 데이터를 채운다. 동기 처리된 경우 HTML 로딩에 문제가 없지만, 비동기 처리의 경우 상황에 따라 데이터가 완전하지 않은 경우가 발생한다. 이 상황에서 요청을 보내면 불완전한 응답을 받게 된다.
- 동기 처리 : 요청에 따른 응답을 기다림, 렌더링 이후 데이터 처리
- 비동기 처리 :요청에 따른 응답을 기다리지 않음, 렌더링과 데이터 처리를 같이 진행
지금까지 스크래퍼의 문제점과 해결 방안
비동기 처리시 데이터가 완전하지 않을 때 요청을 보내면 불완전한 응답을 받을 수 있다. 이 경우에는 임의로 시간을 지연한 후, 데이터 처리가 끝난 후 정보를 가져오면 된다.
from selenium import webdriver
driver = webdriver.Chrome()
driver.implicitly_wait(10)
driver.get("http://www.example.com")
다음으로 키보드 입력, 마우스 클릭 등을 requests로는 진행하기 어렵다. 이 경우 응답을 받은 후에 프로그래밍을 통해 작업을 하면 된다. 즉, 웹 브라우저를 파이썬으로 조작하면 된다. 이는 웹 브라우저를 자동화하는 라이브러리 Selenium으로 가능하다.
from selenium import webdriver
elem = driver.find_element_by_tag_name("hello-input")
elem.send_keys("Hello!")
'[프로그래머스] 데이터 엔지니어링 데브코스 3기 > TIL(Today I Learn)' 카테고리의 다른 글
[TIL - 10일 차] 데이터 엔지니어링 : 파이썬으로 웹 데이터를 크롤하고 분석하기 (5) (0) | 2024.04.05 |
---|---|
[TIL - 9일 차] 데이터 엔지니어링 : 파이썬으로 웹 데이터를 크롤하고 분석하기 (4) (0) | 2024.04.04 |
[TIL - 7일 차] 데이터 엔지니어링 : 파이썬으로 웹 데이터를 크롤하고 분석하기 (2) (0) | 2024.04.02 |
[TIL - 6일 차] 데이터 엔지니어링 : 파이썬으로 웹 데이터를 크롤하고 분석하기 (1) (2) | 2024.04.01 |
[TIL - 5일 차] 데이터 엔지니어링 : 자료구조/알고리즘 풀기 (5) (0) | 2024.03.29 |