부동산 - 조인스랜드 데이터 크롤링

조인스랜드부동산

http://price.joinsland.joins.com/ 데이터 크롤링

2017 FinanceData http://fb.com/financedata

화면 하단에 다음과 같은 데이터가 테이블(table) 표시된다. 이 데이터들이 크롤링하고자 하는 데이터

여기에 표시되는 데이터가 어떻게 구성되어 있는지 개발자 도구로 살펴보자

개발자 도구에서 보면, id="sesae_info_gu" 인 div 태그 하위에 table이 있고, 여기에 데이터들이 구성되어 있음을 확인할 수 있다

하지만 pandas.read_html()로 읽었을 때 테이블이 읽히지 않는다.

import pandas as pd

url = 'http://price.joinsland.joins.com/area/index.asp?mcateGroup=A1&mcateCode=A1A3A4&areaCode=1168010600'
dfs = pd.read_html(url, 'lxml')
dfs

"ValueError: No tables found" 에러가 발생한다

페이지 소스 확인

메뉴에서 "보기 > 개발자 보기 > 소스보기" 선택하여 페이지 소스를 확인

크롬 개발자 도구로 table과 데이터가 보이지만, 소스 보기를 하면 소스에는 데이터가 보이지 않는다

id="sesae_info_gu" 인 div 태그에 table 도 없고 데이터도 없다

이 처럼 크롬개발자 도구에서는 데이터가 있으나, 페이지 소스에는 데이터가 없는 경우는 다음 두 가지 중의 하나

  • Ajax로 데이터를 요청하여 데이터를 표현 → 데이터 요청 URL을 따로 분석
  • JavaScript가 실행되어 데이터를 표현 → Selenium 사용

Ajax (Asynchronous JavaScript and XML, 에이잭스)는 자바스크립트로 요청하는 비동기 데이터 요청.

Ajax 데이터 요청 확인

  • Network 탭을 열고
  • 검색조건이 달라질 때('동'을 선택), 어떤 데이터가 오가는지 살펴본다
  • 요청/응답 데이터들 중에 데이터기 포함되어 오가는지 살펴본다

다음 URL의 인자로 다음 값들이 전달.

http://price.joinsland.joins.com/ajax/price.info.dongapt.asp

  • mcateGroup: A1=아파트, A6=오피스텔
  • mcateCode: A1A3A4
  • areaCode=1168010600

결과적으로 지역코드(areaCode)만 지정하면 가격 데이터를 가져올 수 있다.

지역이름과 코드

(참고) joinsland.joins.com 자체에도 법정동을 조회하는 내용이 있으나 데이터가 온전한 JSON 이 아니어서 가공하기 어렵다

In [1]:
# 구/시/군 (MCODE) 코드 가져오기

import requests

# 서울특별시 하위 '구/시/군' 읽기
area_code = '110000000' 
url = 'http://price.joinsland.joins.com/ajax/area_search.asp?div=MCODE&areaCode=' + area_code
r = requests.get(url)
print(r.text)

# JSON처럼 보이지만 JSON 규격을 따르지 않고 있다
[{seq:'1', idx:1, name:'강남구', label:'강남구', value:'1168000000', cnt:'0'},{seq:'2', idx:2, name:'강동구', label:'강동구', value:'1174000000', cnt:'0'},{seq:'3', idx:3, name:'강북구', label:'강북구', value:'1130500000', cnt:'0'},{seq:'4', idx:4, name:'강서구', label:'강서구', value:'1150000000', cnt:'0'},{seq:'5', idx:5, name:'관악구', label:'관악구', value:'1162000000', cnt:'0'},{seq:'6', idx:6, name:'광진구', label:'광진구', value:'1121500000', cnt:'0'},{seq:'7', idx:7, name:'구로구', label:'구로구', value:'1153000000', cnt:'0'},{seq:'8', idx:8, name:'금천구', label:'금천구', value:'1154500000', cnt:'0'},{seq:'9', idx:9, name:'노원구', label:'노원구', value:'1135000000', cnt:'0'},{seq:'10', idx:10, name:'도봉구', label:'도봉구', value:'1132000000', cnt:'0'},{seq:'11', idx:11, name:'동대문구', label:'동대문구', value:'1123000000', cnt:'0'},{seq:'12', idx:12, name:'동작구', label:'동작구', value:'1159000000', cnt:'0'},{seq:'13', idx:13, name:'마포구', label:'마포구', value:'1144000000', cnt:'0'},{seq:'14', idx:14, name:'서대문구', label:'서대문구', value:'1141000000', cnt:'0'},{seq:'15', idx:15, name:'서초구', label:'서초구', value:'1165000000', cnt:'0'},{seq:'16', idx:16, name:'성동구', label:'성동구', value:'1120000000', cnt:'0'},{seq:'17', idx:17, name:'성북구', label:'성북구', value:'1129000000', cnt:'0'},{seq:'18', idx:18, name:'송파구', label:'송파구', value:'1171000000', cnt:'0'},{seq:'19', idx:19, name:'양천구', label:'양천구', value:'1147000000', cnt:'0'},{seq:'20', idx:20, name:'영등포구', label:'영등포구', value:'1156000000', cnt:'0'},{seq:'21', idx:21, name:'용산구', label:'용산구', value:'1117000000', cnt:'0'},{seq:'22', idx:22, name:'은평구', label:'은평구', value:'1138000000', cnt:'0'},{seq:'23', idx:23, name:'종로구', label:'종로구', value:'1111000000', cnt:'0'},{seq:'24', idx:24, name:'중구', label:'중구', value:'1114000000', cnt:'0'},{seq:'25', idx:25, name:'중랑구', label:'중랑구', value:'1126000000', cnt:'0'}]

