BeautifulSoup
- HTML 및 XML 문서를 파싱하고 데이터를 쉽게 추출할 수 있게 도와주는 라이브러리
- 웹 스크래핑(웹 페이지에서 정보를 수집)하는데 자주 사용
# HTML 예시 1
## 내장 번역기(html.parser)
from bs4 import BeautifulSoup
html_doc = '''
<html>
<head><title>홈페이지</title></head>
<body>
<h1>안녕하세요</h1>
<p>나도 컨텐츠 내용</p>
<p class="content">컨텐츠 내용</p>
<a status="on" href="https://example.com">링크</a>
<img src="https://example.com/image.jpg" alt="예제 이미지" class="image-class">
</body>
</html>
'''
soup = BeautifulSoup(html_doc, 'html.parser')
'''
html_doc (무엇을?)
분석할 대상인 HTML 문자열을 의미합니다. 위에서 정의한 <html>...</html> 텍스트가 여기에 들어갑니다.
'html.parser' (어떻게?)
문자열을 해석할 구문 분석기(Parser)를 지정하는 것입니다.(내장 번역기)
파이썬에 기본으로 내장된 번역기를 쓰겠다는 의미이며, 텍스트에 불과했던 HTML을 head, body, h1, p 같은 태그 단위의 구조(트리 구조)로 쪼개서 해석해 줍니다.
'''
* 또 다른 번역기
- 'html.parser': 파이썬 기본 내장 번역기. 따로 설치할 필요가 없고 무난하지만, 아주 복잡하거나 엉망으로 작성된 HTML을 해석할 때는 종종 실수를 합니다.
- 'lxml': 별도로 설치해야 하는 외부 번역기. C언어로 만들어져서 속도가 엄청나게 빠르고, 엉망인 HTML 코드도 기가 막히게 잘 고쳐서 해석합니다. (실무나 대용량 크롤링에서 가장 많이 씁니다.)
- 'html5lib': 별도로 설치해야 하는 외부 번역기. 웹 브라우저(크롬, 사파리 등)와 완전히 동일한 방식으로 HTML을 해석합니다. 속도는 조금 느리지만 가장 정확하게 해석합니다.
## string, test
# string: 태그 안의 텍스트를 가져오는 역할
print(f"제목: {soup.title.string}")
print(f"h1: {soup.h1.string}")
print(f"p: {soup.p.string}")
print(f"내용: {soup.find('p', class_='content').string}")
print(f"링크: {soup.a['href']}")
print(f"이미지: {soup.img['src']}")
print(f"이미지2: {soup.find('img')['src']}")
-----------------------------------------------------------------
<출력값>
제목: 홈페이지
h1: 안녕하세요
p: 나도 컨텐츠 내용
내용: 컨텐츠 내용
링크: https://example.com
이미지: https://example.com/image.jpg
이미지2: https://example.com/image.jpg
* string vs test
| 특징 | .string | .text |
| 기본 역할 | 태그 안의 단일 문자열 추출 | 태그 안의 모든 문자열 추출 |
| 자식 태그가 있을 때 | None 반환 (가장 큰 차이!) | 자식 태그 속 글자까지 전부 합쳐서 반환 |
| 데이터 타입 | NavigableString 객체 (BeautifulSoup 전용 수프 문자열) | 일반 파이썬 문자열 (str) |
## find
# soup.find('태그명')
print(soup.find('h1').string)
# soup.find(속성='속성값')
print(soup.find(class_='content').string)
print(soup.find('a',status='on'))
print(type(soup.find('a',status='on')))
print(soup.find('a',status='on').string)
# soup.find(attrs={'속성':'속성값'}) => 복합적
print(soup.find(attrs={'class':'content'}).string)
# soup.find('태그명', attrs={'속성':'속성값'})
print(soup.find('p',attrs={'class':'content'}).string)
---------------------------------------------------------
<출력값>
안녕하세요
컨텐츠 내용
<a href="https://example.com" status="on">링크</a>
<class 'bs4.element.Tag'>
링크
컨텐츠 내용
컨텐츠 내용
- find()의 기본 사용법 5가지
- 태그 이름으로 찾기
# 문서에서 가장 처음 나오는 <h1> 태그를 찾음
print(soup.find('h1').string)
- 특정 속성(Attribute)으로 찾기
태그 이름은 상관없고, 특정 속성을 가진 태그를 찾고 싶을 때 씁니다. 단, class 속성은 파이썬의 예약어(클래스 선언할 때 쓰는 class)와 이름이 겹치기 때문에 뒤에 언더바(_)를 붙여 class_라고 적어야 합니다.
# 클래스 이름이 'content'인 첫 번째 태그를 찾음
print(soup.find(class_='content').string)
# status 속성이 'on'인 첫 번째 태그를 찾음
print(soup.find(status='on'))
- 태그 이름과 속성을 동시에 조합해서 찾기
# <p class="content"> 구조를 정확하게 타겟팅
print(soup.find('p', class_='content').string)
- attrs 매개변수로 찾기 (딕셔너리 형태)
속성 이름과 값을 파이썬 딕셔너리 형태({'속성': '값'})로 묶어서 전달하는 방식입니다. [특정 속성(Attribute)으로 찾기]
방식과 결과는 완전히 같지만, 속성명이 특수문자를 포함하거나 동적으로 제어할 때 유용합니다.
# class가 content인 태그 찾기
print(soup.find(attrs={'class':'content'}).string)
- 태그 이름 + attrs 조합하기
# p 태그 중에서 class가 content인 것 찾기
print(soup.find('p', attrs={'class':'content'}).string)
# HTML 예시 2
from bs4 import BeautifulSoup
html_doc = """
<html>
<head><title>예제 페이지</title></head>
<body>
<h1>안녕하세요!</h1>
<p class="content">첫 번째 단락</p>
<div id="gallery", class="gallery">
<img src="https://example.com/image1.jpg" alt="첫 번째 이미지" class="image">
<img src="https://example.com/image2.jpg" alt="두 번째 이미지" class="image">
<img src="https://example.com/image3.jpg" class="image">
</div>
<div id="gallery2", class="gallery">
<img src="https://example.com/image1.jpg" alt="첫 번째 이미지" class="image">
<img src="https://example.com/image2.jpg" alt="두 번째 이미지" class="image">
<img src="https://example.com/image3.jpg" class="image">
</div>
<a href="https://example.com" class="link">링크</a>
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'html.parser')
## find_all()
# 이미지, 대체 텍스트(alt)
img_tags = soup.find_all('img')
for img_tag in img_tags:
print(f"이미지 경로: {img_tag['src']}, 대체 텍스트: {img_tag.get('alt','텍스트 없음')}")
# 대괄호를 원래 쓰지만 만약 []안의 내용이 없으면 에러가 나기 때문에 get 방식을 쓴다.
# 인덱스 값으로 찾기
print(soup.find_all('div')[0].find_all('img')[1]['src'])
print(soup.find(id="gallery"))
print(soup.find_all(class_="gallery"))
-------------------------------------------------------------------------------------------
<출력값>
이미지 경로: https://example.com/image1.jpg, 대체 텍스트: 첫 번째 이미지
이미지 경로: https://example.com/image2.jpg, 대체 텍스트: 두 번째 이미지
이미지 경로: https://example.com/image3.jpg, 대체 텍스트: 텍스트 없음
https://example.com/image2.jpg
<div ,="" class="gallery" id="gallery">
<img alt="첫 번째 이미지" class="image" src="https://example.com/image1.jpg"/>
~ </div>
[<div ,="" class="gallery" id="gallery">
<img alt="첫 번째 이미지" class="image" src="https://example.com/image1.jpg"/>
~ </div>, <div ,="" class="gallery" id="gallery2">
<img alt="첫 번째 이미지" class="image" src="https://example.com/image1.jpg"/>
~ </div>]
- find_all()의 기본 사용법과 특징
- 태그 이름으로 전체 찾기 (find_all('태그명'))
# 문서 내 모든 <img> 태그를 리스트로 묶어서 가져옴
img_tags = soup.find_all('img')
for img_tag in img_tags:
# 대괄호([]) 문법은 속성이 없으면 에러가 나므로, .get()을 쓰는 것이 유용!
print(f"이미지 경로: {img_tag['src']}, 대체 텍스트: {img_tag.get('alt','텍스트 없음')}")
- 인덱스([0], [1]) 값으로 특정 위치 태그 조준하기
find_all()의 결과물은 파이썬 리스트이기 때문에, 대괄호 인덱싱([숫자])을 사용해 원하는 순서의 태그만 골라낼 수 있습니다.
# 1. soup.find_all('div')[0] ➔ 첫 번째 <div> 태그를 선택
# 2. .find_all('img')[1] ➔ 그 <div> 안에서 두 번째 <img> 태그를 선택
# 3. ['src'] ➔ 해당 이미지의 주소를 추출
print(soup.find_all('div')[0].find_all('img')[1]['src'])
- 속성으로 찾기 (id vs class_)
id 속성은 HTML 안에서 단 하나만 존재하는 고유 신분증 같은 개념이라 보통 find()에 사용
class 속성은 여러 태그가 별명처럼 공유해서 사용할 수 있기 때문에 보통 find_all()에 사용
# id가 gallery인 고유한 태그 하나를 찾을 때는 find()가 적합!
print(soup.find(id="gallery"))
# class가 gallery인 '모든' 태그들을 한 번에 리스트로 싹 쓸어 담을 때는 find_all()이 적합!
print(soup.find_all(class_="gallery"))
# HTML 예시 3
import requests
from bs4 import BeautifulSoup
url = "http://lecture.pul.kr/ex2.html"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
products = soup.select('div.product')
total_price = 0
for product in products:
name = product.select_one('h2.name').text
price = product.select_one('span.price').text
print(f"상품명: {name}, 가격: {price}")
int_price = int(price.replace(",","").replace("원",""))
total_price += int_price
print(f"\n해당 페이지에 있는 상품 총 가격은 {total_price:,}원 입니다.")
------------------------------------------------------------------------------
<출력값>
상품명: 스마트폰 A, 가격: 899,000원
상품명: 노트북 B, 가격: 1,299,000원
상품명: 태블릿 C, 가격: 599,000원
상품명: 스마트워치 D, 가격: 299,000원
해당 페이지에 있는 상품 총 가격은 3,096,000원 입니다.
## select(), select_one()
- select_one()의 역할과 사용법
find()와 똑같은 역할, 조건에 맞는 태그 중 가장 먼저 발견되는 '딱 하나'의 태그만 가져옵니다. 결과물은 마찬가지로 Tag 객체입니다.
name = product.select_one('h2.name').text
price = product.select_one('span.price').text
- select()의 역할과 사용법
find_all()과 똑같은 역할, 조건에 맞는 모든 태그를 긁어모아 파이썬 '리스트(List)' 형태로 반환합니다. 결과물은 ResultSet(리스트 뭉텅이)입니다.
products = soup.select('div.product')
- CSS 선택자 핵심 문법 4가지
select 계열 함수가 find 계열보다 압도적으로 편리한 이유는, 복잡한 경로를 문자열 한 줄로 표현할 수 있기 때문입니다.
| 종류 | 선택자 문법 | 의미 | 예시 코드 |
| 태그 | 태그명 | 해당 태그 선택 | soup.select('p') |
| 클래스 | .클래스명 | 특정 클래스를 가진 태그 선택 | soup.select('.content') |
| ID | #ID명 | 고유한 ID를 가진 태그 선택 | soup.select_one('#gallery') |
| 자식/자손 | 띄어쓰기 | ~의 내부에 있는 태그 (경로 추적) | soup.select('div.product h2.name') |
# HTML 활용 1
from bs4 import BeautifulSoup
import requests
from urllib.parse import urljoin
url = 'https://startcoding.pythonanywhere.com/basic'
response = requests.get(url)
response.encoding = 'uft-8' # 인터넷에서 긁어온 데이터(HTML)의 글자가 깨지지 않도록 올바른 한글 번역기(인코딩 형식)를 강제로 지정해 주는 작업
html_doc = response.text
soup = BeautifulSoup(html_doc, 'html.parser')
product_divs = soup.find_all('div', class_='product')
total_price = 0
for div in product_divs:
category = div.find(class_='product-category').text.strip()
name = div.find('h3', class_='product-name').text.strip()
if div.find('h4', class_='product-price').find('del'):
div.find('h4', class_='product-price').find('del').decompose()
price = div.find('h4', class_='product-price').text.strip()
image = div.find('img').get('src')
image_url = urljoin(url, image) # 상대경로 -> 절대경로
print(f"종류: {category},\n상품: {name},\n가격: {price},\n이미지: {image_url}\n\n")
int_price = int(price.replace("원",'').replace(',',''))
total_price += int_price
print(f"해당 페이지 상품 총 가격은 {total_price:,}원 입니다.")
---------------------------------------------------------------------------------------
<출력값>
종류: 노트북,
상품: 에이서 스위프트 GO 16 OLED, 스틸 그레이, 코어i7, 512GB, 16GB, WIN11 Home, SFG16-71-77FT,
가격: 1,419,000원,
이미지: https://startcoding.pythonanywhere.com/static/img/product01.png
...
종류: 노트북,
...
이미지: https://startcoding.pythonanywhere.com/static/img/product10.png
해당 페이지 상품 총 가격은 8,639,000원 입니다.
## urljoin
urljoin()은 파이썬의 기본 내장 라이브러리인 urllib.parse에서 제공하는 함수로, 기준이 되는 주소(Base URL)와 상대 경로(Relative Path)를 결합하여 하나의 완전한 웹 주소(절대 경로, Absolute URL)로 만들어 주는 역할
1. 문법 구조와 매개변수
from urllib.parse import urljoin
완전한_주소 = urljoin(기준_주소, 상대_경로)
2. 상황별 urljoin 작동 방식 (알아두면 좋은 상식)
urljoin은 상대 경로가 어떤 모양이냐에 따라 알아서 대응
① 상위 폴더 기호(../)가 있을 때
- 기준 주소: https://example.com/blog/post1
- 상대 경로: ../img/pic.jpg
- 결과: https://example.com/img/pic.jpg (blog 폴더를 탈출함)
② 그냥 파일 이름이나 하위 폴더만 있을 때
- 기준 주소: https://example.com/blog/
- 상대 경로: detail/page.html
- 결과: https://example.com/blog/detail/page.html (뒤에 자연스럽게 이어 붙임)
③ 슬래시(/)로 시작하는 절대 경로일 때
- 기준 주소: https://example.com/blog/post1
- 상대 경로: /main/logo.png
- 결과: https://example.com/main/logo.png (메인 도메인 바로 뒤에 붙여버림)
④ 이미 완전한 주소(http...)를 넣었을 때
- 기준 주소: https://example.com/blog/post1
- 상대 경로: https://naver.com/logo.png
- 결과: https://naver.com/logo.png (상대 경로가 이미 완전하므로 기준 주소를 무시함)
# 요약정리
| 함수명 | 찾기 방식 | 찾는 개수 | 결과 데이터 타입 | 💡 특징 및 주의사항 |
| find() | 일반 매개변수 (태그, 속성=값) |
선착순 1개 | Tag 객체 (알맹이) |
• 뒤에 또 다른 find()나 .text를 바로 붙여서 쓸 수 있음. • 못 찾으면 None을 반환함. |
| select_one() | CSS 선택자 ('태그.클래스명') |
선착순 1개 | Tag 객체 (알맹이) |
• find()의 CSS 선택자 버전. • 복잡한 경로를 띄어쓰기 한 줄로 파고들 수 있어 편리함. • 못 찾으면 None을 반환함. |
| find_all() | 일반 매개변수 (태그, 속성=값) |
조건 맞는 전부 | ResultSet (리스트 뭉텅이) |
• 뒤에 대뜸 .text나 추가 find()를 바로 붙이면 에러남! • 반드시 반복문(for)을 돌리거나 인덱스([0])로 알맹이를 꺼내야 함. • 못 찾으면 빈 리스트([])를 반환함. |
| select() | CSS 선택자 ('태그.클래스명') |
조건 맞는 전부 | ResultSet (리스트 뭉텅이) |
• find_all()의 CSS 선택자 버전. • 뒤에 대뜸 .text를 붙이면 에러남 (반복문 필수). • 실무에서 가독성이 좋아서 가장 인기가 많음. • 못 찾으면 빈 리스트([])를 반환함. |
느낀점
오늘은 BeautifulSoup을 통한 스크랩핑하는 법에 대해 배웠다. 특정 원하는 부분만 가져오기 위해서는 꼭 알아야하는것이다. 오늘은 기본 어떻게 하는 지만 배웠고 다음주부터 실제 웹사이트에서 크롤링을 해보기 때문에 개념을 놓치면 안될 것 같다.
——————————————————————————
본 후기는 [한글과컴퓨터x한국생산성본부x스나이퍼팩토리] 한컴 AI 아카데미 (B-log) 리뷰로 작성 되었습니다.
'학습일지' 카테고리의 다른 글
| [스나이퍼팩토리] 한컴AI아카데미(26.06.09) 네이버메일보내기자동화(Selenium) (0) | 2026.06.10 |
|---|---|
| [스나이퍼팩토리] 한컴AI아카데미(26.06.08) Selenium (0) | 2026.06.09 |
| [스나이퍼팩토리] 한컴AI아카데미(26.06.04) urllib (0) | 2026.06.04 |
| [스나이퍼팩토리] 한컴AI아카데미(26.06.02) GitHub CI/CD (0) | 2026.06.04 |
| [스나이퍼팩토리] 한컴AI아카데미(26.05.29) Docker compose (0) | 2026.05.29 |