Spark
🏅 AI 마스터
🏡 내집마련 찐친

네이버 부동산 데이터 크롤링하여, 상가 임대료 분석하기~!

특정 지역 상권의 시세를 알아보고 싶었습니다.

첫째, 임차인으로서 적정 상가 시세 평균이 궁금했습니다. (호갱방지)

둘째, 임대인으로서 적정 상가 시세 평균이 궁금했습니다. (공실방지)

층과 평형대를 입력하면, 보증금과 월세 평균이 나오도록 구현해보고 싶었습니다.


하고 싶었던 것

  • 네이버 부동산 데이터 크롤링 하기 (자동 또는 반자동으로)

  • 파이썬을 활용하여 xlsx 또는 csv 데이터 전처리

  • 코드펜 등 이용하여 웹 앱으로 구현


접근

selenium과 chromedriver를 이용하여 크롤링을 시도했으나, 결국 실패를 하고.. (혹시 아시는 분 있으시면 귓뜸 부탁드립니다~!)

웹서핑을 하다가 노코드(excel이용)로 네이버 부동산 크롤링 하는 영상을 찾아 따라 해보았습니다. 참고한 영상은 "No 코딩! 네이버부동산 매물 엑셀로 5초만에 크롤링! (초보자도 OK)" 입니다.

원하는 지역(수원시 영통구 신동), 매물 필터를 적용(상가, 월세)하고 브라우저에서 F12를 눌러 데이터 및 링크를 확인합니다.

링크 정제과정을 거쳐 수정하고, 데이터를 가지고 와 테이블을 만들었습니다.

총 261개 이상 데이터를 크롤링하여 CSV 파일로 변환하였습니다. (반자동으로 해결했습니다. ㅜ)

이제부터 여행가J님의 “코드고 뭐고 스트림릿도 코드펜도 다 필요없는 클로드 개발 을 참고하여 Claude3.5 sonnet Artifact를 이용해 웹 앱을 구현해보았습니다.

대략의 프롬프트는 아래와 같습니다.

코드펜이라는 웹 툴을 이용해서 UI를 구현해줘. 첨부된 csv 파일을 분석하고, 층과 평형대를 입력하면 보증금과 월세 평균을 표시해 주고 관련 매물의 리스트를 보여주는 앱을 만들꺼야. 완전한 앱코드 작성해줘.

샘플 파일은 “경기도 수원시 영통구 신동” 내 상가 중 월세인 매물을 크롤링한 데이터야.

G열의 데이터가 면적값인데, 평으로 환산해주고, 19보다 작으면 10평형 이하, 29보다 작으면 20평형, 39보다 작으면 30평형, 그 이상이면 40평형 이상 이렇게 분류해줘.

층 정보는 D열이고, "/" 앞 글자가 층이고, E열이 보증금, F열이 월세야. 웹앱에서 층과 평형은 리스트로 만들어주면 좋겠어.


최초 UI는 이랬습니다.

