GDSC Sookmyung 활동/10 min Seminar

파이썬으로 웹 스크래핑 시작하기

희._. 2021. 2. 23. 01:15
WikiDocs의  04장 웹스크래핑과 Pandas를 정리한 글입니다.

웹 크롤링과 웹 스크래핑

웹 서버에 저장된 데이터를 가져오는 행위를 웹 크롤링 또는 웹 스크래핑이라고 부른다.

웹 크롤링

Google 등의 대규모 검색 엔진이 GoogleBot과 같은 로봇 크롤러를 인터넷에 보내 인터넷 콘텐츠를 색인화하는 과정을 의미한다. 누군가가 홈페이지를 새로 만들면 어떻게 그 웹사이트가 구글에서 검색이 될까? 구글은 검색을 위해서 인터넷에 연결된 모든 웹 페이지를 돌아다니면서 페이지의 정보를 저장해두기 때문에 검색이 가능하다.

웹 스크래핑

일반적으로 특정한 데이터만을 웹사이트로부터 가져오는 행위를 스크래핑이라고 부른다. 웹 스크래핑은 웹 사이트 상에서 원하는 부분에 위치한 정보를 컴퓨터로 자동 추출하여 수집하는 기술이다.

파이썬으로 웹 스크래핑

HTML 문서 다운로드 및 파싱

웹 스크래핑을 하려면 먼저 HTML 파일을 다운로드한 후, 해당 HTML 문서를 분석해서 원하는 데이터를 가져와야 한다. 파이썬에서 웹 페이지 다운로드는 requests 모듈을 사용하고 웹 페이지에서 원하는 데이터를 가져가는 파싱(parsing)BeautifulSoup 모듈을 사용한다.

1. 웹 페이지 다운로드

requests 모듈get 함수를 사용하면 웹 페이지의 HTML 데이터를 PC에 다운로드할 수 있다.

import requests
url = "http://www.naver.com"
response = requests.get(url)

response 변수는 get() 함수의 반환 값인 Response 객체를 바인딩한다. Response 객체는 클래스이기 때문에 점을 찍으면 클래스의 메서드 또는 프로퍼티에 접근할 수 있다.

Response 클래스는 다양한 기능을 지원하는데, 그중 text 프로퍼티를 이용하면 HTML 코드를 문자열로 다운로드할 수 있다. 즉, requests 모듈의 get 함수를 사용하면 웹 페이지의 HTML을 파이썬 문자열로 다운로드할 수 있다.

Response 객체의 text 프로퍼티에 저장된 HTML 코드

2. 파싱

BeautifulSoup 모듈은 HTML 파일로부터 원하는 데이터를 파싱하는 데 사용한다.

Cf. 파싱((syntatic) parsing) : 일련의 문자열을 의미있는 토큰(token)으로 분해하고 이들로 이루어진 파스 트리(parse tree)를 만드는 과정

💻 예제 : 네이버 금융 사이트의 HTML 데이터 파싱

웹페이지를 분석할 때는 구글에서 개발한 크롬 브라우저를 사용한다. 다양한 분석 기능을 지원하고 HTML 코드를 확인하는 데 유용하다. 이제 크롬에서 아래의 웹 페이지에 접속해 본다.

🔗 https://finance.naver.com/item/main.nhn?code=000660

위 주소로 이동하면, 네이버 금융이 제공하는 SK 하이닉스 페이지를 확인할 수 있다. 웹 주소에서 뒤에 6자리 숫자가 각 주식 종목에 대한 종목 코드이다. 이 값만 변경하면 다른 종목에 대한 정보를 얻을 수 있다. (종목을 바꿨을 때 뒤 6자리 숫자만 변경하는 것을 통해 이 사실을 알 수 있다.)

종목 코드에 따른 종목 페이지

웹 페이지의 오른쪽 위에는 아래 그림과 같이 투자 정보가 요약되어 있다. PER 값을 가져와보자.

SK하이닉스 투자정보

가장 먼저 위 페이지를 구성하는 데 사용하는 HTML 코드를 다운로드 받아야 한다. 이때 마우스를 클릭하여 웹 페이지를 저장하는 것이 아니라 requests 모듈을 사용해서 다운로드한다. 위에서 했던 것과 마찬가지로 웹 페이지 주소를 파이썬 문자열로 표현하고 이를 requests 모듈의 get() 메서드로 전달한다. 메서드의 호출 결과 HTML 문서는 파이썬 문자열 타입으로 표현되며 html이라는 변수가 이를 바인딩한다.