법정동 이름과 코드

부동산 정보등에는 법정동 코드가 사용된다.

  • 법정동 코드(8자리)
  • 2(시/도) + 2자리(구/군/구) + 2자리(읍/면/동) + 2자리(리/단지)

법정동 전체 코드는 아래 링크

상세한 내용은 다음 링크에 설명을 참고

In [2]:
import pandas as pd

def get_areacode():
    df_areacode = pd.read_csv('https://goo.gl/tM6r3v', sep='\t', dtype={'법정동코드':str, '법정동명':str})
    df_areacode = df_areacode[df_areacode['폐지여부'] == '존재']
    df_areacode = df_areacode[['법정동코드', '법정동명']]
    return df_areacode

def get_province():
    df_areacode = get_areacode()
    df_province = df_areacode[ df_areacode['법정동코드'].str.contains('\d{2}0{8}|36110{6}')]
    return df_province
In [3]:
df_areacode = get_areacode()
df_province = get_province()
In [4]:
df_province
Out[4]:
법정동코드 법정동명
0 1100000000 서울특별시
2615 2600000000 부산광역시
2912 2700000000 대구광역시
3244 2800000000 인천광역시
3579 2900000000 광주광역시
3825 3000000000 대전광역시
4009 3100000000 울산광역시
4241 3611000000 세종특별자치시
4391 4100000000 경기도
10867 4200000000 강원도
13666 4300000000 충청북도
16797 4400000000 충청남도
22050 4500000000 전라북도
25397 4600000000 전라남도
30169 4700000000 경상북도
39299 4800000000 경상남도
45741 5000000000 제주특별자치도
In [5]:
df_areacode.head(10)
Out[5]:
법정동코드 법정동명
0 1100000000 서울특별시
1 1111000000 서울특별시 종로구
2 1111010100 서울특별시 종로구 청운동
3 1111010200 서울특별시 종로구 신교동
4 1111010300 서울특별시 종로구 궁정동
5 1111010400 서울특별시 종로구 효자동
6 1111010500 서울특별시 종로구 창성동
7 1111010600 서울특별시 종로구 통의동
8 1111010700 서울특별시 종로구 적선동
9 1111010800 서울특별시 종로구 통인동
In [6]:
df_areacode[ df_areacode['법정동명'].str.contains('서울특별시 강남구') ]
Out[6]:
법정동코드 법정동명
1003 1168000000 서울특별시 강남구
1037 1168010100 서울특별시 강남구 역삼동
1039 1168010300 서울특별시 강남구 개포동
1040 1168010400 서울특별시 강남구 청담동
1041 1168010500 서울특별시 강남구 삼성동
1042 1168010600 서울특별시 강남구 대치동
1043 1168010700 서울특별시 강남구 신사동
1044 1168010800 서울특별시 강남구 논현동
1045 1168011000 서울특별시 강남구 압구정동
1046 1168011100 서울특별시 강남구 세곡동
1047 1168011200 서울특별시 강남구 자곡동
1048 1168011300 서울특별시 강남구 율현동
1049 1168011400 서울특별시 강남구 일원동
1050 1168011500 서울특별시 강남구 수서동
1053 1168011800 서울특별시 강남구 도곡동

데이터 전처리

