혼자 고민해보기_ 개발

Claude 활용 네이버 부동산 매물 수집하기

nuri-story 2025. 5. 24. 13:02

참고 유튜브

https://www.youtube.com/watch?v=V4tjbUQ46Uw&t=185s


코드

CSV 문서로 변환해서 테스트

import requests
import json
import csv
from datetime import datetime
import time

class NaverRealEstateCrawler:
    def __init__(self):
        self.cookies = {
            'NNB': 'REPOEE3TYVIWK',
            'ASID': '738bd02a0000018bcaf202af0000004f',
            '_ga_451MFZ9CFM': 'GS1.1.1722665147.2.1.1722665162.0.0.0',
            '_ga_Q7G1QTKPGB': 'GS1.1.1722665276.1.0.1722665276.0.0.0',
            'naverfinancial_CID': '921ffa2916a840f19cdaf450ab333dd2',
            'NID_AUT': 'maNJRVfRZfYtBA6kCCElhLtdnseKDPsX63iU2vwuH3Hf+mqhRbIxNRR7/sBrv8c7',
            'NID_JKL': 'K7Qu+Lvg6hu9Hprc0Z13eLUbhnpxd11nSlazRlmwP1w=',
            '_ga_RCM29786SD': 'GS1.1.1727568316.3.1.1727568333.0.0.0',
            'NFS': '2',
            '_fbp': 'fb.1.1733047540332.474845353899071337',
            'nstore_session': '6s+yUW4sTUs20wfs/tNrBAzc',
            '_ga_J5CZVNJNQP': 'GS1.1.1734828968.1.0.1734828972.0.0.0',
            'NV_WETR_LOCATION_RGN_M': '"MDk1NDUxMDM="',
            'NV_WETR_LAST_ACCESS_RGN_M': '"MDk1NDUxMDM="',
            '_fwb': '107itr85p91OlxDKh8vV1vr.1741440649696',
            '_ga_EFBDNNF91G': 'GS1.1.1743241835.11.0.1743241840.0.0.0',
            'ba.uuid': '5412b756-6be2-481b-bca3-dc9453f8049c',
            '_ga_EQP4JZZ5VJ': 'GS1.1.1743255876.2.0.1743255876.0.0.0',
            'BNB_FINANCE_HOME_TOOLTIP_STOCK': 'true',
            'BNB_FINANCE_HOME_TOOLTIP_ESTATE': 'true',
            '_ga': 'GA1.2.1205107697.1700030410',
            '_ga_6Z6DP60WFK': 'GS1.2.1745159159.1.0.1745159159.60.0.0',
            'nstore_pagesession': 'js12RdqWVOfvplsMV6G-135711',
            'ab.storage.userId.7d7bb94a-f465-48e5-bec1-35db97daf128': 'g%3Ak0os%7Ce%3Aundefined%7Cc%3A1745742046639%7Cl%3A1745742046640',
            'ab.storage.deviceId.7d7bb94a-f465-48e5-bec1-35db97daf128': 'g%3Ab7f75e9e-32ee-6352-deb3-9ab73f4459a6%7Ce%3Aundefined%7Cc%3A1745742046641%7Cl%3A1745742046641',
            'ab.storage.sessionId.7d7bb94a-f465-48e5-bec1-35db97daf128': 'g%3Aef42f330-0fde-f838-d4a0-a8682c56cfee%7Ce%3A1745743846735%7Cc%3A1745742046640%7Cl%3A1745742046735',
            'NACT': '1',
            'page_uid': 'jvZEowqVOsossPowS9dssssssZV-019315',
            'NAC': 'AJnGBQAwjf9VA',
            'SRT30': '1748053745',
            'NID_SES': 'AAABtn4ilEz+qs+jVZ+/Xd+pKvVD2uFym66rsgUzSAN6GJHJ9+GkR5Kdvb6HAjq0AURhTLVYY2A3ElxUexyElSgmys4lano8tdeviKjlj5qxMAcpY4niXCx3bX9yz2TgZy4Q72dC5qzViJB46S2Refpn0iZYnH89Mj1EGmnBuV39YkW1cflNtuJOYIl5ZYYrAPR+FUPQOOhyScucVxq1DpevdJYFFUAMLvituflO7R9SDvCCh+8bukILobBGVNe3WGH9UIdqE5NRW4OF3K5ZvlNe/8PaE6I+twnTcrnoL4gjdAY4P7n7Mb9dqjDNqt9jTu+r+6FXY02pd5SV5y/dF/4DoYeI0wTiJL8JQTk3TAG03pYmSKcSuwCLbdyLnGJcK1ykKvef2kJAyTwLNFOqwMG46fx7hC1tTwkwsIv5GWbjP1EXeWxisZ35cgZp0LzO3NoBe/r3UmrRqToUzIRlHSHz5d9+aK15z8ZlJl38MuktdZDqRev7HCtcUvA0uJE04kyF2h03LLdtkmiAtAEMvkvIlXKJ9naMJgXiHnSxkkjEcHeStWXJpZxYfiDDxkf3/buzFwaCs2eFUsUeMn4HRp9sZq8=',
            'SRT5': '1748055206',
            'nhn.realestate.article.rlet_type_cd': 'A01',
            'nhn.realestate.article.trade_type_cd': '""',
            'nhn.realestate.article.ipaddress_city': '1100000000',
            'landHomeFlashUseYn': 'Y',
            'realestate.beta.lastclick.cortar': '4100000000',
            'REALESTATE': 'Sat%20May%2024%202025%2011%3A53%3A50%20GMT%2B0900%20(Korean%20Standard%20Time)',
            'BUC': 'jX8TzKX0jkTs2LTom2dHhzpzdbqMKgShauc7WvKutnw=',
        }

        self.headers = {
            'accept': '*/*',
            'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
            'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlJFQUxFU1RBVEUiLCJpYXQiOjE3NDgwNTUyMzAsImV4cCI6MTc0ODA2NjAzMH0.mEmiTcb0P5uTI6spw5k6_EQQSO3sL3yRQPTbD72TYpE',
            'priority': 'u=1, i',
            'referer': 'https://new.land.naver.com/search?ms=37.7219314,127.0423986,19&a=APT:PRE:ABYG:JGC&e=RETAIL&articleNo=2527822843',
            'sec-ch-ua': '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"macOS"',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-origin',
            'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
        }

    def crawl_complex(self, complex_no, start_page=1, end_page=10):
        """특정 아파트 단지의 매물 정보를 크롤링합니다."""
        all_articles = []

        for page in range(start_page, end_page + 1):
            print(f"페이지 {page}/{end_page} 크롤링 중...")

            url = f'https://new.land.naver.com/api/articles/complex/{complex_no}?realEstateType=APT%3APRE%3AABYG%3AJGC&tradeType=&tag=%3A%3A%3A%3A%3A%3A%3A%3A&rentPriceMin=0&rentPriceMax=900000000&priceMin=0&priceMax=900000000&areaMin=0&areaMax=900000000&oldBuildYears&recentlyBuildYears&minHouseHoldCount&maxHouseHoldCount&showArticle=false&sameAddressGroup=false&minMaintenanceCost&maxMaintenanceCost&priceType=RETAIL&directions=&page={page}&complexNo={complex_no}&buildingNos=&areaNos=&type=list&order=rank'

            try:
                response = requests.get(url, cookies=self.cookies, headers=self.headers)
                response.raise_for_status()

                data = response.json()
                articles = data.get('articleList', [])

                if not articles:
                    print(f"페이지 {page}에 더 이상 매물이 없습니다.")
                    break

                all_articles.extend(articles)

                # 더 이상 데이터가 없으면 중단
                if not data.get('isMoreData', False):
                    print("모든 매물을 가져왔습니다.")
                    break

                # API 부하 방지를 위한 딜레이 (페이지가 많으므로 시간 증가)
                time.sleep(1.5)

            except Exception as e:
                print(f"페이지 {page} 크롤링 중 오류 발생: {e}")
                # 오류 발생해도 계속 진행
                continue

        print(f"총 {len(all_articles)}개의 매물을 수집했습니다.")
        return all_articles

    def parse_price(self, price_str):
        """가격 문자열을 원 단위로 변환합니다."""
        try:
            price_str = price_str.replace(',', '').strip()
            price_won = 0

            # 억 단위 처리
            if '억' in price_str:
                parts = price_str.split('억')
                eok = int(parts[0].strip())
                price_won += eok * 100000000

                # 만 단위가 있는 경우
                if len(parts) > 1 and parts[1].strip():
                    man_str = parts[1].strip()
                    if '만' in man_str:
                        man = int(man_str.replace('만', '').strip())
                        price_won += man * 10000
            # 만 단위만 있는 경우
            elif '만' in price_str:
                man = int(price_str.replace('만', '').strip())
                price_won += man * 10000

            return price_won
        except:
            return 0

    def parse_articles(self, articles):
        """크롤링한 매물 정보를 파싱하여 리스트로 변환합니다."""
        parsed_data = []

        for article in articles:
            # 가격 정보 처리
            price_str = article.get('dealOrWarrantPrc', '')
            price_won = self.parse_price(price_str)

            # 태그 리스트를 문자열로 변환
            tags = ', '.join(article.get('tagList', []))

            parsed_article = {
                '매물번호': article.get('articleNo'),
                '아파트명': article.get('articleName'),
                '거래유형': article.get('tradeTypeName'),
                '가격(원)': price_won,
                '가격(표시)': article.get('dealOrWarrantPrc'),
                '면적(㎡)': article.get('area1'),
                '전용면적(㎡)': article.get('area2'),
                '층수': article.get('floorInfo'),
                '향': article.get('direction'),
                '동': article.get('buildingName'),
                '확인일자': article.get('articleConfirmYmd'),
                '특징': article.get('articleFeatureDesc'),
                '태그': tags,
                '중개사': article.get('realtorName'),
                '중개업체': article.get('cpName'),
                '동일주소매물수': article.get('sameAddrCnt'),
                '최고가': article.get('sameAddrMaxPrc'),
                '최저가': article.get('sameAddrMinPrc'),
                '크롤링시간': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            }

            parsed_data.append(parsed_article)

        return parsed_data

    def save_to_csv(self, data, filename='naver_real_estate_data.csv'):
        """파싱된 데이터를 CSV 파일로 저장합니다."""
        if not data:
            print("저장할 데이터가 없습니다.")
            return

        # 첫 번째 딕셔너리의 키를 헤더로 사용
        headers = list(data[0].keys())

        # CSV 파일 작성 (UTF-8 BOM으로 한글 깨짐 방지)
        with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=headers)
            writer.writeheader()
            writer.writerows(data)

        print(f"{filename} 파일로 저장되었습니다. (총 {len(data)}개 매물)")

