Categories: 미분류

[심화] streamlit 에 부동산 호가 수집 정보 서비스 하기

[심화] streamlit 에 부동산 호가 수집 정보 서비스 하기 편은 앞서 만든 코드를 이제 streamlit에서 서비스를 하기 위한 강의 입니다. 이 서비스를 통해 각 사용자가 입력하는 값에 따라 정보를 추출해서 보여줄 수 있기 때문에 매우 유용한 정보가 되리라 생각합니다.

[심화] streamlit 에 부동산 호가 수집 정보 서비스 하기

이 편을 보기 전에 전 포스팅을 참고 하시면 이해가 더욱 되시리라 생각을 합니다.

2024.09.15 – [부동산/자동화 프로젝트] – 부동산 매물 정보 수집하기 – 부동산 데이터 네이버 부동산 크롤링 및 가공 #1

2024.09.15 – [부동산/자동화 프로젝트] – 부동산 매물 정보 수집하기 – 부동산 데이터 네이버 부동산 크롤링 및 가공 #2

2024.09.15 – [부동산/자동화 프로젝트] – 부동산 매물 정보 수집하기 – 부동산 데이터 네이버 부동산 크롤링 및 가공 #3

2024.09.17 – [부동산/자동화 프로젝트] – [고급] 부동산 정보 필터 고도화 – 네이버 매물 정리하기

2024.09.18 – [부동산/자동화 프로젝트] – [고급] 부동산 정보 필터 고도화 – 네이버 매물 정리하기 2

이제 우리가 할 것은 스트림릿에 코드를 올려서 코드를 실행하기만 하면 됩니다. 먼저 streamlit 스트림릿에 가입을 합니다. 

https://streamlit.io/

Streamlit • A faster way to build and share data apps

Streamlit is an open-source Python framework for data scientists and AI/ML engineers to deliver interactive data apps – in only a few lines of code.

streamlit.io

가입을 한 후 아래 코드를 넣어 줍니다. 저는 코드를 직접 넣었습니다.

import streamlit as st
import pandas as pd
from io import BytesIO
import requests
import json
from bs4 import BeautifulSoup

# JSON 파일에서 법정동 코드 가져오기
def get_dong_codes_for_city(city_name, sigungu_name=None, json_path='district.json'):
    try:
        with open(json_path, 'r', encoding='utf-8') as file:
            data = json.load(file)
    except FileNotFoundError:
        st.error(f"Error: The file at {json_path} was not found.")
        return None, None

    for si_do in data:
        if si_do['si_do_name'] == city_name:
            if sigungu_name and sigungu_name != '전체':
                for sigungu in si_do['sigungu']:
                    if sigungu['sigungu_name'] == sigungu_name:
                        return [sigungu['sigungu_code']], [
                            {'code': dong['code'], 'name': dong['name']} for dong in sigungu['eup_myeon_dong']
                        ]
            else:
                sigungu_codes = [sigungu['sigungu_code'] for sigungu in si_do['sigungu']]
                dong_codes = [
                    {'code': dong['code'], 'name': dong['name']}
                    for sigungu in si_do['sigungu']
                    for dong in sigungu['eup_myeon_dong']
                ]
                return sigungu_codes, dong_codes
    return None, None

# 아파트 코드 리스트 가져오기
def get_apt_list(dong_code):
    down_url = f'https://new.land.naver.com/api/regions/complexes?cortarNo={dong_code}&realEstateType=APT&order='
    header = {
        "Accept-Encoding": "gzip",
        "Host": "new.land.naver.com",
        "Referer": "https://new.land.naver.com/complexes/102378",
        "Sec-Fetch-Dest": "empty",
        "Sec-Fetch-Mode": "cors",
        "Sec-Fetch-Site": "same-origin",
        "User-Agent": "Mozilla/5.0"
    }

    try:
        r = requests.get(down_url, headers=header)
        r.encoding = "utf-8-sig"
        data = r.json()

        if 'complexList' in data and isinstance(data['complexList'], list):
            df = pd.DataFrame(data['complexList'])
            required_columns = ['complexNo', 'complexName', 'buildYear', 'totalHouseholdCount', 'areaSize', 'price', 'address', 'floor']

            for col in required_columns:
                if col not in df.columns:
                    df[col] = None

            return df[required_columns]
        else:
            st.warning(f"No data found for {dong_code}.")
            return pd.DataFrame(columns=required_columns)

    except Exception as e:
        st.error(f"Error fetching data for {dong_code}: {e}")
        return pd.DataFrame(columns=required_columns)