http://price.joinsland.joins.com/ajax/price.info.dongapt.asp

  • mcateGroup A1=아파트, A6=오피스텔
  • mcateCode: A1A3A4
  • areaCode=1168010600 # 강남구 대치동
In [7]:
import pandas as pd
url = 'http://price.joinsland.joins.com/ajax/price.info.dongapt.asp?mcateGroup=A1&mcateCode=A1A3A4&areaCode=' + '1168010600'
    
print(url)
dfs = pd.read_html(url)
df = dfs[0]
df.head(10)
http://price.joinsland.joins.com/ajax/price.info.dongapt.asp?mcateGroup=A1&mcateCode=A1A3A4&areaCode=1168010600
Out[7]:
단지명 면적 (㎡) 매매가 (만원) 매물 전세가 (만원) 매물.1 문의처
0 개포우성1차(아파트) 102 167,500 ~ 177,500 0 75,000 ~ 85,000 0.0 NaN
1 148 215,000 ~ 235,000 0 110,000 ~ 125,000 0 NaN NaN
2 181 245,000 ~ 262,500 0 110,000 ~ 130,000 0 NaN NaN
3 214 270,000 ~ 310,000 0 130,000 ~ 150,000 0 NaN NaN
4 개포우성2차(아파트) 101 167,500 ~ 177,500 0 75,000 ~ 85,000 0.0 NaN
5 146 220,000 ~ 242,500 0 110,000 ~ 130,000 0 NaN NaN
6 180 242,500 ~ 270,000 0 125,000 ~ 140,000 0 NaN NaN
7 대치대우아이빌멤버스4차(주상복합) 31 20,000 ~ 21,000 0 15,000 ~ 16,000 0.0 NaN
8 39 22,000 ~ 24,000 1 19,000 ~ 21,000 0 NaN NaN
9 44 28,000 ~ 30,000 0 23,000 ~ 24,000 0 NaN NaN

In [8]:
import pandas as pd
import requests
from bs4 import BeautifulSoup

url = 'http://price.joinsland.joins.com/ajax/price.info.dongapt.asp?mcateGroup=A1&mcateCode=A1A3A4&areaCode=' + '1168010600'

r = requests.get(url)
soup = BeautifulSoup(r.text, 'lxml')
In [9]:
table = soup.find('table')
trs = table.tbody.find_all('tr')
In [10]:
rowspan_val = ''

for tr in trs[:20]: # 처음 20개 확인
    tds = tr.find_all('td')
    if tds[0].has_attr('rowspan'):
        rowspan_val = tds[0].text
        단지명, 면적, 매매가, 매물, 전세가, 전세_매물 = tds[0].text, tds[1].text, tds[2].text, tds[3].text, tds[4].text, tds[5].text
    else:
        단지명, 면적, 매매가, 매물, 전세가, 전세_매물 = rowspan_val, tds[0].text, tds[1].text, tds[2].text, tds[3].text, tds[4].text
    매매가_저 = int(매매가.split(' ~ ')[0].replace(',', ''))
    매매가_고 = int(매매가.split(' ~ ')[1].replace(',', ''))
    전세가_저 = int(전세가.split(' ~ ')[0].replace(',', ''))
    전세가_고 = int(전세가.split(' ~ ')[1].replace(',', ''))
    print(단지명, 면적, 매매가_저, 매매가_고, 매물, 전세가_저, 전세가_고, 전세_매물)
개포우성1차(아파트) 102 167500 177500 0 75000 85000 0
개포우성1차(아파트) 148 215000 235000 0 110000 125000 0
개포우성1차(아파트) 181 245000 262500 0 110000 130000 0
개포우성1차(아파트) 214 270000 310000 0 130000 150000 0
개포우성2차(아파트) 101 167500 177500 0 75000 85000 0
개포우성2차(아파트) 146 220000 242500 0 110000 130000 0
개포우성2차(아파트) 180 242500 270000 0 125000 140000 0
대치대우아이빌멤버스4차(주상복합) 31 20000 21000 0 15000 16000 0
대치대우아이빌멤버스4차(주상복합) 39 22000 24000 1 19000 21000 0
대치대우아이빌멤버스4차(주상복합) 44 28000 30000 0 23000 24000 0
대치대우아이빌멤버스4차(주상복합) 51 32000 34000 0 24000 26000 1
대치대우아이빌멤버스4차(주상복합) 53 33000 34000 0 25000 27000 0
대치대우아이빌멤버스4차(주상복합) 58 37000 39000 0 30000 32000 0
대치대우아이빌멤버스4차(주상복합) 63 44000 46000 1 32000 34000 0
대치대우아이빌멤버스4차(주상복합) 68 44000 46000 0 32000 34000 0
대치대우아이빌명문가7차(주상복합) 41A 28000 30000 0 22500 24500 0
대치대우아이빌명문가7차(주상복합) 43B 28000 30000 0 22500 24500 1
대치대우아이빌명문가7차(주상복합) 45 28500 30500 0 22500 24500 0
대치대우아이빌명문가7차(주상복합) 53 32000 34000 0 25000 27500 0
대치대우아이빌명문가7차(주상복합) 58 39000 42000 0 30500 32500 0
In [11]:
# DataFrame으로 만들기
values_list = []

