[파이썬 크롤링/부동산 데이터] scrapy를 이용하여 재귀적으로 부동산 공공 데이터 연속적으로 가져오기 및 excel 저장

| 들어가기 전에


GIT 저장소


지금 포스팅은 국토교통부에서 제공하는 부동산 공공데이터 API를 사용합니다. 아래 포스팅을 보시고 먼저 부동산 공공데이터 API를 신청해주시길 바래요!


[유용한 정보들] - 국토교통부 공공데이터 부동산 실거래가 API 신청 방법



이전 포스팅


[파이썬/파이썬 웹 크롤링 - 부동산 공공데이터] - [파이썬 크롤링/부동산 데이터] 스크래피(scrapy) startproject로 초기 프로젝트 구성하기

[파이썬/파이썬 웹 크롤링 - 부동산 공공데이터] - [파이썬 크롤링/부동산 데이터] scrapy를 이용한 부동산 공공 데이터 간단하게 받아오기

[파이썬/파이썬 웹 크롤링 - 부동산 공공데이터] - [파이썬 크롤링/부동산 데이터] scrapy를 이용한 부동산 공공 데이터 파싱 및 추출하기

[파이썬/파이썬 웹 크롤링 - 부동산 공공데이터] - [파이썬 크롤링/부동산 데이터] scrapy를 이용한 부동산 공공 데이터 저장하기(csv/excel)


포스팅에 있는 내용을 따라하기 위해서는 openpyxl 패키지를 설치해야합니다. openpyxl은 DataFrame 타입의 파이썬 객체를  excel 파일로 저장할 때 쓰이는 engine입니다. 전에 쓰였던 xlsxwriter를 쓰지 않는 이유는 아래에 설명하겠습니다.


pip install openpyxl


 재귀적으로 이용해서 공공데이터를 연속적으로 가져오기



프로젝트 구조

| scrapy.cfg
\---invest_crawler
| consts.py
| settings.py
| __init__.py
|
+---items
| | apt_trade.py
| | __init__.py
|
+---spiders
| | apt_trade_spiders.py
| | __init__.py


소스 코드


consts.py

# 샘플 더미 데이터 입니다. 어떻게 세팅하는 지 보여드리기 위해 넣은 데이터이기 때문에 그대로 사용하시면 에러가 납니다.
APT_DETAIL_ENDPOINT = "http://openapi.molit.go.kr:8081/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTrade?serviceKey=asdsdfsdfWiZGAJkCsr3wM0YkDO%2BssYpNXZ%2FEWZfuIW5k%2FcHFtD5k1zcCVasdfEtBQID5rIcjXsg%3D%3D&"


apt_trade.py

import scrapy


class AptTradeScrapy(scrapy.Item):

apt_name = scrapy.Field()
address_1 = scrapy.Field()
address_2 = scrapy.Field()
address_3 = scrapy.Field()
address_4 = scrapy.Field()
address = scrapy.Field()
age = scrapy.Field()
level = scrapy.Field()
available_space = scrapy.Field()
trade_date = scrapy.Field()
trade_amount = scrapy.Field()

def to_dict(self):
return {
'아파트': self['apt_name'],
'시/도': self['address_1'],
'군/구': self['address_2'],
'동/읍/면': self['address_3'],
'번지': self['address_4'],
'전체주소': self['address'],
'연식': self['age'],
'층': self['level'],
'면적': self['available_space'],
'거래일자': self['trade_date'],
'매매가격': self['trade_amount']
}


apt_trade_spiders.py

import datetime as dt
from urllib.parse import urlencode

import scrapy
from dateutil.relativedelta import relativedelta
from openpyxl import Workbook, load_workbook
from scrapy import Selector

import invest_crawler.consts as CONST
from invest_crawler.items.apt_trade import AptTradeScrapy

import pandas as pd


class TradeSpider(scrapy.spiders.XMLFeedSpider):
name = 'trade'

def start_requests(self):
date = dt.datetime(2006, 1, 1)
Workbook().save('APT_TRADE.xlsx')
yield from self.get_realestate_trade_data(date)