import requests

url = "https://finance.naver.com/item/main.nhn?code=000660"
html = requests.get(url).text

HTML 문서에서 원하는 데이터를 쉽게 파싱하기 위해서 BeautifulSoup 모듈을 사용한다. 우선 모듈을 임포트하고 BeautifulSoup 객체를 생성한다. 객체를 생성할 때 html 데이터와 HTML 문서를 파싱하는 데 사용할 모듈의 이름인 "html5lib"을 넘겨주면 된다.

import requests
from bs4 import BeautifulSoup

url = "https://finance.naver.com/item/main.nhn?code=000660"
html = requests.get(url).text

soup = BeautifulSoup(html, "html5lib")

생성한 BeautifulSoup 객체는 HTML 파싱을 위한 다양한 메서드를 갖고 있다. 그 중에서 select() 메서드를 사용하면 CSS 셀렉터를 사용해서 HTML 문서를 파싱할 수 있다.

🔎 CSS 셀렉터란?

CSS 규칙을 적용할 요소를 정의한다.

셀렉터 종류 셀렉터 문법 셀렉터 기능
TAG 셀렉터 태그 이름 특정 태그에 스타일 적용
ID 셀렉터 #ID 이름 ID 값을 갖는 하나의 태그에 스타일 적용
Class 셀렉터 .Class 이름 Class 이름을 갖는 모든 태그에 스타일 적용
(태그 종류가 달라도 됨)

 

import requests
from bs4 import BeautifulSoup

url = "https://finance.naver.com/item/main.nhn?code=000660"
html = requests.get(url).text

soup = BeautifulSoup(html, "html5lib")
tags = soup.select("#_per")
tag = tags[0]
print(tag.text)    # 34.63

위 코드를 실행하면 웹 페이지에 있던 PER 값이 화면에 출력된다.

이제부터 어떻게 이런 일이 가능했는지 하나씩 확인해보자.

HTML 문서에서 특정 데이터가 있는 태그를 가져오려면 HTML 문서 구조를 알아야 한다. 크롬은 이를 위한 좋은 도구를 제공하고 있다. 크롬 웹 브라우저에서 그림과 같이 PER 값에 마우스 커서를 위치시키고 마우스 오른쪽 버튼을 눌러 '검사' 메뉴를 선택한다.

크롬 브라우저에서 검사 기능 사용

크롬 웹 브라우저의 검사 기능은 웹 페이지에서 선택한 부분에 대응되는 HTML 코드를 보여준다. 그림에서 34.63이라는 값에는 <em> </em> 태그가 대응되는 것을 확인할 수 있다. 어떻게 하면 HTML 문서에 있는 수많은 태그 중 <em> 태그의 값만 가져올 수 있을까?

속성이 부여된 em 태그

만약 HTML 태그가 여러 번 사용되었다면 HTML 전체 문서 중 원하는 데이터의 위치를 더욱 상세히 기술해야 한다. 반면에 찾고자 하는 태그가 한 번만 사용됐다면 쉽게 그 위치를 찾을 수 있다.

그림을 보면 <em> 태그는 웹페이지에서 여러 번 사용됐지만, 해당 <em> 태그는 id = "_per"이라는 속성이 있다는 것을 알 수 있다. id 속성은 HTML 문서에서 한 번만 나올 수 있기 때문에 <em> 태그보다는 id 속성으로 태그를 찾는 것이 좋다. CSS의 ID 셀렉터를 이용하자!

BeautifulSoup 객체에는 CSS 셀렉터로 태그를 가져오는 select() 메서드가 있다. 다음 코드를 보면 select 메서드에 "#_per"이라고 인자를 넘겨주는데, 여기서 '#'의 의미는 id를 의미한다. 따라서 "#_per"은 태그 중 id="_per"인 태그를 선택 (select) 하라는 의미다.

tags = soup.select("#_per")

select() 메서드는 조건에 만족하는 모든 태그를 파이썬 리스트로 반환한다. 사용한 id 속성은 HTML 문서에서 한 번만 나오기 때문에 리스트에 하나의 데이터만 들어가 있다. 따라서 리스트의 0번을 인덱싱하면 원하는 태그를 얻을 수 있다.