# 아파트 코드로 상세 정보 가져오기
def get_apt_details(apt_code):
    details_url = f'https://fin.land.naver.com/complexes/{apt_code}?tab=complex-info'
    article_url = f'https://fin.land.naver.com/complexes/{apt_code}?tab=article&tradeTypes=A1'
    
    header = {
        "Accept-Encoding": "gzip",
        "Host": "fin.land.naver.com",
        "Referer": "https://fin.land.naver.com/",
        "Sec-Fetch-Dest": "empty",
        "Sec-Fetch-Mode": "cors",
        "Sec-Fetch-Site": "same-origin",
        "User-Agent": "Mozilla/5.0"
    }
    
    try:
        # 기본 정보 가져오기
        r_details = requests.get(details_url, headers=header)
        r_details.encoding = "utf-8-sig"
        soup_details = BeautifulSoup(r_details.content, 'html.parser')
        
        apt_name_tag = soup_details.find('span', class_='ComplexSummary_name__vX3IN')
        apt_name = apt_name_tag.text.strip() if apt_name_tag else 'Unknown'
        detail_dict = {'complexNo': apt_code, 'complexName': apt_name}
        
        detail_items = soup_details.find_all('li', class_='DataList_item__T1hMR')
        for item in detail_items:
            term = item.find('div', class_='DataList_term__Tks7l').text.strip()
            definition = item.find('div', class_='DataList_definition__d9KY1').text.strip()
            if term in ['공급면적', '전용면적', '해당면적 세대수', '현관구조', '방/욕실', '위치', '사용승인일', '세대수', '난방', '주차', '전기차 충전시설', '용적률/건폐율', '관리사무소 전화', '건설사']:
                detail_dict[term] = definition

        # 매물 정보 가져오기
        r_article = requests.get(article_url, headers=header)
        r_article.encoding = "utf-8-sig"
        soup_article = BeautifulSoup(r_article.content, 'html.parser')
        
        listings = []
        for item in soup_article.find_all('li', class_='ComplexArticleItem_item__L5o7k'):
            listing = {}
            name_tag = item.find('span', class_='ComplexArticleItem_name__4h3AA')
            listing['매물명'] = name_tag.text.strip() if name_tag else 'Unknown'
            price_tag = item.find('span', class_='ComplexArticleItem_price__DFeIb')
            listing['매매가'] = price_tag.text.strip() if price_tag else 'Unknown'
            
            summary_items = item.find_all('li', class_='ComplexArticleItem_item-summary__oHSwl')
            if len(summary_items) >= 4:
                listing['면적'] = summary_items[1].text.strip() if len(summary_items) > 1 else 'Unknown'
                listing['층수'] = summary_items[2].text.strip() if len(summary_items) > 2 else 'Unknown'
                listing['방향'] = summary_items[3].text.strip() if len(summary_items) > 3 else 'Unknown'
            
            image_tag = item.find('img')
            listing['이미지'] = image_tag['src'] if image_tag else 'No image'
            comment_tag = item.find('p', class_='ComplexArticleItem_comment__zN_dK')
            listing['코멘트'] = comment_tag.text.strip() if comment_tag else 'No comment'
            
            combined_listing = {**detail_dict, **listing}
            listings.append(combined_listing)
        
        return listings
    
    except Exception as e:
        st.error(f"Error fetching details for {apt_code}: {e}")
        return []