def get_realestate_trade_data(self, date):
page_num = 1
urls = [
CONST.APT_DETAIL_ENDPOINT
]
params = {
"pageNo": str(page_num),
"numOfRows": "999",
"LAWD_CD": "44133",
"DEAL_YMD": date.strftime("%Y%m"),
}
for url in urls:
url += urlencode(params)
yield scrapy.Request(url=url, callback=self.parse, cb_kwargs=dict(page_num=page_num, date=date))

def parse(self, response, page_num, date):
selector = Selector(response, type='xml')
items = selector.xpath('//%s' % self.itertag) # self.intertag는 기본적으로 item으로 되어 있음
if not items:
return

"""
To remove 'Sheet' worksheet created automatically
"""
# if date.strftime("%Y%m") == "200604":
# wb = load_workbook('APT_TRADE.xlsx')
# wb.remove(wb['Sheet'])
# wb.save('APT_TRADE.xlsx')
# return

apt_trades = [self.parse_item(item) for item in items]
apt_dataframe = pd.DataFrame.from_records([apt_trade.to_dict() for apt_trade in apt_trades])

writer = pd.ExcelWriter('APT_TRADE.xlsx', engine='openpyxl', mode='a')
apt_dataframe.to_excel(writer, sheet_name='천안-' + date.strftime("%Y%m"), index=False)
writer.save()

date += relativedelta(months=1)
yield from self.get_realestate_trade_data(date)

def parse_item(self, item):
state = "천안시"
district = "서북구"

try:
apt_trade_data = AptTradeScrapy(
apt_name=item.xpath("./아파트/text()").get(),
address_1=state,
address_2=district,
address_3=item.xpath("./법정동/text()").get().strip(),
address_4=item.xpath("./지번/text()").get(),
address=state + " " + district + " " + item.xpath("./법정동/text()").get().strip() + " " +
item.xpath("./지번/text()").get(),
age=item.xpath("./건축년도/text()").get(),
level=item.xpath("./층/text()").get(),
available_space=item.xpath("./전용면적/text()").get(),
trade_date=item.xpath("./년/text()").get() + "/" +
item.xpath("./월/text()").get() + "/" +
item.xpath("./일/text()").get(),
trade_amount=item.xpath("./거래금액/text()").get().strip().replace(',', ''),
)

except Exception as e:
print(e)
self.logger.error(item)
self.logger.error(item.xpath("./아파트/text()").get())

