Channel : Korea EN-CORE
Date : 2018/03/13
5/5

강의자료 : goo.gl/E5qjoN

웹페이지 크롤링은 크게 3가지로 나눠볼 수 있다.


Python에서 크롤링에 주로 사용하는 Library에도 3가지가 있다.

1) Requests

웹브라우저에서 처음에 HTML을 받아오면 거기엔 정보가 어느 위치에 있는 지 정보만 있다. CSS는 어디에 있고, JS는 어디에 있는지 정보가 있으며, 웹브라우저는 다시 그 CSS, JS 파일들을 불러와서 어떻게 보여지는 지 계산해서 보여주는 것이다.

Requests는 HTML만 받아오고, 추가적으로 CSS, JS 처리를 하지 않는 라이브러리다.

2) Selenium

Selenium은 사람이 사용하는 브라우저들을 컨트롤하는 라이브러리이다. 장점은 어떤 웹브라우저이건 Selenium을 통해 동일하게 컨트롤할 수 있다는 것이다.

리소스를 많이 먹지만, Requests로 불가능했던 JS, CSS를 사람이 접근할 때와 동일하게 처리할 수 있다.

3) BeautifulSoup4

HTML 파서이다.


웹 페이지의 종류에 따라 Requests를 사용하기도 하고, Selenium을 사용하기도 한다. BeautifulSoup4은 HTML 파싱에 필수적으로 사용되는 라이브러리이다.


Hello, Ask Django라는 메시지를 보여주는 웹페이지를 크롤링해보자.

코드는 아래와 같이 작성한다.

import requests
from bs4 import BeautifulSoup

html = requests.get('http://웹페이지주소').text
soup = BeautifulSoup(html, 'html.parser')
print(soup.find('h1').text)

그런데 위 코드를 실행하면 다음과 같은 에러 메시지를 출력하게 된다.

AttributeError: ‘NoneType’ object has no attribute ‘text’

h1 태그를 찾을 수 없었다는 말이다. 왜?

내 예상엔 HTML 코드는 다음과 같을 것으로 예상했다.

하지만 실제 HTML 코드는 다음과 같이 되어 있었다.

브라우저 개발자도구(F12)를 통해 보면 분명히 h1 태그가 존재하는 데, 왜 h1 태그가 없는 거지?

우리는 보통 ‘페이지 소스보기’를 하지 않고, 브라우저의 개발자도구를 통해 ‘Elements’를 찍어본다.

그렇다면 어떻게 해야 하나?

위 페이지는 Requests로 크롤링은 포기해야 한다. Selenium으로 해야 한다.

from bs4 import BeautifulSoup
from selenium import webdriver

browser = webdriver.Firefox()
browser.get('http://웹페이지주소')
soup = BeautifulSoup(browser.page_source, 'html.parser')
print(soup.find('h1').text) # 'Hello, AskDjango!!!'

잘 된다.


그렇다면 크롤링할 때 무조건 Selenium을 쓰면 되겠네?

그렇다. 써도 된다. 아무 생각없이 쓰기엔 Selenium이 좋다.

가급적 Requests를 통해 처리할 수 있다면 처리하는 것이 효율이 훨씬 좋다.

뭘 쓸지 헛갈린다. 정리가 필요하다.


어떤 Library를 써야 하나요? 판단 기준은 ?


1번 Case) 네이버 웹툰 크롤링

신의탑을 크롤링해본다고 하자.

주소는 https://comic.naver.com/webtoon/list.nhn?titleId=183559&weekday=mon 이다.

페이지 소스보기를 통해 소스를 열고 ‘3부 65화’를 찾아보면 찾아진다.

			<tr>
				<td>
					<a href="/webtoon/detail.nhn?titleId=183559&no=484&weekday=mon" onclick="nclk_v2(event,'lst.img','183559','484')">
						<img src="https://shared-comic.pstatic.net/thumb/webtoon/183559/484/thumbnail_202x120_f6076e67-501c-408a-8693-6f7c5e1c9081.jpg" title="3부 65화" alt="3부 65화" width="71" height="41" onERROR="this.src='https://ssl.pstatic.net/static/comic/images/migration/common/non71_41.gif'">
						<span class="mask"></span>
					</a>
				</td>
				<td class="title">
				<a href="/webtoon/detail.nhn?titleId=183559&no=484&weekday=mon" onclick="nclk_v2(event,'lst.title','183559','484')">3부 65화</a>
						</td>
				<td>
					<div class="rating_type">
						<span class="star"><em style="width:96.57%">평점</em></span>
						<strong>9.66</strong>
					</div>
				</td>
				<td class="num">2020.05.31</td>
			</tr>

