python을 활용한 웹툰 크롤링 - 강사님 코드 복습

|

패스트캠퍼스 웹 프로그래밍 수업을 듣고 중요한 내용을 정리했습니다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
이 포스팅에서는 크롤링하는 방법에 대해 설명합니다. 본 스크립트는 학습 목적으로만 사용해주세요.


import os
from urllib import parse

import requests
from bs4 import BeautifulSoup


class Episode:
    def __init__(self, webtoon_id, no, url_thumbnail,
                 title, rating, created_date):
        self.webtoon_id = webtoon_id
        self.no = no
        self.url_thumbnail = url_thumbnail
        self.title = title
        self.rating = rating
        self.created_date = created_date

    @property
    def url(self):
        """
        self.webtoon_id, self.no 요소를 사용하여
        실제 에피소드 페이지 URL을 리턴
        :return:
        """
        url = 'http://comic.naver.com/webtoon/detail.nhn?'
        params = {
            'titleId': self.webtoon_id,
            'no': self.no,
        }
        episode_url = url + parse.urlencode(params)
        return episode_url

    def get_image_url_list(self):
        print('get_image_url_list start')
        # 해당 에피소드의 이미지들의 URL문자열들을 리스트에 담아 리턴
        # 1. html 문자열 변수 할당
        #  파일명: episode_detail-{webtoon_id}-{episode_no}.html
        #  없으면 자신의 url property값으로 requests사용 결과를 저장
        # 2. soup객체 생성 후 파싱
        #  div.wt_viewer의 자식 img요소들의 src속성들을 가져옴
        # 적절히 list에 append하고 리턴하자

        # 웹툰 에피소드의 상세 페이지를 저장할 파일 경로
        #  웹툰의 고유ID와 에피소드의 고유ID를 사용해서 겹치지 않는 파일명을 만들어준다
        file_path = 'data/episode_detail-{webtoon_id}-{episode_no}.html'.format(
            webtoon_id=self.webtoon_id,
            episode_no=self.no,
        )
        print('file_path:', file_path)

        # 위 파일이 있는지 검사
        if os.path.exists(file_path):
            print('os.path.exists: True')
            # 있다면 읽어온 결과를 html변수에 할당
            html = open(file_path, 'rt').read()
        else:
            # 없다면 self.url에 requests를 사용해서 요청
            #  요청의 결과를 html변수에 할당
            #  요청의 결과를 file_path에 해당하는 파일에 기록
            print('os.path.exists: False')
            print(' http get request, url:', self.url)
            response = requests.get(self.url)
            html = response.text
            open(file_path, 'wt').write(html)

        # html문자열로 BeautifulSoup객체 생성
        soup = BeautifulSoup(html, 'lxml')

        # img목록을 찾는다. 위치는 "div.wt_viewer > img"
        img_list = soup.select('div.wt_viewer > img')

        # 이미지 URL(src의 값)을 저장할 리스트
        # url_list = []
        # for img in img_list:
        #     url_list.append(img.get('src'))

        # img목록을 순회하며 각 item(BeautifulSoup Tag object)에서
        #  'src'속성값을 사용해 리스트 생성
        return [img.get('src') for img in img_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_id}'
        headers = {
            'Referer': url_referer,
        }
        response = requests.get(url_img, headers=headers)

        # 이미지 URL에서 이미지명을 가져옴
        file_name = url_img.rsplit('/', 1)[-1]

        # 이미지가 저장될 폴더 경로, 폴더가 없으면 생성해준다
        dir_path = f'data/{self.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)


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 = ''

    def _get_info(self, attr_name):
        if not getattr(self, attr_name):
            self.set_info()
        return getattr(self, attr_name)

    @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):
        if not self._html:
            # 인스턴스의 html속성값이 False(빈 문자열)일 경우
            # HTML파일을 저장하거나 불러올 경로
            file_path = 'data/episode_list-{webtoon_id}.html'.format(webtoon_id=self.webtoon_id)
            # HTTP요청을 보낼 주소
            url_episode_list = 'http://comic.naver.com/webtoon/list.nhn'
            # HTTP요청시 전달할 GET Parameters
            params = {
                'titleId': self.webtoon_id,
            }
            # 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)
                print(response.url)
                # 요청 응답객체의 text속성값을 html변수에 할당
                html = response.text
                # 받은 텍스트 데이터를 HTML파일로 저장
                open(file_path, 'wt').write(html)
            self._html = html
        return self._html

    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:
        """
        # 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_id=self.webtoon_id,
                no=no,
                url_thumbnail=url_thumbnail,
                title=title,
                rating=rating,
                created_date=created_date,
            )
            # episode_lists Episode 인스턴스들 추가
            episode_list.append(new_episode)
        self._episode_list = episode_list

    @property
    def episode_list(self):
        # self.episode_list가 빈 리스트가 아니라면
        #  -> self.episode_list를 반환
        # self.episode_list가 비어있다면
        #  채우는 함수를 실행해서 self.episode_list리스트에 값을 채운 뒤
        #  self.episode_list를 반환

        # 다했으면
        # episode_list속성이름을 _episode_list로 변경
        # 이 함수의 이름을 episode_list로 변경 후 property설정
        if not self._episode_list:
            self.crawl_episode_list()
        return self._episode_list

        # if self.episode_list:
        #     return self.episode_list
        # else:
        #     self.crawl_episode_list()
        #     return self.episode_list


if __name__ == '__main__':
    # 조건적으로 실행, 파이썬 자체가 내부적으로 사용하는 특별한 변수명. 우리가 실행했을때만 이 코드가 실행이 된다.
    webtoon1 = Webtoon(651673)
    print(webtoon1.title)
    print(webtoon1.author)
    print(webtoon1.description)
    e1 = webtoon1.episode_list[0]
    e1.download_all_images()

python을 활용한 웹툰 크롤링 4단계 - 최종 과제

|

패스트캠퍼스 웹 프로그래밍 수업을 듣고 중요한 내용을 정리했습니다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
이 포스팅에서는 크롤링하는 방법에 대해 설명합니다. 본 스크립트는 학습 목적으로만 사용해주세요.


import os
from urllib.parse import urlencode

import requests
from bs4 import BeautifulSoup


def webtoon_crawler(webtoon_id):
    """
   webtoon_id 를 매개변수로 입력받아서
   웹툰 title, author,description을 딕셔너리로 넘겨주는 함수
   :param webtoon_id:
   :return:
   """

    file_path = 'data/episode_list-{webtoon_id}.html'.format(webtoon_id=webtoon_id)
    # file_path 바뀐부분 참고!
    url_episode_list = 'http://comic.naver.com/webtoon/detail.nhn'
    params = {
        'titleId': webtoon_id,
    }

    if os.path.exists(file_path):
        html = open(file_path, 'rt').read()
    else:

        response = requests.get(url_episode_list, params)
        print(response.url)
        html = response.text
        open(file_path, 'wt').write(html)

    soup = BeautifulSoup(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)
    description = soup.select_one('div.detail > p').get_text(strip=True)

    # print(title)
    # print(author)
    # print(description)

    info = dict()
    # 웹툰의 정보를 딕셔너리 형태로 받아준다.(title, author, description)
    info['title'] = title
    info['author'] = author
    info['description'] = description

    return info
    # return된 info안에는 웹툰의 title, author, description정보가 들어가있다.
    # 이렇게 return된 info값은 class Webtoon에 활용된다.

def episode_crawler(webtoon_id):
    """
    webtoon_id를 매개변수로 입력받아서
    webtoon_id, title, url_thumbnail, no, rating, created_date의 정보를 가져오는 크롤러 함수
    :param webtoon_id:
    :return:
    """
    file_path = 'data/episode_list-{webtoon_id}.html'.format(webtoon_id=webtoon_id)
    # file_path 바뀐 부분 참고

    if os.path.exists(file_path):
        html = open(file_path, 'rt').read()

    soup = BeautifulSoup(html, 'lxml')
    table = soup.select_one('table.viewList')
    tr_list = table.select('tr')

    episode_lists = list()
    # for문 바깥에서 episode_lists라는 빈 리스트를 만든다.

    for index, tr in enumerate(tr_list[1:]):
        if tr.get('class'):
            continue

        url_thumbnail = tr.select_one('td:nth-of-type(1) img').get('src')

        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)
        no = query_dict['no'][0]

        title = tr.select_one('td:nth-of-type(2) > a').get_text(strip=True)
        rating = tr.select_one('td:nth-of-type(3) strong').get_text(strip=True)
        created_date = tr.select_one('td:nth-of-type(4)').get_text(strip=True)

        # print(url_thumbnail)
        # print(title)
        # print(rating)
        # print(created_date)
        # print(no)

        new_episode = Episode(
            # 크롤링한 결과를 class Episode의 new_episode 인스턴스로 생성
            # class Episode의 인스턴스들을 만든것 (이 부분 이해가 잘 안가는데, 용어가 헷갈려서 그런듯)
            # 클래스에 인스턴스 생성 부분 공부가 필요하다.

            webtoon_id= webtoon_id,
            no=no,
            url_thumbnail =url_thumbnail,
            title=title,
            rating = rating,
            created_date = created_date,
        )

        episode_lists.append(new_episode)
        # episode_lists에 Episode인스턴스를 추가
        # print(episode_lists)
        # 이 정보들을 넣고 새로운 인스턴스들을 만든다.

    return episode_lists

# ep = episode_crawler(703845)
# print(q)
# for item in ep:
#     print(item.no, item.title)


class Episode:
    def __init__(self, webtoon_id, no, url_thumbnail, title, rating, created_date):
        self.webtoon_id = webtoon_id
        self.no = no
        self.url_thumbnail = url_thumbnail
        self.title = title
        self.rating = rating
        self.created_date = created_date

    @property
    def url(self):
        url = 'https://comic.naver.com/webtoon/detail.nhn?'
        params = {
            'titleId': self.webtoon_id,
            # 내가 원하는 webtoon의 id
            'no': self.no,
            # 내가 원하는 webtoon의 no
        }

        episode_url = url + urlencode(params)
        # urllib, parse, urlencode 다시 읽어보기
        return episode_url


class Webtoon:
    def __init__(self, webtoon_id):
        self.webtoon_id = webtoon_id

        info = webtoon_crawler(webtoon_id)
        # 위에 만들어놓았던 webtoon_id를 매개변수로 받은 webtoon_crawler함수에서
        # return했던 info를 가져온다.
        self.title = info['title']
        self.author= info['author']
        self.description = info['description']
        self.episode_list = list()

    def update(self):
        """
        update함수를 실행하면
        해당 웹툰 아이디에 따른 에피소드 정보들을 에피소드 인스턴스로 저장
        :return:
        """

        result = episode_crawler(self.webtoon_id)
        # self.webtoon_id를 하는 이유는 해당 웹툰의 id(즉 내가 얻고 싶은 웹툰의 id)를 가져와야 하니까 -> self
        # print(result)
        self.episode_list = result
        # 내가 원하는 episode의 list = result
        # print(self.episode_list) 역시 위의 print(result)와 같은 결과를 가져오는 것을 볼 수 있었다.


if __name__ == '__main__':
# __name__ =='__main__'이 무엇인지는 알겠는데, 이걸 왜 여기서 써야하는 지 모르겠다.
    webtoon2 = Webtoon(651673)
    print(webtoon2.title)
    webtoon2.update()
# print(webtoon2.title)

    for episode in webtoon2.episode_list:
      # webtoon2의 episode_list를 쭉 돌아서
        print(episode.url)
        # 결국 우리가 출력하고자 하는 것은 각 episode들의 url

python을 활용한 웹툰 크롤링 3단계 - nth-of-type

|

패스트캠퍼스 웹 프로그래밍 수업을 듣고 중요한 내용을 정리했습니다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
이 포스팅에서는 크롤링하는 방법에 대해 설명합니다. 본 스크립트는 학습 목적으로만 사용해주세요.


3단계 문제

에피소드 정보 목록을 가져오기

모든 에피소드의 목록들을 다 가져와 아래의 순서대로 나열한 뒤

  • url_thumnail: 썸네일 URL
  • title: 제목
  • rating: 별점
  • create_date: 등록일
  • no: 에피소드 상세페이지의 고유번호
  • 각 에피소드들은 하나의 dict데이터로 만들어 모든 에피소드들을 list에 넣는다.
# 각 정보들이 table속성 내 존재함으로
table = soup.select_one('table.viewList')
# table 변수는 table 중 클래스 이름이 viewList인 애들만 가지고 와 할당한다.
tr_list = table.select('tr')
# tr_list는 위의 table변수에서 tr에 해당하는 애들 모두를 가져와 할당한다.

for index, tr in enumerate(tr_list):
  # tr_list를 매개변수로 하여 index, tr을 for문 돌려
  if tr.get('class'):
    # 만약 우리가 가지고 오는 tr중에 class를 가지는 tr이 있다면
    continue
    # 넘어가고 그렇지 않다면

  url_thumnail = tr.select_one('td:nth-of-type(1) img').get('src')
  # class를 가지지 않는 tr의 첫번째 td의 하위 img태그의 'src'속성값
  print(url_thumnail)

  from urllib import parse
  url_detail = tr.select_one('td:nth-of-type(1) > a').get('href')
  # tr의 첫 번째 td 자식 a태그의 'href' 속성값 (바로 아래에 있는 애가 자식 > )
  query_string = parse.urlsplit(url_detail).query
  # query의 string부분만 출력
  query_dict = parse.parse_qs(query_string)
  # parse_qs : 스트링으로 주어진 쿼리를 해석한다, 공백같은 문자를 공백으로 두는게 아니라 %뒤의 취급할 수 있는 글로 변환
  no = query_dict['no'][0]

  print(url_detail)
  print(query_string)
  print(query_dict)
  print(no)

  title = tr.select_one('td:nth-of-type(2) a').get_text(strip=True)
  # tr의 두번째 td 하위 a태그의 text출력
  print(title)

  rating = tr.select_one('td:nth-of-type(3) strong').get_text(strip=True)
  # tr의 세번째 td 하위 strong태그의 text출력
  print(rating)


  create_date = tr.select_one('td:nth-of-type(4)').get_text(strip=True)
  # tr의 네번째 td의 text 출력
  print(create_date)

출력 결과물

http://thumb.comic.naver.net/webtoon/703845/10/thumbnail_202x120_dd67c15f-af5e-4149-a71e-720b6ca388c7.jpg
/webtoon/detail.nhn?titleId=703845&no=10&weekday=wed
titleId=703845&no=10&weekday=wed
{'titleId': ['703845'], 'no': ['10'], 'weekday': ['wed']}
10
7화
9.98
2018.03.27

좀 더 구체적으로 보자면

url_detail = /webtoon/detail.nhn?titleId=703845&no=10&weekday=wed

query_string = titleId=703845&no=10&weekday=wed

query_dict = {'titleId': ['703845'], 'no': ['10'], 'weekday': ['wed']}

no = 10

python을 활용한 웹툰 크롤링 2단계 - BeautifulSoup

|

패스트캠퍼스 웹 프로그래밍 수업을 듣고 중요한 내용을 정리했습니다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
이 포스팅에서는 크롤링하는 방법에 대해 설명합니다. 본 스크립트는 학습 목적으로만 사용해주세요.


2단계 문제

제목, 저자, 웹툰정보 탐색하기

BeautifulSoup 활용하기

html변수를 사용해 soup변수에 BeautifulSoup객체를 생성하고 soup객체에서

  • 제목: 죽음에 관하여 (재)
  • 작가: 시니/혀노
  • 설명: 삶과 죽음의 경계선, 그 곳엔 누가 있을까의 내용을 가져와 title, author, description변수에 할당.
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'lxml')

big_title = soup.select_one('div.detail > h2')
# select: list로 반환하여, 전체를 다 찾는 방법
# select_one: 내가 원하는 태그, 클래스만 찾는 방법

# html파일 내 div 클래스 이름이 detail인 것 중 자식태그가 h2
print(big_title)
출력 결과물

<h2>
                                죽음에 관하여 (재)<span class="wrt_nm">
                                                        시니 / 혀노</span>
</h2>
title = big_title.contents[0].strip()
# big_title의 1번째 요소, h2의 머리와 꼬리를 제거(strip())
author = big_title.contents[1].get_text(strip=True)
# big_title의 두번째 요소, span태그의 text만 가져올 때(.get_text(strip=True))
# tag로부터 문자열을 가져올때는 get_text를 사용한다. (작가정보가 span tag)
출력 결과물

죽음에 관하여 (재)
시니 / 혀노

python을 활용한 웹툰 크롤링 1단계 - os.path.exists, requests get parameters

|

패스트캠퍼스 웹 프로그래밍 수업을 듣고 중요한 내용을 정리했습니다.
개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
이 포스팅에서는 크롤링하는 방법에 대해 설명합니다. 본 스크립트는 학습 목적으로만 사용해주세요.


1단계 문제

HTML 받아와서 html변수에 문자열을 할당한다.

os.path.exists 활용하기

  • (파이썬 공식 문서 확인 url: https://docs.python.org/3/library/os.path.html)

requests get parameters

  • data/episode_list.html파일이 없다면
  • 죽음에 관하여 (재) 페이지를 requests를 사용해서 pycharm내 data/episode_list.html에 저장한다.
  • 네이버 웬툰 url: http://comic.naver.com/webtoon/list.nhn?titleId=703845&weekday=wed
  • list.nhn뒤 ? 부터는 url에 넣지 말고 get parameters로 처리한다.
  • (requests문서의 ‘Passing Parameters In URLs’ 확인 url: http://docs.python-requests.org/en/master/user/quickstart/#passing-parameters-in-urls)
  • 저장 후에는 파일을 불러와 html 변수에 할당한다.

  • 그리고 이미 data/episode_list.html이 있다면 html 변수에 파일을 불러와 할당한다.
import requests
import os
# 내장함수 os를 불러오는 것 잊지말자

if os.path.exists('data/episode_list.html'):
  # HTML 파일이 로컬에 저장되어 있는지를 검사
  html = open('data/episode_list.html', 'rt').read()
  # 저장되어 있다면, 해당 파일을 읽어서 html변수에 할당

else:
  payload = {'titleID':'703845', 'weekday':'wed'}
  # HTTP요청시 전달할 GET parameters
  response = requests.get('http://comic.naver.com/webtoon/list.nhn', params=payload)
  # 저장되어 있지않다면 requests를 사용해 HTTP get요청
  html = response.text
  # 요청 응답 객체의 text속성값을 html변수에 할당
  with open('data/episode_list.html', 'wt') as f:
    f.write(html)
    # 받은 텍스트 데이터(html)를 로컬에 쓰기 모드로 가져옴

강사님 코드

import requests
import os

file_path = 'data/episode_list.html'
url_episode_path = 'http://comic.naver.com/webtoon/list.nhn?titleId=703845&weekday=wed'
params = {'titleID':'703845', 'weekday':'wed'}

if os.path.exists(file_path):
  html = open(file_path, 'rt').read()

else:
  response = requests.get(url_episode_path, params)
  html = response.text
  open(file_path, 'wt').write(html)