본문 바로가기

Data Handling/Data Collecting

selenium - 지하철역 인근 점포 수(카페, 호프집, 제과점 등) 크롤링

서울역 근처에는 카페가 몇 개 있을까?

 

강남역 근처에는 빵집이 몇 개 있을까?

 

이태원역 근처에는 술집이 몇 개 있을까?

 

 

 

오늘은 이 물음에 대한 솔루션을 주제로 포스팅을 준비해봤습니다.

바로 시작해볼게요!


준비물


1. 서울시 상권 데이터 

먼저 서울시의 '상권분석서비스'에서 특정 지역의 점포 수를 크롤링하는 과정입니다.

준비물에 걸어둔 링크로 들어가시면, 지역별로 다양한 형태의 데이터를 제공하는 것을 확인할 수 있습니다.

 

그 중 저는 2018년 1, 2, 3, 4분기, 2019년 1분기의 외식업 데이터,

특히 제과점, 호프/주점, 카페 수 데이터를 필요로 합니다. 

 

해당 데이터는 url로 들어가면 바로 보여지는 형태가 아닌,

특정 조건을 선택하고 검색 버튼을 눌러야 데이터가 보여지는 형태이기 때문에 selenium을 사용하고,

웹 상에서 클릭, 선택과 같은 동작은 xpath를 이용하여 구현합니다.

 

코드를 살펴볼게요.

 

from selenium import webdriver

url = 'https://golmok.seoul.go.kr/regionAreaAnalysis.do'
driver = webdriver.Chrome("본인의 웹드라이버 경로/chromedriver.exe") # webdriver 실행(크롬 창 생성)
driver.implicitly_wait(3)
driver.get(url) # url로 접속

driver.find_element_by_xpath('//*[@id="loginPop"]/div/button[1]').click() # 비회원 로그인

# 검색 조건 설정
year_path = '//*[@id="selectYear"]/option[2]'
driver.find_element_by_xpath(year_path).click() # [3]-2018년// [2]-2019년
driver.implicitly_wait(1)

sem_path = '//*[@id="selectQu"]/option[4]'
driver.find_element_by_xpath(sem_path).click() # [4] : 1분기 ~ [1] : 4분기

driver.implicitly_wait(1)
driver.find_element_by_xpath('//*[@id="induL"]/option[2]').click() # 외식업
driver.implicitly_wait(1)
driver.find_element_by_xpath('//*[@id="induM"]/option[11]').click() # 커피/음료 ([1] : 전체 ~ [11] : 커피/음료)
driver.implicitly_wait(1)
driver.find_element_by_xpath('//*[@id="infoCategory"]/option[2]').click() # 점포수
driver.implicitly_wait(1)

driver.find_element_by_xpath('//*[@id="presentSearch"]').click() # 검사 버튼

box = []
for num in [2,20,36,53,71,87,102,119,140,154,169,189,206,221,238,257,278,294,305,324,340,362,381,404,432]: # 모든 구에 대해 동 단위로 확장
    btn = '//*[@id="table1"]/tbody/tr[' + str(num) + ']/td[1]/span/a'
    driver.find_element_by_xpath(btn).click() # 동 단위로 상세검색  
    driver.implicitly_wait(1)
    containers = driver.find_elements_by_css_selector('tr.leaf.collapsed')
    for container in containers:
        sm_box = []
        values = container.find_elements_by_css_selector('td')
        if values[0].text != '':
            sm_box.append(values[0].text)
            sm_box.append(values[8].text)
            box.append(sm_box)
            driver.implicitly_wait(0.1)
        else:
            pass
        
big_box = []
big_box.append(box) # 분기를 바꿔가며 big_box에 append 해줌

 

코드의 마지막 줄을 보면 데이터가 얼마되지 않기 때문에 직접 분기를 변경하며 반복작업 한 것을 알 수 있습니다.

만약 처리해야할 데이터 수가 많다면 반복문을 만들어 자동화 시키면 되겠죠?

 

