Webtoon Crawler-2

Webtoon Crawler - 2

예제에 나와있는 표현을 이해하지 못해서 멘탈 바사삭.

해당 문장이 정확히 어떤 의미인지를 알아야 코드 적용이 가능하기 때문에

최대한 내가 이해할수 있는 정도로 풀어서 정리.

(내 생각은 ##으로 표시.)

  • 예제는 webtoon crawler - 1 에 이어서 그대로

    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
        ## episode_url을 리턴해주는 함수
        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,
            }
            ## urlencode -> params dictionary에 있는 key와 value값을 인용하여 url주소의 ? 뒤에 붙여줌.
            ## ->.nhn?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라는 변수에 파일을 불러올 경로를 할당.
            ## 문자열 포맷팅을 통해서 Episode클래스의 인스턴스 변수인 self.webtoon_id와 self.no를 넣음.
            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를 사용해서 요청
                print('os.path.exists: False')
                print(' http get request, url:', self.url)
                ## response변수에 url GET요청
                response = requests.get(self.url)
                #   요청의 결과를 html 변수에 할당
                ## 요청 결과의 텍스트 문서를 html변수에 할당
                html = response.text
                #   요청의 결과를 file_path에 해당하는 파일에 기록
                ## file_path에 html의 내용을 덮어쓴다.
                open(file_path, 'wt').write(html)
      
            # html문자열로 BeautifulSoup객체 생성
            soup = BeautifulSoup(html, 'lxml')
      
            # img목록을 찾는다. 위치는 "div.wt_viewer > img"
            ## division의 wt_viewer클래스의 자식요소인 img 태그를 select해서 img_list에 할당
            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'속성값을 사용해 리스트 생성
            ## img_list를 반복하면서 img란 변수에 넣어주고
            ## img요소중 src속성을 갖는 것만 list에 추가시킨다.
      
            return [img.get('src') for img in img_list]
      
        def download_all_images(self):
            ## get_image_url_list를 호출한 리턴 값을 url 변수에 순차적으로 넣어주고
            ## 그 url변수를 download함수의 매개변수로 넣어줌
            for url in self.get_image_url_list():
                self.download(url)
      
        def download(self, url_img):
            """
            :param url_img: 실제 이미지의 URL
            :return:
            """
            # 서버에서 거부하지 않도록 HTTP헤더 중 'Referer'항목을 채워서 요청
            ## 텍스트를 제외한 모든게 binary 데이터
            ## 네이버 웹툰에서는 파일을 요청하면 기본적으로 거부를 함. 크롤러를 사용해서 읽을수도 있기 때문에
            ## 따라서 그냥 시도를 하게 되면 권한 없다고 하며 에러를 띄움
            ## http요청을 보낼때 그 전에 내가 어디있었는지 전송하는게 referer,
            ## 브라우저에서 어딘가를 클릭했을때는 자동으로 서버에 전송.
            ## 따라서 url_list를 알기 위해서 그 전에 보내야되는 url은 웹툰 자체의 url.
            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에서 이미지명을 가져옴
            ## rsplit -> str.rsplit([sep[, maxsplit]])
            ## maxsplit은 분할의 최대수, sep은 구분기호 -> 오른쪽에서부터 지정된 구분 기호로 분리
            ## 끝에 [-1]은 list의 마지막을 뜻함.
            file_name = url_img.rsplit('/', 1)[-1]
            print(file_name)
      
            # 이미지가 저장될 폴더 경로, 폴더가 없으면 생성해준다
            ## os.makedirs(path[, mode]) / path = 디렉토리가 생성 될 곳, 폴더가 없으면 생성해줌
            ## exist_ok=True -> 디렉토리가 이미 있으면 예외를 발생시키지 않는다
            dir_path = f'data/{self.webtoon_id}/{self.no}'
            os.makedirs(dir_path, exist_ok=True)
      
            # 이미지가 저장될 파일 경로, 'wb'모드로 열어 이진데이터를 기록한다
            file_path = f'{dir_path}/{file_name}'
            ## file_path파일에 이진데이터 타입의 내용을 쓴다
            ## .write(response.content)의 의미는 잘 모르겠음..
            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):
            ## getattr(object, name[, default])
            ## name은 문자열이어야 하고 문자열이 객체의 속성중 하나의 이름이면 결과는 속성의 값.
            ## ex) getattr(x. 'foobar') = x.foobar와 동등함.
            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()
    

Refactor(ubuntu pycharm 기준 shift+f6)

참조되는 객체를 전부 바꿔줌. find in path 나 find replace도 있지만 될수 있으면 refactor쓸것.

Comments