# 사용 예시
if __name__ == "__main__":
    # 크롤러 인스턴스 생성
    crawler = NaverRealEstateCrawler()

    # 호원한승미메이드 아파트 (complex_no: 19494) 크롤링
    complex_no = 19494
    print(f"아파트 단지 번호 {complex_no} 크롤링 시작...")
    print("1페이지부터 10페이지까지 크롤링합니다.")

    # 매물 정보 크롤링 (1페이지부터 10페이지까지)
    articles = crawler.crawl_complex(complex_no, start_page=1, end_page=10)

    if articles:
        # 데이터 파싱
        parsed_data = crawler.parse_articles(articles)

        # CSV 파일로 저장
        crawler.save_to_csv(parsed_data, f'howon_hanseung_mimade_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv')

        # 결과 요약 출력
        print("\n=== 크롤링 결과 요약 ===")
        print(f"총 매물 수: {len(parsed_data)}")

        # 가격 통계 계산 (pandas 없이)
        prices = [item['가격(원)'] for item in parsed_data if item['가격(원)'] > 0]
        if prices:
            avg_price = sum(prices) / len(prices)
            max_price = max(prices)
            min_price = min(prices)
            print(f"평균 가격: {avg_price:,.0f}원")
            print(f"최고 가격: {max_price:,.0f}원")
            print(f"최저 가격: {min_price:,.0f}원")

        # 면적별 매물 수 계산
        area_count = {}
        for item in parsed_data:
            area = item['면적(㎡)']
            area_count[area] = area_count.get(area, 0) + 1

        print("\n면적별 매물 수:")
        for area in sorted(area_count.keys()):
            print(f"{area}㎡: {area_count[area]}개")
    else:
        print("크롤링된 매물이 없습니다.")

 

 

