👨🏼‍💻

ch2 - 4. BeautifulSoup으로 크롤링

BeautifulSoup으로 크롤링

라이브러리 설치하기

BeautifulSoup 를 사용하기 위해 필요한 라이브러리인 'requests' 와 'beautifulsoup'를 설치한다.
pip install requests pip install beautifulsoup4
 

Requests

'requests' 는 HTTP 요청을 보내는데 사용하는 라이브러리이다. 아래의 간단한 예제를 작성해보자.
import requests url = 'http://paullab.co.kr' response = requests.get(url) print(response.headers) # 헤더의 내용 확인 (.headers) print(response.encoding). # 인코딩 방식 확인 (.encoding) print(response.status_code). # 상태코드 확인 (.status_code) print(response.ok) # 정상적 접속 확인 (.ok) response.encoding = 'utf-8' print(response.text) # html 내용 확인 (.text)
'requests' 를 이용하여 'http://paullab.co.kr' 에 요청을 보내고 그 결과들을 확인할 수 있다. 이 때, 인코딩 방식을 'utf-8' 로 해주어야 한글이 정상적으로 출력되는 것을 볼 수 있다.
 

Requests 활용 (1)

특정 페이지의 소스코드를 파일로 저장하는 코드이다.
import requests paullab_url = 'http://paullab.co.kr' response = requests.get(paullab_url) response.encoding = 'utf-8' html = response.text f = open('test.html', 'w') f.write(html) f.close()
 

Requests 활용 (2)

특정 페이지에서 특정 단어가 몇 번 언급되었는지 출력하는 코드이다.
import requests paullab_url = 'http://paullab.co.kr' response = requests.get(paullab_url) response.encoding = 'utf-8' html = response.text spliteHTML = html.split(' ') word = input("검색할 단어를 입력하세요 : ") print(spliteHTML.count(word))
 

BeautifulSoup

str 타입의 html 데이터를 html 구조를 가진 데이터로 가공해주는 라이브러리이다.
import requests from bs4 import BeautifulSoup response = requests.get('http://paullab.co.kr') response.encoding = 'utf-8' html = response.text soup = BeautifulSoup(html, 'html.parser') print(soup.title) # title 태그 출력 print(soup.find('title')) # title 태그 출력 print(soup.find(id='content')) # id 가 'content'인 태그 출력 print(soup.find_all('a')) # 모든 a 태그 출력 print(soup.select('#content')) # id 가 'content'인 모든 태그 출력 print(soup.select('.container > a')) # 'container' class 안에 모든 a 태그 출력
태그에 접근하는 방법 중 select 를 활용하면 태그에 좀 더 세밀한 접근이 가능하다. select 를 사용하기 위해서는 selector 의 개념을 이해해야 한다.

Selector

1 <a href='home.html'></a> 2 <li class='study'> 3 <p id='text'> 4 <a href = 'http://paullab.co.kr'> </a> 5 </p> 6 </li>
다음과 같은 HTML 소스에서 4번 라인에 접근하고 싶을 때, 'a' 태그를 탐색하는 방법이 있다. 하지만, 단순하게 a 태그에 접근하는 것은 1번 라인에 먼저 접근하게 되는 결과를 불러일으킨다. 'li 태그안에 p 태그 안에 a 태그' 와 같은 접근 방법을 사용해야만 4번 라인만 검출할 수 있다. 이 때, 활용하는 것이 Selector 접근법이다.
 
Selector 에서 class 를 지칭할 때는 '.' 을 사용하고, id 를 지칭할 때는 '#' 를 사용한다. 그리고 탐색하고자 하는 태그가 특정태그 하위에 있을 때 '>' 를 사용하면 태그들을 이어줄 수 있다. 따라서 4번 라인에 접근하기 위해서는 '.study > #text > a' 와 같이 표현하면 된다.
 

BeautifulSoup 활용 (1)

paullab.co.kr 에서 바울랩의 강의목록을 크롤링해보자. 다음과 같이 lecture 클래스에 강의 제목이 있는 것을 확인할 수 있다. Selector 를 적절히 사용해서 코드를 작성해보도록 한다.
notion imagenotion image
import requests from bs4 import BeautifulSoup paullab_url = 'http://paullab.co.kr' response = requests.get(paullab_url) response.encoding = 'utf-8' html = response.text soup = BeautifulSoup(html, 'html.parser') lecture_list = soup.select('.lecture ') # lecture 클래스 탐색 print(dir(lecture_list)) for lecture in lecture_list: print(lecture.text)
 

