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

2021. 3. 28. 02:55 Python/파이썬 웹 크롤링

| 들어가기 전에

 

GIT 저장소

 

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

 

[기타 정보/ETC] - 국토교통부 공공데이터 부동산 실거래가 API 신청 방법

 

 

이전 포스팅

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

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

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

[Python/파이썬 웹 크롤링] - [파이썬 크롤링/부동산 데이터] 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에 저장하는 지 알아보도록 하겠습니다.



출처: https://engkimbs.tistory.com/966?category=807933 [새로비]