수학과의 좌충우돌 프로그래밍

[crawling] 03. BeautifulSoup으로 웹 크롤링 하기 본문

웹프로그래밍/crawling

[crawling] 03. BeautifulSoup으로 웹 크롤링 하기

ssung.k 2019. 3. 22. 12:47

BeautifulSoup 으로 웹 크롤링하기

앞에서 우리는 이미 BeuatifulSoup 를 설치 하였고 import 가 잘 되는지도 확인해보았습니다. 혹시 BeuatifulSoup4 버젼을 설치해야한다고 했던 게 기억나시나요?

그냥 BeuatifulSoup 를 설치하게 되면, 이는 BeuatifulSoup3 버젼이고 python3과 호환이 되지 않습니다. 이 점 다시 한 번 유의해주시기 바랍니다.

말하는 김에 한 가지 더 유의해야 할 점이 있습니다. 우리는 우리가 원하는 정보를 얻기 위해서 각 태그에 접근을 할 것입니다. 그리고 개발자도구 를 통해 어떤 태그인지를 확인 할 것 입니다. 여기서 문제가 발생합니다. 웹 페이지는 일종의 문서이기 때문에 웹 브라우저가 이를 해석하는 방식의 차이가 있습니다. 즉 해석하면서 없던 태그가 생겨나기도 한다는 것입니다. 그리고 requests를 통해서는 최초의 html코드만 가져올 수 있기 때문에 새롭게 생긴 태그에 대해서는 데이터를 가져올 수 없습니다.

이제 본격적으로 BeautifulSoup 사용법에 대해 알아보도록 하겠습니다.

 

BeautifulSoup 맛보기

requests 를 통해 html 코드를 가져와야 하지만 지금은 BeautifulSoup 가 어떻게 동작하는지 알아볼 것이기 때문에 html 코드는 가져왔다는 전제하에 진행하겠습니다.

html = '''
    <ol class="fruit">
        <li>사과</li>
        <li>바나나</li>
        <li>딸기</li>
        <li>수박</li>
    </ol>
    <ol class="vegetable">
        <li>무</li>
        <li>상추</li>
        <li>콩나물</li>
        <li>깻잎</li>
    </ol>
'''

우리의 목표은 채소에 해당하는 항목들을 가져오는 것입니다. 그리고 그러기 위해서는 BeautifulSoup 함수를 사용합니다. 첫번째 인자로 가져온 데이터를 두 번째 인자로 어떤 도구로 데이터를 원하는 형태로 만들어 낼 지를 결정합니다. 그리고 이 도구를 parser라고 부릅니다. parser는 여러 종류가 있지만 BeautifulSoup 에서 기본제공하는 html.parser 를 사용하도록 하겠습니다.

soup = BeautifulSoup(html, 'html.parser')

지금은 html 이 이미 원하는 형태로 존재하기 때문에 souphtml 은 차이가 없습니다. 뒤에 활용하면서 차이에 대해 다시 한 번 언급하도록 하겠습니다.

다음으로는 우리가 원하는 세부적인 데이터로 접근해야합니다. 이 때 접근하는 방법은 select 메소드와 css선택자를 통해 접근 할 수 있습니다. 위에서는 li 태그 안에 있는 채소의 항목에 대한 정보가 필요하므로 아래와 같이 접근합니다.

soup.select('li') 

결과는 다음과 같이 list 형태입니다.

[<li>사과</li>,
 <li>바나나</li>,
 <li>딸기</li>,
 <li>수박</li>,
 <li>무</li>,
 <li>상추</li>,
 <li>콩나물</li>,
 <li>깻잎</li>]

과일의 항목 역시 li 태그를 사용하였기에 원하지 않은 과일의 정보가 포함되어 있습니다. 좀 더 세부적인 접근이 필요합니다.

soup.select('.vegetable li')

아래와 같이 채소 목록만 불러올 수 있습니다.

[<li>무</li>,
 <li>상추</li>,
 <li>콩나물</li>,
 <li>깻잎</li>]

이제 각 list 안의 각 원소에 접근하기 위해서 반복문을, 태그 안의 내용에 접근하기 위해 text 메소드를 사용하겠습니다.

vegetable_list = []
for tag in soup.select('.vegetable li'):
    vegetable_list.append(tag.text)

정상적으로 출력되는걸 확인할 수 있습니다.

# vegetable_list
['무', '상추', '콩나물', '깻잎']

 

BeautifulSoup 활용하기

이번에는 '네이버 웹툰' 의 인기 급상승 만화 부분을 크롤링해서 가져와보도록 하겠습니다. 바로 이 부분입니다.


각각 인기순과 업데이트순에 따라 1위 부터 10위의 해당되는 만화가 있습니다. 이 내용을 가져와 보도록 하죠.

response = requests.get('https://comic.naver.com/webtoon/weekday.nhn')
html = response.text

requests를 이용하여 해당 url 의 html 코드를 가져왔습니다.

'\r\n\r\n\r\n  \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n<!DOCTYPE html>\r\n<html lang="ko">
\r\n<head>\r\n\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\t<meta http-equiv=
"X-UA-Compatible" content="IE=edge,chrome=1">\r\n\t\t\r\n\t<meta http-equiv=
"Content-type" content="text/html; charset=UTF-8">\r\n\t<title>네이버 만화 > 요일별  웹툰 > 전체웹툰</title>
[생략]