BeautifulSoup 활용 (2)

다음으로 paullab.co.kr/bookservice 에서 바울랩에서 제공하는 Book Service 목록을 크롤링해보자. 다음과 같이 특정 div 태그안의 h2 태그에 책제목이 있는 것을 알 수 있다. Selector 를 적절히 사용해서 코드를 작성해보도록 한다.
notion imagenotion image
import requests from bs4 import BeautifulSoup paullab_url = 'http://paullab.co.kr/bookservice/' response = requests.get(paullab_url) response.encoding = 'utf-8' html = response.text soup = BeautifulSoup(html, 'html.parser') bookservices = soup.select('.col-lg-6 > h2') # col-lg-6 클래스 안의 h2 태그 탐색 for no, book in enumerate(bookservices, 1): print(no, book.text)
 

크롤링의 원리, HTTP

되도록 h1 을 사용하라고 하셨는데 하다보니 다른 h 태그들을 사용하게 되네요 ㅠㅠ sub 주제인걸 말하고 싶었던건데 양식에 안맞는다면 바로 고치겠습니다.
 
먼저 크롤링이 무엇인지 부터 알아보겠습니다. 크롤링이란 사람이 웹페이지에 접속해서 정보를 찾는 과정을 프로그램을 통해 찾아 수집하고 원하는 형태에 맞게 가공하는 모든 과정을 의미합니다.
크롤링에 대한 설명을 하기 전에 먼저 알아야 할 개념이 있는데 바로 HTTP입니다. 아마 많이 익숙하실겁니다. 우리가 웹 상에서 어느 페이지에 접속하기 위해 입력하는 주소가 대부분 http 로 시작하기 때문이죠.
 

HTTP 에 대해서 알아보자

 

HTTP란?

HTTP는 HyperText Transfer Protocol 의 약자로 인터넷 통신을 위해 사용되는 프로토콜입니다. 또 어려운 단어가 등장했네요. 그러면 프로토콜은 무엇일까요? 쉽게 생각해서 컴퓨터 간의 의사소통에 사용되는 언어입니다. 사람 간의 대화에서도 서로 이해할 수 있는 같은 언어를 써야하는 것 처럼 컴퓨터끼리도 의사소통을 위한 언어를 통일 시켜놓았고 이를 프로토콜이라고 합니다. 즉 , HTTP 라는 것도 컴퓨터 간의 소통하는 언어라는 것이죠.
URL에서 볼 수 있는 httpURL에서 볼 수 있는 http
URL에서 볼 수 있는 http

server 와 client

 
HTTP 는 서버(server)클라이언트(client) 의 대화입니다. 여기서 서버는 django, Spring 등으로 구현한 백앤드, 클라이언트는 웹 브라우저라고 이해하면 됩니다. 대화를 하는 방식은 클라이언트가 서버에 요청(request)을 하고, 서버는 그 요청에 대한 반응(response)을 돌려주는 식인데 여기서 요청이라고 하는 것은 어려워 보이지만 우리가 평소에도 하고 있습니다. 예를 들면 웹페이지에 접속하기, 새로고침, 링크 클릭 등 이죠. 이렇게 URL을 통해 요청을 보내면 server는 이에 맞는 응답을 하는데 마찬가지로 친숙하게 웹 화면을 보여주는 html 파일, 이미지 등으로 응답합니다.
 

HTTP 메소드

notion imagenotion image
많은 메소드가 있지만 GETPOST 가 제일 중요하다. GET과 POST 는 각각 엽서와 소포에 비유할 수 있습니다. GET은 엽서처럼 주소에 내용이 표시되고 큰 물건은 담지 못하는 것처럼 파일을 담지 못합니다. POST는 주소에 내용을 남기지 않고 대신 큰 물건, 파일 등을 업로드 할 수 있습니다.
 