있다. ‘3부 65화’가 소스보기에서 찾을 수 있다. 제일 좋은 경우이다. Requests를 쓰면 된다.


2번 Case) 만약 내가 원하는 컨텐츠를 Ajax로 별도로 받아온다면

이때는 개발자도구 Network 탭을 확인해보자.

위의 네이버 웹툰 화면에서 개발자도구로 확인해보면 다음과 같다.

위 Network 탭의 의미는 신의 탑 리스트 화면을 완성하기 위해 93번의 호출이 있었다는 의미이다.

위 Resource들 중에 내가 원하는 컨텐츠를 찾을 수 있다면, ajax를 통해 컨텐츠를 별도로 가져오는 웹페이지도 Requests를 활용할 수 있다.

그게 안된다면 Selenium을 활용해야 한다.


2번 Case) 멜론 사이트

멜론 사이트 검색바에 ‘아이유’라고 입력(Enter치지 않고)하면 나오는 정보를 확인해보자.

멜론 사이트에 접속해서 검색바에 ‘아이유’라고 입력하면 Enter를 치지 않아도, 다음과 같이 아이유에 대한 정보들이 자동으로 나타난다.

이 자동으로 나타난 정보들에서 ‘아이유’의 곡 목록들을 가져오고 싶다고 해보자.

이 때 가져오는 정보들은 개발자도구의 Network탭에서 확인해볼 수 있다.

검색바에 ‘아이유’라고 타이핑하면 Network탭은 다음과 같은 리소스들을 호출해 온다.

이 중 아래에서 2번째 index.json?… 로 되어 있는 부분을 클릭하면, 해당 Resource의 정보를 확인해볼 수 있다.

강의 시에는 다음과 같이 정보를 받아올 수 있었다고 한다.

하지만, 20년 6월 6일 현재 시점 기준으로는 멜론 사이트에서 뭔가 크롤링을 막는 조치를 취했나 보다. 현 시점 기준으로 클릭하면 다음과 같이 정보를 가져올 수 없다는 메시지가 나타난다.

그럼에도 위 index.json?… 부분을 더블 클릭하면 아래와 같이 json 결과를 읽어볼 수 있다. 완전히 막힌 건 아닌건가?

강의 시점과 달라졌지만, 어쨋건 우리가 원하는 결과가 json 형태로 읽어들일 수 있다면 Requests 라이브러리를 사용해서 한번에 받아올 수 있다고 한다.

현 시점에서도 가능할 지는 실제로 해봐야 알 수 있을 거 같다.

위 결과를 쉽게 보기 위해 chrome extension 중에 jsonview를 설치해보자.

https://chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc?hl=ko-KR

위 extension이 설치된 뒤 json 결과 페이지를 새로고침하면 아래와 같이 이해하기 쉬운 view로 볼 수 있게된다.

위 화면을 호출하는 주소 URL은 다음과 같다.

https://www.melon.com/search/keyword/index.json?jscallback=jQuery19108315664434940384_1591454674591&query=%25EC%2595%2584%25EC%259D%25B4%25EC%259C%25A0&_=1591454674615

위 조소 중 query= 이후 부분에 다른 가수의 이름을 넣어서 다시 호출해보자. 예를 들어 지코로 입력해보자.

그러면 다음과 같이 ‘지코’에 대한 검색 결과가 나오는 것을 볼 수 있다.

유레카 !!!

위와 같이 정보를 불러오는 Logic을 분석할 수 있다면 Requests를 사용하여 크롤링할 수 있다.

그래서 강사는

  • 소스코드 보기를 통해 내가 원하는 컨텐츠가 있는지 보고
  • 그게 아니면 개발자도구의 Network 탭을 통해 내가 원하는 컨텐츠가 왔다 갔다하는지 보고
  • 2개 다 아니다 싶으면 Selenium을 사용한다고 한다.

실습) 네이버 웹툰 가져오기

신의 탑 65화를 열어서 개발자도구를 통해 확인해보면 잘게 쪼개진 이미지의 경로를 확인해볼 수 있다.

class=”wt_viewer”라고 된 부분을 보면 image의 경로를 학인해볼 수 있다. 아래로 내려보면 각각 쪼개진 image들을 확인해 볼 수 있다.

마우스 우클릭해서 ‘새 tab에서 열기’를 선택하면 이미지가 열리는 것을 확인할 수 있다.

어 잘 되네? image 주소도 알 수 있고, image 다운로드도 되네? 이제 네이버 웹툰을 내 하드에 저장할 수 있게된건가?