# 아파트 정보를 수집하는 함수
def collect_apt_info_for_city(city_name, sigungu_name, dong_name=None, json_path='district.json'):
    sigungu_codes, dong_list = get_dong_codes_for_city(city_name, sigungu_name, json_path)

    if dong_list is None:
        st.error(f"Error: {city_name} not found in JSON.")
        return None

    all_apt_data = []
    dong_code_name_map = {dong['code']: dong['name'] for dong in dong_list}
    
    # 수집 중 표시를 위한 placeholder
    placeholder = st.empty()

    if dong_name and dong_name != '전체':
        dong_code_name_map = {k: v for k, v in dong_code_name_map.items() if v == dong_name}

    for dong_code, dong_name in dong_code_name_map.items():
        placeholder.write(f"{dong_name} ({dong_code}) - 수집중입니다.")
        apt_codes = get_apt_list(dong_code)

        if not apt_codes.empty:
            for _, apt_info in apt_codes.iterrows():
                apt_code = apt_info['complexNo']
                apt_name = apt_info['complexName']
                placeholder.write(f"{apt_name} ({apt_code}) - 수집중입니다.")
                listings = get_apt_details(apt_code)
                
                if listings:
                    for listing in listings:
                        listing['dong_code'] = dong_code
                        listing['dong_name'] = dong_name
                        all_apt_data.append(listing)
        else:
            st.warning(f"No apartment codes found for {dong_code}")

    # 수집이 완료된 후, 수집 중 메시지를 지우기
    placeholder.empty()

    if all_apt_data:
        final_df = pd.DataFrame(all_apt_data)
        final_df['si_do_name'] = city_name
        final_df['sigungu_name'] = sigungu_name
        final_df['dong_name'] = dong_name if dong_name else '전체'
        
        # 데이터프레임 결과 출력
        st.write("아파트 정보 수집 완료:")
        st.dataframe(final_df)

        # 엑셀 파일로 저장
        output = BytesIO()
        with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
            final_df.to_excel(writer, index=False)
        output.seek(0)

        # 엑셀 파일 다운로드 버튼
        st.download_button(
            label="Download Excel",
            data=output,
            file_name=f"{city_name}_{sigungu_name}_apartments.xlsx",
            mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        )

        # CSV 파일 다운로드 버튼
        csv = final_df.to_csv(index=False).encode('utf-8')
        st.download_button(
            label="Download CSV",
            data=csv,
            file_name=f"{city_name}_{sigungu_name}_apartments.csv",
            mime="text/csv"
        )
    else:
        st.write("No data to save.")

# Streamlit 앱 실행
st.title("아파트 정보 수집기")

# 사용자 입력 받기
city_name = st.text_input("시/도 이름 입력", "서울특별시")
sigungu_name = st.text_input("구/군/구 이름 입력", "강남구")
dong_name = st.text_input("동 이름 입력 (선택사항)", "전체")

if st.button("정보 수집 시작"):
    collect_apt_info_for_city(city_name, sigungu_name, dong_name)

코드를 넣고 스트림릿에서 실행을 해봅니다.

[심화] streamlit 에 부동산 호가 수집 정보 서비스 하기

매우 정보가 잘 나오고 있네요. 이제 아파트 정보를 모두 수집하면 결과가 어떻게 나올까요?

수집이 완료되면 이렇게 표로도 보여주고 엑셀 또는 CSV 파일 형태로 다운도 받을 수 있도록 코드가 잘 완료 되었습니다. 

urjent

Share
Published by
urjent

Recent Posts

미국 이란 2 차 협상 이번 주말 파키스탄서 개최, 합의문 서명 가능성과 시장 영향력 분석

미국과 이란의 2 차 종전 협상이 이르면 이번 주말 파키스탄 이슬라마바드에서 열릴 것이라는 구체적인 관측이…

2시간 ago

미국 이란 2 차 협상 이번 주말 파키스탄서 개최, 합의문 서명 가능성과 시장 영향력 분석

미국과 이란의 2 차 종전 협상이 이르면 이번 주말 파키스탄 이슬라마바드에서 열릴 것이라는 구체적인 관측이…

2시간 ago

2026 년 교육급여바우처 신청 방법과 지원 금액 총정리 (4 월 최신 기준)

2026 년 교육급여바우처는 중위소득 50% 이하 가구의 자녀에게 연간 약 128 만 원의 교육비를 직접…

5시간 ago

4 월 슈퍼태풍 신라쿠 강타와 2026 년 여름철 한반도 기상 재해 위험도 분석

4 월에 발생한 초강력 슈퍼태풍으로 인해 괌과 사이판 지역이 초토화되었으며 한국인 여행객 수천 명이 현지에…

15시간 ago

정선경 야쿠자 루머의 진실과 일본 근황, 평범한 회사원 남편의 실체

정선경 야쿠자 루머는 남편의 신상이 공개되지 않아 발생한 근거 없는 소문에 불과하며, 실제 남편은 평범한…

2일 ago

정선경 야쿠자 루머의 진실과 일본 근황, 평범한 회사원 남편의 실체

정선경 야쿠자 루머는 남편의 신상이 공개되지 않아 발생한 근거 없는 소문에 불과하며, 실제 남편은 평범한…

2일 ago