tag = tags[0]
print(tag)         # <em id="_per">34.63</em>
print(tag.text)    # 34.63

<em> 태그와 </em> 사이의 값만 가져오기 위해서는 text 속성에 접근하면 된다.

PER 값을 스크래핑하는 코드를 함수로 만들면 다음과 같다. get_per() 함수는 함수 입력으로 종목 코드를 입력받는다. 나머지 함수 본체의 코드는 앞서 설명한 것과 같다. 다만 파싱된 값을 float 값으로 변환한 후 이를 리턴하도록 구현했다.

import requests
from bs4 import BeautifulSoup

def get_per(code):
    url = "https://finance.naver.com/item/main.nhn?code=000660"
    html = requests.get(url).text

    soup = BeautifulSoup(html, "html5lib")
    tags = soup.select("#_per")
    tag = tags[0]
    return float(tag.text)

print(get_per("000660")) # SK 하이닉스 PER 값
print(get_per("005930")) # 삼성전자 PER 값

ID가 없는 일반적인 경우에 대한 스크래핑

외국인 소진율 값을 스크래핑해보자. 크롬 웹 브라우저의 검사 기능을 사용해서 태그를 살펴보면 그림과 같이 <em> 태그임을 알 수 있다. 그런데 이번에는 <em> 태그에 id 속성이 없다. 실제로 웹 페이지에서 데이터를 스크래핑하다 보면 이런 경우가 많다.

외국인 소진율과 HTML 코드

ID가 없는 태그의 경우에는 HTML 문서에서 태그의 상대적인 위치를 나열함으로써 데이터를 파싱하는 다중 셀렉터를 사용해야 한다.

그림에서 HTML 태그를 보면 외국인 소진율은 <table> 안에 <tbody> 안에 <tr> 안에 <td> 안에 <em> 태그 안에 들어 있다. 이를 CSS 다중 셀렉터로 작성하면 아래와 같다.

tbody tr td em

아래는 soup.select() 메서드를 수정한 코드다. ID가 있는 경우와 처음 5 줄은 동일하다. select() 메서드는 조건을 만족하는 태그를 파이썬 리스트로 반환하기 때문에 이를 모두 출력하기 위해 for 문을 사용했다.

import requests
from bs4 import BeautifulSoup

url = "https://finance.naver.com/item/main.nhn?code=000660"
html = requests.get(url).text

soup = BeautifulSoup(html, "html5lib")
tags = soup.select("table tbody tr td em")    # soup.select("#_per")

for tag in tags:
    print(tag.text)

코드를 실행해보면 원하는 외국인 소진율뿐만 아니라 수많은 값이 출력된다. 다중 셀렉터가 모호해서 같은 조건을 갖는 다른 태그들도 함께 출력된 것이다.

생략
..................
상향 60
상향 35
하향 50

다중 셀렉터를 보다 구체화해 원하는 값만 선택해보자.
그림 <외국인 소진율과 HTML 코드>의 HTML 코드를 자세히 살펴보면, table 태그에는 lwidth 클래스 속성이, tr 태그에는 strong 클래스 속성이 부여되어 있다. 이러한 사항들을 CSS 셀렉터에 반영한다. 클래스 속성은 마침표와 클래스 이름을 적어서 나타낸다.

tags = soup.select(".lwidth tbody .strong td em")    # soup.select("table tbody tr td em")

코드를 수정 후 실행하면 외국인 소진율만 화면에 출력되는 것을 확인할 수 있다. 이처럼 한 번에 원하는 값을 얻어내기는 쉽지 않다. 기본적인 뼈대를 작성하고 클래스 셀렉터 및 아이디 셀렉터를 활용해서 셀렉터를 구체화해야 한다.

크롬 브라우저를 사용하면 다중 셀렉터를 쉽게 만들 수 있다. 그림과 같이 외국인 소진율에 대응되는 <em> 태그에 마우스 오른쪽 버튼을 클릭한 후 Copy → Copy selector 메뉴를 선택한다.

크롬 브라우저의 자동 셀렉터 생성 기능