header와 body

 
요청과 응답은 header와 body로 구분되어있습니다. body에는 파일의 내용 등이 담길 수 있는데 GET 요청과 같은 경우에는 body가 존재하지 않습니다. (그래서 파일을 업로드 할 수 없는거죠.) GET 응답과 POST의 요청, 응답은 둘 다 header와 body로 구분되어 있습니다. header와 body는 빈 줄로서 구분을 해줍니다. 뒤에서 크롤링 실습을 할 때 다시 한 번 언급하도록 하겠습니다.
  • header
    • header 는 key/value 형식으로 데이터를 저장합니다. 한 번 header를 확인해볼까요? header를 확인하기 위해서는 httpie 라는 패키지를 이용해서 확인할 수 있습니다.
    • httpie 패키지 설치
    • pip install httpie
    • google의 header 내용을 확인해볼까요?
    • http --header https://google.com
      google의 header 정보google의 header 정보
      google의 header 정보
      header 는 이 외에도 설정할 것이 많지만 크롤링과 관련해서는 아래의 설정들을 주의하셔야 합니다.
       
      notion imagenotion image

      왜 HTTP를 알아야하는가?

       
      간략하게나마 HTTP가 무엇인지 알아보았습니다. 이것이 크롤링과 무슨 관계 길래 이렇게 두서없이 설명을 했을까요? 크롤링이 HTTP 프로토콜 통신을 하기 때문입니다. 크롤링할 대상들이 모두 웹 상에 존재하니 정보를 가져오기 위해선 당연한거죠. 이제 본격적으로 크롤링에 대해 알아보도록 하겠습니다.
       

      크롤링

       
      이제 다시 크롤링으로 돌아오겠습니다. 크롤링을 구현하는 방법으로는 request, selenium, API 등이 있습니다. 첫번째와 두번째는 python 라이브러리를 통해서 세 번째는 instagram, 네이버 블로그 검색 등 이미 구현된 API를 가져다가 쓰는 방식입니다.
      여기서 주의해야 할 점 은 API를 통한 크롤링은 허가되는 부분에 한해서만 가능토록 만들어져 있기 때문에 문제 될 것이 없지만 라이브러리를 통한 크롤링은 저작권 문제를 야기할 수 있습니다. 웹 상에 떠돌아다니는 정보도 저작권이 있기 때문이죠. 또한 크롤링을 해오는 사이트가 변경됨에 따라 프로그램을 수정해야하는 소요가 생길 수 있습니다.
       

      request VS selenium

       
      두 라이브러리는 사용하는 경우가 다릅니다. 물론 이분법적인 논리를 펼칠 수는 없지만 대체로 그렇습니다.
    • request
      • request는 html에 처음부터 있던 부분을 크롤링할 경우 사용합니다. 이게 무슨소리냐고요? html은 동적으로 javascript 에 의해서 만들어질 수도 있기 때문이죠. 웹에서 마우스 우클릭 → 페이지 소스보기 로 들어가면 초기의 html 코드를 볼 수 있습니다.
        마우스 우클릭이 안되는 경우에는 크롬의 Enable Right Click 확장을 사용해보세요.
      네이버의 페이지 소스보기네이버의 페이지 소스보기
      네이버의 페이지 소스보기
       
    • selenium
      • 처음 html 파일에는 없었지만 javascript 를 통해 처리될 경우, 로그인이 필요한 경우에 사용합니다.
        개발자도구 → Elements 에 있는 건 전부 가능합니다.
        네이버 개발자도구의 Elements네이버 개발자도구의 Elements
        네이버 개발자도구의 Elements
         

        크롤링을 할 마지막 준비

         
        이제 request 방식으로 크롤링을 해보도록 하겠습니다. 그러기 위해서는 다음과 같이 라이브러리들을 import 해야합니다.
        pip install requests pip install BeatifulSoup4
        유의할 점은 BeatifulSoup4 를 붙여 4 버젼을 받아야합니다. 그렇지 않으면 3 버젼이 다운로드 됩니다.
        import 가 잘되는지 확인해봅시다.
        import requests from bs4 import BeautifulSoup
 

requests 를 통한 HTTP 요청

 
이번에는 requests 라는 라이브러리를 통해서 본격적으로 데이터를 가져와보도록 하겠습니다. 위에서 HTTP 메소드에 대해서 이야기를 했었고 requests를 통해서 모든 메소드에 대해서 접근이 가능하지만 가장 많이 쓰이고 중요한 GETPOST 에 대해서만 다뤄보도록 하겠습니다.
response = requests.get('http://naver.com')
다른 HTTP 메소드를 사용하길 원하신다면 get 대신 원하는 메소드를 사용하시면 됩니다.

GET 요청