table = soup.find('table', attrs={'class':'tbl_compare'})
trs = table.tbody.find_all('tr')

rowspan_val = ''

for tr in trs:
    tds = tr.find_all('td')
    if tds[0].has_attr('rowspan'):
        rowspan_val = tds[0].text
        단지명, 면적, 매매가, 매물, 전세가, 전세_매물 = tds[0].text, tds[1].text, tds[2].text, tds[3].text, tds[4].text, tds[5].text
    else:
        단지명, 면적, 매매가, 매물, 전세가, 전세_매물 = rowspan_val, tds[0].text, tds[1].text, tds[2].text, tds[3].text, tds[4].text
    매매가_저 = int(매매가.split(' ~ ')[0].replace(',', ''))
    매매가_고 = int(매매가.split(' ~ ')[1].replace(',', ''))
    전세가_저 = int(전세가.split(' ~ ')[0].replace(',', ''))
    전세가_고 = int(전세가.split(' ~ ')[1].replace(',', ''))
    values_list.append([단지명, 면적, 매매가_저, 매매가_고, 매물, 전세가_저, 전세가_고, 전세_매물])
    
cols = ['단지명', '면적', '매매가_저', '매매가_고', '매물', '전세가_저', '전세가_고', '전세가_매물']   
df = pd.DataFrame(values_list, columns=cols)

print ("건수:", len(df))
df.head(20)
건수: 154
Out[11]:
단지명 면적 매매가_저 매매가_고 매물 전세가_저 전세가_고 전세가_매물
0 개포우성1차(아파트) 102 167500 177500 0 75000 85000 0
1 개포우성1차(아파트) 148 215000 235000 0 110000 125000 0
2 개포우성1차(아파트) 181 245000 262500 0 110000 130000 0
3 개포우성1차(아파트) 214 270000 310000 0 130000 150000 0
4 개포우성2차(아파트) 101 167500 177500 0 75000 85000 0
5 개포우성2차(아파트) 146 220000 242500 0 110000 130000 0
6 개포우성2차(아파트) 180 242500 270000 0 125000 140000 0
7 대치대우아이빌멤버스4차(주상복합) 31 20000 21000 0 15000 16000 0
8 대치대우아이빌멤버스4차(주상복합) 39 22000 24000 1 19000 21000 0
9 대치대우아이빌멤버스4차(주상복합) 44 28000 30000 0 23000 24000 0
10 대치대우아이빌멤버스4차(주상복합) 51 32000 34000 0 24000 26000 1
11 대치대우아이빌멤버스4차(주상복합) 53 33000 34000 0 25000 27000 0
12 대치대우아이빌멤버스4차(주상복합) 58 37000 39000 0 30000 32000 0
13 대치대우아이빌멤버스4차(주상복합) 63 44000 46000 1 32000 34000 0
14 대치대우아이빌멤버스4차(주상복합) 68 44000 46000 0 32000 34000 0
15 대치대우아이빌명문가7차(주상복합) 41A 28000 30000 0 22500 24500 0
16 대치대우아이빌명문가7차(주상복합) 43B 28000 30000 0 22500 24500 1
17 대치대우아이빌명문가7차(주상복합) 45 28500 30500 0 22500 24500 0
18 대치대우아이빌명문가7차(주상복합) 53 32000 34000 0 25000 27500 0
19 대치대우아이빌명문가7차(주상복합) 58 39000 42000 0 30500 32500 0

함수로 만들기

In [12]:
# 조인스부동산 아파트 조회

import pandas as pd
import requests
from bs4 import BeautifulSoup

def get_areacode():
    df_areacode = pd.read_csv('https://goo.gl/tM6r3v', sep='\t', dtype={'법정동코드':str, '법정동명':str})
    df_areacode = df_areacode[df_areacode['폐지여부'] == '존재']
    df_areacode = df_areacode[['법정동코드', '법정동명']]
    return df_areacode