그리고 신사동의 경우, 관악구와 강남구 두 곳에 있기 때문에 병합(merge)하는 과정에서 문제가 생깁니다.

따라서 저는 해당 인덱스를 찾아 아래와 같이 값을 수정해주었습니다.

 

df1 = pd.DataFrame(big_box[0],columns = ['dong','cafe_18_1'])
df2 = pd.DataFrame(big_box[1],columns = ['dong','cafe_18_2'])
df3 = pd.DataFrame(big_box[2],columns = ['dong','cafe_18_3'])
df4 = pd.DataFrame(big_box[3],columns = ['dong','cafe_18_4'])
df5 = pd.DataFrame(big_box[4],columns = ['dong','cafe_19_1'])

# 신사동이 두 개
for df in [df1,df2,df3,df4,df5]:
    df.loc[331,'dong'] = '관악_신사동'
    df.loc[357,'dong'] = '강남_신사동'
    
cafe_df = pd.merge(df1,df2, on= 'dong')
cafe_df = pd.merge(cafe_df,df3, on= 'dong')
cafe_df = pd.merge(cafe_df,df4, on= 'dong')
cafe_df = pd.merge(cafe_df,df5, on= 'dong')

 

이런 방법으로 제과점, 호프/주점, 카페 수를 크롤링하면 아래와 같은 데이터를 만들 수 있습니다.

그 과정에서 잘 안되시거나, 궁금한 점이 있다면 댓글 달아주세요! 

 

 

자!! 이제 동 별로 점포 수를 크롤링 했습니다.

이걸 지하철역 데이터와 합치기 위해서는 각 지하철역이 어떤 동에 속하는지 알아야겠죠?

따라서 지하철역의 주소 데이터를 찾아보았습니다.

2. 지하철역 주소 데이터

지하철역 주소도 크롤링 해야하나 걱정하던 찰나, 해당 데이터를 발견했습니다.

이때까지만 해도 일이 빨리 끝날줄 알고 좋았습니다.

동을 기준으로 병합해주면 되니깐요!

아래는 다운 받은 데이터 입니다.

 

 

그런데 이게 웬걸?

동이 맵핑이 안됩니다.

 

자세히 들여다보니 이상한게 많습니다.

다운받은 데이터에는 종로 1가, 종로 2가,, 이렇게 되어있는게, 크롤링한 데이터에는 종로 1,2,3,4가 로 묶여있습니다.

 

결국 몇 시간을 연구한 끝에 깨달았습니다.

우리나라 동은 법정동과 행정동 두 가지 기준이 있다는 것을요!!!!

법정동은 법으로 지정한 구역 단위(?) 이고, 행정동은 편의상 나눈 구역 단위 입니다.

즉 인구가 많은 법정동은 편의상 여러개의 행정동으로 나눌 수 있고(주민센터가 여러 개임),

인구가 적은 법정동은 편의상 여러개의 법정동을 하나의 행정동으로 묶을 수 있습니다.

 

하,,

이걸 어떻게 또 바꿔주나,,

법정동, 행정동 맵핑 데이터가 존재하길 기도하며 다시 찾아보았습니다.

없으면,, 크롤링 해야죠.

 

3. 법정동, 행정동 맵핑 데이터

정말 다행히도 해당 데이터를 찾을 수 있었습니다.

아래는 제가 찾은 데이터 입니다.

낯익은 동들이 많이 보이네요!

 

 

자, 이제 모든 준비가 끝났습니다.

법정동으로 되어있는 지하철역 주소를 행정동으로 변경하고,

크롤링한 점포 수 데이터와 병합하면 끝입니다.

 

일단 법정동의 지하철역 주소를 행정동과 맵핑시켜보겠습니다.

 

import pandas as pd
import numpy as np

# 역별 주소 가져오기 (station_address.csv : http://data.seoul.go.kr/dataList/OA-12035/A/1/datasetView.do)
st = pd.read_csv('data/station_address.csv', encoding='utf-8') # 법정동
shop = pd.read_csv('data/shop.csv', encoding='cp949') # 행정동