데이터를 불러오는 방법은 생각보다 간단합니다. 그리고 그 간단함이 바로 requests 라이브러리의 가장 큰 장점 중 하나죠.
import requests response = requests.get('http://naver.com') response.text
결과를 확인하면 아래와 같습니다.
[생략] <meta name="description" content="네이버 메인에서 다양한 정보와 유용한 컨텐츠를 만나 보세요"/> [생략]
단 3줄만으로 naver 의 html 코드를 가져오는데 성공했습니다. 물론 저 데이터를 바로 사용할 수는 없지만 보기 좋게 가공하는 과정은 뒤에서 다루도록 하고 지금은 데이터를 성공적으로 가져오는데 초점을 맞추도록 하겠습니다.

requests 객체의 메소드

 
여기서부터는 requests 를 import 하는 과정은 생략하도록 하겠습니다. 맨 위에서 했다는 전제로 진행을 하죠.
이번에는 유용하게 사용되는 requests의 메소드들을 알아보도록 하겠습니다.
response = requests.get('http://naver.com') response.status_code response.ok response.content response.text
마찬가지로 naver 메인 페이지의 정보를 가져오는 상황입니다. 결과값을 확인하고 각 쓰임들을 알아보도록 하겠습니다.
200 True b'<!doctype html> [생략] [생략]<meta name="description" content="네이버 메인에서 다양한 정보와 유용한 컨텐츠를 만나 보세요"/>[생략]
  • status_code
    • 응답 상태 코드를 return하게 됩니다. 응답 상태 코드란 말 그대로 HTTP 요청에 대해서 요청이 성공했는지 실패했는지 혹은 어떤 상태인지를 말해줍니다. 각 요청에 대한 건 https://developer.mozilla.org/ko/docs/Web/HTTP/Status 에서 자세히 확인하실 수 있습니다. 결과 값으로 나온 200은 성공했다는 의미입니다.
  • ok
    • status_code의 값을 통해 값이 결정되는데 200이상 400미만, 즉 성공했을 경우에는 True 를 그렇지 않을 경우에는 False 를 return 하게 됩니다. 데이터를 잘 불러오고 있나 확인이 가능하죠.
  • content
    • content는 가져온 데이터를 bytes 타입으로 return 합니다. 일반적으로는 사람이 보고 데이터를 읽을 수가 없습니다. 그렇기 때문에 인코딩 작업이 필요하죠.
  • text
    • 예시에서도 살펴봤듯이 str 타입의 데이터를 return 합니다. 기본적으로 content 의 bytes 타입의 데이터를 인코딩하는 방식이죠. 그 결과 우리도 데이터를 쉽게 읽을 수 있습니다.
그러면 여기서 한 가지 의문이 들 수 있습니다. requests는 content를 어떤 방식으로 디코딩 하여 text로 바꿔주는지 어떻게 알 수 있을까요? 정답은 응답헤더 에 있습니다.
 

