본문 바로가기
데이터 분석/파이썬 데이터 분석가 되기+챗GPT

05장 웹 데이터 수집 라이브러리, 뷰티풀수프

by yEvery 2025. 2. 16.

05.1 웹 데이터 수집 기본 개념 알아보기

 

robot.txt: 웹 사이트 소유자가 크롤러에게 제공하는 지침서, 접근할 수 있는 영역과 없는 영역을 명확히 정의

웹 크롤링: 크롤러라는 프로그램을 인터넷으로 보내 인터넷 콘텐츠를 자동으로 수집하고 색인화하는 것

웹 스크래핑: 특정 웹 사이트에서 특정 페이지의 데이터 수집을 목표로 하는 프로그램

요청: 서버에 데이터 요청

: 서버에 데이터를 요청하는 행위

응답: 서버가 데이터 보내줌

서버가 요청에 대해 데이터를 보내주는 행위

파이썬 + 뷰티풀수프: 데이터 속에서 원하는 데이터 찾기

응답으로 받은 데이터에서 원하는 값만 찾을 때 파이썬과 뷰티풀수프 사용, 원하는 데이터의 위치는 CSS Selector로 지정

soup.select('p.scraping')
soup.select('body > p')
soup.select('#title')

CSS Selector: CSS에서 사용되는 선택자를 활용해 HTML 요소를 선택하는 방법

  • 클래스 선택자: .을 사용
  • 자식 선택자: >을 사용
  • ID 선택자: #을 사용
import requests
from bs4 import BeautifulSoup

리퀘스트(Requests) 라이브러리: HTTP 요청을 통해 HTML 코드를 받아오는 역할

뷰티풀수프(BeautifulSoup): 파이썬으로 웹 데이터를 수집할 때 사용하는 라이브러리

                                             리퀘스트 라이브러리에서 받아온 HTML을 파싱하여 분석

html = """
<html>
    <body>
      <h1 id='title'>파이썬 데이터 분석가 되기</h1>
      <p id='body'>오늘의 주제는 웹 데이터 수집</p>
      <p class='scraping'>삼성전자 일별 시세 불러오기</p>
      <p class='scraping'>이해 쏙쏙 Selena!</p>
    </body>
</html>
"""

html, body, h1, p: 엘리먼트 또는 요소

id: 어트리뷰트 또는 속성

# 뷰티풀수프 불러오기
from bs4 import BeautifulSoup

# html.parser로 앞에서 입력한 HTML 코드를 파싱하게 하고 그 결과를 soup에 저장
soup = BeautifulSoup(html, 'html.parser')
print(soup)

위와 같이 뷰티풀수프를 사용하면 아래와 같이 html 코드를 파싱할 수 있다.

for stripped_text in soup.stripped_strings:
  print(stripped_text)

soup.stripped_strings 속성을 사용하여 정제된 텍스트를 추출할 수 있다. 좌우 공백을 모두 제거한다.

first_p = soup.find('p')
all_p = soup.find_all('p')

title = soup.find(id='title')
scraping_all = soup.find_all(class_='scraping')

first_scraping = soup.find(attrs={'class': 'scraping'})
body_element = soup.find(attrs={'id': 'body'})

find() 함수: 특정 태그의 첫 번째 요소를 검색

find_all() 함수: 특정 태그의 모든 요소를 검색

각각 매개변수를 활용하여 특정 id나 특정 class에 해당하는 값을 찾을 수 있다.

attrs 매개변수를 활용하여 딕셔너리 형태로 전달하는 것도 가능하다.

 

05.2 야후 파이낸스 주가 데이터 웹 스크래핑하기

import requests
from bs4 import BeautifulSoup

# stock_url 변수에 URL 저장
stock_url = 'https://finance.yahoo.com/quote/005930.KS/history/'

# 웹 페이지 요청
res = requests.get(stock_url)

# 응답에서 HTML 문서만 가져오기
html = res.text
print(html) # 값을 제대로 출력하지 못함(status code : 404)

앞에서 한 것과 같은 방식으로 html 코드를 받아오려고 하면 제대로 출력되지 않는다.

import requests
from bs4 import BeautifulSoup

stock_url = 'https://finance.yahoo.com/quote/005930.KS/history/'

# 헤더 값 설정
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
    }

# stock_url을 그냥 요청하는 것이 아니라, 헤더를 담아 요청
res = requests.get(stock_url, headers=headers)
html = res.text
print(html)
헤더 값 추가하기

헤더(header): 웹 브라우저(클라이언트)와 웹 서버 간에 주고 받는 다양한 정보를 담는 그릇과 같은 역할

사용자 에이전트 값은 HTTP 헤더에 담을 수 있다.

