본문 바로가기

Python/Crawler

우리 지역의 성범죄자는 몇 명이나 될까? (1) - python을 이용한 크롤링, 시각화

- 이 포스팅은 실패기입니다.

- 모든 사람이 성공할 순 없으니까요 . . .

 SQL 공부를 하다가 너무 재미없고 막대 그래프 위주 시각화에서 벗어나 지도를 이용한 시각화를 해보고 싶었다. 제일 만만한게 오픈 API이고 그 중에서도 성범죄를 선택하였다. 아무래도 생존과 직결되는 문제니까 말이다. 데이터는 공공 API를 이용했다. 

 근데 엄~청 접근하기 힘들게 만들어져있다. 어떻게 막은건지 몰라도 직접적으로 검색하기 힘들다..ㅎㅎ 이해X 범죄자 인권이 그렇게 중요한가. 아무튼 이 API는 XML 파일이라 tag를 가져오기만 하면 된다.

 이렇게 실시간으로 데이터를 준다. bs4를 이용해 파싱한 이후 가져오면 끝. 

def get_real_criminal():
	data_list = ['강원도', '경기도', '경상남도', '경상북도', '광주광역시', '대구광역시', '대전광역시', '부산광역시', '서울특별시', '세종특별자치시', '울산광역시', '인천광역시', '전라남도', '전라북도', '제주특별자치도', '충청남도', '충청북도']
	map_list = ['Gangwon', 'Gyeonggi', 'South Gyeongsang','North Gyeongsang','Gwangju','Daegu','Daejeon','Busan','Seoul','Sejong','Ulsan','Incheon','South Jeolla','North Jeolla','Jeju','South Chungcheong','North Chungcheong']
	try:
		criminal_data = {}
		url = 'http://116.67.77.182/openapi/SOCitysStats'
		data = requests.get(url)
		temp = BeautifulSoup(data.content, "html.parser")
		for n in temp.find_all('city'):
			city = n.find('city-name').get_text()
			count = n.find('city-count').get_text()
			temp_index = data_list.index(city)
			criminal_data[map_list[temp_index]] = count
		# data = data.get_text()
		# data = data[2:]
	except:
		pass
		
	return criminal_data 

 이따 사용할 지도가 영어로 되어있어서 사전 변환을 조금 해주었다. city 태그를 찾아서 그 안의 요소를 저장한 다음 criminal_data에 직접 넣어주는 것이다.

{'Gangwon': '134', 'Gyeonggi': '917', 'South Gyeongsang': '284', 'North Gyeongsang': '269', 'Gwangju': '111', 'Daegu': '204', 'Daejeon': '108', 'Busan': '231', 'Seoul': '593', 'Sejong': '10', 'Ulsan': '75', 'Incheon': '250', 'South Jeolla': '197', 'North Jeolla': '200', 'Jeju': '57', 'South Chungcheong': '210', 'North Chungcheong': '119'}

 이런 깜찍한 결과값이 나온다.

 이제 이걸 지도로 그려보자. 사용할 지도는 이 곳에서 다운로드 받았다.

svg로 그려진 지도는 path에 id나 name이 있어 연결하여 style을 넣어줄 수 있다. css랑 비슷하다. 다운받은 svg를 우클릭하여 소스를 살펴본다.

 <path d="M311.6 226.7l1 2.4-0.8 0.4-1.4 1-0.5 0.7-2.8 0.6-1.2-0.2-5.4 2.4-2 1.8-1.1 2-6.9 5.2-1.5-0.3-2.1 0-0.7-2.5-1.2-0.8-0.6 0.7-0.8-1.9-1-0.2-1.6 1.7-1.2-0.6-1.7-0.4 1-1-0.3-1.1-1.7-1.6-0.5-1.1 0.6-1.5 1-0.2 1.3 0.6 7.2-5 5.1-0.6 3.5 0.1 3.2-1.7 1.1-4.5 2.2-0.8 1.9 1.9 2.7 1.2 1.8 0.2 1.3 0.7 1.2 1.6 0.9 0.8z m22.7 25l-0.4-1.4-8.3-1-3.9 4.9-4-3 1.6-3.7 2.1-3.1-1.3-2.3-5.4-2.9 0.3-3.6-0.5-2.9 1.6-2.7 4.5-1.3 1.4-0.7 0.3-1.7-2.1-0.6-0.6 1.8-3.3 0.3-0.8-2-0.4-3.6 0.8-4.9 15.3-0.5 9.4 9-0.1 4.7 1.7 3.9 2.6 1.7 1.5 2.4-1.5 5-2.2 4.3-3.3 2.4-5 1.5z"
 id="KOR2495" name="Incheon">
 </path>

 제일 짧아보이는 인천을 보면 위와 같이 path에 좌표가 적혀있고 id, name을 가지고 있다. 여기서 영어로 입력되어 있기 때문에 위에 사전에서 영어로 값을 저장해준 것이다. 먼저 map에 넣을 style을 지정해준다.

