22대 총선 결과는 아무리 들여다봐도 이해가 안되는 부분이 많습니다.
여조, 사전투표, 출구조사는 일관되게 나오는데 본투표만 이상하다?
좀 이해가 가기 어렵습니다.
샤이한 2찍은 여조에도 안나타나고 사전투표도 안하다가
본투표에서는 조용히 나타나서는 출구조사원들한테는 일관되게 반대로 대답하는데
또 조국신당을 찍는 투표는 하더라?
대체 어떤 집단일까요?
의문을 제기하면 또 몇몇은 득달같이 재개발 심리가 어떻다, 주변 분위기가 어떻다,
후보자가 부족했다 이런 헛소리들을 합니다.
이런 개별적인 경험이 어떻게 전체 데이터에 영향을 미칠까요.
그래서 선관위 홈페이지에 있는 자료를 어떻게 긁어 모을까 고민하다가 챗지피티에 물어가며 2일만에 구현해보았습니다.
크롤링이라는건 처음해봤습니다. 지피티한테 물어물어 하다보니 생각보다 쉽게 구현이 되더군요.
(물론 쉽진 않았습니다. 페이지 로딩 속도, Select항목에서 선택하고 대기하기 등등 쉽지많은 않았습니다.)
아무튼 모두의 집단지성이 만들어낸 코드이니 가져다가 필요하신대로 쓰시기 바랍니다.
아마 코드를 좀 더 잘 다루시는 분들이라면 쉽게 다른 자료와 비효해보는 알고리즘을 추가할 수 도 있을꺼라 생각됩니다.
아마도 여기 계신분들이라면 파이썬 실행하는건 어느정도 가능하시리라 봅니다.
혹시 모르신다면
파이썬, pip3 설치하기를 검색해보시거나
https://www.codeit.kr/tutorials/6/pip-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-windows 요런 문서를 한번 따라해 보시기 바랍니다.
그러면 cmd 창을 열어서 pip3 를 실행해볼 수 있습니다.
pip3는 필요한 파이썬 라이브러리를 설치해주는 도구입니다.
셀레니움 이라는 라이브러리를 설치하기 위해
터미널을 열고 `pip3 install selenium` 이라고 쳐보세요.
그리고나서 아무 폴더나 만든 다음 election.py 라는 파일을 하나 만듭니다. (사실 아무 파일명.py라고 해도 됩니다)
그리고 메모장으로 열고 아래 코드를 복사, 붙여넣기 해보세요.
그리고 해당 폴더에서 터미널을 열고
`python3 election.py
라고 입력하면 선관위 홈페이지에 접속하여 알아서 개표 단위별로 개표결과를 죽 긁어서 csv 파일로 만들어줍니다.
혹시 중간에 멈춘다면 그냥 다시 실행하시면 마지막으로 저장한 csv 파일 이후부터 다시 긁어오기가 시작됩니다.
300여개 투표소의 데이터를 추출하느라 대략 1시간~2시간 정도 걸립니다.
아무튼 한참 기다리면 되긴 될겁니다.
전문가 분들이라면 좀 더 쉽게 하실 수 있겠지만 한두명이 분석하기에는 어렵습니다.
하지만 모두가 모여서 들여다보고 분석하다 보면 분명 무언가를 찾을 수 있을것이라 봅니다.
잘 안되거나 모르는 부분이 있으시면 최대한 아는대로 답변 드리겠습니다.
참고
사전투표 결과와 분석 편의를 위해 각 투표소 소계에는 사전투표 갯수를 뺀 숫자로 변경하였습니다.
그래서 선관위의 개표 결과와 차이가 있는것처럼 보일 수 있습니다.
```
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import csv
import math
import glob
class ElectionCrawl(object):
def main(self):
# 현재 폴더에 있는 csv 파일에서 행정단위명 추출하기
csvfiles = []
for file in glob.glob("*.csv"):
csvfiles.append(file)
citynames_to_skip = []
sggnames_to_skip = []
townnames_to_skip = []
for fname in csvfiles:
strs = fname.strip(".csv").split("-")
if strs[0] not in citynames_to_skip:
citynames_to_skip.append(strs[0])
if strs[1] not in sggnames_to_skip:
sggnames_to_skip.append(strs[1])
if strs[2] not in townnames_to_skip:
townnames_to_skip.append(strs[2])
# Selenium을 사용하여 웹페이지에 접속
self.driver = webdriver.Chrome() # 크롬 드라이버 사용
self.driver.implicitly_wait(10)
url = "http://info.nec.go.kr/main/showDocument.xhtml?electionId=0020240410&topMenuId=VC&secondMenuId=VCCP08"
self.driver.get(url)
# 국회의원 선거 항목을 선택하기 위해 버튼 클릭하고 기다리기
button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.ID, "electionId2")))
button.click()
time.sleep(1)
with open("note.txt","w") as self.txtfile:
# 시도 목록 확인
city_code = self.driver.find_element(By.XPATH, '//*[@id="cityCode"]')
city_list = [option.text for option in city_code.find_elements(By.TAG_NAME, "option")]
city_list = city_list[1:]
for city in city_list:
#if city not in citynames_to_skip:
city_select = Select(self.driver.find_element(By.ID, "cityCode"))
city_select.select_by_visible_text(city)
time.sleep(1)
# 시군구 목록 확인
sgg_city_code = self.driver.find_element(By.XPATH, '//*[@id="sggCityCode"]')
sgg_city_list = [option.text for option in sgg_city_code.find_elements(By.TAG_NAME, "option")]
sgg_city_code_lst = sgg_city_list[1:]
for sgg_city in sgg_city_code_lst:
if sgg_city not in sggnames_to_skip:
sgg_city_select = Select(self.driver.find_element(By.ID, "sggCityCode"))
sgg_city_select.select_by_visible_text(sgg_city)
time.sleep(1)
# 읍면동 확인
town_code = self.driver.find_element(By.XPATH, '//*[@id="townCodeFromSgg"]')
town_list = [option.text for option in town_code.find_elements(By.TAG_NAME, "option")]
self.txt_to_write = ""
if len(town_list) > 1:
town_list = town_list[1:]
for town in town_list:
town_select = Select(self.driver.find_element(By.ID, "townCodeFromSgg"))
town_select.select_by_visible_text(town)
time.sleep(1)
self.anaylyze_data()
else:
#하위 선거구가 없는 경우
self.anaylyze_data()
self.txtfile.close()
# 웹브라우저 종료
self.driver.quit()
def anaylyze_data(self):
# 분석 관련 파라미터 설정
party_lists = ["더불어민주당", "국민의힘", "개혁신당"]
headers_to_remove = ["후보자별 득표수","무효\n투표수","기권자수","계"] #제거할 목록
headers_to_tail = ["계", "무효\n투표수","기권자수"] # 가장 오른쪽으로 오는 헤더 목록
# 검색 버튼 클릭
self.driver.find_element(By.XPATH, '//*[@id="spanSubmit"]').click()
time.sleep(1)
elect_name = self.driver.find_element(By.XPATH, '//*[@id="electionName"]').text.strip("[]")
city_name = self.driver.find_element(By.XPATH, '//*[@id="cityName"]').text.strip("[]")
sggCity_name = self.driver.find_element(By.XPATH, '//*[@id="sggCityName"]').text.strip("[]")
town_name = self.driver.find_element(By.XPATH, '//*[@id="townNameFromSgg"]').text.strip("[]")
# 테이블 요소 찾기
table_element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.XPATH, '//*[@id="table01"]')))
# 헤더 추출
header_cells = table_element.find_elements(By.TAG_NAME, "th")
header_data = []
# party_lists의 정당명 순서로 후보자 이름을 추출
candidates = []
data_end_idx = 0 #데이터 추가를 위해 필요함
for head_idx, cell in enumerate(header_cells):
#print(cell.text)
for idx, party in enumerate(party_lists):
if party in cell.text:
candidates.append(cell.text.strip(party_lists[idx].replace("\n","")).strip("\n"))
if not any(item in cell.text for item in headers_to_remove):
header_data.append(cell.text)
if "계" in cell.text:
data_end_idx = head_idx
header_data += headers_to_tail
# 헤더 목록 가공
header_title = party_lists[0]+"\n-"+party_lists[1] #민주-국힘
#파일명 만들기
fname = city_name+"-"+sggCity_name+"-"+town_name+"-"
for candidate in candidates:
fname+=candidate+"_"
fname+=".csv"
self.txt_to_write+=fname
print(fname)
# 테이블 행과 열 추출
rows = table_element.find_elements(By.TAG_NAME, "tr")
#1차 데이터 가공 - 각 cell의 값을 data의 리스트로 복사한다
pre_vote_found = False
data = []
row_num = 0
for row in rows:
# 행의 각 셀 추출
cells = row.find_elements(By.TAG_NAME, "td")
cell_data = []
if len(cells) > 1:
if cells[0].text.strip(): #첫번째 셀에 무언가 있으면
pre_vote_found = False
if not pre_vote_found:
for cell in cells:
cell_data.append(cell.text)
if cells[1].text == "관내사전투표": #관내사전투표 항목이 있으면 나머지는 스킵한다.
pre_vote_found = True
data.append(cell_data)
row_num+=1
# #2차 데이터 분석 - data에 추가할 항목들을 붙여넣는다.
for i in range(len(data)):
if len(data[i]) > 1:
#소계에서 관내사전투표 값을 뺀다.
if data[i][1] == "관내사전투표":
for j in range(2,len(data[i])):
total_vote = int(data[i-1][j].replace(',',''))
prev_vote = int(data[i][j].replace(',',''))
data[i-1][j] = str(total_vote-prev_vote)
self.save_csv_file(fname, header_data, data)
def save_csv_file(self, fname, header, data):
#csv 파일 열고 데이터 쓰기
with open(fname, "w", newline="") as csvfile:
csvwriter = csv.writer(csvfile)
# 헤더 쓰기
csvwriter.writerow(header)
# 실제로 데이터를 csv 파일에 쓴다.
for i in range(len(data)):
if len(data[i]) > 2:
csvwriter.writerow(data[i])
csvfile.close()
if __name__ == "__main__":
ElectionCrawl().main()
```
한번 꼭 돌려보세요.