자 이제 코딩한번 해보자.

1) jupyter notebook 실행하기

jupyter notebook은 웹브라우저 기반의 python shell이다. 웹브라우저 기반이기 때문에 text뿐 아니라 image, video 모두 출력 결과를 확인할 수 있다.

브라우저에 jupyter notebook이 나타나면 새로운 노트북을 생성해본다.

2) requests로 위 웹툰 image 가져와보자.

이미지를 불러왔는데, 이미지가 보이는 게 아니라 이상한 HTML 문들이 보인다.

저작권이 있는 이미지를 보호하고자 하는 웹사이트 들은 Referer 헤더값을 통해 현재 이미지에 대한 호출이 어떤 페이지를 거쳐왔느냐를 확인하여, 지정된 페이지를 통한 호출만 허용하도록 설계하기 때문이다.

3) Referer 값을 설정하여 정상 호출 흉내내기

Referer 값은 본래 웹툰 페이지의 주소를 입력해보자.

https://comic.naver.com/webtoon/detail.nhn?titleId=183559&no=484&weekday=mon

코드를 아래와 같이 변경하고 실행하면 결과가 바뀐 것을 볼 수 있다.

이제 파일로 저장해서 확인하도록 코드를 조금 바꿔보자.

import requests

url = 'https://image-comic.pstatic.net/webtoon/183559/484/20200511141501_acd7e5deb5fee526e9a5c054e21df43b_IMAG01_1.jpg'

headers = {
    'referer' : 'https://comic.naver.com/webtoon/detail.nhn?titleId=183559&no=484&weekday=mon'
}

res = requests.get(url, headers=headers)

with open("test.jpg", "wb") as f:
    f.write(res.content)

파일이 성공적으로 저장되어 있다.

  • referer : 요청하는 페이지의 주소가 무엇인지
  • user-agent : 요청하는 Client는 누구인지에 대한 정보로 (OS, 브라우저 종류 등), requests의 기본 값은 python이다. (일부 사이트의 경우 user-agent값이 python이면 무조건 거부하기도 한다.) http://www.useragentstring.com/ 에 가서 필요한 값을 받아서 활용해보자.
  • accept-language : Facebook의 경우 영문 윈도우로 접근하면 영어로 나오고, 일본어 윈도우로 나오면 일본어로 나온다. 다국어 사이트의 경우 accept-language 값을 확인하여 서비스를 제공하기 때문에, accept-language값이 없으면 접근이 안되는 경우가 있다.
headers = {
    'referer' : 'https://comic.naver.com/webtoon/detail.nhn?titleId=183559&no=484&weekday=mon', 
    'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}

장고 활용해보기

Data 저장하기

  • database를 이용해서 저장할 수 있다.
  • python은 sqlite를 기본 지원한다. 1
  • 이제 sqlite를 이용할 수 있다.
  • 문제는 database를 다른 것으로 바꾸면 코드를 새로 짜거나, 고쳐야 할 수 있다.
  • 그래서 우린 orm 을 사용한다.
  • orm엔 django model과 sqlalchemy 모델 2가지가 가장 유명하다.
  • 그 중 django model을 사용해보자.

이제 해보려고 하는 것은

  • 장고 프로젝트 생성, superuser 계정 생성
  • 모델 생성/마이그레이션
  • 크롤링 결과를 모델을 통해 DB에 저장이다.

Step1. 장고 프로젝트 생성, 장고 실행

그 다음 웹브라우저에서 localhost:8000 으로 접속하면 다음과 같은 화면이 나타나는 것을 확인할 수 있다.

youtube 강좌와 나오는 화면이 다르다. django의 버전 차이로 보인다. 내 실행환경은 django=3.0.7이고, 강좌의 버전은 1.11.9를 이용하고 있다. 하지만 그냥 해보자. 강좌에서 굳이 버전 맞추는 것을 강조하지 않았기 때문에 일단 해보고 안되면 그 때 문제를 해결해보자.

step2. django app을 만들어본다.

장고 서버를 띄웠던 power shell은 그대로 두고, 새로운 power shell을 실행시켜서 다음과 같이 실행해본다.

그 다음 djangocrawling 디렉토리를 visual studio code로 오픈한다.

step3. models.py에 국회의원에 대한 class를 생성한다.

djangocrawling\assembly\models.py

from django.db import models

class Member(models.Model):
    name = models.CharField(max_length=20) #이름은 최대 20자

step4. settings.py에 방금 만든 assembly app을 추가한다.

djangocrawling\settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'assembly'
]

