#문과생도AI#4주차#네이버부동산#매물#셀레니움#크롤링
11기 문과생도AI에서 다룬 주요주제는 크롤링이며 4주간 도전은 다음과 같습니다.
1주차 과제 : Trafilatura 활용하기
2주차 과제 : 키워드입력 + 네이버뉴스 100개 크롤링 해보기
3주차 과제 : 네이버뉴스100개 크롤링결과물을 Gradio + 텔레그램로 보내기
2024-07-06 08-45-22 (2)_vrew_exported.mp4
크롤링을 시작하기전에 다음을 알고 있으면 좋을 것 같아 소개해드립니다.
과제해결에 도움을 주신 이른아침님 덕분에 크롤링을 하려면 Element , Network , API, RPA(Uipath), WebSocket,… 등 여러가지를 사용하는 방식이 있다는 것을 알게 되었습니다.
이중에서 첫 3가지에 대해 좀더 쉽게 초등학생도 이해할 수있는 표현으로 해보면…
4주차과제
4주차 과제로 해보려는 했던것은, 셀레니움활용한 네이버부동산 크롤링입니다.
네이버부동산을 누르면 다음과 같은 화면이 나옵니다.
매물들을 하나씩 클릭해서 확인해봐야 하는데, 한번에 엑셀로 보면 좋겠다는 생각이 듭니다.
그래서 이번에 크롤링을 통해 단지별 매물데이터를 모아보기로 했습니다.
1. 싸이트구조 파악
크롤링을 하려면 페이지의 구조를 잘 살펴보아야 합니다.
우선 싸이트에 들어가서 어떤 순서로 할지 , 어느버튼을 눌러야 할지 찾아보았습니다.
그다음에 개발자도구에 들어가서 해당버튼의 Selector/X-path 를 찾았습니다.
2. Seletor/X-path 찾기
1~6의 순서대로 클릭을 해서 개발자도구에서 버튼을 확인해봅니다.
1) 매매 1번 : /html/body/div[4]/div[3]/div[2]/div/div[1]/div[1]/ul/div[2]/li[2]
2) 리스트 2번 : /html/body/div[4]/div[3]/div[2]/div/div[1]/div[1]/p
3) 서울시 3번 : /html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[1]/ul/li[1] → 17
4) 서초구 4번 :/html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[2]/ul/li[15] → 25
5) 반포동 5번 :/html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[3]/div/ul/li[2]→[87]
6) 확인매물 6번 : /html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[4]/p[2]/a
이것으로 해당지역으로 이동하기 위한 구조입니다.
위의 정보를 제공하고 지피티에게 코딩을 요청했습니다. 그 코딩을 실행을 해보니 에러가 났었고,
지피티는 아래내용을 추가수정하라고 합니다.
3. 클릭가능 할 때까지 대기
웹 페이지의 요소가 클릭 가능할 때까지 최대 10초간 대기하도록 설정했습니다. 이러한 변경을 통해 요소가 페이지에 나타날 때까지 기다리므로, NoSuchElementException 오류를 방지할 수 있습니다.
4. 전체 화면나오도록 WenDriver 적용
마지막에 나오는 지도화면도 적어서 전체화면이 나오도록 코드를 추가했습니다
이제 1단계로 전환된 화면에서 제가 필요한 정보를 추출하는 단계입니다
5. 단지정보추출
단지정보를 확인하려면 다음 X-path 버튼을 눌러야합니다
단지 : /html/body/div[2]/div/section/div[2]/div/div[1]/div/div/a/span[4]
서초구 반포동에 있는 단지의 처음단지와 마지막단지의 X-path구조는 다음과 같습니다.
강남원효성버튼 : /html/body/div[2]/div/section/div[2]/div/div[1]/div/div/div/div[3]/ul/li[1]
효성 버튼 : /html/body/div[2]/div/section/div[2]/div/div[1]/div/div/div/div[3]/ul/li[110]
우선 여기서 단지정보만 추출해보도록 지피티에게 애기해보겠습니다
이부분이 추가되었습니다
코딩을 실행하고 나오 터미널에 보이는 결과물입니다.
결과물이 제대로 나옵니다.
6. 코딩실행 영상
2024-07-04 16-21-59 (1)_vrew_exported (1).mp4
7. 의문점
이런 과정을 계속 for문을 써서 시도/시군구/동별로 반복시킨다면 시간과 한계는 있겠지만,
전체단지를 구해볼수는 있을 것 같긴한데, 이 방법이 효율적인지는 모르겠습니다.
8. 이른아침님의 새로운방식 제안
랭체인의 이른아침에 님이 API방식으로 아이디어를 주셨는데, 개발자도구의 네트워크에서 API를 찾아서 실행을 시키면 동작이 아주 잘됩니다.
제안하신 코딩결과 모든데이터를 csv로 출력해보았더니 , 데이터종류가 구별이 어렵군요
그중에 우선 필요한 정보만 뽑기위해 그 코딩을 약간의 변형을 해보았습니다.
어느게 어느건지 구별이 안되어서 개발자도구> 네트워크> 검색>래미안 을 누르고 머리글의 요청url을 복사해서 붙여보았습니다.
아래 결과물에서 필요한 데이터(단지명, 매매가, 면적, 특징) 보여주도록 조정을 했고, html로 출력을하여 여행가J님의 단톡방에서 소개해주었던 codepen에 넣어서 결과 출력해보았습니다.
결 과물이 이쁘게 잘나왔습니다. 중간에 네이버가 접속을 중단시킬때가 있지만 , 전지역을 하는게 아니면 문제가 없을 것 같습니다.
9. 11기 소감
마지막과제는 여러 모로 의미가 깊었습니다. 저처럼 코드의 문외한조차 이정도 수준까지 도달할 수 있다는 것을 보여준 사례례라고 생각합니다.
처음엔 모든 코드가 낯설고, 하나하나가 어렵게 느껴졌는데 , 어느새 자신도 모르는 사이에 지피티와 문제를 해결해 나가고 있는 저를 발견하게 됩니다. 단순히 코딩을 넘어서, 과제의 해결에 대한 접근 방식을 바꾸어주었다는 좋은 또다른 경험이었습니다.
4주동안 파트너님과 다른분들의 도움 덕분에 많은 것을 배울수 있어서 즐거웠습니다.
참고로 전체코딩은 아래와 같습니다.
[ X-path ]
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time
# 드라이버 설정
options = webdriver.ChromeOptions()
options.add_argument("--start-maximized") # 시작 시 최대화
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
# 네이버 부동산 페이지 접속
driver.get("https://land.naver.com/")
# 매매 버튼 클릭
driver.find_element(By.XPATH, '/html/body/div[4]/div[3]/div[2]/div/div[1]/div[1]/ul/div[2]/li[2]').click()
# 리스트 버튼 클릭
driver.find_element(By.XPATH, '/html/body/div[4]/div[3]/div[2]/div/div[1]/div[1]/p').click()
# 서울시 선택
driver.find_element(By.XPATH, '/html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[1]/ul/li[1]').click()
# 서초구 선택
driver.find_element(By.XPATH, '/html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[2]/ul/li[15]').click()
# 반포동 선택 (명시적 대기 사용)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.XPATH, '/html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[3]/div/ul/li[2]')))
element.click()
# 확인매물 버튼 클릭
driver.find_element(By.XPATH, '/html/body/div[4]/div[3]/div[2]/div/div[1]/div[3]/div[3]/div[4]/p[2]/a').click()
# 전체 화면 모드로 전환
driver.fullscreen_window()
# 단지 정보 탭으로 전환
driver.find_element(By.XPATH, '/html/body/div[2]/div/section/div[2]/div/div[1]/div/div/a/span[4]').click()
# 단지명을 저장할 리스트
complex_names = []
# 반복문 내에서 각 단지명을 추출하는 부분 수정
for i in range(1, 111):
xpath = f'/html/body/div[2]/div/section/div[2]/div/div[1]/div/div/div/div[3]/ul/li[{i}]'
# 요소가 존재하고 가시적일 때까지 대기
complex_name_element = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.XPATH, xpath))
)
complex_name = complex_name_element.text
complex_names.append(complex_name)
print(f"단지 {i}: {complex_name}")
# 드라이버 종료
driver.quit()
# 추출된 단지명 출력
for name in complex_names:
print(name)
[ Network방법 ]
import requests
import json
from datetime import datetime
DISTRICT_CODES = {
"강남구": "1168000000", "강동구": "1174000000", "강북구": "1130500000",
"강서구": "1150000000", "관악구": "1162000000", "광진구": "1121500000",
"구로구": "1153000000", "금천구": "1154500000", "노원구": "1135000000",
"도봉구": "1132000000", "동대문구": "1123000000", "동작구": "1159000000",
"마포구": "1144000000", "서대문구": "1141000000", "서초구": "1165000000",
"성동구": "1120000000", "성북구": "1129000000", "송파구": "1171000000",
"양천구": "1147000000", "영등포구": "1156000000", "용산구": "1117000000",
"은평구": "1138000000", "종로구": "1111000000", "중구": "1114000000",
"중랑구": "1126000000"
}
SORT_OPTIONS = {
"1": "rank", # 랭킹순
"2": "dates", # 최신순
"3": "lowPrc", # 가격순
"4": "highSpc" # 면적순
}
def fetch_naver_land_data(district, sort_option, page):
url = "https://m.land.naver.com/cluster/ajax/articleList"
cortar_no = DISTRICT_CODES.get(district)
if not cortar_no:
print(f"Error: '{district}' is not a valid district name.")
return None
params = {
"rletTpCd": "APT",
"tradTpCd": "A1:B1:B2:B3",
"cortarNo": cortar_no,
"page": str(page),
"sort": sort_option
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
try:
response = requests.get(url, params=params, headers=headers)
response.raise_for_status()
data = response.json()
return data
except requests.RequestException as e:
print(f"Error fetching data: {e}")
return None
def get_sort_option():
print("\n정렬 옵션:")
for key, value in SORT_OPTIONS.items():
print(f"{key}. {value}")
while True:
choice = input("정렬 옵션을 선택하세요 (1-4): ")
if choice in SORT_OPTIONS:
return SORT_OPTIONS[choice]
print("잘못된 선택입니다. 다시 시도해주세요.")
def get_page_count():
while True:
try:
count = int(input("출력할 페이지 수를 입력하세요 (0 입력 시 모든 데이터 추출): "))
if count >= 0:
return count
print("0 이상의 숫자를 입력해주세요.")
except ValueError:
print("올바른 숫자를 입력해주세요.")
def save_to_html(data, filename):
if not data:
print("저장할 데이터가 없습니다.")
return
# 포함할 키를 정의
include_keys = ['cortarNo', 'rletTpNm', 'hanPrc', 'spc1', 'spc2', 'direction', 'atclFetrDesc', 'tagList']
# HTML 테이블 생성
html = "<html><head><meta charset='UTF-8'><title>부동산 데이터</title></head><body>"
html += "<table border='1'><thead><tr>"
for key in include_keys:
html += f"<th>{key}</th>"
html += "</tr></thead><tbody>"
for row in data:
html += "<tr>"
for key in include_keys:
html += f"<td>{row.get(key, '')}</td>"
html += "</tr>"
html += "</tbody></table></body></html>"
with open(filename, 'w', encoding='utf-8') as output_file:
output_file.write(html)
print(f"데이터가 {filename}에 저장되었습니다.")
def main():
print("서울시 구 목록:")
for district in DISTRICT_CODES.keys():
print(district)
district = input("\n데이터를 조회할 구 이름을 입력하세요: ")
if district not in DISTRICT_CODES:
print(f"Error: '{district}'는 유효한 구 이름이 아닙니다.")
return
sort_option = get_sort_option()
page_count = get_page_count()
all_data = []
page = 1
total_items = 0
while page_count == 0 or page <= page_count:
data = fetch_naver_land_data(district, sort_option, page)
if not data or 'body' not in data or not data['body']:
break
items = data['body']
if not items:
break
all_data.extend(items)
total_items += len(items)
print(f"페이지 {page} 데이터 추출 완료 (누적 {total_items}개)")
if len(items) < 20: # 마지막 페이지
break
page += 1
if all_data:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{district}_{sort_option}_{total_items}items_{timestamp}.html"
save_to_html(all_data, filename)
else:
print("추출된 데이터가 없습니다.")
if __name__ == "__main__":
main()