첨부된 파일 분석도 잘 안되고, 디자인도 맘에 들지 않아 몇 차례 프롬프트를 변경해가며 코드를 수정 했습니다. 최종 코드는 아래와 같습니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>신동상가 매물 분석 🏢</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet">
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');
        
        body {
            font-family: 'Noto Sans KR', sans-serif;
            line-height: 1.6;
            margin: 0;
            padding: 0;
            background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .container {
            max-width: 800px;
            width: 100%;
            background-color: rgba(255, 255, 255, 0.9);
            padding: 40px;
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.1);
        }
        h1 {
            text-align: center;
            color: #333;
            font-size: 2.5em;
            margin-bottom: 30px;
        }
        .file-upload {
            display: flex;
            justify-content: center;
            align-items: center;
            margin-bottom: 20px;
        }
        .file-upload label {
            background-color: #fed6e3;
            color: #333;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        .file-upload label:hover {
            background-color: #ffc0cb;
        }
        .file-upload input[type="file"] {
            display: none;
        }
        select, button {
            display: block;
            width: 100%;
            padding: 12px;
            margin-bottom: 20px;
            border: 2px solid #ddd;
            border-radius: 8px;
            font-size: 1em;
            transition: all 0.3s ease;
        }
        select:focus, button:focus {
            outline: none;
            border-color: #a8edea;
        }
        button {
            background-color: #a8edea;
            color: #333;
            border: none;
            cursor: pointer;
            font-weight: bold;
        }
        button:hover {
            background-color: #8ed6d2;
        }
        #result {
            margin-top: 30px;
            padding: 20px;
            background-color: #f0f8ff;
            border-radius: 8px;
            font-size: 1.1em;
            text-align: center;
        }
        table {
            width: 100%;
            border-collapse: separate;
            border-spacing: 0;
            margin-top: 30px;
            background-color: white;
            box-shadow: 0 0 20px rgba(0,0,0,0.1);
            border-radius: 10px;
            overflow: hidden;
        }
        th, td {
            padding: 15px;
            text-align: left;
            border-bottom: 1px solid #eee;
        }
        th {
            background-color: #a8edea;
            color: #333;
            font-weight: bold;
        }
        tr:last-child td {
            border-bottom: none;
        }
        tr:hover {
            background-color: #f5f5f5;
        }
        .icon {
            margin-right: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🏢 신동상가 매물 분석 🔍</h1>
        <div class="file-upload">
            <label for="csvFile"><i class="fas fa-file-upload icon"></i>CSV 파일 업로드</label>
            <input type="file" id="csvFile" accept=".csv">
        </div>
        <select id="floor">
            <option value="">층 선택 🏗️</option>
            <option value="B">지하 🕳️</option>
            <option value="1">1층 🏠</option>
            <option value="2">2층 🏢</option>
            <option value="3">3층 🏢</option>
            <option value="4">4층 🏙️</option>
            <option value="5">5층 🏙️</option>
            <option value="6+">6층 이상 🏬</option>
        </select>
        <select id="size">
            <option value="">평형 선택 📏</option>
            <option value="10">10평형 이하</option>
            <option value="20">20평형</option>
            <option value="30">30평형</option>
            <option value="40">40평형 이상</option>
        </select>
        <button onclick="analyze()"><i class="fas fa-chart-bar icon"></i>분석하기</button>
        <div id="result"></div>
        <table id="propertyList">
            <thead>
                <tr>
                    <th>층 🏢</th>
                    <th>평형 📐</th>
                    <th>보증금 💰</th>
                    <th>월세 💸</th>
                    <th>매물 설명 📝</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>

    <script>
        let data = [];
        const floorSelect = document.getElementById('floor');
        const sizeSelect = document.getElementById('size');
        const resultDiv = document.getElementById('result');
        const propertyListBody = document.querySelector('#propertyList tbody');
        const fileInput = document.getElementById('csvFile');

        fileInput.addEventListener('change', function(e) {
            const file = e.target.files[0];
            Papa.parse(file, {
                complete: function(results) {
                    data = results.data.slice(1); // 첫 번째 행(헤더)를 제외
                    console.log('Parsed CSV data:', data);
                },
                header: false,
                dynamicTyping: true
            });
        });

        function getFloorCategory(floorInfo) {
            if (typeof floorInfo !== 'string') return '';
            const floor = parseInt(floorInfo.split('/')[0]);
            if (floorInfo.startsWith('B')) return 'B';
            if (floor >= 6) return '6+';
            return floor.toString();
        }

        function getFloorEmoji(floor) {
            switch(floor) {
                case 'B': return '🕳️';
                case '1': return '🏠';
                case '2': return '🏢';
                case '3': return '🏢';
                case '4': return '🏙️';
                case '5': return '🏙️';
                case '6+': return '🏬';
                default: return '🏗️';
            }
        }

        function getSizeCategory(size) {
            const sizeInPyeong = size / 3.3058;
            if (sizeInPyeong < 19) return "10";
            if (sizeInPyeong < 29) return "20";
            if (sizeInPyeong < 39) return "30";
            return "40";
        }

        function analyze() {
            const selectedFloor = floorSelect.value;
            const selectedSize = sizeSelect.value;

            const filteredData = data.filter(row => {
                if (row.length < 13) return false; // 데이터 형식 검증
                const floor = getFloorCategory(row[3]);
                const size = getSizeCategory(parseFloat(row[6]));
                return (!selectedFloor || floor === selectedFloor) && 
                       (!selectedSize || size === selectedSize);
            });

            if (filteredData.length === 0) {
                resultDiv.innerHTML = "<i class='fas fa-exclamation-circle'></i> 해당 조건의 매물이 없습니다.";
                propertyListBody.innerHTML = '';
                return;
            }

            const avgDeposit = filteredData.reduce((sum, row) => sum + parseFloat(row[4]), 0) / filteredData.length;
            const avgRent = filteredData.reduce((sum, row) => sum + parseFloat(row[5]), 0) / filteredData.length;

            resultDiv.innerHTML = `<i class='fas fa-chart-line'></i> 평균 보증금: ${avgDeposit.toFixed(2)}만원, 평균 월세: ${avgRent.toFixed(2)}만원`;

            propertyListBody.innerHTML = '';
            filteredData.forEach(row => {
                const tr = document.createElement('tr');
                const floor = getFloorCategory(row[3]);
                tr.innerHTML = `
                    <td>${floor === 'B' ? '지하' : floor === '6+' ? '6층 이상' : floor + '층'} ${getFloorEmoji(floor)}</td>
                    <td>${(parseFloat(row[6]) / 3.3058).toFixed(2)}평 📏</td>
                    <td>${row[4]}만원 💰</td>
                    <td>${row[5]}만원 💸</td>
                    <td>${row[12]} 📝</td>
                `;
                propertyListBody.appendChild(tr);
            });
        }
    </script>
</body>
</html>


결론

최종 UI 및 결과는 아래와 같습니다. 층과 평형은 리스트로 고를 수 있게 구현했고, 업데이트 된 크롤링 데이터를 업로드 할 수 있게 구현했습니다.

1층과 10평형대 이하를 선택하면, 보증금 약 2400만원, 월세는 약150만원으로 파악이 되었고, 관련 목록은 아래와 같이 볼 수 있게 되었습니다. (단위를 따로 설정 안했는데 만원으로 맞춰주네요~! 신기!!)

데이터 및 분석에 오류가 없는지 확인 차원에서 다른 매물도 조회 해보았습니다. (잘 나오네요~ 휴~~ 😮‍💨 29.95평형이 30평형으로 조회 되었으나.. 반올림 했다 치고 넘어가 봅니다. ㅋ)


추가 시도할 것

  • 네이버 부동산 데이터 자동 크롤링

  • 시계열 데이터를 추가하여 동향 차트 추가

  • Claude 또는 ChatGPT API와 연계하여 추가 분석 및 인사이트 추가

  • 하단에 추출된 리스트를 누르면 해당 매물로 이동할 수 있도록 Hyper-link 추가


Insight


감사합니다~!


#11기 내집마련

7
5개의 답글

👉 이 게시글도 읽어보세요