diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..2101d39 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,38 +1,39 @@ --- name: Bug report -about: Create a report to help us improve +about: 버그 세부 사항을 입력해 주세요. title: '' labels: '' assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. +## 버그 설명 +버그가 무엇인지 명확하고 간결하게 설명합니다. -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +## 재현 방법 +버그를 재현하는 단계를 설명해 주세요: +1. '...'로 이동 +2. '....'를 클릭 +3. '....'까지 아래로 스크롤 +4. 오류 발생 -**Expected behavior** -A clear and concise description of what you expected to happen. +## 예상된 결과 +정상적인 경우 예상되는 상황에 대해 명확하고 간결하게 설명합니다. -**Screenshots** -If applicable, add screenshots to help explain your problem. +## 스크린샷 +해당되는 경우 문제를 설명하는 데 도움이 되는 스크린샷을 추가하세요. -**Desktop (please complete the following information):** +## 문제 발생 환경 +**데스크탑 (다음 정보를 입력해 주세요):** - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + - 브라우저 [e.g. chrome, safari] + - 버전 [e.g. 22] -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] +**모바일 (다음 정보를 입력해 주세요):** + - 기기명: [e.g. iPhone6] - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + - 브라우저 [e.g. chrome, safari] + - 버전 [e.g. 22] -**Additional context** -Add any other context about the problem here. +## 추가 정보 +문제에 대한 추가적인 정보가 있다면 입력해 주세요. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..349c2c2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,20 @@ --- name: Feature request -about: Suggest an idea for this project +about: Feature 작업 사항을 입력해 주세요. title: '' labels: '' assignees: '' --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +## 문제 상황 +해결하려는 문제가 있다면 무엇인지 명확하고 간결하게 설명합니다. (ex. [...] 가 안 돼서 짜증나요!) -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +## 해결 방안 제안 +해결하려는 방법을 명확하고 간결하게 설명합니다. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +## 고려한 다른 대안들 +고려해 본 다른 대안이나 기능에 대해 명확하고 간결하게 설명합니다. -**Additional context** -Add any other context or screenshots about the feature request here. +## 추가 정보 +여기에 기능 요청에 대한 다른 상황이나 스크린샷을 추가하세요. diff --git a/scrap/local_councils/basic.py b/scrap/local_councils/basic.py index 3df915b..b9d2cd7 100644 --- a/scrap/local_councils/basic.py +++ b/scrap/local_councils/basic.py @@ -1,36 +1,39 @@ from urllib.parse import urlparse -from scrap.utils.types import CouncilType, Councilor, ScrapResult +from scrap.utils.types import CouncilType, Councilor, ScrapResult, ScrapBasicArgument from scrap.utils.requests import get_soup from scrap.utils.utils import getPartyList import re import requests +import copy regex_pattern = re.compile(r"정\s*\S*\s*당", re.IGNORECASE) # Case-insensitive party_keywords = getPartyList() party_keywords.append("무소속") -pf_elt = [None, "div", "div"] -pf_cls = [None, "profile", "profile"] -pf_memlistelt = [None, None, None] +def find(soup, element, class_): + if class_ is None: + return soup.find(element) + else: + return soup.find(element, class_) -name_elt = [None, "em", "em"] -name_cls = [None, "name", "name"] -name_wrapelt = [None, None, None] -name_wrapcls = [None, None, None] -pty_elt = [None, "em", "em"] -pty_cls = [None, None, None] -pty_wrapelt = [None, None, None] -pty_wrapcls = [None, None, None] +def find_all(soup, element, class_): + if class_ is None: + return soup.find_all(element) + else: + return soup.find_all(element, class_) -def get_profiles(soup, element, class_, memberlistelement): +def get_profiles(soup, element, class_, memberlistelement, memberlistclass_): # 의원 목록 사이트에서 의원 프로필을 가져옴 if memberlistelement is not None: - soup = soup.find_all(memberlistelement, class_="memberList")[0] - return soup.find_all(element, class_) - + try: + soup = find_all(soup, memberlistelement, + class_=memberlistclass_)[0] + except Exception: + raise RuntimeError('[basic.py] 의원 목록 사이트에서 의원 프로필을 가져오는데 실패했습니다.') + return find_all(soup, element, class_) def getDataFromAPI(url_format, data_uid, name_id, party_id) -> Councilor: # API로부터 의원 정보를 가져옴 @@ -42,27 +45,36 @@ def getDataFromAPI(url_format, data_uid, name_id, party_id) -> Councilor: ) + def get_name(profile, element, class_, wrapper_element, wrapper_class_): # 의원 프로필에서 의원 이름을 가져옴 if wrapper_element is not None: - profile = profile.find_all(wrapper_element, class_=wrapper_class_)[0] - name_tag = profile.find(element, class_) + profile = find_all(profile, wrapper_element, class_=wrapper_class_)[0] + name_tag = find(profile, element, class_) + if name_tag.find('span'): + name_tag = copy.copy(name_tag) + # span 태그 안의 것들을 다 지움 + for span in name_tag.find_all('span'): + span.decompose() name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" - if len(name) > 10: # strong태그 등 많은 걸 name 태그 안에 포함하는 경우. 은평구 등. - name = name_tag.strong.get_text(strip=True) if name_tag.strong else "이름 정보 없음" - name = name.split("(")[0].split(":")[-1] # 이름 뒷 한자이름, 앞 '이 름:' 제거 - - # 수식어가 이름 뒤에 붙어있는 경우 - while len(name) > 5: - if name[-3:] in ["부의장"]: # 119 등. - name = name[:-3].strip() - else: - break - while len(name) > 4: - if name[-2:] in ["의원", "의장"]: # 강서구 등. - name = name[:-2].strip() - else: - break # 4자 이름 고려. + + # name은 길고 그 중 strong태그 안에 이름이 있는 경우. 은평구, 수원시 등. + if name_tag.strong is not None: + name = name_tag.strong.get_text( + strip=True) if name_tag.strong else "이름 정보 없음" + name = name.split('(')[0].split( + ':')[-1].strip() # 이름 뒷 한자이름, 앞 '이 름:' 제거 + # TODO : 만약 이름이 우연히 아래 단어를 포함하는 경우를 생각해볼만 함. + if len(name) > 3: + # 수식어가 이름 앞이나 뒤에 붙어있는 경우 + for keyword in ['부의장', '의원', '의장']: # 119, 강서구 등 + if keyword in name: + name = name.replace(keyword, '').strip() + for keyword in party_keywords: + if keyword in name: # 인천 서구 등 + name = name.replace(keyword, '').strip() + break + name = name.split(' ')[0] # 이름 뒤에 직책이 따라오는 경우 return name @@ -73,68 +85,90 @@ def extract_party(string): return None -def get_party( - profile, element, class_, wrapper_element, wrapper_class_, party_in_main_page, url -): - # 의원 프로필에서 의원이 몸담는 정당 이름을 가져옴 - if not party_in_main_page: - parsed_url = urlparse(url) - base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" - # 프로필보기 링크 가져오기 - profile_link = profile.find("a", class_="start") - profile_url = base_url + profile_link["href"] +def goto_profilesite(profile, wrapper_element, wrapper_class_, wrapper_txt, url): + # 의원 프로필에서 프로필보기 링크를 가져옴 + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + # 프로필보기 링크 가져오기 + profile_link = find(profile, wrapper_element, class_=wrapper_class_) + if wrapper_txt is not None: + profile_links = find_all(profile, 'a', class_=wrapper_class_) + profile_link = [ + link for link in profile_links if link.text == wrapper_txt][0] + if profile_link is None: + raise RuntimeError('[basic.py] 의원 프로필에서 프로필보기 링크를 가져오는데 실패했습니다.') + # if base_url[-1] != '/': + # base_url = base_url + '/' + profile_url = base_url + profile_link['href'] + try: profile = get_soup(profile_url, verify=False) - party_pulp_list = list( - filter( - lambda x: regex_pattern.search(str(x)), profile.find_all(element, class_) - ) - ) + except Exception: + raise RuntimeError('[basic.py] \'//\'가 있진 않나요?', ' url: ', profile_url) + return profile + + +def get_party(profile, element, class_, wrapper_element, wrapper_class_, wrapper_txt, url): + # 의원 프로필에서 의원이 몸담는 정당 이름을 가져옴 + if wrapper_element is not None: + profile = goto_profilesite( + profile, wrapper_element, wrapper_class_, wrapper_txt, url) + party_pulp_list = list(filter(lambda x: regex_pattern.search( + str(x)), find_all(profile, element, class_))) + if party_pulp_list == []: + raise RuntimeError('[basic.py] 정당정보 regex 실패') party_pulp = party_pulp_list[0] - party_string = party_pulp.get_text(strip=True) - party_string = party_string.split(" ")[-1].strip() + party_string = party_pulp.get_text(strip=True).split(' ')[-1] while True: if (party := extract_party(party_string)) is not None: return party - if (party_span := party_pulp.find_next("span")) is not None: - party_string = party_span.text.split(" ")[-1] + if (party_pulp := party_pulp.find_next('span')) is not None: + party_string = party_pulp.text.strip().split(' ')[-1] else: - return "정당 정보 파싱 불가" + return "[basic.py] 정당 정보 파싱 불가" + +def get_party_easy(profile, wrapper_element, wrapper_class_, wrapper_txt, url): + # 의원 프로필에서 의원이 몸담는 정당 이름을 가져옴 + if wrapper_element is not None: + profile = goto_profilesite( + profile, wrapper_element, wrapper_class_, wrapper_txt, url) + party = extract_party(profile.text) + assert (party is not None) + return party -def scrap_basic(url, cid, encoding="utf-8") -> ScrapResult: - """의원 상세약력 스크랩 + +def scrap_basic(url, cid, args: ScrapBasicArgument, encoding='utf-8') -> ScrapResult: + '''의원 상세약력 스크랩 :param url: 의원 목록 사이트 url :param n: 의회 id :param encoding: 받아온 soup 인코딩 :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 - """ + ''' soup = get_soup(url, verify=False, encoding=encoding) councilors: list[Councilor] = [] - party_in_main_page = any(keyword in soup.text for keyword in party_keywords) - profiles = get_profiles( - soup, pf_elt[cid - 1], pf_cls[cid - 1], pf_memlistelt[cid - 1] - ) - print(cid, "번째 의회에는,", len(profiles), "명의 의원이 있습니다.") # 디버깅용. + profiles = get_profiles(soup, args.pf_elt, args.pf_cls, + args.pf_memlistelt, args.pf_memlistcls) + print(cid, '번째 의회에는,', len(profiles), '명의 의원이 있습니다.') # 디버깅용. for profile in profiles: - name = get_name( - profile, - name_elt[cid - 1], - name_cls[cid - 1], - name_wrapelt[cid - 1], - name_wrapcls[cid - 1], - ) - party = get_party( - profile, - pty_elt[cid - 1], - pty_cls[cid - 1], - pty_wrapelt[cid - 1], - pty_wrapcls[cid - 1], - party_in_main_page, - url, - ) - + name = party = '' + try: + name = get_name(profile, args.name_elt, args.name_cls, + args.name_wrapelt, args.name_wrapcls) + except Exception as e: + raise RuntimeError( + '[basic.py] 의원 이름을 가져오는데 실패했습니다. 이유 : ' + str(e)) + try: + party = get_party(profile, args.pty_elt, args.pty_cls, + args.pty_wrapelt, args.pty_wrapcls, args.pty_wraptxt, url) + except Exception as e: + try: + party = get_party_easy( + profile, args.pty_wrapelt, args.pty_wrapcls, args.pty_wraptxt, url) + except Exception: + raise RuntimeError( + '[basic.py] 의원 정당을 가져오는데 실패했습니다. 이유: ' + str(e)) councilors.append(Councilor(name=name, party=party)) return ScrapResult( @@ -143,6 +177,7 @@ def scrap_basic(url, cid, encoding="utf-8") -> ScrapResult: councilors=councilors, ) - -if __name__ == "__main__": - print(scrap_basic("https://www.yscl.go.kr/kr/member/name.do", 3)) # 서울 용산구 +if __name__ == '__main__': + args3 = ScrapBasicArgument( + pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em') + print(scrap_basic('https://www.yscl.go.kr/kr/member/name.do', 3, args3)) # 서울 용산구 \ No newline at end of file diff --git a/scrap/local_councils/gangwon.py b/scrap/local_councils/gangwon.py new file mode 100644 index 0000000..5a81520 --- /dev/null +++ b/scrap/local_councils/gangwon.py @@ -0,0 +1,6 @@ +from urllib.parse import urlparse +import re + +from scrap.utils.types import CouncilType, Councilor, ScrapResult, ScrapBasicArgument +from scrap.utils.requests import get_soup +from scrap.local_councils.basic import scrap_basic diff --git a/scrap/local_councils/gwangju.py b/scrap/local_councils/gwangju.py new file mode 100644 index 0000000..b34f872 --- /dev/null +++ b/scrap/local_councils/gwangju.py @@ -0,0 +1,5 @@ +"""광주광역시를 스크랩. 60-64번째 의회까지 있음. +""" +from scrap.utils.types import CouncilType, Councilor, ScrapResult +from scrap.utils.requests import get_soup +from scrap.local_councils.basic import * \ No newline at end of file diff --git a/scrap/local_councils/gyeonggi.py b/scrap/local_councils/gyeonggi.py new file mode 100644 index 0000000..7fc2627 --- /dev/null +++ b/scrap/local_councils/gyeonggi.py @@ -0,0 +1,111 @@ +"""경기도를 스크랩. +""" +from scrap.utils.types import CouncilType, Councilor, ScrapResult +from scrap.utils.requests import get_soup +from scrap.local_councils.basic import * + +def get_profiles_88(soup, element, class_, memberlistelement, memberlistclass_): + # 의원 목록 사이트에서 의원 프로필을 가져옴 + if memberlistelement is not None: + try: + soup = soup.find_all(memberlistelement, id=memberlistclass_)[0] + except Exception: + raise RuntimeError('[basic.py] 의원 목록 사이트에서 의원 프로필을 가져오는데 실패했습니다.') + return soup.find_all(element, class_) + +def get_party_88(profile, element, class_, wrapper_element, wrapper_class_, url): + # 의원 프로필에서 의원이 몸담는 정당 이름을 가져옴 + if wrapper_element is not None: + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + # 프로필보기 링크 가져오기 + profile_link = find(profile, wrapper_element, class_=wrapper_class_).find('a') + profile_url = base_url + profile_link['href'] + profile = get_soup(profile_url, verify=False, encoding='euc-kr') + party_pulp_list = list(filter(lambda x: regex_pattern.search(str(x)), find_all(profile, element, class_))) + if party_pulp_list == []: raise RuntimeError('[basic.py] 정당정보 regex 실패') + party_pulp = party_pulp_list[0] + party_string = party_pulp.get_text(strip=True).split(' ')[-1] + while True: + if (party := extract_party(party_string)) is not None: + return party + if (party_pulp := party_pulp.find_next('span')) is not None: + party_string = party_pulp.text.strip().split(' ')[-1] + else: + return "[basic.py] 정당 정보 파싱 불가" + +def scrap_88(url, args: ScrapBasicArgument) -> ScrapResult: + '''의원 상세약력 스크랩 + :param url: 의원 목록 사이트 url + :param args: ScrapBasicArgument 객체 + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + ''' + cid = 88 + encoding = 'euc-kr' + soup = get_soup(url, verify=False, encoding=encoding) + councilors: list[Councilor] = [] + party_in_main_page = any(keyword in soup.text for keyword in party_keywords) + profiles = get_profiles_88(soup, args.pf_elt, args.pf_cls, args.pf_memlistelt, args.pf_memlistcls) + print(cid, '번째 의회에는,', len(profiles), '명의 의원이 있습니다.') # 디버깅용. + + for profile in profiles: + name = get_name(profile, args.name_elt, args.name_cls, args.name_wrapelt, args.name_wrapcls) + party = '' + try: + party = get_party_88(profile, args.pty_elt, args.pty_cls, args.pty_wrapelt, args.pty_wrapcls, url) + except Exception: + party = get_party_easy(profile, args.pty_wrapelt, args.pty_wrapcls, args.pty_wraptxt, url) + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id=str(cid), + council_type=CouncilType.LOCAL_COUNCIL, + councilors=councilors + ) + +def get_party_103(profile, element, class_, wrapper_element, wrapper_class_, url): + # 의원 프로필에서 의원이 몸담는 정당 이름을 가져옴 + if wrapper_element is not None: + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + # 프로필보기 링크 가져오기 + profile_link = profile.find(wrapper_element, class_=wrapper_class_) + profile_url = base_url + '/member/' + profile_link['href'] + profile = get_soup(profile_url, verify=False) + party_pulp_list = list(filter(lambda x: regex_pattern.search(str(x)), find_all(profile, element, class_))) + if party_pulp_list == []: raise RuntimeError('[basic.py] 정당정보 regex 실패') + party_pulp = party_pulp_list[0] + party_string = party_pulp.get_text(strip=True).split(' ')[-1] + while True: + if (party := extract_party(party_string)) is not None: + return party + if (party_pulp := party_pulp.find_next('span')) is not None: + party_string = party_pulp.text.strip().split(' ')[-1] + else: + return "[basic.py] 정당 정보 파싱 불가" + +def scrap_103(url, args: ScrapBasicArgument) -> ScrapResult: + '''의원 상세약력 스크랩 + :param url: 의원 목록 사이트 url + :param args: ScrapBasicArgument 객체 + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + ''' + cid = 103 + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + party_in_main_page = any(keyword in soup.text for keyword in party_keywords) + profiles = get_profiles_88(soup, args.pf_elt, args.pf_cls, args.pf_memlistelt, args.pf_memlistcls) + print(cid, '번째 의회에는,', len(profiles), '명의 의원이 있습니다.') # 디버깅용. + + for profile in profiles: + name = get_name(profile, args.name_elt, args.name_cls, args.name_wrapelt, args.name_wrapcls) + party = get_party_103(profile, args.pty_elt, args.pty_cls, args.pty_wrapelt, args.pty_wrapcls, url) + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id=str(cid), + council_type=CouncilType.LOCAL_COUNCIL, + councilors=councilors + ) \ No newline at end of file diff --git a/scrap/local_councils/incheon.py b/scrap/local_councils/incheon.py index fb70b4a..740e1ad 100644 --- a/scrap/local_councils/incheon.py +++ b/scrap/local_councils/incheon.py @@ -1,8 +1,8 @@ -from urllib.parse import urlparse - +"""인천광역시를 스크랩. 50-57번째 의회까지 있음. +""" from scrap.utils.types import CouncilType, Councilor, ScrapResult from scrap.utils.requests import get_soup - +from scrap.local_councils.basic import * def scrap_50(url="https://www.icjg.go.kr/council/cnmi0101c") -> ScrapResult: """인천시 중구 페이지에서 의원 상세약력 스크랩 @@ -205,6 +205,43 @@ def scrap_56( councilors=councilors, ) +def scrap_57(url, args) -> ScrapResult: + """인천시 서구 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + cid = 57 + + profiles = get_profiles(soup, args.pf_elt, args.pf_cls, args.pf_memlistelt, args.pf_memlistcls) + print(cid, '번째 의회에는,', len(profiles), '명의 의원이 있습니다.') # 디버깅용. + + for profile in profiles: + name = get_name(profile, args.name_elt, args.name_cls, args.name_wrapelt, args.name_wrapcls) + + party = '정당 정보 없음' + party_pulp = find(profile, args.pty_elt, class_=args.pty_cls) + if party_pulp is None: raise AssertionError('[incheon.py] 정당정보 실패') + party_string = party_pulp.get_text(strip=True) + party_string = party_string.split(' ')[-1].strip() + while True: + party = extract_party(party_string) + if party is not None: + break + if (party_pulp := party_pulp.find_next('span')) is not None: + party_string = party_pulp.text.split(' ')[-1] + else: + raise RuntimeError("[incheon.py] 정당 정보 파싱 불가") + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id=str(cid), + council_type=CouncilType.LOCAL_COUNCIL, + councilors=councilors + ) -if __name__ == "__main__": - print(scrap_56()) +if __name__ == '__main__': + print(scrap_56()) \ No newline at end of file diff --git a/scrap/local_councils/seoul.py b/scrap/local_councils/seoul.py index 8e5bd44..c59b815 100644 --- a/scrap/local_councils/seoul.py +++ b/scrap/local_councils/seoul.py @@ -1,13 +1,10 @@ from urllib.parse import urlparse -import re from scrap.utils.types import CouncilType, Councilor, ScrapResult from scrap.utils.requests import get_soup -def scrap_1( - url="https://council.jongno.go.kr/council/councilAsemby/list/estList.do?menuNo=400021", -) -> ScrapResult: +def scrap_1(url = 'https://bookcouncil.jongno.go.kr/record/recordView.do?key=99784f935fce5c1d7c8c08c2f9e35dda1c0a6128428ecb1a87f87ee2b4e82890ffcf12563e01473f') -> ScrapResult: """서울시 종로구 페이지에서 의원 상세약력 스크랩 :param url: 의원 목록 사이트 url @@ -16,16 +13,19 @@ def scrap_1( soup = get_soup(url, verify=False) councilors: list[Councilor] = [] - for profile in soup.find_all("div", class_="chairman-info"): - name_tag = profile.find_next("strong") - name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" - party = "정당 정보 없음" - # TODO + for profile in soup.find_all('div', class_='pop_profile'): + info = profile.find("div", class_="info") + data_ul = info.find("ul", class_="detail") + data_lis = data_ul.find_all("li") + name = data_lis[0].find("span").get_text(strip=True) + party = data_lis[2].find("span").get_text(strip=True) + name = name if name else "이름 정보 없음" + party = party if party else '정당 정보 없음' councilors.append(Councilor(name=name, party=party)) return ScrapResult( - council_id="seoul-junggu", + council_id="seoul-jongno", council_type=CouncilType.LOCAL_COUNCIL, councilors=councilors, ) diff --git a/scrap/metropolitan_council.py b/scrap/metropolitan_council.py new file mode 100644 index 0000000..1c19078 --- /dev/null +++ b/scrap/metropolitan_council.py @@ -0,0 +1,464 @@ +from urllib.parse import urlparse + +from scrap.utils.types import CouncilType, Councilor, ScrapResult +from scrap.utils.requests import get_soup + + +def scrap_metro_1(url = 'https://www.smc.seoul.kr/main/memIntro01.do?menuId=001002001001') -> ScrapResult: + '''서울시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + ''' + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + # 프로필 링크 스크랩을 위해 base_url 추출 + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + for profile in soup.find_all('input', class_='memLinkk'): + name = profile['value'].strip() if profile else '이름 정보 없음' + party = '정당 정보 없음' + + # 프로필보기 링크 가져오기 + profile_url = base_url + '/home/' + profile['data-url'] + profile_soup = get_soup(profile_url, verify=False) + + party_info = profile_soup.find('div', class_='profile') + if party_info and (party_span := party_info.find('li')) is not None: + party = party_span.find_next('li').get_text(strip=True) + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="seoul", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_2(url = 'https://council.busan.go.kr/council/past02') -> ScrapResult: + '''부산시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + ''' + soup = get_soup(url, verify=False).find('ul', class_='inmemList') + councilors: list[Councilor] = [] + + for profile in soup.find_all('a', class_='detail'): + name = profile.get_text(strip=True) if profile else '이름 정보 없음' + party = '정당 정보 없음' + + # 프로필보기 링크 가져오기 + profile_url = profile['href'] + profile_soup = get_soup(profile_url, verify=False) + + party_info = profile_soup.find('ul', class_='vs-list-st-type01') + if party_info and (party_span := party_info.find('li')) is not None: + party = party_span.find_next('li').find_next('li').get_text(strip=True).split()[-1].strip() + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="busan", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_3(url="https://council.daegu.go.kr/kr/member/active") -> ScrapResult: + """대구시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all("div", class_="pop_profile"): + name_tag = profile.find("p", class_="name") + name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" + + party = '정당 정보 없음' + party_info = profile.find("em", string="소속정당") + if party_info: + party = party_info.find_next("span").get_text(strip=True) + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="daegu", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_4(url="https://www.icouncil.go.kr/main/member/name.jsp") -> ScrapResult: + """인천시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False).find('table', class_='data').find('tbody') + councilors: list[Councilor] = [] + + for profile in soup.find_all("tr"): + columns = profile.find_all('td') + + name_tag = columns[0] + name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = columns[1] + party = party_tag.get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="incheon", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_5(url="https://council.gwangju.go.kr/index.do?PID=029") -> ScrapResult: + """광주시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False).find('table', class_='data').find('tbody') + councilors: list[Councilor] = [] + + # TODO + + return ScrapResult( + council_id="gwangju", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_6(url="https://council.daejeon.go.kr/svc/cmp/MbrListByPhoto.do") -> ScrapResult: + """대전시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False).find('ul', class_='mlist') + councilors: list[Councilor] = [] + + for profile in soup.find_all("dl"): + name_tag = profile.find('dd', class_='name') + name = name_tag.find('strong').get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = name_tag.find_next('dd').find_next('dd') + party = party_tag.find('i').get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="daejeon", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_7(url="https://www.council.ulsan.kr/kor/councillor/viewByPerson.do") -> ScrapResult: + """울산시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for name_tag in soup.find_all("div", class_='name'): + name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = name_tag.find_next('li').find_next('li') + party = party_tag.get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="ulsan", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_8(url="https://council.sejong.go.kr/mnu/pom/introductionMemberByName.do") -> ScrapResult: + """세종시 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False).find('ul', class_='ml') + councilors: list[Councilor] = [] + + for profile in soup.find_all('dl'): + name_tag = profile.find('dd', class_='name') + name = name_tag.find(string=True, recursive=False).strip() if name_tag else "이름 정보 없음" + + party_tag = name_tag.find_next('dd').find_next('dd') + party = party_tag.get_text(strip=True).split()[-1].strip() if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="sejong", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_9(url="https://www.ggc.go.kr/site/main/memberInfo/actvMmbr/list?cp=1&menu=consonant&sortOrder=MI_NAME&sortDirection=ASC") -> ScrapResult: + """경기도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False).find('div', class_='paging2 clearfix') + councilors: list[Councilor] = [] + + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + for page in soup.find_all('a'): + page_url = base_url + page['href'] + page_soup = get_soup(page_url, verify=False).find('ul', class_='memberList3 clear') + for profile in page_soup.find_all('li', recursive=False): + name_tag = profile.find('p', class_='f22 blue3') + name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = profile.find('li', class_='f15 m0') + party = party_tag.get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="gyeonggi", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_10(url="https://council.chungbuk.kr/kr/member/active.do") -> ScrapResult: + """충청북도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all('div', class_='profile'): + name_tag = profile.find('em', class_='name') + name = name_tag.get_text(strip=True).split()[0].strip() if name_tag else "이름 정보 없음" + + party_tag = profile.find('em', string='소속정당') + party = party_tag.find_next('span').find_next('span').get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="chungbuk", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_11(url="https://council.chungnam.go.kr/kr/member/name.do") -> ScrapResult: + """충청남도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all('div', class_='profile'): + name_tag = profile.find('em', class_='name') + name = name_tag.get_text(strip=True).split()[0].strip() if name_tag else "이름 정보 없음" + + party_tag = profile.find('em', string='소속정당 : ') + party = party_tag.find_next('span').get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="chungnam", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_12(url="https://www.assem.jeonbuk.kr/board/list.do?boardId=2018_assemblyman&searchType=assem_check&keyword=1&menuCd=DOM_000000103001000000&contentsSid=453") -> ScrapResult: + """전라북도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all('li', class_='career'): + name_tag = profile.find('tr', class_='name') + name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = profile.find('tr', class_='list1') + party = party_tag.find('td', class_='co2').get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="jeonbuk", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_13(url="https://www.jnassembly.go.kr/profileHistory.es?mid=a10202010000&cs_daesoo=12") -> ScrapResult: + """전라남도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all('tbody'): + name_tag = profile.find('p') + name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = profile.find('th', string='소속정당') + party = party_tag.find_next('td', class_='txt_left').get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="jeonnam", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_14(url="https://council.gb.go.kr/kr/member/name") -> ScrapResult: + """경상북도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all('div', class_='profile'): + name_tag = profile.find('div', class_='name') + name = name_tag.find('strong').get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = profile.find('em', string='소속정당') + party = party_tag.find_next('span').find_next('span').get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="gyeongbuk", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_15(url="https://council.gyeongnam.go.kr/kr/member/active.do") -> ScrapResult: + """경상남도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all('div', class_='profile'): + name_tag = profile.find('div', class_='name') + name = name_tag.find('strong').get_text(strip=True).split('(')[0].strip() if name_tag else "이름 정보 없음" + + party_tag = profile.find('em', class_='ls2', string='정당') + party = party_tag.find_next('span').get_text(strip=True) if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="gyeongnam", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_16(url="https://council.gangwon.kr/kr/member/name.do") -> ScrapResult: + """강원도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for profile in soup.find_all('div', class_='profile'): + name_tag = profile.find('em', class_='name') + name = name_tag.get_text(strip=True) if name_tag else "이름 정보 없음" + + party_tag = profile.find('em', string='소속정당') + party = party_tag.find_next('span').get_text(strip=True).split()[-1].strip() if party_tag else "정당 정보 없음" + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="gangwon", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + +def scrap_metro_17(url="https://www.council.jeju.kr/cmember/active/name.do") -> ScrapResult: + """제주도 페이지에서 의원 상세약력 스크랩 + + :param url: 의원 목록 사이트 url + :return: 의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + """ + + soup = get_soup(url, verify=False) + councilors: list[Councilor] = [] + + for tag in soup.find_all('p', class_='name'): + text = tag.get_text(strip=True).split("(") + # print(text) + name = text[0].strip() + party = text[1][:-1].strip() + + councilors.append(Councilor(name=name, party=party)) + + return ScrapResult( + council_id="jeju", + council_type=CouncilType.METROPOLITAN_COUNCIL, + councilors=councilors + ) + + + +if __name__ == '__main__': + print(scrap_metro_17()) \ No newline at end of file diff --git a/scrap/national_council.py b/scrap/national_council.py index fbc79f7..b058abf 100644 --- a/scrap/national_council.py +++ b/scrap/national_council.py @@ -8,43 +8,42 @@ def scrap_national_council(cd: int) -> ScrapResult: - """열린국회정보 Open API를 이용해 역대 국회의원 인적사항 스크랩 - _data 폴더에 assembly_api_key.json 파일을 만들어야 하며, - 해당 JSON은 {"key":"(Open API에서 발급받은 인증키)"} 꼴을 가져야 한다. - https://open.assembly.go.kr/portal/data/service/selectAPIServicePage.do/OBL7NF0011935G18076#none - - :param cd: 국회의원 대수. 제20대 국회의원을 스크랩하고자 하면 20 - :return: 국회의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 - """ - - key_json_path = os.path.join(BASE_DIR, "_data", "assembly_api_key.json") - if not os.path.exists(key_json_path): - raise Exception( - "열린국회정보 Open API에 회원가입 후 인증키를 발급받아주세요.\nhttps://open.assembly.go.kr/portal/openapi/openApiDevPage.do" - ) - with open(key_json_path, "r") as key_json: - assembly_key = json.load(key_json)["key"] - - request_url = f"https://open.assembly.go.kr/portal/openapi/npffdutiapkzbfyvr?KEY={assembly_key}&UNIT_CD={cd + 100000}" - response = requests.get(request_url) - - if response.status_code != 200: - raise Exception(f"Open API 요청에 실패했습니다 (상태 코드 {response.status_code})") - - root = ET.fromstring(response.text) - councilors: list[Councilor] = [] - - for row in root.iter("row"): - councilors.append( - Councilor(name=row.find("HG_NM").text, party=row.find("POLY_NM").text) - ) - - return ScrapResult( - council_id="national", - council_type=CouncilType.NATIONAL_COUNCIL, - councilors=councilors, - ) - - -if __name__ == "__main__": - print(scrap_national_council(20)) + '''열린국회정보 Open API를 이용해 역대 국회의원 인적사항 스크랩 + _data 폴더에 assembly_api_key.json 파일을 만들어야 하며, + 해당 JSON은 {"key":"(Open API에서 발급받은 인증키)"} 꼴을 가져야 한다. + https://open.assembly.go.kr/portal/data/service/selectAPIServicePage.do/OBL7NF0011935G18076#none + + :param cd: 국회의원 대수. 제21대 국회의원을 스크랩하고자 하면 21 + :return: 국회의원들의 이름과 정당 데이터를 담은 ScrapResult 객체 + ''' + + key_json_path = os.path.join(BASE_DIR, '_data', 'assembly_api_key.json') + if not os.path.exists(key_json_path): + raise Exception('열린국회정보 Open API에 회원가입 후 인증키를 발급받아주세요.\nhttps://open.assembly.go.kr/portal/openapi/openApiDevPage.do') + with open(key_json_path, 'r') as key_json: + assembly_key = json.load(key_json)['key'] + + request_url = f"https://open.assembly.go.kr/portal/openapi/nwvrqwxyaytdsfvhu?KEY={assembly_key}&pSize=500&UNIT_CD={cd + 100000}" + response = requests.get(request_url) + + if response.status_code != 200: + raise Exception(f'Open API 요청에 실패했습니다 (상태 코드 {response.status_code})') + + root = ET.fromstring(response.text) + councilors: list[Councilor] = [] + + for row in root.iter('row'): + councilors.append(Councilor( + name=row.find('HG_NM').text, + party=row.find('POLY_NM').text + )) + + return ScrapResult( + council_id='national', + council_type=CouncilType.NATIONAL_COUNCIL, + councilors=councilors + ) + + +if __name__ == '__main__': + print(scrap_national_council(21)) \ No newline at end of file diff --git a/scrap/utils/spreadsheet.py b/scrap/utils/spreadsheet.py index 7ce9082..a4bcf25 100644 --- a/scrap/utils/spreadsheet.py +++ b/scrap/utils/spreadsheet.py @@ -5,6 +5,9 @@ import gspread from scrap.local_councils.seoul import * +from scrap.local_councils.incheon import * +from scrap.local_councils.gwangju import * +from scrap.local_councils.gyeonggi import * from scrap.local_councils import * from requests.exceptions import Timeout @@ -46,54 +49,120 @@ def main() -> None: client: gspread.client.Client = google_authorization() # 스프레드시트 열기 - spreadsheet: gspread.Spreadsheet = client.open_by_url( - "https://docs.google.com/spreadsheets/d/1Eq2x7xZCw_5ng2GdHDnpUIhhwbmOAKEl4abX09JLyuA/edit#gid=1044938838" - ) - worksheet: gspread.Worksheet = spreadsheet.get_worksheet( - 1 - ) # 원하는 워크시트 선택 (0은 첫 번째 워크시트입니다.) + link = 'https://docs.google.com/spreadsheets/d/1fBDJjkw8FSN5wXrvos9Q2wDsyItkUtNFGOxUZYE-h0M/edit#gid=1127955905' # T4I-의회목록 + spreadsheet: gspread.Spreadsheet = client.open_by_url(link) + worksheet: gspread.Worksheet = spreadsheet.get_worksheet(0) # 원하는 워크시트 선택 (0은 첫 번째 워크시트입니다.) + # TODO - 홈페이지 위 charset=euc-kr 등을 인식해 바로 가져오기. + euc_kr = [6, 13, 16, 31, 72, 88, 112, 154, 157, 163, 167, 181, 197, 202] + special_functions = list(range(1, 57)) + [57, 88, 103] + args = { + 2 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 3 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + # 인천 + 57 : ScrapBasicArgument(pf_elt='div', pf_cls='box', name_elt='p', name_cls='mem_tit2', pty_elt='p', pty_cls='mem_tit2'), + 58 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 59 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_elt='em'), + # 광주 + 60 : ScrapBasicArgument(pf_elt='div', pf_cls='content', name_elt='h5', pty_wrapelt='a', pty_elt='li'), + 61 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + # 62 : TODO! /common/selectCouncilMemberProfile.json 을 어떻게 얻을지.. + # 63 : TODO! 홈페이지 터짐 + # 64 : TODO! /common/selectCouncilMemberProfile.json 을 어떻게 얻을지.. + # 대전 + 65 : ScrapBasicArgument(pf_elt='dl', pf_cls='profile', name_elt='strong', name_cls='name', pty_elt='strong'), + 66 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_elt='em'), + 67 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='member', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='dd'), + 68 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 69 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + # 울산 + 70 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='memberName', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='dd'), + 71 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='memberName', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='dd'), + 72 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='li', name_cls='name', pty_elt='li'), + 73 : ScrapBasicArgument(pf_elt='dl', pf_cls='profile', name_elt='strong', name_cls='name', pty_elt='li'), + 74 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_wrapelt='a', pty_wrapcls='start', pty_elt='li'), + # 경기 + 75 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_elt='em'), + 76 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 77 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='mbrListByName', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='dd'), + 78 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_wrapelt='a', pty_wrapcls='end', pty_elt='li'), + 79 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 80 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 81 : ScrapBasicArgument(pf_memlistelt='div', pf_memlistcls='member_list', pf_elt='dd', name_elt='p', pty_elt='tr'), + 82 : ScrapBasicArgument(pf_memlistelt='div', pf_memlistcls='cts1426_box', pf_elt='div', pf_cls='conbox', name_elt='p', pty_elt='li'), + # 경기 - 동두천 + 83 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_wrapelt='a', pty_wrapcls='start', pty_elt='li'), + 84 : ScrapBasicArgument(pf_elt='div', pf_cls='law_box', name_elt='span', name_cls='name', pty_elt='p'), + 85 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_elt='em'), + 86 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 87 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 88 : ScrapBasicArgument(pf_memlistelt='div', pf_memlistcls='member_list', pf_elt='dl', pf_cls='box', name_elt='span', name_cls='name', pty_wrapelt='p', pty_wrapcls='btn', pty_elt='li'), + 89 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='memberName', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='span'), + 90 : ScrapBasicArgument(pf_elt='dl', pf_cls='profile', name_elt='strong', name_cls='name', pty_elt='li'), + # 경기 - 화성 + 91 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='mbr0101', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='dd'), + 92 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='member', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='dd'), + 93 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_wrapelt='a', pty_wrapcls='end', pty_elt='li'), + 94 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='mbrListByName', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='dd'), + 95 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='member', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='tr'), + 96 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_elt='em'), + 97 : ScrapBasicArgument(pf_memlistelt='ul', pf_memlistcls='memberList', pf_elt='li', name_elt='strong', pty_wrapelt='a', pty_elt='tr'), + 98 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 99 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 100 : ScrapBasicArgument(pf_elt='div', pf_cls='list', name_elt='h4', name_cls='h0', pty_elt='li'), + # 경기 - 광주 + 101 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 102 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_wrapelt='a', pty_wrapcls='start', pty_elt='li'), + 103 : ScrapBasicArgument(pf_elt='div', pf_cls='col-sm-6', name_elt='h5', name_cls='h5', pty_wrapelt='a', pty_wrapcls='d-inline-block', pty_elt='li'), + 104 : ScrapBasicArgument(pf_elt='div', pf_cls='text_box', name_elt='h3', name_cls='h0', pty_wrapelt='a', pty_wraptxt='누리집', pty_elt='li'), + 105 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + # 강원 + # 106 : TODO! 정당정보 없음 + # TODO! 107이 get_soup에서 실패 중 - HTTPSConnectionPool(host='council.wonju.go.kr', port=443): Max retries exceeded with url: /content/member/memberName.html (Caused by SSLError(SSLError(1, '[SSL: DH_KEY_TOO_SMALL] dh key too small (_ssl.c:1007)'))) + 107 : ScrapBasicArgument(pf_memlistelt='div', pf_memlistcls='content', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='span'), + 108 : ScrapBasicArgument(pf_elt='dl', pf_cls='profile', name_elt='strong', pty_elt='li'), + 109 : ScrapBasicArgument(pf_memlistelt='section', pf_memlistcls='memberName', pf_elt='dl', name_elt='dd', name_cls='name', pty_elt='span'), + 110 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + # 111 : TODO! 정당 없고 홈페이지는 깨짐 + 112 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='em', name_cls='name', pty_elt='em'), + 113 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_cls='name', pty_elt='li'), + 115 : ScrapBasicArgument(pf_elt='div', pf_cls='profile', name_elt='div', name_cls='name', pty_elt='li'), + # TODO : 정당이 주석처리되어 있어서 soup가 인식을 못함. + 116 : ScrapBasicArgument(pf_elt='div', pf_cls='memberName', name_cls='name',pty_elt='dd'), + } # 데이터 가져오기 data: list[dict] = worksheet.get_all_records() + result: str = '' - print(scrap_junggu(data[1]["상세약력 링크"])) - print(scrap_gwangjingu(data[4]["상세약력 링크"])) - print(scrap_dongdaemungu(data[5]["상세약력 링크"])) - for n in range(65, 75): - function_name = f"scrap_{n}" - if hasattr(sys.modules[__name__], function_name): - function_to_call = getattr(sys.modules[__name__], function_name) - print(function_to_call) - if n in [66, 70, 74]: - result = function_to_call() # 스프레드시트 링크 터짐 (울산 울주군처럼 애먼데 링크인 경우도 있다) + error_times = 0 + parse_error_times = 0 + timeouts = 0 + N = 226 + # for n in range (113, 169): + for n in range(107, 108): + encoding = 'euc-kr' if n in euc_kr else 'utf-8' + try: + if n in special_functions: + function_name = f"scrap_{n}" + if hasattr(sys.modules[__name__], function_name): + function_to_call = getattr(sys.modules[__name__], function_name) + if n < 57: + result = str(function_to_call(data[n - 1]['상세약력 링크']).councilors) + else: + result = str(function_to_call(data[n - 1]['상세약력 링크'], args=args[n]).councilors) else: - result = function_to_call(data[n - 1]["상세약력 링크"]) + result = str(scrap_basic(data[n - 1]['상세약력 링크'], n, args[n], encoding).councilors) + if '정보 없음' in result: + print("정보 없음이 포함되어 있습니다.") + parse_error_times += 1 print(result) - else: - print(f"함수 {function_name}를 찾을 수 없습니다.") - - ## 아래 테스트를 위해서는 위 프린트문을 모두 주석처리해 주세요. - # error_times = 0 - # parse_error_times = 0 - # timeouts = 0 - # N = 226 - # for n in range (N): - # encoding = 'euc-kr' if n in [5, 12, 15, 30, 111, 153, 156, 162, 166, 180, 196, 201] else 'utf-8' - # try: - # result = str(scrap_basic(data[n]['상세약력 링크'], f'district-{n}', encoding)) - # if '정보 없음' in result: - # print("정보 없음이 포함되어 있습니다.") - # parse_error_times += 1 - # print(result) - # except Timeout: - # print(f"Request to {data[n]['상세약력 링크']} timed out.") - # timeouts += 1 - # except Exception as e: - # print(f"An error occurred for district-{n}: {str(e)}") - # error_times += 1 - # continue # 에러가 발생하면 다음 반복으로 넘어감 - # print(f"| 총 실행 횟수: {N} | 에러 횟수: {error_times} | 정보 없음 횟수: {parse_error_times} | 타임아웃 횟수: {timeouts} |") - - -if __name__ == "__main__": + except Timeout: + print(f"Request to {data[n - 1]['상세약력 링크']} timed out.") + timeouts += 1 + except Exception as e: + print(f"오류 : [district-{n}] {str(e)}") + error_times += 1 + continue # 에러가 발생하면 다음 반복으로 넘어감 + print(f"| 총 실행 횟수: {N} | 에러 횟수: {error_times} | 정보 없음 횟수: {parse_error_times} | 타임아웃 횟수: {timeouts} |") +if __name__ == '__main__': main() diff --git a/scrap/utils/types.py b/scrap/utils/types.py index 481bfa0..67acc1f 100644 --- a/scrap/utils/types.py +++ b/scrap/utils/types.py @@ -14,6 +14,7 @@ class CouncilType(str, Enum): LOCAL_COUNCIL = "local_council" NATIONAL_COUNCIL = "national_council" + METROPOLITAN_COUNCIL = "metropolitan_council" """ 기초의회 """ @@ -53,3 +54,54 @@ class ScrapResult: """ 의회 의원 목록입니다. """ + + +class ScrapBasicArgument: + ''' + scrap_basic에 쓸 argument입니다 + ''' + def __init__(self, + pf_elt: str | None = None, + pf_cls: str | None = None, + pf_memlistelt: str | None = None, + pf_memlistcls: str | None = None, + name_elt: str | None = None, + name_cls: str | None = None, + name_wrapelt: str | None = None, + name_wrapcls: str | None = None, + pty_elt: str | None = None, + pty_cls: str | None = None, + pty_wrapelt: str | None = None, + pty_wrapcls: str | None = None, + pty_wraptxt: str | None = None): + """ + ScrapBasicArgument 클래스의 생성자입니다. + + Args: + - pf_elt (str | None): pf의 요소 이름. 일반적으로 'div'입니다. + - pf_cls (str | None): pf의 클래스 이름. 일반적으로 'profile'입니다. + - pf_memlistelt (str | None): pf 멤버 목록 요소 이름. + - pf_memlistelt (str | None): pf 멤버 목록 클래스 이름. + - name_elt (str | None): 이름 요소의 이름. 존재한다면 일반적으로 'em'입니다. + - name_cls (str | None): 이름 요소의 클래스 이름. 일반적으로 'name'입니다. + - name_wrapelt (str | None): 이름 래퍼 요소의 이름. + - name_wrapcls (str | None): 이름 래퍼 요소의 클래스 이름. + - pty_elt (str | None): 속성 요소의 이름. + - pty_cls (str | None): 속성 요소의 클래스 이름. + - pty_wrapelt (str | None): 속성 래퍼 요소의 이름. 존재한다면 일반적으로 'a'입니다. + - pty_wrapcls (str | None): 속성 래퍼 요소의 클래스 이름. 존재한다면 일반적으로 'start'입니다. + - pty_wraptxt (str | None): 속성 래퍼 요소의 텍스트. + """ + self.pf_elt = pf_elt + self.pf_cls = pf_cls + self.pf_memlistelt = pf_memlistelt + self.pf_memlistcls = pf_memlistcls + self.name_elt = name_elt + self.name_cls = name_cls + self.name_wrapelt = name_wrapelt + self.name_wrapcls = name_wrapcls + self.pty_elt = pty_elt + self.pty_cls = pty_cls + self.pty_wrapelt = pty_wrapelt + self.pty_wrapcls = pty_wrapcls + self.pty_wraptxt = pty_wraptxt \ No newline at end of file