복사된 CSS selector는 다음과 같다. CSS selector에서 > 기호는 자식 태그를 의미한다. 아래의 CSS selector는 ID가 'tab_con1'인 태그 안에 있는 세 번째 div 태그 안에 table 태그 안에 tbody 태그 안에 strong 클래스 속성이 부여된 tr 태그 안에 td 태그 안에 있는 em 태그를 의미한다. HTML 문서 전체에서 이런 태그 계층 구조를 가진 em 태그를 찾는 것이다.

#tab_con1 > div:nth-child(3) > table > tbody > tr.strong > td > em

위에서 복사한 CSS selector를 파이썬 BeautifulSoup의 select() 메서드 입력 값으로 사용해보자.

tags = soup.select("#tab_con1 > div:nth-child(3) > table > tbody > tr.strong > td > em")

코드를 실행해보면 nth-of-type을 사용하라는 에러 메시지가 출력된다. 이는 BeautifulSoup 모듈이 CSS 셀렉터를 100% 지원하지 않아 nth-child를 인식하지 못하기 때문에 발생하는 에러다. BeautifulSoup은 특정 위치의 자식 태그를 선택할 때 nth-child()대신, nth-of-type()을 사용해야 한다.

Cf. nth-child vs. nth-of-type

nth-child(n) nth-of-type(n)
부모 엘리먼트의 모든 자식 엘리먼트 중 n번째 부모 엘리먼트의 특정 자식 엘리먼트 중 n번째

외국인 소진율 HTML 코드

부모 엘리먼트가 ID가 'tab_con1'인 태그이고, 자식 엘리먼트로는 순서대로 h3 div div가 있다. 따라서 nth-child의 파라미터는 전체 자식 엘리먼트 중 세 번째로, 3이고 nth-of-type의 파라미터는 div 태그인 자식 엘리먼트 중 두 번째로, 2이다.

코드를 아래와 같이 수정하면 외국인 소진율 50.24%만 화면에 출력된다.

tags = soup.select("#tab_con1 > div:nth-of-type(2) > table > tbody > tr.strong > td > em")

전체 코드

ID를 이용하여 PER 값을 스크래핑하는 함수

import requests
from bs4 import BeautifulSoup

def get_per(code):
    url = "https://finance.naver.com/item/main.nhn?code=000660"
    html = requests.get(url).text

    soup = BeautifulSoup(html, "html5lib")
    tags = soup.select("#_per")
    tag = tags[0]
    return float(tag.text)

print(get_per("000660")) # SK 하이닉스 PER 값
print(get_per("005930")) # 삼성전자 PER 값

ID가 없는 일반적인 경우에 대한 스크래핑

import requests
from bs4 import BeautifulSoup

url = "https://finance.naver.com/item/main.nhn?code=000660"
html = requests.get(url).text

soup = BeautifulSoup(html, "html5lib")
# 직접 만든 CSS 셀렉터
tags = soup.select(".lwidth tbody .strong td em")
# 크롬 브라우저가 지원하는 CSS 셀렉터
# tags = soup.select("#tab_con1 > div:nth-of-type(2) > table > tbody > tr.strong > td > em")

tag = tags[0] # 원하는 값만을 파싱했기 때문에 0번만 인덱싱
print(tag.text) # 50.24%

정리

직접 CSS 셀렉터를 만드는 방법에서부터 크롬 브라우저가 지원하는 CSS 셀렉터 기능을 사용하는 방법에 대해서 알아보았다. HTML과 CSS의 상세한 문법보다는 HTML 코드의 구조를 알고 CSS 셀렉터를 만들어 낼 수 있는 능력이 중요하다. 사실 크롬 브라우저가 지원하는 CSS 셀렉터를 사용한다면 정말 손쉽게 웹 스크래핑이 가능하다. 한 번 코드를 따라해보고 웹 스크래핑을 시작해보았으면 한다!

참고로 웹에서 데이터를 얻어오는 방법으로는 웹 API를 사용하는 방법도 있는데, 궁금하다면 아래 Reference 의 두 번째 링크에 들어가서 RestfulAPI 섹션을 봐보는 것을 추천한다.

Reference

 

nth-child와 nth-of-type의 차이

nth-child와 nth-of-type 이라 했지만 first-child, first-of-type이나 last-child, last-of-type등 모든요소에서 공통적으로 적용되는 개념이다. 간단하게 비교를 해보자면 nth-child(n)  nth-of-type(n)  부모..

firerope.tistory.com