python을 활용한 웹툰 크롤링 완성 - crawl.py
10 Jun 2018 | python 크롤링패스트캠퍼스 웹 프로그래밍 수업을 듣고 중요한 내용을 정리했습니다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
이 포스팅에서는 크롤링하는 방법에 대해 설명합니다. 본 스크립트는 학습 목적으로만 사용해주세요.
import os
from urllib import parse
import requests
from bs4 import BeautifulSoup
class EpisodeImage:
def __init__(self, episode, url):
self.episode = episode
self.url = url
class Episode:
def __init__(self, webtoon, no, url_thumbnail,
title, rating, created_date):
self.webtoon = webtoon
self.no = no
self.url_thumbnail = url_thumbnail
self.title = title
self.rating = rating
self.created_date = created_date
self.image_list = list()
@property
def url(self):
"""
self.webtoon, self.no 요소를 사용하여
실제 에피소드 페이지 URL을 리턴
:return:
"""
url = 'http://comic.naver.com/webtoon/detail.nhn?'
params = {
'titleId': self.webtoon.webtoon_id,
'no': self.no,
}
episode_url = url + parse.urlencode(params)
return episode_url
def get_image_url_list(self):
file_path = f'./data/{self.webtoon.webtoon_id}-{self.no}.html'
if not os.path.exists(file_path):
with open(file_path, 'wt') as f:
response = requests.get(self.url)
f.write(response.text)
html = response.text
else:
html = open(file_path, 'rt').read()
soup = BeautifulSoup(html, 'lxml')
wt_viewer = soup.select_one('div.wt_viewer').select('img')
for img in wt_viewer:
new_ep_image = EpisodeImage(
episode=self.no,
url=img.get('src')
)
self.image_list.append(new_ep_image)
return self.image_list
def download_all_images(self):
for url in self.get_image_url_list():
self.download(url)
def download(self, url_img):
"""
:param url_img: 실제 이미지의 URL
:return:
"""
# 서버에서 거부하지 않도록 HTTP헤더 중 'Referer'항목을 채워서 요청
url_referer = f'http://comic.naver.com/webtoon/list.nhn?titleId={self.webtoon}'
headers = {
'Referer': url_referer,
}
response = requests.get(url_img.url, headers=headers)
# 이미지 URL에서 이미지명을 가져옴
file_name = url_img.url.rsplit('/', 1)[-1]
# 이미지가 저장될 폴더 경로, 폴더가 없으면 생성해준다
dir_path = f'data/{self.webtoon.webtoon_id}/{self.no}'
os.makedirs(dir_path, exist_ok=True)
# 이미지가 저장될 파일 경로, 'wb'모드로 열어 이진데이터를 기록한다
file_path = f'{dir_path}/{file_name}'
open(file_path, 'wb').write(response.content)
# 저장된 이미지를 인터넷에서 볼 수 있도록 html 파일로 생성
with open('data/{}/{}.html'.format(self.webtoon.webtoon_id, self.no), 'a') as f:
f.write('<img src = {}/{}>'.format(self.no, file_name))
class Webtoon:
def __init__(self, webtoon_id):
self.webtoon_id = webtoon_id
self._title = None
self._author = None
self._description = None
self._episode_list = list()
self._html = ''
self.page = 1
def _get_info(self, attr_name):
if not getattr(self, attr_name):
self.set_info()
return getattr(self, attr_name)
@property
def episode_list(self):
if not self._episode_list:
self.crawl_episode_list()
return self._episode_list
@property
def title(self):
return self._get_info('_title')
@property
def author(self):
return self._get_info('_author')
@property
def description(self):
return self._get_info('_description')
@property
def html(self):
# 인스턴스의 html속성값이 False(빈 문자열)일 경우
# HTML파일을 저장하거나 불러올 경로
file_path = 'data/_episode_list-{webtoon_id}-{page}.html'.format(webtoon_id=self.webtoon_id,
page=self.page)
# HTTP요청을 보낼 주소
url_episode_list = 'http://comic.naver.com/webtoon/list.nhn'
# HTTP요청시 전달할 GET Parameters
params = {
'titleId': self.webtoon_id,
'page': self.page,
}
# HTML파일이 로컬에 저장되어 있는지 검사
if os.path.exists(file_path):
# 저장되어 있다면, 해당 파일을 읽어서 html변수에 할당
html = open(file_path, 'rt').read()
else:
# 저장되어 있지 않다면, requests를 사용해 HTTP GET요청
response = requests.get(url_episode_list, params)
# 요청 응답객체의 text속성값을 html변수에 할당
html = response.text
# 받은 텍스트 데이터를 HTML파일로 저장
open(file_path, 'wt').write(html)
self._html = html
return self._html
@property
def info(self):
return '{title} \n' \
'작가 : {author} \n' \
'설명 : {description} \n' \
'총 연재횟수 : {episode_list} 회'.format(title=self.title,
author=self.author,
description=self.description,
episode_list=len(self.episode_list))
@classmethod
def all_webtoon_crawler(cls, keyword):
url = 'https://comic.naver.com/webtoon/weekday.nhn'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'lxml')
all_webtoon_list = soup.select('div.col_inner > ul > li > a')
result = list()
for webtoon in all_webtoon_list:
title = webtoon.get_text()
if keyword in title:
href = webtoon.get('href', '')
query_string = parse.urlsplit(href).query
query_dict = dict(parse.parse_qsl(query_string))
titleId = query_dict['titleId']
check = [item for item in result if item['titleId'] == titleId]
if not check:
result.append({
'titleId': titleId,
'title': title,
})
return result
@classmethod
def search_webtoon(cls, keyword):
"""
keyword와 일치하는 웹툰 제목 검색
:param keyword:
:return:
"""
search_result = cls.all_webtoon_crawler(keyword)
result_list = list()
if search_result:
for webtoon in search_result:
webtoon_title_id = cls(webtoon_id=webtoon['titleId'])
result_list.append(webtoon_title_id)
return result_list
def set_info(self):
"""
자신의 html속성을 파싱한 결과를 사용해
자신의 title, author, description속성값을 할당
:return: None
"""
# BeautifulSoup클래스형 객체 생성 및 soup변수에 할당
soup = BeautifulSoup(self.html, 'lxml')
h2_title = soup.select_one('div.detail > h2')
title = h2_title.contents[0].strip()
author = h2_title.contents[1].get_text(strip=True)
# div.detail > p (설명)
description = soup.select_one('div.detail > p').get_text(strip=True)
# 자신의 html데이터를 사용해서 (웹에서 받아오거나, 파일에서 읽어온 결과)
# 자신의 속성들을 지정
self._title = title
self._author = author
self._description = description
def crawl_episode_list(self):
"""
자기자신의 webtoon_id에 해당하는 HTML문서에서 Episode목록을 생성
:return:
"""
while True:
# BeautifulSoup클래스형 객체 생성 및 soup변수에 할당
soup = BeautifulSoup(self.html, 'lxml')
# 에피소드 목록을 담고 있는 table
table = soup.select_one('table.viewList')
# table내의 모든 tr요소 목록
tr_list = table.select('tr')
# list를 리턴하기 위해 선언
# for문을 다 실행하면 episode_lists 에는 Episode 인스턴스가 들어가있음
episode_list = list()
# 첫 번째 tr은 thead의 tr이므로 제외, tr_list의 [1:]부터 순회
for index, tr in enumerate(tr_list[1:]):
# 에피소드에 해당하는 tr은 클래스가 없으므로,
# 현재 순회중인 tr요소가 클래스 속성값을 가진다면 continue
if tr.get('class'):
continue
# 현재 tr의 첫 번째 td요소의 하위 img태그의 'src'속성값
url_thumbnail = tr.select_one('td:nth-of-type(1) img').get('src')
# 현재 tr의 첫 번째 td요소의 자식 a태그의 'href'속성값
from urllib import parse
url_detail = tr.select_one('td:nth-of-type(1) > a').get('href')
query_string = parse.urlsplit(url_detail).query
query_dict = parse.parse_qs(query_string)
# print(query_dict)
no = query_dict['no'][0]
# 현재 tr의 두 번째 td요소의 자식 a요소의 내용
title = tr.select_one('td:nth-of-type(2) > a').get_text(strip=True)
# 현재 tr의 세 번째 td요소의 하위 strong태그의 내용
rating = tr.select_one('td:nth-of-type(3) strong').get_text(strip=True)
# 현재 tr의 네 번째 td요소의 내용
created_date = tr.select_one('td:nth-of-type(4)').get_text(strip=True)
# 매 에피소드 정보를 Episode 인보스턴스로 생성
# new_episode = Episode 인스턴스
new_episode = Episode(
webtoon=self,
no=no,
url_thumbnail=url_thumbnail,
title=title,
rating=rating,
created_date=created_date,
)
# episode_lists Episode 인스턴스들 추가
self._episode_list.append(new_episode)
# no가 1인경우 break
if no == '1':
break
# 그 경우가 아니면 page를 1씩 추가하여 다음 페이지 웹툰 리스트 크롤링
else:
self.page += 1