네이버부동산.py

import streamlit as st
import requests
import json
import pandas as pd
from datetime import datetime
import time
import plotly.express as px
import plotly.graph_objects as go

# 페이지 설정
st.set_page_config(
    page_title="네이버 부동산 매물 조회",
    page_icon="🏠",
    layout="wide"
)

class NaverRealEstateAPI:
    def __init__(self):
        self.cookies = {
            'NNB': 'REPOEE3TYVIWK',
            'ASID': '738bd02a0000018bcaf202af0000004f',
            '_ga_451MFZ9CFM': 'GS1.1.1722665147.2.1.1722665162.0.0.0',
            '_ga_Q7G1QTKPGB': 'GS1.1.1722665276.1.0.1722665276.0.0.0',
            'naverfinancial_CID': '921ffa2916a840f19cdaf450ab333dd2',
            'NID_AUT': 'maNJRVfRZfYtBA6kCCElhLtdnseKDPsX63iU2vwuH3Hf+mqhRbIxNRR7/sBrv8c7',
            'NID_JKL': 'K7Qu+Lvg6hu9Hprc0Z13eLUbhnpxd11nSlazRlmwP1w=',
            '_ga_RCM29786SD': 'GS1.1.1727568316.3.1.1727568333.0.0.0',
            'NFS': '2',
            '_fbp': 'fb.1.1733047540332.474845353899071337',
            'nstore_session': '6s+yUW4sTUs20wfs/tNrBAzc',
            '_ga_J5CZVNJNQP': 'GS1.1.1734828968.1.0.1734828972.0.0.0',
            'NV_WETR_LOCATION_RGN_M': '"MDk1NDUxMDM="',
            'NV_WETR_LAST_ACCESS_RGN_M': '"MDk1NDUxMDM="',
            '_fwb': '107itr85p91OlxDKh8vV1vr.1741440649696',
            '_ga_EFBDNNF91G': 'GS1.1.1743241835.11.0.1743241840.0.0.0',
            'ba.uuid': '5412b756-6be2-481b-bca3-dc9453f8049c',
            '_ga_EQP4JZZ5VJ': 'GS1.1.1743255876.2.0.1743255876.0.0.0',
            'BNB_FINANCE_HOME_TOOLTIP_STOCK': 'true',
            'BNB_FINANCE_HOME_TOOLTIP_ESTATE': 'true',
            '_ga': 'GA1.2.1205107697.1700030410',
            '_ga_6Z6DP60WFK': 'GS1.2.1745159159.1.0.1745159159.60.0.0',
            'nstore_pagesession': 'js12RdqWVOfvplsMV6G-135711',
            'ab.storage.userId.7d7bb94a-f465-48e5-bec1-35db97daf128': 'g%3Ak0os%7Ce%3Aundefined%7Cc%3A1745742046639%7Cl%3A1745742046640',
            'ab.storage.deviceId.7d7bb94a-f465-48e5-bec1-35db97daf128': 'g%3Ab7f75e9e-32ee-6352-deb3-9ab73f4459a6%7Ce%3Aundefined%7Cc%3A1745742046641%7Cl%3A1745742046641',
            'ab.storage.sessionId.7d7bb94a-f465-48e5-bec1-35db97daf128': 'g%3Aef42f330-0fde-f838-d4a0-a8682c56cfee%7Ce%3A1745743846735%7Cc%3A1745742046640%7Cl%3A1745742046735',
            'NACT': '1',
            'page_uid': 'jvZEowqVOsossPowS9dssssssZV-019315',
            'NAC': 'AJnGBQAwjf9VA',
            'SRT30': '1748053745',
            'NID_SES': 'AAABtn4ilEz+qs+jVZ+/Xd+pKvVD2uFym66rsgUzSAN6GJHJ9+GkR5Kdvb6HAjq0AURhTLVYY2A3ElxUexyElSgmys4lano8tdeviKjlj5qxMAcpY4niXCx3bX9yz2TgZy4Q72dC5qzViJB46S2Refpn0iZYnH89Mj1EGmnBuV39YkW1cflNtuJOYIl5ZYYrAPR+FUPQOOhyScucVxq1DpevdJYFFUAMLvituflO7R9SDvCCh+8bukILobBGVNe3WGH9UIdqE5NRW4OF3K5ZvlNe/8PaE6I+twnTcrnoL4gjdAY4P7n7Mb9dqjDNqt9jTu+r+6FXY02pd5SV5y/dF/4DoYeI0wTiJL8JQTk3TAG03pYmSKcSuwCLbdyLnGJcK1ykKvef2kJAyTwLNFOqwMG46fx7hC1tTwkwsIv5GWbjP1EXeWxisZ35cgZp0LzO3NoBe/r3UmrRqToUzIRlHSHz5d9+aK15z8ZlJl38MuktdZDqRev7HCtcUvA0uJE04kyF2h03LLdtkmiAtAEMvkvIlXKJ9naMJgXiHnSxkkjEcHeStWXJpZxYfiDDxkf3/buzFwaCs2eFUsUeMn4HRp9sZq8=',
            'SRT5': '1748055206',
            'nhn.realestate.article.rlet_type_cd': 'A01',
            'nhn.realestate.article.trade_type_cd': '""',
            'nhn.realestate.article.ipaddress_city': '1100000000',
            'landHomeFlashUseYn': 'Y',
            'realestate.beta.lastclick.cortar': '4100000000',
            'REALESTATE': 'Sat%20May%2024%202025%2011%3A53%3A50%20GMT%2B0900%20(Korean%20Standard%20Time)',
            'BUC': 'jX8TzKX0jkTs2LTom2dHhzpzdbqMKgShauc7WvKutnw=',
        }
        
        self.headers = {
            'accept': '*/*',
            'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
            'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlJFQUxFU1RBVEUiLCJpYXQiOjE3NDgwNTUyMzAsImV4cCI6MTc0ODA2NjAzMH0.mEmiTcb0P5uTI6spw5k6_EQQSO3sL3yRQPTbD72TYpE',
            'priority': 'u=1, i',
            'referer': 'https://new.land.naver.com/search',
            'sec-ch-ua': '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"macOS"',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-origin',
            'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
        }
    
    def fetch_complex_data(self, complex_no, page=1):
        """특정 아파트 단지의 매물 정보를 API로 가져옵니다."""
        url = f'https://new.land.naver.com/api/articles/complex/{complex_no}'
        params = {
            'realEstateType': 'APT:PRE:ABYG:JGC',
            'tradeType': '',
            'tag': '::::::::',
            'rentPriceMin': 0,
            'rentPriceMax': 900000000,
            'priceMin': 0,
            'priceMax': 900000000,
            'areaMin': 0,
            'areaMax': 900000000,
            'oldBuildYears': '',
            'recentlyBuildYears': '',
            'minHouseHoldCount': '',
            'maxHouseHoldCount': '',
            'showArticle': 'false',
            'sameAddressGroup': 'false',
            'minMaintenanceCost': '',
            'maxMaintenanceCost': '',
            'priceType': 'RETAIL',
            'directions': '',
            'page': page,
            'complexNo': complex_no,
            'buildingNos': '',
            'areaNos': '',
            'type': 'list',
            'order': 'rank'
        }
        
        try:
            response = requests.get(url, params=params, cookies=self.cookies, headers=self.headers)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            st.error(f"API 요청 중 오류 발생: {e}")
            return None
    
    def parse_price(self, price_str):
        """가격 문자열을 원 단위로 변환합니다."""
        try:
            price_str = price_str.replace(',', '').strip()
            price_won = 0
            
            if '억' in price_str:
                parts = price_str.split('억')
                eok = int(parts[0].strip())
                price_won += eok * 100000000
                
                if len(parts) > 1 and parts[1].strip():
                    man_str = parts[1].strip()
                    if '만' in man_str:
                        man = int(man_str.replace('만', '').strip())
                        price_won += man * 10000
            elif '만' in price_str:
                man = int(price_str.replace('만', '').strip())
                price_won += man * 10000
            
            return price_won
        except:
            return 0
    
    def process_articles(self, articles):
        """API 응답에서 매물 정보를 처리합니다."""
        processed_data = []
        
        for article in articles:
            price_won = self.parse_price(article.get('dealOrWarrantPrc', ''))
            
            processed_article = {
                '매물번호': article.get('articleNo'),
                '거래유형': article.get('tradeTypeName'),
                '가격': article.get('dealOrWarrantPrc'),
                '가격(원)': price_won,
                '면적(㎡)': article.get('area1'),
                '전용면적(㎡)': article.get('area2'),
                '층수': article.get('floorInfo'),
                '향': article.get('direction'),
                '동': article.get('buildingName'),
                '특징': article.get('articleFeatureDesc'),
                '중개사': article.get('realtorName'),
                '확인일자': article.get('articleConfirmYmd')
            }
            processed_data.append(processed_article)
        
        return pd.DataFrame(processed_data)