서버에서는 HTTP 헤더에서 사용자 에이전트 값을 확인한다.

사용자 에이전트

웹 사이트에서 웹 스크래핑으로 인한 웹 사이트의 부정적인 영향을 최소화하기 위해 웹 스크래핑 프로그램을 차단할 때가 있는데, 이때 사용자 에이전트 값을 확인하여 사람인지 봇인지 구분한다.

사용자 에이전트 값을 추가하는 것만으로 충분하지 않을 때가 있으므로 Accept 값도 함께 헤더에 추가한다.

# res.text를 뷰티풀수프에 전달하여 html.parser로 파싱
soup = BeautifulSoup(html, 'html.parser')
print(soup)

받은 html을 파싱한다.

# 뷰티풀수프로 tr 요소 모두 찾아오기
print(soup.find_all('tr'))

가져오려는 요소가 tr이므로 해당 요소만 추출한다.

first_tr = soup.find('tr')
# 'tr' 요소가 있을 경우, 클래스 정보를 가져옴
first_class = first_tr.get('class')[0]
print(first_class)

데이터를 제공하는 사이트는 CSS 클래스 이름을 자주 변경한다. 스크래핑할 때 사이트 방문 시점의 클래스 값을 직접 확인해야 한다.

# tr 태그에 있는 td 태그 중 class가 first_class인 경우 추출
print(soup.find_all("td", class_=first_class))

데이터를 한 줄씩 가져왔다.

여기서 원하는 데이터만 또 추출해야 한다.

# YYYY년 MM월 DD일인 형식으로 날짜(Date) 처리
print(pd.to_datetime(soup.find_all("td", class_=first_class)[0].text).strftime('%Y년 %m월 %d일')  )

# .00을 원으로 대체하여 종가(Close) 처리
print(soup.find_all("td", class_=first_class)[4].text.replace('.00', '원')  )

날짜와 종가에 해당하는 부분만 가져온다.

날짜

pd.to_datetime() 함수를 통해 날짜를 datetime 객체로 변환한다.

strftime을 사용하여 포맷팅한다.

종가

replace() 함수로 .00을 '원'으로 대체한다.

# tr 태그 조건에 해당하는 모든 정보 검색
rows = soup.find_all('tr')

# 첫 번째 tr 태그(헤더)를 제외하고 순회
for i in range(1, len(rows)):
    # 각 tr 태그 내의 모든 td 태그를 찾기
  cells = rows[i].find_all('td')

    # 조건문 : td 태그가 7개(날짜, 시가, 고가, 저가, 종가, 조정 종가, 거래량)인 경우만 처리
    # 참고, 배당금 정보 포함된 행은 td 태그가 2개이므로 len(cells)가 2임. 따라서 처리하지 않음
    # <for문 안에 if문 중첩! 들여쓰기 주의>
  if len(cells) == 7:
    date = pd.to_datetime(cells[0].text).strftime('%Y년 %m월 %d일')
    close_price = cells[4].text.replace('.00', '원')
    print('날짜 :', date, '/ 종가 :', close_price)

 

for i in range(1, len(rows))

tr 요소에서 첫 번째(0번째)는 헤더이므로 제외한다.

날짜와 종가에 대한 정보를 모두 가져온다.

if len(cells) == 7:
        # 그래프 표시를 위해 축약형으로 날짜 처리
        date = pd.to_datetime(cells[0].text, format = '%b %d, %Y')

        # 그래프 표시를 위해 종가 처리
        close_price = cells[4].text.replace(',', '').replace('.00', '')

        # 날짜를 dates 리스트에 추가
        dates.append(date)
        # 종가를 정수로 변환하여 prices 리스트에 추가
        prices.append(int(close_price))

그래프를 그리기 위해 if문 안에 내용을 위와 같이 변경한다.

# 데이터를 DataFrame으로 변환
stock_data = pd.DataFrame({'date': dates, 'price': prices})

# y축 눈금 간격 설정
min_price = min(stock_data['price'])
max_price = max(stock_data['price'])
y_ticks = range(min_price, max_price, 3000)

# 그래프 그리기
plt.figure(figsize=(10, 5))
plt.plot(stock_data['date'], stock_data['price'], marker='o', label='price')
plt.xlabel('Date')
plt.ylabel('Closing Price (KRW)')
plt.title('Samsung Electronics Stock Price')
plt.legend()
plt.grid(True)
plt.yticks(y_ticks)
plt.show()

y축에 모든 종가 정보를 표시하면 복잡해지므로 최솟값과 최댓값을 저장하여 y축 눈금의 구간을 정한다.

위와 같이 작성하면 아래와 같은 그래프를 그릴 수 있다.