응답헤더

 
헤더의 내용을 확인하기 위해서는 headers 메소드를 사용합니다.
response = requests.get('http://naver.com')response.headers
그 결과는 아래와 같습니다.
{'Server': 'NWS', 'Date': 'Wed, 20 Mar 2019 16:19:26 GMT', 'Content-Type': 'text/html; charset=UTF-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Set-Cookie': 'PM_CK_loc=d60457a8fc51c5f539b7c71e807c9cc3f89533c169b3a46c94604f64e7dff11c; Expires=Thu, 21 Mar 2019 16:19:26 GMT; Path=/; HttpOnly', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'P3P': 'CP="CAO DSP CURa ADMa TAIa PSAa OUR LAW STP PHY ONL UNI PUR FIN COM NAV INT DEM STA PRE"', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', 'Content-Encoding': 'gzip', 'Strict-Transport-Security': 'max-age=63072000; includeSubdomains', 'Referrer-Policy': 'unsafe-url'}
보기만 해도 어려워보이지만 지금 모든 내용을 다 알 필요는 없습니다. 그래도 한 가지 알 수 있는 건 dictionary 라는 점입니다. 확인해보도록 할까요?
type(response.headers)
결과는 예상과 다릅니다.
requests.structures.CaseInsensitiveDict
맨 뒤에 Dict가 붙은걸로 봐서 Dict 과 같은 기능을 하긴 하지만 requests 만의 dictionary 입니다. 가장 두드러진 특징은 key 값으로 접근 할 때 대소문자의 구분이 없다는 점이죠.
다시 본론으로 돌아와서 저 많은 내용 중에 디코딩, 인코딩과 관련된 내용은 다음과 같이 확인할 수 있습니다.
response.headers['content-type']
결과값은 아래와 같습니다.
'text/html; charset=UTF-8'
UTF-8 방식으로 디코딩 하는걸 확인할 수 있습니다. 이 방식만 알고 있다면, text 를 사용하지 않고 content를 직접 디코딩해서 사용할 수 있습니다. 아래와 같이 말이죠.
response.content.decode('utf8')
결과는 text 와 동일합니다.
하지만 우리 마음대로 되지는 않을 겁니다. content-typecharset 이 지정되어있지 않으면 어떻게 될까요? 디코딩을 제대로 하지 못해 글자를 알 수 볼 수 없을 겁니다.
 

한글이 깨질 때는 어떻게 해야하지?

 
여태까지와 동일하게 크롤링을 시도했을 때, 한글이 깨지는 경우가 발생하기도 합니다. 아래 예시를 함께 보도록 하죠.
response = requests.get('http://www.puka.co.kr/') response.text[50:60]
알아 볼 수 없는 결과가 출력이 됩니다.
Ä¡¿øÃÑ¿¬ÇÕȸ ºÎ»êÁöÈ'
위에서도 말했듯이, 그 이유는 charset 이 지정되어 있지 않기 때문입니다.
response.headers['content-type']
결과는 아래와 같습니다.
'text/html'
이럴 경우에는 어떤 방식으로 디코딩 되었는지 직접 찾아줘야 합니다. 해당 사이트로 접속한 후에, 다음과 같은 방법으로 페이지의 html 코드를 확인할 수 있습니다.
오른쪽 마우스 -> 페이지 소스보기
meta 태그 를 찾고 charset 이 어떻게 지정되어 있는지를 확인합니다.
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
euc-kr 를 이용해서 디코딩하면 원하는 결과를 얻을 수 있을 겁니다.
# 방법 1 response.content.decode('euc-kr') # 방법 2 response.encoding = 'euc-kr'response.text
방법 1은 content의 내용을 직접 디코딩하는 방식이고, 방법 2는 인코딩 방식을 알려줘서 text 가 제대로 된 방식을 찾도록 도와줍니다. 결과로 반가운 한글을 확인할 수 있습니다.
[생략] <title>::: 한국유치원총연합회 부산지회 :::</title>\r\n [생략]
 

queryString

 
뜬금없이 또 생소한 단어가 나왔지만 집고 넘어가야 할 부분입니다. 먼저 용어에 대한 설명부터 하자면,
웹 클라이언트에서 서버로 전달하는 데이터 방식
우리는 기존에 웹 클라이언트가 서버로 데이터를 전달할 때 url 을 통해 전달한다는 사실을 알았습니다. 그리고 url을 살펴보도록 하죠. 제가 아까 본 네이버 스포츠 뉴스의 기사입니다.
우리가 주목해야할 부분은 ? 이후의 부분으로서 바로 이 부분을 queryString 이라고 합니다. & 와 = 을 통해 값들을 연결하고 있는데 이는 dictionary에서 , 와 : 에 대응됩니다. 즉 위 url 에서의 queryString 을 dictionary 로 나타내면 다음과 같습니다.
{'oid':'065', 'aid':'0000178691'}
그리고 복잡해보이던 위 url 을 좀 더 가독성 좋게 표현할 수 있습니다.
params = {'oid':'065', 'aid':'0000178691'} response = requests.get('https://sports.news.naver.com/basketball/news/read.nhn', params = params) response.text[520:560]
params 를 통해 queryString 에 해당하는 부분을 dictionary 로 넘겨줘도 데이터를 잘 불러올 수 있습니다.
[KBL시상식] 1위와 MVP의 엇갈림, 현대모비스와 KCC의 희비
 

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 이 이미 원하는 형태로 존재하기 때문에 soup 와 `html` 은 차이가 없습니다. 뒤에 활용하면서 차이에 대해 다시 한 번 언급하도록 하겠습니다.
다음으로는 우리가 원하는 세부적인 데이터로 접근해야합니다. 이 때 접근하는 방법은 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 활용하기

이번에는 '네이버 웹툰' 의 인기 급상승 만화 부분을 크롤링해서 가져와보도록 하겠습니다. 바로 이 부분입니다.
notion imagenotion image
각각 인기순과 업데이트순에 따라 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 입니다.
notion imagenotion image
 
인기순과 업데이트순은 각각 id 값으로 realTimeRankFavoriterealTimeRankUpdate 를 가집니다.
notion imagenotion image
 
우리가 원하는 만화의 제목은 li 태그 안 a 태그 안에 위치해있습니다.
notion imagenotion image
 
인기순에 해당하는 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_tagfavor_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'])