colors = ['#f7f7f7','#E9E9E9','#DCDCDC','#CFCFCF','#C2C2C2','#B5B5B5','#A8A8A8','#9B9B9B','#8E8E8E','#808080','#737373','#666666','#595959','#4C4C4C','#3F3F3F','#323232','#252525']
path_style = 'font-size:12px;fill-rule:nonzero;stroke:#FFFFFF;stroke-opacity:1;4stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt;marker-start:none;stroke-linejoin:bevel;fill:'
map_list = ['Gangwon', 'Gyeonggi', 'South Gyeongsang','North Gyeongsang','Gwangju','Daegu','Daejeon','Busan','Seoul','Sejong','Ulsan','Incheon','South Jeolla','North Jeolla','Jeju','South Chungcheong','North Chungcheong']

colors = 지도에 넣을 임의의 색상이다. 나는 검은색 위주로 사용하였다.

path_style = 마찬가지

map_list = 이따가 tag에서 읽어올 값을 가지고 있다.

def draw_map_svg():
	#map colors
	colors = ['#f7f7f7','#E9E9E9','#DCDCDC','#CFCFCF','#C2C2C2','#B5B5B5','#A8A8A8','#9B9B9B','#8E8E8E','#808080','#737373','#666666','#595959','#4C4C4C','#3F3F3F','#323232','#252525']
	path_style = 'font-size:12px;fill-rule:nonzero;stroke:#FFFFFF;stroke-opacity:1;4stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt;marker-start:none;stroke-linejoin:bevel;fill:'
	map_list = ['Gangwon', 'Gyeonggi', 'South Gyeongsang','North Gyeongsang','Gwangju','Daegu','Daejeon','Busan','Seoul','Sejong','Ulsan','Incheon','South Jeolla','North Jeolla','Jeju','South Chungcheong','North Chungcheong']
	#data
	criminal_data = get_real_criminal()
	
	map = BeautifulSoup(svg, selfClosingTags=['defs','sodipodi:namedview'], features="lxml")
	paths = map.find_all('path')
	
	criminal_sort = sorted(criminal_data, key=lambda k : criminal_data[k])

	for p in paths:
		if p['name'] in map_list:
			# print(p['name'],criminal_data[p['name']])
			# p['style'] = path_style + colors[criminal_data.index(p)-1]
			p['style'] = path_style + colors[criminal_sort.index(p['name'])]
	print(map.prettify())

map을 beautifulsoup로 읽어온 뒤 path를 저장해준다. 그 뒤에 path에서 고유한 id를 찾아 style을 넣어주기만 하면 되는 것이다. 나는 범죄자 인구수별로 색을 주고 싶어서 sort 해준 뒤 높은(범죄자가 많은) 순서대로 진한 색으로 넣어주었다. (지금보니 안된다 수정이 필요하다)이후 map을 다시 저장해주면 된다.

 풀코드는 다음과 같다.

import requests
from bs4 import BeautifulSoup
import csv
import matplotlib.pylab as plt
from matplotlib import font_manager, rc
import numpy as np

plt.rcParams['axes.unicode_minus'] = False
fontpath = "C:/Windows/Fonts/malgun.ttf"
font_name = font_manager.FontProperties(fname=fontpath).get_name()
rc('font',family=font_name)

#실시간 공개현황 지도
url = 'https://sexoffender.go.kr/m3s3.nsc'
svg = open('kr.svg','r').read()

def get_real_criminal():
	data_list = ['강원도', '경기도', '경상남도', '경상북도', '광주광역시', '대구광역시', '대전광역시', '부산광역시', '서울특별시', '세종특별자치시', '울산광역시', '인천광역시', '전라남도', '전라북도', '제주특별자치도', '충청남도', '충청북도']
	map_list = ['Gangwon', 'Gyeonggi', 'South Gyeongsang','North Gyeongsang','Gwangju','Daegu','Daejeon','Busan','Seoul','Sejong','Ulsan','Incheon','South Jeolla','North Jeolla','Jeju','South Chungcheong','North Chungcheong']
	try:
		criminal_data = {}
		url = 'http://116.67.77.182/openapi/SOCitysStats'
		data = requests.get(url)
		temp = BeautifulSoup(data.content, "html.parser")
		for n in temp.find_all('city'):
			city = n.find('city-name').get_text()
			count = n.find('city-count').get_text()
			temp_index = data_list.index(city)
			criminal_data[map_list[temp_index]] = int(count)
		# data = data.get_text()
		# data = data[2:]
	except:
		pass
	return criminal_data 
    