# 서울시 전철 데이터만 사용
st['시'] = st['상세주소'].apply(lambda x: x.split()[0])
st.loc[38, '시'] = '서울특별시' # 오기입 수정
st = st[st['시'] == '서울특별시']

# 법정동, 행정동 맵핑 (https://www.mois.go.kr/frt/bbs/type001/commonSelectBoardArticle.do?bbsId=BBSMSTR_000000000052&nttId=69109)
mapping = pd.read_csv('data/mapping_dong.csv', encoding='utf-8') # 동리명 : 법정동 / 읍면동명 : 행정동
mapping = mapping[mapping['시도명'] == '서울특별시']
mapping_dict = {i:j for i,j in zip(mapping['동리명'], mapping['읍면동명'])}
st['행정동'] = st['동'].apply(lambda x: mapping_dict[x] if x in mapping_dict.keys() else 'nan')

 

딕셔너리를 이용하여 시도했는데, 몇 가지 실패한 데이터가 보입니다. 

양이 적기 때문에 직접 수정해줍니다.

 

# 맵핑 되지 않은 데이터 확인
st[(st['행정동'] == 'nan')|(st['행정동'].isnull())]

# 맵핑되지 않은 데이터 수작업
st.loc[0,'행정동'] = mapping[mapping['동리명'] == '봉래동2가']['읍면동명'].values
st.loc[112,'행정동'] = mapping[mapping['동리명'] == '동자동']['읍면동명'].values
st.loc[191,'행정동'] = mapping[mapping['동리명'] == '용산동4가']['읍면동명'].values
st.loc[200,'행정동'] = mapping[mapping['동리명'] == '보문동1가']['읍면동명'].values

 

 

이제 지하철역과 점포 수 데이터를 병합하면 됩니다.

1차적으로 병합한 결과입니다.

 

# 지하철역과 해당 행정동의 커피, 제과, 술집 점포수 데이터 병합
st.reset_index(drop=True, inplace=True)
shop = shop.rename({'dong':'행정동'}, axis='columns')
st = st[['역명','호선','행정동']]
st_shop = pd.merge(st, shop, on='행정동', how='left')

# null값 확인
st_shop.isnull().sum()

 

 

72개의 오류가 보입니다.

원인을 분석해보니 지하철역 데이터에는 '창신제3동', '행당제2동' 과 같이 표기되어 있는 반면,

점포 수 데이터에서는 '창신3동', '행당2동' 과 같이 표기되어 있었습니다.

따라서 72개의 오류에 대해 '제' 를 빼주는 작업을 한 후, 다시 병합했습니다.

 

# 문제분석 ==> '제' 삭제
del_list = st_shop[st_shop['cafe_18_1'].isnull()]['행정동'].unique()
st['행정동'] = st['행정동'].apply(lambda x: x.replace('제','') if x in del_list else x)

# 다시 합치고 null값 확인
st_shop = pd.merge(st, shop, on='행정동', how='left')
st_shop.isnull().sum()

 

 

아직 두 개의 결측치 데이터가 있습니다.

왜 이런 결측치가 생기게 되었는지 찾아보니,

'제'를 삭제하는 과정에서 '홍제제2동' 의 경우에 '홍2동'으로 되어버린 문제를 발견했습니다.

결측치가 2개밖에 되지 않기 때문에 직접 수정해주고 다시 합쳐봅니다.

 

st.loc[65, '행정동'] = '홍제2동'
st.loc[66, '행정동'] = '홍제2동'

# 다시 합치고 null값 확인
st_shop = pd.merge(st, shop, on='행정동', how='left')
st_shop.isnull().sum()

 


드디어 완성했습니다!!

지금 당장은 지하철 인근의 제과점, 호프/주점, 카페 수 데이터만을 만들었지만,

코드를 살짝만 변화시키면 지하철 인근 인구수, 점포 생존율 등 훨씬 다양한 정보를 가져올 수도 있겠네요.

제 코드가 많은 분들에게 도움이 되셨으면 좋겠습니다 ㅎㅎ 모두 화이팅이에요!