From 4cf94bea298b38c36444bac8499c0dfc263a11c5 Mon Sep 17 00:00:00 2001 From: geun9716 Date: Tue, 22 Mar 2022 18:04:53 +0900 Subject: [PATCH 1/7] add day Field in self.date --- korea_news_crawler/articlecrawler.py | 60 ++++++++++++++++++++++++++-- korea_news_crawler/exceptions.py | 16 ++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/korea_news_crawler/articlecrawler.py b/korea_news_crawler/articlecrawler.py index 524ee5a..a68b537 100644 --- a/korea_news_crawler/articlecrawler.py +++ b/korea_news_crawler/articlecrawler.py @@ -18,7 +18,7 @@ def __init__(self): self.categories = {'정치': 100, '경제': 101, '사회': 102, '생활문화': 103, '세계': 104, 'IT과학': 105, '오피니언': 110, 'politics': 100, 'economy': 101, 'society': 102, 'living_culture': 103, 'world': 104, 'IT_science': 105, 'opinion': 110} self.selected_categories = [] - self.date = {'start_year': 0, 'start_month': 0, 'end_year': 0, 'end_month': 0} + self.date = {'start_year': 0, 'start_month': 0, 'start_day' : 0, 'end_year': 0, 'end_month': 0, 'end_day':0} self.user_operating_system = str(platform.system()) def set_category(self, *args): @@ -27,16 +27,70 @@ def set_category(self, *args): raise InvalidCategory(key) self.selected_categories = args - def set_date_range(self, start_year, start_month, end_year, end_month): - args = [start_year, start_month, end_year, end_month] + @staticmethod + def get_month_day(year, month): + month_day = {1:31, 2:28, 3:31, 4:30, 5:31, 6:30, 7:31, 8:31, 9:30, 10:31, 11:30, 12:30} + + # Check Leap Year + IsLeapYear = False + if year % 4 == 0: + if year % 100 == 0: + if year % 400 == 0: + IsLeapYear = True + else: + IsLeapYear = False + else: + IsLeapYear = True + else: + IsLeapYear = False + if IsLeapYear: + month_day[2] = 29 + + return month_day[month] + + def set_date_range(self, start_date:str, end_date:str): + start = list(map(int, start_date.split("-"))) + end = list(map(int, end_date.split("-"))) + + # Setting Start Date + if len(start) == 1: # Input Only Year + start_year = start[0] + start_month = 1 + start_day = 1 + elif len(start) == 2: # Input Year and month + start_year, start_month = start + start_day = 1 + elif len(start) == 3: # Input Year, month and day + start_year, start_month, start_day = start + + # Setting End Date + if len(end) == 1: # Input Only Year + end_year = end[0] + end_month = 12 + end_day = 31 + elif len(end) == 2: # Input Year and month + end_year, end_month = end + end_day = self.get_month_day(end_year, end_month) + elif len(end) == 3: # Input Year, month and day + end_year, end_month, end_day = end + + args = [start_year, start_month, start_day, end_year, end_month, end_day] + if start_year > end_year: raise InvalidYear(start_year, end_year) if start_month < 1 or start_month > 12: raise InvalidMonth(start_month) if end_month < 1 or end_month > 12: raise InvalidMonth(end_month) + if start_day < 1 or self.get_month_day(start_year, start_month) < start_day: + raise InvalidDay(start_day) + if end_day < 1 or self.get_month_day(end_year, end_month) < end_day: + raise InvalidDay(end_day) if start_year == end_year and start_month > end_month: raise OverbalanceMonth(start_month, end_month) + if start_year == end_year and start_month == end_month and start_day > end_day: + raise OverbalanceDay(start_day, end_day) + for key, date in zip(self.date, args): self.date[key] = date print(self.date) diff --git a/korea_news_crawler/exceptions.py b/korea_news_crawler/exceptions.py index eebae56..9ede287 100644 --- a/korea_news_crawler/exceptions.py +++ b/korea_news_crawler/exceptions.py @@ -53,6 +53,15 @@ def __init__(self, month): def __str__(self): return self.message +# 일이 올바르지 않을 때 +class InvalidDay(Exception): + def __init__(self, day): + self.message = f'{day} is an invalid day' + + def __str__(self): + return self.message + + # 시작 달과 끝나는 달이 올바르지 않을 때 class OverbalanceMonth(Exception): @@ -62,6 +71,13 @@ def __init__(self, start_month, end_month): def __str__(self): return self.message +class OverbalanceDay(Exception): + def __init__(self, start_day, end_day): + self.message = f'{start_day}(start day) is an overbalance with {end_day}(end day)' + + def __str__(self): + return self.message + # 실행시간이 너무 길어서 데이터를 얻을 수 없을 때 class ResponseTimeout(Exception): From 7f607b204987d9a9a16d6d212532d110e5ee603f Mon Sep 17 00:00:00 2001 From: geun9716 Date: Tue, 22 Mar 2022 18:25:44 +0900 Subject: [PATCH 2/7] Add Writer day Field --- korea_news_crawler/writer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/korea_news_crawler/writer.py b/korea_news_crawler/writer.py index 80968a4..3cca41f 100644 --- a/korea_news_crawler/writer.py +++ b/korea_news_crawler/writer.py @@ -7,9 +7,10 @@ class Writer(object): def __init__(self, category, article_category, date): self.start_year = date['start_year'] self.start_month = f'0{date["start_month"]}' if len(str(date['start_month'])) == 1 else str(date['start_month']) + self.start_day = f'0{date["start_day"]}' if len(str(date['start_day'])) == 1 else str(date['start_day']) self.end_year = date['end_year'] self.end_month = f'0{date["end_month"]}' if len(str(date['end_month'])) == 1 else str(date['end_month']) - + self.end_day = f'0{date["end_day"]}' if len(str(date['end_day'])) == 1 else str(date['end_day']) self.file = None self.initialize_file(category, article_category) @@ -20,7 +21,7 @@ def initialize_file(self, category, article_category): if os.path.exists(output_path) is not True: os.mkdir(output_path) - file_name = f'{output_path}/{category}_{article_category}_{self.start_year}{self.start_month}_{self.end_year}{self.end_month}.csv' + file_name = f'{output_path}/{category}_{article_category}_{self.start_year}{self.start_month}{self.start_day}_{self.end_year}{self.end_month}{self.end_day}.csv' if os.path.isfile(file_name): raise ExistFile(file_name) From 66cf00f6ac71e515413bb15b28565476ae618f52 Mon Sep 17 00:00:00 2001 From: geun9716 Date: Wed, 23 Mar 2022 13:59:55 +0900 Subject: [PATCH 3/7] process set day --- korea_news_crawler/articlecrawler.py | 52 ++++++++++++++++++---------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/korea_news_crawler/articlecrawler.py b/korea_news_crawler/articlecrawler.py index a68b537..57b57fd 100644 --- a/korea_news_crawler/articlecrawler.py +++ b/korea_news_crawler/articlecrawler.py @@ -70,7 +70,7 @@ def set_date_range(self, start_date:str, end_date:str): end_day = 31 elif len(end) == 2: # Input Year and month end_year, end_month = end - end_day = self.get_month_day(end_year, end_month) + end_day = calendar.monthrange(end_year, end_month)[1] elif len(end) == 3: # Input Year, month and day end_year, end_month, end_day = end @@ -82,9 +82,9 @@ def set_date_range(self, start_date:str, end_date:str): raise InvalidMonth(start_month) if end_month < 1 or end_month > 12: raise InvalidMonth(end_month) - if start_day < 1 or self.get_month_day(start_year, start_month) < start_day: + if start_day < 1 or calendar.monthrange(start_year, start_month)[1] < start_day: raise InvalidDay(start_day) - if end_day < 1 or self.get_month_day(end_year, end_month) < end_day: + if end_day < 1 or calendar.monthrange(end_year, end_month)[1] < end_day: raise InvalidDay(end_day) if start_year == end_year and start_month > end_month: raise OverbalanceMonth(start_month, end_month) @@ -96,32 +96,46 @@ def set_date_range(self, start_date:str, end_date:str): print(self.date) @staticmethod - def make_news_page_url(category_url, start_year, end_year, start_month, end_month): + def make_news_page_url(category_url, date): made_urls = [] - for year in range(start_year, end_year + 1): - target_start_month = start_month - target_end_month = end_month - - if start_year != end_year: - if year == start_year: - target_start_month = start_month + for year in range(date['start_year'], date['end_year'] + 1): + if date['start_year'] == date['end_year']: + target_start_month = date['start_month'] + target_end_month = date['end_month'] + else: + if year == date['start_year']: + target_start_month = date['start_month'] target_end_month = 12 - elif year == end_year: + elif year == date['end_year']: target_start_month = 1 - target_end_month = end_month + target_end_month = date['end_month'] else: target_start_month = 1 target_end_month = 12 - + for month in range(target_start_month, target_end_month + 1): - for month_day in range(1, calendar.monthrange(year, month)[1] + 1): + if date['start_month'] == date['end_month']: + target_start_day = date['start_day'] + target_end_day = date['end_day'] + else: + if year == date['start_year'] and month == date['start_month']: + target_start_day = date['start_day'] + target_end_day = calendar.monthrange(year, month)[1] + elif year == date['end_year'] and month == date['end_month']: + target_start_day = 1 + target_end_day = date['end_day'] + else: + target_start_day = 1 + target_end_day = calendar.monthrange(year, month)[1] + + for day in range(target_start_day, target_end_day + 1): if len(str(month)) == 1: month = "0" + str(month) - if len(str(month_day)) == 1: - month_day = "0" + str(month_day) + if len(str(day)) == 1: + day = "0" + str(day) # 날짜별로 Page Url 생성 - url = category_url + str(year) + str(month) + str(month_day) + url = category_url + str(year) + str(month) + str(day) # totalpage는 네이버 페이지 구조를 이용해서 page=10000으로 지정해 totalpage를 알아냄 # page=10000을 입력할 경우 페이지가 존재하지 않기 때문에 page=totalpage로 이동 됨 (Redirect) @@ -149,7 +163,7 @@ def crawling(self, category_name): # 기사 url 형식 url_format = f'http://news.naver.com/main/list.nhn?mode=LSD&mid=sec&sid1={self.categories.get(category_name)}&date=' # start_year년 start_month월 ~ end_year의 end_month 날짜까지 기사를 수집합니다. - target_urls = self.make_news_page_url(url_format, self.date['start_year'], self.date['end_year'], self.date['start_month'], self.date['end_month']) + target_urls = self.make_news_page_url(url_format, self.date) print(category_name + " Urls are generated") print("The crawler starts") From 4795bba3e84fd0fd1c514b37396036d31e709d54 Mon Sep 17 00:00:00 2001 From: geun9716 Date: Wed, 23 Mar 2022 14:15:00 +0900 Subject: [PATCH 4/7] delete get_month_day --- korea_news_crawler/articlecrawler.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/korea_news_crawler/articlecrawler.py b/korea_news_crawler/articlecrawler.py index 57b57fd..a547fad 100644 --- a/korea_news_crawler/articlecrawler.py +++ b/korea_news_crawler/articlecrawler.py @@ -27,27 +27,6 @@ def set_category(self, *args): raise InvalidCategory(key) self.selected_categories = args - @staticmethod - def get_month_day(year, month): - month_day = {1:31, 2:28, 3:31, 4:30, 5:31, 6:30, 7:31, 8:31, 9:30, 10:31, 11:30, 12:30} - - # Check Leap Year - IsLeapYear = False - if year % 4 == 0: - if year % 100 == 0: - if year % 400 == 0: - IsLeapYear = True - else: - IsLeapYear = False - else: - IsLeapYear = True - else: - IsLeapYear = False - if IsLeapYear: - month_day[2] = 29 - - return month_day[month] - def set_date_range(self, start_date:str, end_date:str): start = list(map(int, start_date.split("-"))) end = list(map(int, end_date.split("-"))) @@ -162,7 +141,7 @@ def crawling(self, category_name): writer = Writer(category='Article', article_category=category_name, date=self.date) # 기사 url 형식 url_format = f'http://news.naver.com/main/list.nhn?mode=LSD&mid=sec&sid1={self.categories.get(category_name)}&date=' - # start_year년 start_month월 ~ end_year의 end_month 날짜까지 기사를 수집합니다. + # start_year년 start_month월 start_day일 부터 ~ end_year년 end_month월 end_day일까지 기사를 수집합니다. target_urls = self.make_news_page_url(url_format, self.date) print(category_name + " Urls are generated") @@ -254,5 +233,5 @@ def start(self): if __name__ == "__main__": Crawler = ArticleCrawler() Crawler.set_category('생활문화') - Crawler.set_date_range(2018, 1, 2018, 2) + Crawler.set_date_range('2018-01', '2018-02') Crawler.start() From 3e9a985b7a7c1a9a65a4cedb9b5213e7d37e74b5 Mon Sep 17 00:00:00 2001 From: geun9716 Date: Wed, 23 Mar 2022 14:15:10 +0900 Subject: [PATCH 5/7] Change sample input --- korea_news_crawler/sample.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/korea_news_crawler/sample.py b/korea_news_crawler/sample.py index 0818d5b..6faef12 100644 --- a/korea_news_crawler/sample.py +++ b/korea_news_crawler/sample.py @@ -1,9 +1,8 @@ from korea_news_crawler.articlecrawler import ArticleCrawler - if __name__ == "__main__": Crawler = ArticleCrawler() # 정치, 경제, 생활문화, IT과학, 사회, 세계 카테고리 사용 가능 - Crawler.set_category("IT과학", "경제", "생활문화", "IT과학", "사회", "세계") - # 2017년 12월부터 2018년 1월까지 크롤링 시작 - Crawler.set_date_range(2017, 12, 2018, 1) + Crawler.set_category("IT과학", "세계") + # 2017년 12월 (1일) 부터 2018년 1월 13일까지 크롤링 시작 YYYY-MM-DD의 형식으로 입력 + Crawler.set_date_range('2017-12', '2018-01-13') Crawler.start() From 55799d4b3ecd5560dd8c7a48a0b0adba26615665 Mon Sep 17 00:00:00 2001 From: Seong JuWon Date: Sun, 27 Mar 2022 23:14:42 +0900 Subject: [PATCH 6/7] edit printing message --- korea_news_crawler/articlecrawler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/korea_news_crawler/articlecrawler.py b/korea_news_crawler/articlecrawler.py index a547fad..2d883fa 100644 --- a/korea_news_crawler/articlecrawler.py +++ b/korea_news_crawler/articlecrawler.py @@ -143,10 +143,9 @@ def crawling(self, category_name): url_format = f'http://news.naver.com/main/list.nhn?mode=LSD&mid=sec&sid1={self.categories.get(category_name)}&date=' # start_year년 start_month월 start_day일 부터 ~ end_year년 end_month월 end_day일까지 기사를 수집합니다. target_urls = self.make_news_page_url(url_format, self.date) + print(f'{category_name} Urls are generated') - print(category_name + " Urls are generated") - print("The crawler starts") - + print(f'{category_name} is collecting ...') for url in target_urls: request = self.get_url_data(url) document = BeautifulSoup(request.content, 'html.parser') From 55537823d486eba4794459b909ee1d7cae2c0984 Mon Sep 17 00:00:00 2001 From: Seong JuWon Date: Sun, 27 Mar 2022 23:23:33 +0900 Subject: [PATCH 7/7] release package (1.51v) --- LICENSE | 2 +- dist/KoreaNewsCrawler-1.51-py3-none-any.whl | Bin 0 -> 12502 bytes setup.cfg | 2 +- setup.py | 6 +++--- 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 dist/KoreaNewsCrawler-1.51-py3-none-any.whl diff --git a/LICENSE b/LICENSE index 896da30..0b7f64c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 lumyjuwon +Copyright (c) 2022 lumyjuwon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/dist/KoreaNewsCrawler-1.51-py3-none-any.whl b/dist/KoreaNewsCrawler-1.51-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..b71894c3fff43109c649fa4033751d84632ab973 GIT binary patch literal 12502 zcma)?WmFv7wzeC0hv4q60fI|#Z`@sjL!&{1yAwRPyCk@~y9IZGyYq3bob0o|+K=gL~WansVsBdfP>ZEV% zXy|Hf>d36GZ)t1ktgp{x?+%KpM($!k{CF$<3{#51qQb($#>!d+`kpf|@hvYZJz%1} z5S2Bd%k5ONO;m;Uh9Sh>w~vZz6}X#SOy9#R#lnYK|LJy_YxYQjbzb0|_tQs>gZCrE z8o8FRD#+u7w*DXxn#%-TsJQl2P7Nj47N1POX!4D87m5suoPhjvmk88OOFpG{uaBvw zZ2AVZ;y-uN>)@#(HbD3}5&-}(qy+#d{=5@IM`uf8Yt#SQ*xP=3wSL(x51_r;cD#1a zL5t8)563ytB8zO_Fu_d#q?iYC#SC#E$;+)dF$d#RJ~xqw5%G{jMsfy+SGZWgARlrh ze-XSx*WUm%`V8c~1dlNg5ATQqPrwt15glC3ceyH87Fo@G-mig)pOrP{X$f*{J z%hYdyrM=|K5Y9^>$|}^`)Qakqi%N0t?WrT8wo>26ma}S1wd6({9Ng?JIku9^&?-4rJ)sKg=ageB%uv>HWq z)_E(f3_1w2{4_&~elHT*0dS05 zv!KE+Odc_eQ?C2y%19+@ai(ZF-0)zD@ho==vfaf74?>&WrZur6pIgX`!!lx|u0yc^ z&9^DO2IC2=c`Atv$J+8keHm2kNK6z0le&jWy&rK7GBi)YMg=@MV?A}O0RWs|%CB

=Rez;Mm9B+Q|L$ztJf&zT$&_@JV=QW1 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 aGf*kYK)ofK007e4pZYDtr7-;C?*9Rru7