def draw_map_svg():
	#map colors
	colors = ['#f7f7f7','#E9E9E9','#DCDCDC','#CFCFCF','#C2C2C2','#B5B5B5','#A8A8A8','#9B9B9B','#8E8E8E','#808080','#737373','#666666','#595959','#4C4C4C','#3F3F3F','#323232','#252525']
	path_style = 'font-size:12px;fill-rule:nonzero;stroke:#FFFFFF;stroke-opacity:1;4stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt;marker-start:none;stroke-linejoin:bevel;fill:'
	map_list = ['Gangwon', 'Gyeonggi', 'South Gyeongsang','North Gyeongsang','Gwangju','Daegu','Daejeon','Busan','Seoul','Sejong','Ulsan','Incheon','South Jeolla','North Jeolla','Jeju','South Chungcheong','North Chungcheong']
	#data
	criminal_data = get_real_criminal()
	
	map = BeautifulSoup(svg, selfClosingTags=['defs','sodipodi:namedview'], features="lxml")
	paths = map.find_all('path')
	
	criminal_sort = sorted(criminal_data, key=lambda k : criminal_data[k])

	# script_tags = map.find('circle')
	# script_tags.insert_before(svg)
	for p in paths:
		if p['name'] in map_list:
			# print(p['name'],criminal_data[p['name']])
			# p['style'] = path_style + colors[criminal_data.index(p)-1]
			p['style'] = path_style + colors[criminal_sort.index(p['name'])]
	print(map.prettify())
		
	
if __name__ == '__main__':
	draw_map_svg()

지도에 매핑시키고 나면 이렇게 입력된다. 사실 원하던 그림은 이게 아니였다. 조금 수정해보자면 ...

이런걸 원했다..ㅠ_ㅠ 근데 별의 별 방법을 써도 마음에 가게 안되어서... 완벽하게 css를 쓰기도 애매하고 d3가 그리워지는 순간이였다... 아마 csv 방식 말고 plt를 쓰거나 다른 방식으로 매핑하면 될거같은데 오늘은 일단 시간이 없어서 여기까지 했다. 

 딱 봤을 땐 경기도가 많아보이지만 인구수를 고려해야 한다. 이것은 2일차로 넘어가서 생각해봐야한다. 일단 아쉬운대로 막대그래프로 표현해보았다.

def draw_plot():
	criminal_data = get_real_criminal_plot()
	criminal_data = sorted(criminal_data.items(), key=lambda t:t[1], reverse=True)
	
	plt.bar(range(len(criminal_data)), [val[1] for val in criminal_data])
	plt.xticks(range(len(criminal_data)), [val[0] for val in criminal_data])
	plt.xticks(rotation=70)
	plt.title('지역별 성범죄자 수')
	plt.ylabel('명')
	plt.xlabel('지역')
	plt.show()

 내일은 꼭 맵에 숫자를 띄어보ㅏ야겠다 ㅠ_ㅠ

 막간 팁)

 팁이기도 하고 이걸 언제 고칠까 궁금하기도 해서 포스팅해본다. 원래 성범죄자를 '상세하게' 검색하려면 이렇게 실명 인증을 해야한다.

안하면 데이터로 사용한 시도별로 정리된 자료밖에 열람할 수 없다. 하지만 이 페이지에 들어가서 소스보기를 보면 다음과 같은 주석을 볼 수 있다.

박효련 주무관님이 누군진 몰라도 5년전의 주석으로 이름을 남기고 계신다. 크롬의 개발자 모드는 현재 보고 있는 html을 수정할 수 있다.

우클릭해서 edit html~~ 누르면 저런 창이 뜬다. 저기서 주석을 해제하면 . . .

이런 창이 뜬다. 아직도 먹혀서 크롤링해서 쓸까 하다가 잡혀갈까봐 안썼다. 엄청 상세자료는 안나오고 인증 안하고 받는 자료보다 조금 더 상세한 자료가 뜨는 정도이다. 참고로 우리동네에 짱 많이 산다~~ 이사가야지~~~