def get_province():
    df_areacode = get_areacode()
    df_province = df_areacode[ df_areacode['법정동코드'].str.contains('\d{2}0{8}|36110{6}')]
    return df_province


def joins_realasset(areacode = ''):
    url = 'http://price.joinsland.joins.com/ajax/price.info.dongapt.asp?mcateGroup=A1&mcateCode=A1A3A4&areaCode=' + areacode

    r = requests.get(url)
    soup = BeautifulSoup(r.text, 'lxml')
    values_list = []

    table = soup.find('table', attrs={'class':'tbl_compare'})
    trs = table.tbody.find_all('tr')
    rowspan_val = ''

    for tr in trs:
        tds = tr.find_all('td')
        if tds[0].has_attr('rowspan'):
            rowspan_val = tds[0].text
            단지명, 면적, 매매가, 매물, 전세가, 전세_매물 = tds[0].text, tds[1].text, tds[2].text, tds[3].text, tds[4].text, tds[5].text
        else:
            단지명, 면적, 매매가, 매물, 전세가, 전세_매물 = rowspan_val, tds[0].text, tds[1].text, tds[2].text, tds[3].text, tds[4].text
        매매가_저 = int(매매가.split(' ~ ')[0].replace(',', ''))
        매매가_고 = int(매매가.split(' ~ ')[1].replace(',', ''))
        전세가_저 = int(전세가.split(' ~ ')[0].replace(',', ''))
        전세가_고 = int(전세가.split(' ~ ')[1].replace(',', ''))
        values_list.append([단지명, 면적, 매매가_저, 매매가_고, 매물, 전세가_저, 전세가_고, 전세_매물])

    cols = ['단지명', '면적', '매매가_저', '매매가_고', '매물', '전세가_저', '전세가_고', '전세가_매물']   
    df = pd.DataFrame(values_list, columns=cols)
    return df

검색과 활용

In [13]:
df_areacode = get_areacode()
In [14]:
area = '신사동'
area_code = df_areacode[ df_areacode['법정동명'].str.contains(area) ]
area_code
Out[14]:
법정동코드 법정동명
582 1138010900 서울특별시 은평구 신사동
1043 1168010700 서울특별시 강남구 신사동
In [15]:
area = '강남구 신사동'
area_code = df_areacode[ df_areacode['법정동명'].str.contains(area) ]['법정동코드'].values[0]
area_code
Out[15]:
'1168010700'
In [16]:
joins_realasset(area_code)
Out[16]:
단지명 면적 매매가_저 매매가_고 매물 전세가_저 전세가_고 전세가_매물
0 대원칸타빌(아파트) 85 61000 68500 0 43500 46500 0
1 대원칸타빌(아파트) 107B 80000 85000 0 55000 60000 0
2 대원칸타빌(아파트) 123 92500 101000 0 60000 65000 0
3 대원칸타빌(아파트) 144 111000 122500 0 75000 85000 0
4 로데오현대(아파트) 73 57500 62500 0 34500 38000 0
5 로데오현대(아파트) 114 78000 85000 0 47500 52500 0
6 로데오현대(아파트) 122 84000 91500 0 51000 58000 0
7 신성(아파트) 72 58500 67500 0 37500 41000 0
8 신성(아파트) 98 80500 88750 0 47500 55000 0
9 압구정하이츠파크(아파트) 234 277500 297500 0 177500 190000 0
10 압구정하이츠파크(아파트) 270 295000 316000 0 180000 200000 3
11 현대맨션(아파트) 147 166000 176000 0 74500 80000 0
12 현대맨션(아파트) 147 166000 176000 0 74500 80000 0
13 현대맨션(아파트) 159 172000 181500 0 79500 88000 0
14 현대맨션(아파트) 162 172000 181500 0 79500 88000 0
15 현대맨션(아파트) 191 176500 184000 0 81500 89500 0
16 현대맨션(아파트) 194 176500 184000 0 81500 89500 0
17 현대맨션(아파트) 195B 176500 184000 0 81500 89500 0
18 현대맨션(아파트) 215A 219500 237000 0 97500 106500 0
19 현대맨션(아파트) 213B 219500 237000 0 97500 106500 0
20 현대맨션(아파트) 215C 219500 237000 0 97500 106500 0
21 현대맨션(아파트) 218 219500 237000 0 97500 106500 0

댓글

Comments powered by Disqus