제어문자들이 너무 많아서 알아보기가 힘듭니다. 이 때 위에서 언급 했던 parser를 통해 데이터를 원하는 형태로 바꿔줄 수 있습니다.

soup = BeautifulSoup(html, 'html.parser')

깔끔하게 html 코드만 남은 걸 확인할 수 있습니다.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"/>
<meta content="text/html; charset=utf-8" http-equiv="Content-type"/>
<title>네이버 만화 &gt; 요일별  웹툰 &gt; 전체웹툰</title>
<meta content="네이버 웹툰" property="og:title"/>
[생략]

 

이제 개발자도구에서 원하는 부분을 찾아 어떤 css 선택자로 접근해야하는지 알아봅시다. 


' 인기급상승 만화 ' 라고 친절하게 주석으로 표시가 되어있네요. 이는 div 태그로 묶여있으며 이 때 class는 asideBox 입니다.


 

인기순과 업데이트순은 각각 id 값으로 realTimeRankFavorite와 realTimeRankUpdate 를 가집니다.


우리가 원하는 만화의 제목은 li 태그 안 a 태그 안에 위치해있습니다.


인기순에 해당하는 favor_list 와 업데이트순에 해당하는 update_list를 만들어 이 안에 각 만화 제목들을 추가해주도록 하겠습니다. 코드는 다음과 같습니다.

favor_list = []
update_list = []
for tag in soup.select('.asideBox'):
    for favor_tag in tag.select('#realTimeRankFavorite li a'):
        favor_list.append(favor_tag.text)
    for update_tag in tag.select('#realTimeRankUpdate li a'):
        update_list.append(update_tag['title'])

 

아마 여기서 이상한 부분을 눈치채셨을 겁니다. 바로 updade_tag['title'] 입니다. 물론 각 리스트의 결과는 문제 없이 잘 출력이 됩니다.

# favor_list
['외모지상주의-227화 가출팸 [09]',
 '갓 오브 하이스쿨-403화',
 '스위트홈-74화',
 '걸어서 30분-79화 조금 더 옆에 있었으면',
 '개를 낳았다-41화',
 '화장 지워주는 남자-47화',
 '용비불패 완전판-21화',
 '금붕어-8화',
 '내 여자친구는 상남자-75. 그러지마',
 '테러맨-시즌2 49화']

#update_list
['꽃미남 저승사자-37화',
 '금붕어-8화',
 '밥 먹고 갈래요?-봄동부침개 화',
 '몽홀-제 32화 혼란과 음모 - 4',
 '걸어서 30분-79화 조금 더 옆에 있었으면',
 '가우스전자 시즌3~4-시즌4 275화 담판',
 '1초-1화',
 '내 여자친구는 상남자-75. 그러지마',
 '자취로운 생활-65. 멋 좀 부려보자',
 '냐한남자-54화']

 

update_tag 를 favor_tag 와 마찬가지로 text 를 통해서 표현해보았습니다.

for update_tag in tag.select('#realTimeRankUpdate li a'):
        update_list.append(update_tag.text)

 

결과가 예상과 다르게 제어문자를 많이 포함하고 있습니다.

['\r\n\t\t\t\t\t\r\n\t\t\t\t\t꽃미남 저승사자-37화\r\n\t\t\t\t',
 '\r\n\t\t\t\t\t\r\n\t\t\t\t\t금붕어-8화\r\n\t\t\t\t',
 '\r\n\t\t\t\t\t\r\n\t\t\t\t\t밥 먹고 갈래요?-봄동부침개 화\r\n\t\t\t\t',
 '\r\n\t\t\t\t\t\r\n\t\t\t\t\t몽홀-제 32화 혼란과 음모 - 4\r\n\t\t\t\t',
 '\r\n\t\t\t\t\t\r\n\t\t\t\t\t걸어서 30분-79화 조금 더 옆에 있었으면\r\n\t\t\t\t',       '\r\n\t\t\t\t\t\r\n\t\t\t\t\t가우스전자 시즌3~4-시즌4 275화 담판\r\n\t\t\t\t',      '\r\n\t\t\t\t\t\r\n\t\t\t\t\t1초-1화\r\n\t\t\t\t',
 '\r\n\t\t\t\t\t\r\n\t\t\t\t\t내 여자친구는 상남자-75. 그러지마\r\n\t\t\t\t',      '\r\n\t\t\t\t\t\r\n\t\t\t\t\t자취로운 생활-65. 멋 좀 부려보자\r\n\t\t\t\t',     '\r\n\t\t\t\t\t\r\n\t\t\t\t\t냐한남자-54화\r\n\t\t\t\t']

 

왜 이런 일이 발생하는지 html 코드를 확인해보도록 합시다. 이유는 모르겠지만 a 태그 안에서도 비어있는 공간이 많습니다. 그래서 사용하기 위해서는 별도의 가공이 필요하죠.

[생략]
<ol id="realTimeRankUpdate" class="asideBoxRank" style="display:none;">
	<li class="rank01">
		<a onclick="nclk_v2(event,'rnk*u.cont','721461','1')" href="/webtoon/detail.nhn?titleId=721461&no=38" title="꽃미남 저승사자-37화">
					
			꽃미남 저승사자-37화
		</a>
[생략]

 

a 태그의 title속성을 보면 웹툰의 제목과 같음을 볼 수 있습니다. 따라서 다음과 같이 title속성에 접근하였습니다.

for update_tag in tag.select('#realTimeRankUpdate li a'):
        update_list.append(update_tag['title'])

 

Comments