# Streamlit 앱 메인 함수
def main():
    st.title("🏠 네이버 부동산 매물 조회 시스템")
    st.markdown("---")
    
    # 사이드바 설정
    with st.sidebar:
        st.header("⚙️ 설정")
        
        # 아파트 단지 정보 입력
        complex_no = st.number_input(
            "아파트 단지 번호",
            min_value=1,
            value=19494,
            help="네이버 부동산에서 아파트 단지의 고유 번호"
        )
        
        # 페이지 범위 설정
        col1, col2 = st.columns(2)
        with col1:
            start_page = st.number_input("시작 페이지", min_value=1, value=1)
        with col2:
            end_page = st.number_input("종료 페이지", min_value=1, value=10)
        
        # 조회 버튼
        search_button = st.button("🔍 매물 조회", type="primary", use_container_width=True)
    
    # API 인스턴스 생성
    api = NaverRealEstateAPI()
    
    # 메인 영역
    if search_button:
        with st.spinner("매물 정보를 가져오는 중..."):
            all_data = []
            progress_bar = st.progress(0)
            status_text = st.empty()
            
            # 각 페이지별로 데이터 가져오기
            for page in range(start_page, end_page + 1):
                status_text.text(f"페이지 {page}/{end_page} 처리 중...")
                
                data = api.fetch_complex_data(complex_no, page)
                
                if data and 'articleList' in data:
                    articles = data['articleList']
                    if articles:
                        df_page = api.process_articles(articles)
                        all_data.append(df_page)
                    
                    # 더 이상 데이터가 없으면 중단
                    if not data.get('isMoreData', False):
                        status_text.text(f"페이지 {page}에서 조회 완료")
                        break
                
                # 진행률 업데이트
                progress = (page - start_page + 1) / (end_page - start_page + 1)
                progress_bar.progress(progress)
                
                # API 부하 방지
                time.sleep(1)
            
            progress_bar.empty()
            status_text.empty()
        
        # 결과 표시
        if all_data:
            df_total = pd.concat(all_data, ignore_index=True)
            
            # 통계 정보 표시
            st.header("📊 매물 통계")
            col1, col2, col3, col4 = st.columns(4)
            
            with col1:
                st.metric("총 매물 수", f"{len(df_total)}개")
            
            with col2:
                avg_price = df_total['가격(원)'].mean()
                st.metric("평균 가격", f"{avg_price/100000000:.1f}억원")
            
            with col3:
                max_price = df_total['가격(원)'].max()
                st.metric("최고 가격", f"{max_price/100000000:.1f}억원")
            
            with col4:
                min_price = df_total['가격(원)'].min()
                st.metric("최저 가격", f"{min_price/100000000:.1f}억원")
            
            # 탭 생성
            tab1, tab2, tab3 = st.tabs(["📋 매물 목록", "📈 차트 분석", "💾 데이터 다운로드"])
            
            with tab1:
                st.subheader("전체 매물 목록")
                
                # 필터링 옵션
                col1, col2, col3 = st.columns(3)
                with col1:
                    selected_area = st.selectbox(
                        "면적 선택",
                        ["전체"] + sorted(df_total['면적(㎡)'].unique().tolist())
                    )
                with col2:
                    selected_dong = st.selectbox(
                        "동 선택",
                        ["전체"] + sorted(df_total['동'].dropna().unique().tolist())
                    )
                with col3:
                    selected_direction = st.selectbox(
                        "향 선택",
                        ["전체"] + sorted(df_total['향'].dropna().unique().tolist())
                    )
                
                # 필터링 적용
                filtered_df = df_total.copy()
                if selected_area != "전체":
                    filtered_df = filtered_df[filtered_df['면적(㎡)'] == selected_area]
                if selected_dong != "전체":
                    filtered_df = filtered_df[filtered_df['동'] == selected_dong]
                if selected_direction != "전체":
                    filtered_df = filtered_df[filtered_df['향'] == selected_direction]
                
                # 데이터 표시
                st.dataframe(filtered_df, use_container_width=True)
            
            with tab2:
                st.subheader("가격 분포 분석")
                
                # 면적별 가격 분포
                fig1 = px.box(
                    df_total,
                    x='면적(㎡)',
                    y='가격(원)',
                    title='면적별 가격 분포',
                    labels={'가격(원)': '가격 (원)', '면적(㎡)': '면적 (㎡)'}
                )
                fig1.update_layout(yaxis_tickformat=',.0f')
                st.plotly_chart(fig1, use_container_width=True)
                
                # 동별 매물 수
                dong_counts = df_total['동'].value_counts()
                fig2 = px.bar(
                    x=dong_counts.index,
                    y=dong_counts.values,
                    title='동별 매물 수',
                    labels={'x': '동', 'y': '매물 수'}
                )
                st.plotly_chart(fig2, use_container_width=True)
                
                # 층수별 가격 분석
                df_floor = df_total.copy()
                df_floor['층'] = df_floor['층수'].str.extract(r'(\d+)/')
                df_floor['층'] = pd.to_numeric(df_floor['층'], errors='coerce')
                
                fig3 = px.scatter(
                    df_floor.dropna(subset=['층']),
                    x='층',
                    y='가격(원)',
                    color='면적(㎡)',
                    title='층수별 가격 분포',
                    labels={'가격(원)': '가격 (원)', '층': '층수'}
                )
                fig3.update_layout(yaxis_tickformat=',.0f')
                st.plotly_chart(fig3, use_container_width=True)
            
            with tab3:
                st.subheader("데이터 다운로드")
                
                # CSV 다운로드
                csv = df_total.to_csv(index=False, encoding='utf-8-sig')
                st.download_button(
                    label="📥 CSV 파일 다운로드",
                    data=csv,
                    file_name=f"naver_real_estate_{complex_no}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
                    mime="text/csv"
                )
                
                # 엑셀 다운로드
                try:
                    import io
                    buffer = io.BytesIO()
                    with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
                        df_total.to_excel(writer, sheet_name='매물목록', index=False)
                    
                    st.download_button(
                        label="📥 Excel 파일 다운로드",
                        data=buffer.getvalue(),
                        file_name=f"naver_real_estate_{complex_no}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
                        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                    )
                except:
                    st.info("Excel 다운로드를 위해서는 xlsxwriter 패키지가 필요합니다.")
        else:
            st.warning("조회된 매물이 없습니다.")
    
    else:
        # 초기 화면
        st.info("👈 왼쪽 사이드바에서 아파트 단지 번호와 페이지 범위를 설정한 후 '매물 조회' 버튼을 클릭하세요.")
        
        # 사용 안내
        with st.expander("📖 사용 방법"):
            st.markdown("""
            1. **아파트 단지 번호 입력**: 네이버 부동산에서 조회하고자 하는 아파트의 단지 번호를 입력합니다.
               - 예: 호원한승미메이드 = 19494
            
            2. **페이지 범위 설정**: 조회할 페이지의 시작과 끝을 설정합니다.
               - 각 페이지당 약 20개의 매물이 표시됩니다.
            
            3. **매물 조회**: 설정을 완료한 후 '매물 조회' 버튼을 클릭합니다.
            
            4. **결과 확인**:
               - 📋 매물 목록: 전체 매물을 표로 확인
               - 📈 차트 분석: 가격 분포와 통계를 시각화
               - 💾 데이터 다운로드: CSV 또는 Excel 파일로 저장
            """)