return apt_trade_data
  • 아래 scrapy crawl가 시작되는 start_requests에서 APT_TRADE.xlsx 파일을 생성하는 코드와 2006/1/1 을 나타내는 date를 정의했습니다. 부동산 공공데이터는 2006년 1월 데이터부터 제공하기 때문에 그렇습니다. 
  • yield from 키워드의 역할은 간단합니다. yield 로 데이터를 산출하는 메서드의 값을 반환받아 다시 반환하는 역할을 할 뿐입니다. 이 yield from 키워드로 get_realestate_trade_data 메서드의 반환값을 반환합니다.
        def start_requests(self):
    date = dt.datetime(2006, 1, 1)
    Workbook().save('APT_TRADE.xlsx')
    yield from self.get_realestate_trade_data(date)
  • get_realestate_trade_data에서 눈여겨 봐야할 것은 scrapy.Request 부분입니다. 여기서 callback과 cb_kwargs라는 인수가 들어가 있는 것을 알 수 있습니다. callback은 Request 요청을 통해 온 응답을 처리하는 콜백 메서드를 정의하는 인수입니다. cb_kwargs는 callback 메서드에 들어가는 인수와 그에 해당하는 인수값을 정의하는 데 쓰입니다. 
  • cb_kwargs를 통해 parse 메서드에 page_num과 date 값을 전달할 수 있습니다.
        def get_realestate_trade_data(self, date):
    page_num = 1
    urls = [
    CONST.APT_DETAIL_ENDPOINT
    ]
    params = {
    "pageNo": str(page_num),
    "numOfRows": "999",
    "LAWD_CD": "44133",
    "DEAL_YMD": date.strftime("%Y%m"),
    }
    for url in urls:
    url += urlencode(params)
    yield scrapy.Request(url=url, callback=self.parse, cb_kwargs=dict(page_num=page_num, date=date))
  • parse 메서드에서 pandas의 ExcelWriter 객체를 통해 DataFrame 객체를 excel 형태로 저장하는 것을 볼 수 있습니다. 중요한 것은 excel에 다가 매번 save() 메서드로 저장하기 때문에 IO 횟수가 많아져서 속도가 느려질 것입니다. 하지만 save 메서드를 사용하지 않으면 2006~2020년도까지의 방대한 데이터가 메모리에 올라가게 되어 크롤러가 제대로 동작하지 않게 됩니다. 따라서 속도가 상당히 느려지더라도 save 메서드를 통해 요청때마다 데이터를 저장합니다. ( memory가 엄청나신 분들은 save 메서드를 크롤링이 끝나는 마지막에 추가하여 속도를 높여보세요 ㅎㅎ )
  • relativedelta 메서드를 통해 date에 한 달씩 추가하여 정보를 요청하는 것을 볼 수 있습니다. 만약 200601 데이터를 받았다면 get_realestate_trade_data 메서드를 통해 그 다음 200602로 부동산 매매 데이터를 요청할 것입니다. 이러한 재귀적인 해결방식으로 코드를 길게 짜지 않고 깔끔하게 데이터를 받아올 수 있습니다. 202004 이후로 데이터를 받게 되면 if not items 구문에 의해 받아오는 item이 없어지게 되므로 이 크롤러는 종료될 것입니다.
  • ExcelWriter에서 쓰는 engine을 openpyxl을 쓰는 이유는 xlsxwriter 엔진이 2배 정도 빠르지만 아직 ExcelWriter에서 append mode ( mode='a' ) 를 지원하지 않기 때문입니다. 물론 xlsxwriter로 다른 처리를 하면 같은 excel파일에 여러 sheet를 저장할 수 있지만 코드량이 많아지고 유지보수 하기 어려워지는 단점이 있습니다.
        def parse(self, response, page_num, date):
    selector = Selector(response, type='xml')
    items = selector.xpath('//%s' % self.itertag) # self.intertag는 기본적으로 item으로 되어 있음
    if not items:
    return

    """
    To remove 'Sheet' worksheet created automatically
    """
    # if date.strftime("%Y%m") == "200604":
    # wb = load_workbook('APT_TRADE.xlsx')
    # wb.remove(wb['Sheet'])
    # wb.save('APT_TRADE.xlsx')
    # return

    apt_trades = [self.parse_item(item) for item in items]
    apt_dataframe = pd.DataFrame.from_records([apt_trade.to_dict() for apt_trade in apt_trades])

    writer = pd.ExcelWriter('APT_TRADE.xlsx', engine='openpyxl', mode='a')
    apt_dataframe.to_excel(writer, sheet_name='천안-' + date.strftime("%Y%m"), index=False)
    writer.save()

    date += relativedelta(months=1)
    yield from self.get_realestate_trade_data(date)



결과 화면





위의 코드를 실행할 때의 크나큰 단점은 메모리 용량 때문에 데이터를 받아올 때마다 Excel에 저장하여 속도가 매우 느려진다는 데에 있습니다( 제 local 컴퓨터 기준 40~50분). 또한 200601 부터 202004 년도까지의 데이터를 받아와 엑셀시트에 저장하면 이 많은 데이터를 관리하고 분석하기가 매우 어려워집니다. 


이 부분을 해소하기 위해서 쓰이는 것이 바로 우리가 흔히 알고 있는 Database System입니다. 전문용어로 RDBMS( Relational Database Management System ) 이라 하죠. Excel이 가지고 있는 위 문제를 Database System은 수월하게 해결할 수 있습니다.


다음 포스팅에는 scrapy를 통해 공공데이터를 Excel이 아닌 DB에 저장하는 지 알아보도록 하겠습니다.

이 글을 공유하기

댓글(0)

Designed by JB FACTORY