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