if __name__ == "__main__":
    main()

 

 

 

 

 

streamlit 실행

streamlit run 네이버부동산.py

 

 

 

 

결과

 

 

참고자료

 

curl 복사해서 크롤링 데이터 확인

https://curlconverter.com/

 

Convert curl commands to code

Privacy We do not transmit or record the curl commands you enter or what they're converted to. This is a static website (hosted on GitHub Pages) and the conversion happens entirely in your browser using JavaScript. There is also a VS Code extension and a c

curlconverter.com

 

 

 

 

 

출처 및 참고 유튜브

https://www.youtube.com/watch?v=V4tjbUQ46Uw&t=185s

 

 

 

의견

AI로 너무 빠르게 만들었다. 만든 시간이 1시간 밖에 안걸린 것 같다 ;;
이것저것 깔기 싫어서 영상처럼 주피터를 활용하고 몇가지 방법밖에 안쓴 것 같은데 끝이났다.
진짜 이제 AI로 할 수 있는게 무궁무진해졌다. 너무 재밌다.

 

 

근데 이번에 새로나온 클로드 opus4를 써봤는데 생각보다 너무 좋아서 놀랐다
다만 너무 비싸서 테스트용으로 쓸 수 밖에 없었다.

'혼자 고민해보기_ 개발' 카테고리의 다른 글

[Python] Python Advanced  (0) 2024.12.29
[Python] 환경 설정  (0) 2024.12.29
Test Code -Nest.js  (0) 2023.11.07
cypress로 e2e테스트 이해하기  (0) 2023.11.07
Redis 캐시(Cache)적용을 통한 조회 성능 개선  (1) 2023.11.02