step5. django의 migration 기능 실행

makemigrations 와 migrate 2개 명령을 실행시키고, dir로 확인해보면 db.sqlite3파일의 크기가 증가한 것을 볼 수 있다.

Step6. admin.py에 방금 만든 파일을 등록한다.

djangocrawling\assembly\admin.py

from django.contrib import admin
from .models import Member

admin.site.register(Member)

Step7. superuser를 생성한다.

이제 웹브라우저에서 localhost:8000/admin 주소로 접근하면 다음과 같다.

askdjango, 1234로 로그인해보자.

여기에서 assemby, Members 우측의 Add 버튼 누르면 이름을 넣고 Member를 추가할 수 있다.

여기에 Name을 입력하고 Save하면 DB에 Member가 저장된다.

Step8. 국회 사이트를 분석해보자.

https://assembly.go.kr/assm/memact/congressman/memCond/memCond.do 로 접속하면 다음과 같이 개발자도구에 호출되는 리소스를 확인할 수 있다.

위 Network 탭의 Name 목록 중 memCondListAjax.do를 마우스 우클릭하여 새탭에서 열어본다.

주소 : https://assembly.go.kr/assm/memact/congressman/memCond/memCondListAjax.do

6명의 국회의원 정보가 나온다.

이제 위 주소에 살짝 인자를 주어보자.

https://assembly.go.kr/assm/memact/congressman/memCond/memCondListAjax.do?rowPerPage=300

으로 주고 실행해보자. 국회의원 전체 목록에 접근되는 것을 알 수 있다.

Step9. Jupyter Notebook으로 이름만 크롤링해오는 코드를 짜보자.

html이 복잡하게 나오는 것을 알 수 있다. 이제 저기에서 국회의원 이름을 잘 가져오면 된다.

위의 rowPerPage=300으로 호출한 화면에서 ‘페이지 소스보기’를 통해 원하는 패턴을 찾을 수도 있다. 하지만 너무 복잡해보이니(구조화가 안되어 있어서), 이 경우엔 ‘개발자도구’를 띄워서 보도록 하자.

‘페이지 소스보기’를 통해 본 HTML과 ‘개발자도구’를 통해 본 HTML이 똑같다. 즉, 이 사이트는 js를 통해 컨텐츠의 변화가 없는 페이지라는 뜻이다.

이제 우리는 href=”javascript:jsMempop …” 을 갖는 a 태그를 추출할 계획이다.

아래와 같이 코드를 추가하고 실행하면 의원 이름이 있는 a 태그만을 추출할 수 있다.

이제 우리는 a 태그 안에 있는 문자열만 추출하면 된다. 코드를 조금 수정하면 아래와 같이 이름을 가져올 수 있다.

Step10. django shell을 실행하고 여기에서 위 코드를 붙여놓고 실행해보자.

이제 위 코드를 조금 바꿔서 names 어레이에 저장해보자.

이제 names의 데이터를 Member 모델로 생성해보자.

실행이 완료되고 웹브라우저의 http://localhost:8000/admin/assembly/member/ 페이지를 새로고침하면 아래와 같이 데이터가 추가된 모습을 확인할 수 있다.

위 Member object 대신 국회의원의 이름으로 보여지면 좋겠다.

models.py를 아래와 같이 수정한다.

from django.db import models

class Member(models.Model):
    name = models.CharField(max_length=20) #이름은 최대 20자
    
    def __str__(self):
        return self.name

웹브라우저를 다시 새로고침하면

위 화면은 admin에서만 보인다. 일반 user들에게도 보이는 페이지를 만들어보자.

Step11. 일반 User용 화면 만들기

urls.py를 아래와 같이 수정한다.

from django.contrib import admin
from django.urls import path
from assembly import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index),
]

그리고 views.py를 수정한다.

from django.shortcuts import render
from django.views.generic import ListView
from .models import Member

index = ListView.as_view(model=Member)

그리고 웹브라우저에서 localhost:8000을 다시 접근하면 에러가 나타난걸 볼 수 있다. 내용은 memeber_list.html이 없다는 얘기다.

assembly/templates/assembly 경로로 디렉토리를 만들어주고, 여기에 member_list.html 파일을 생성한다.

{% for member in member_list %}
    {{ member}}
{% endfor %}

그리고 서버를 재기동하면2 다음과 같이 출력되는 것을 볼 수 있다.

Footnotes

  1. 기능은 일반 database와 동일하나, single user만을 위한 제약이 있다.
  2. python manage.py runserver

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다