끄적끄적

2025.07.07 데이터 크롤링 세션 3 본문

[스파르타]내일배움캠프 데이터 분석 트랙/Session

2025.07.07 데이터 크롤링 세션 3

kminx 2025. 7. 9. 16:49
튜터 김대영 튜터님
학습 목표 1. 웹 페이지의 동적 로딩 방식 (JavaScript, AJAX)을 이해합니다.
2. Selenium 라이브러리를 사용하여 동적으로 로드되는 웹 페이지를 제어하고 크롤링합니다.
3. 웹 개발자 도구의 Network 탭을 활용하여 API 호출을 식별하고 분석합니다.
4. requests 라이브러리를 사용하여 직접 API를 호출하고 JSON 데이터를 파싱합니다.
5. Selenium과 requests를 조합하여 효율적인 크롤링 전략을 수립합니다.

1️⃣ 동적 웹페이지와 AJAX 통신 이해

이전 1, 2회차에서 다룬 정적 웹페이지는 서버에서 HTML 파일을 통째로 받아와 브라우저가 그대로 표시하는 방식이었다. 그러나 요즘 대부분의 웹사이트는 동적 웹페이지로 작동한다.

  • 동적 웹페이지: 웹 페이지의 일부 내용이 사용자의 행동(스크롤, 버튼 클릭 등)이나 시간에 따라 JavaScript를 통해 비동기적으로 로드되거나 변경되는 웹 페이지이다.
  • AJAX (Asynchronous JavaScript and XML): 웹 페이지 전체를 다시 로드하지 않고도 백그라운드에서 서버와 데이터를 주고받아 웹 페이지의 특정 부분만 업데이트하는 기술이다. 이 과정에서 서버와 주고받는 데이터는 주로 API (Application Programming Interface)를 통해 이루어진다.

 

📌 중요할까요?

우리가 requests와 BeautifulSoup만으로는 웹 페이지의 모든 내용을 가져올 수 없는 이유가 여기에 있다. requests는 최초 서버가 보내는 HTML만 가져오므로 JavaScript가 나중에 추가하는 내용은 가져오지 못한다. 이때 Selenium이나 직접 API를 호출하는 방법이 필요하다.

 

 

2️⃣ Selenium을 활용한 동적 웹페이지 크롤링

Selenium은 실제 웹 브라우저(Chrome, Firefox 등)를 파이썬 코드로 직접 제어해 웹 페이지를 여는 것은 물론 스크롤, 클릭, 키보드 입력 등 사용자처럼 다양한 상호작용을 수행한다. 이를 통해 JavaScript로 동적으로 로드되는 내용도 가져올 수 있다.

 

📌 Selenium 설치 및 웹 드라이버 설정

Selenium을 사용하려면 웹 브라우저를 제어할 수 있는 웹 드라이버(WebDriver)가 필요하다. 여기서는 Chrome 브라우저용 ChromeDriver를 사용한다.

  1. Selenium 라이브러리 설치:
  2. pip install selenium
  3. ChromeDriver 다운로드:
    • 현재 사용하고 있는 Chrome 브라우저의 버전을 확인합니다. (Chrome 설정 > Chrome 정보)
    • ChromeDriver 다운로드 페이지에 접속하여 본인 Chrome 버전과 일치하는 ChromeDriver를 다운로드한다.
    • 다운로드한 chromedriver.exe 파일을 Python 스크립트를 실행하는 디렉토리나 시스템 PATH에 추가된 디렉토리에 저장한다.

 

📌 Selenium 기본 사용법

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 # 대기 조건 정의
import time

# 1. WebDriver 객체 생성 (ChromeDriver 경로 지정)
# chromedriver.exe 파일이 현재 스크립트와 같은 폴더에 있거나 PATH에 추가되어 있어야 합니다.
driver = webdriver.Chrome() # 또는 webdriver.Chrome('/path/to/chromedriver')

try:
    # 2. 웹 페이지 열기
    url = "<https://www.naver.com>"
    driver.get(url)
    print(f"'{url}' 페이지를 열었습니다.")
    time.sleep(2) # 페이지 로딩을 위해 2초 대기 (필요시 더 늘릴 수 있음)

    # 3. 요소 찾기 (ID, Class Name, CSS Selector, XPath 등 사용)
    # By.ID, By.CLASS_NAME, By.CSS_SELECTOR, By.XPATH 등 다양한 방법이 있습니다.
    # 여기서는 검색창을 찾아봅니다.
    search_input = driver.find_element(By.ID, 'query') # 네이버 검색창의 ID는 'query'

    # 4. 요소와 상호작용하기
    search_input.send_keys("파이썬 크롤링") # 텍스트 입력
    search_input.submit() # 엔터 키를 누르는 것과 동일

    print("검색어를 입력하고 검색했습니다.")
    time.sleep(3) # 검색 결과 페이지 로딩 대기

    # 5. 동적으로 로드된 내용 가져오기 (예: 검색 결과 중 첫 번째 링크 텍스트)
    # 명시적 대기: 특정 요소가 나타날 때까지 기다립니다. (가장 좋은 방법)
    # 내가 원하는 요소가 뜰 때까지 10초 기다리겠다. (아래는 li.bx a.info가 DOM에 존재하는지)
    first_result_link = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, 'li.bx a.info')) # CSS Selector 예시
    )
    print(f"첫 번째 검색 결과 링크 텍스트: {first_result_link.text}")

    # 6. 현재 페이지의 HTML 소스 가져오기
    html_source = driver.page_source
    # print(html_source[:500]) # HTML 소스의 일부 출력

finally:
    # 7. WebDriver 종료 (브라우저 창 닫기)
    driver.quit()
    print("브라우저를 종료했습니다.")

 

📌Selenium으로 클릭 및 스크롤 자동화

인스타그램과 같이 스크롤로 데이터가 계속 추가되는 사이트를 크롤링하고 싶을 때 사용 가능하다.

# ... (driver 초기화 코드 동일) ...
driver = webdriver.Chrome()

try:
    driver.get("<https://www.daum.net>") # 다음 뉴스 페이지 예시 (실제 동작 확인 필요)
    time.sleep(2)

    # 1. 버튼 클릭하기 (예: 뉴스 탭 클릭)
    # 개발자 도구로 뉴스 탭 버튼의 CSS Selector나 XPath를 찾습니다.
    # news_tab = driver.find_element(By.CSS_SELECTOR, '#gnbService > li:nth-child(2) > a')
    # news_tab.click()
    # time.sleep(3)

    # 2. 스크롤 내리기 (동적으로 콘텐츠 로드하는 페이지에 유용)
    print("페이지 하단으로 스크롤합니다.")
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(2) # 추가 콘텐츠 로드를 위해 대기

    print("페이지 상단으로 스크롤합니다.")
    driver.execute_script("window.scrollTo(0, 0);")
    time.sleep(2)

finally:
    driver.quit()

 

번개장터 페이지에서 상품 이름과 가격을 크롤링하기(스크롤 5번)

# 번개장터 크롤링 코드

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 # 대기 조건 정의
import time

# ... (driver 초기화 코드 동일) ...
driver = webdriver.Chrome()

try:
    driver.get("https://m.bunjang.co.kr/") # 번개장터
    time.sleep(2)

    product_list = []  # 데이터를 저장할 리스트

    for i in range(5):
        # 방금 찾은 상품 div 출력해보기
        product_div = driver.find_elements(By.CLASS_NAME, "styled__ProductWrapper-sc-32dn86-1")

        for product in product_div:
            product_name = product.find_element(By.CLASS_NAME, "sc-fcdeBU").text
            product_price = product.find_element(By.CLASS_NAME, "sc-gmeYpB").text
            product_list.append({
                "name": product_name,
                "price": product_price
            })

        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(2) # 추가 콘텐츠 로드를 위해 대기
    print(product_list)
finally:
    driver.quit()

 

웹페이지를 아래로 스크롤할 때마다 새로운 JSON 데이터를 받아오는 서비스는 보통 커서(cursor) 기반 페이지네이션을 쓴다.
한 번 호출할 때 “다음 묶음 데이터를 어디서부터 이어서 달라”는 위치 표시자(cursor)가 응답 JSON에 같이 오고, 다음 요청의 파라미터로 그 cursor 값을 보내야 이어지는 데이터가 내려온다.

이러한 커서 토큰은 암호화가 되어있기에 requests가 아닌 selenium으로 처리하는 것이 필요하다.

 

 

3️⃣ Network 탭을 활용한 API 호출 분석 및 requests로 직접 가져오기

많은 웹사이트가 보이는 데이터를 JavaScript로 동적으로 로드할 때 실제로는 백그라운드에서 특정 API를 호출해 JSON 데이터를 받아온다. 이 API를 직접 파악하고 requests로 호출하면 Selenium보다 훨씬 빠르고 효율적인 크롤링을 할 수 있다.

 

📌 API 요청 형태 두 가지

Network 탭의 Payload 영역에서 요청 데이터가 나타나는 방식은 크게 두 가지다.

구분특징주 사용 메서드requests 호출 예시

구분 특징 주 사용 메서드 requests 호출 예시
Query String
Parameters
URL 끝에 ?key=value&… 형태로 붙는다.
캐싱·링크 공유에 유리하지만 길이 제한이 있다.
GET requests.get(url, params=query)
Request
Payload
HTTP 메시지 바디에 JSON·폼 데이터로 담긴다.
대량·민감 데이터 전송에 적합하다.
POST
(PUT·PATCH 등)
requests.post(url, json=payload)
또는 data=payload

 

📌 Network 탭에서 API 호출 식별하기

  1. Chrome 개발자 도구 열기 (F12)
  2. Network 탭으로 이동: 새로고침하거나 페이지를 스크롤/클릭하여 동적으로 로드되는 부분을 관찰한다.
  3. 필터링:
    • 'XHR' 필터: 대부분의 AJAX/API 호출은 XHR(XMLHttpRequest) 또는 Fetch 요청으로 나타난다.
    • 'Fetch/XHR' 필터 (최신 Chrome): XHR과 Fetch API 요청을 함께 보여준다.
    • 'JS', 'Doc', 'Img' 등 다른 필터를 해제하여 API 호출에 집중할 수 있다.
  4. 요청 분석:
    • 의심되는 요청(이름, Type, Size 등 확인)을 클릭한다.
    • Headers 탭: 요청 URL, 요청 방식 (GET/POST), 요청 헤더(User-Agent, Referer, Authorization 등), 쿼리 문자열(Query String Parameters) 등을 확인한다. 이 정보들은 requests로 API를 호출할 때 필요하다.
    • Payload 탭: POST 요청인 경우 서버로 전송된 데이터(JSON, FormData 등)를 확인한다.
    • Preview / Response 탭: 서버로부터 받은 응답 데이터를 확인합니다. 대부분 JSON 형태일 것이다.

 

📌 requests로 직접 API 호출 및 JSON 데이터 파싱

Network 탭에서 파악한 정보를 바탕으로 requests를 사용하여 API를 직접 호출한다.

실습 예시: 가상의 뉴스 웹사이트에서 API로 기사 목록 가져오기

(실제 웹사이트의 API는 계속 변경될 수 있으므로, 아래는 예시로 작성된 코드이다. 실제 API는 Network 탭 분석을 통해 파악해야 한다.)

import requests
import json # JSON 데이터를 파싱하기 위해 필요

# 1. Network 탭에서 파악한 API 정보 설정
api_url = "<https://api.example.com/news/articles>" # 실제 API URL로 변경!
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept": "application/json", # 서버에게 JSON 형식으로 응답을 받고 싶다고 알림
    # "Referer": "<https://www.example.com/news>", # 필요하다면 Referer 헤더 추가
    # "Authorization": "Bearer YOUR_API_TOKEN" # 인증 토큰이 필요할 경우
}
params = { # GET 요청에 필요한 쿼리 파라미터 (예: 페이지 번호, 카테고리)
    "page": 1,
    "category": "politics",
    "limit": 10
}
# POST 요청인 경우 data 또는 json 설정:
# payload = {"startDate": "2023-01-01", "endDate": "2023-12-31"}

try:
    # 2. GET 요청 보내기 (POST 요청인 경우 requests.post() 사용)
    response = requests.get(api_url, headers=headers, params=params, timeout=10)
    response.raise_for_status() # HTTP 오류 발생 시 예외 발생

    # 3. JSON 데이터 파싱
    # response.json() 메서드는 응답 본문을 JSON으로 파싱하여 Python 딕셔너리/리스트로 반환
    data = response.json()

    print(f"API 호출 성공! 응답 데이터 타입: {type(data)}")

    # 4. 파싱된 데이터 활용
    if isinstance(data, dict) and "articles" in data:
        articles = data["articles"]
        print(f"총 {len(articles)}개의 기사 목록을 가져왔습니다.")
        for i, article in enumerate(articles[:3]): # 첫 3개 기사 정보 출력
            print(f"--- 기사 {i+1} ---")
            print(f"  제목: {article.get('title', '제목 없음')}")
            print(f"  날짜: {article.get('publishedAt', '날짜 없음')}")
            print(f"  URL: {article.get('url', 'URL 없음')}")
            print(f"  요약: {article.get('description', '요약 없음')[:50]}...")
    else:
        print("API 응답 구조가 예상과 다릅니다.")
        print(json.dumps(data, indent=2)) # 전체 응답 데이터 구조 확인

except requests.exceptions.HTTPError as e:
    print(f"HTTP 에러 발생: {e.response.status_code} - {e.response.text}")
except requests.exceptions.ConnectionError as e:
    print(f"네트워크 연결 오류: {e}")
except requests.exceptions.Timeout as e:
    print(f"요청 타임아웃: {e}")
except requests.exceptions.RequestException as e:
    print(f"그 외 요청 오류: {e}")
except json.JSONDecodeError as e:
    print(f"JSON 파싱 오류: {e}. 응답 텍스트: {response.text[:200]}")
except Exception as e:
    print(f"예상치 못한 오류 발생: {e}")

 

네이버 증권 (삼성전자) 크롤링하기

# 네이버 증권 크롤링 코드 requests 이용

import requests
import json # JSON 데이터를 파싱하기 위해 필요

# 1. Network 탭에서 파악한 API 정보 설정
# type=recent&code=005930&page=1  => parameters
api_url = "https://finance.naver.com/item/item_right_ajax.naver" # 실제 API URL로 변경!
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept": "application/json", # 서버에게 JSON 형식으로 응답을 받고 싶다고 알림
    "Cookie": "NNB=HB7GTPOU7RWGO; ASID=738a25fa00000194cb92ef240000004a; NFS=2; _fbp=fb.1.1741591077607.217294079627861186; cto_bundle=9K_SuF95T0RPY2poSURlZDJzJTJCZ0pmJTJCQVUzSUFyMk5MNFRwT3RkSEFrYzIyVTBWVWg1JTJCREZENXlHdXZCMWl0UFI2ZGNhbEJSYjYlMkZLdEVWb1VSMnZLNDklMkY0SUJhUUIwalJ2SkRmTUIlMkZyandVZGlHMFp5bW16VHNMaFluam9QcVNjUmt2SUJhMTNPZmp5cTQ1OSUyQjd5OU1kZWQ5OUdLbExkeWl0eUpRU3VGRExuenZhVWFoWFNsM0puQWc5WGZzVFN4TzZUSGZvSHBCUTFCMkhXT2RxWjVkVjhoVnFidW96JTJCU1Nla2dLTmJDQ3VEaUdOb1d1VWRRSVJEb1U2NjZ6V0hTSXNrSU9DJTJGWmNDSkZOSmZHRWx2ayUyQjVRWklubGRuajMzcVBGUHhlV1BnU1pZRzJiVXFyWm15R1JYTkdleDNJdW16RllSY3VjdFFuMnllVVMzVDh5TmdoZW5kTUZKSmp1WFdEYzcxd3lrT041VlZtayUzRA; _tt_enable_cookie=1; _ttp=01JT4THWXQSAMYJ9TJAQMATSRD_.tt.1; _ga=GA1.1.GA1.1.GA1.1.1866807985.1741591078; ttcsid_CRLT6VRC77UC5E4HNKOG=1746066273208::7dZajmfVtFg2ZHJZB_kJ.1.1746066353881; ttcsid=1746066273208::WnPYNOmvtqg-f221WlDX.1.1746066353881; _ga_9JHCQLWL5X=GS1.1.1746066272.1.1.1746066354.0.0.0; _ga_NFRXYYY5S0=GS1.1.1746066272.1.1.1746066354.0.0.0; _ga_Q7G1QTKPGB=GS1.1.1746066272.1.1.1746066354.0.0.0; _ga_6X0XMCB9L6=GS2.1.s1746773487$o1$g0$t1746773489$j0$l0$h0; NAC=4QeeHgBxTTe2C; _gcl_au=1.1.956905134.1749690647; _ga_EEN65PKS7L=GS2.1.s1749690647$o10$g1$t1749690723$j60$l0$h0; naver_stock_codeList=005930%7C; NACT=1; SRT30=1751883918; page_uid=jbXmudqptbNssuUBbDwssssstSG-138087; SRT5=1751885483; BUC=ys9Dyu8w-9YIXLKjMmF6OklneW_HugxDuoW0zQqqUFg=; JSESSIONID=8F7B3A4F990AD704AAC86B6747D86C68",
    "Referer": "https://finance.naver.com/item/main.naver?code=005930", # 필요하다면 Referer 헤더 추가
    # "Authorization": "Bearer YOUR_API_TOKEN" # 인증 토큰이 필요할 경우
}
params = { # GET 요청에 필요한 쿼리 파라미터 (예: 페이지 번호, 카테고리)
    "type": "recent",
    "code": "005930",
    "page": 1
}
# POST 요청인 경우 data 또는 json 설정:
# payload = {"startDate": "2023-01-01", "endDate": "2023-12-31"}

try:
    # 2. GET 요청 보내기 (POST 요청인 경우 requests.post() 사용)
    response = requests.get(api_url, headers=headers, params=params, timeout=10)
    response.raise_for_status() # HTTP 오류 발생 시 예외 발생

    # 3. JSON 데이터 파싱
    # response.json() 메서드는 응답 본문을 JSON으로 파싱하여 Python 딕셔너리/리스트로 반환
    data = response.json()
    item = data["item_list"][0]

    my_data = {
        "종목명": item["itemname"],
        "주가변화율": item["change_rate"],
        "주가변화량": item["change_val"],
        "종목코드": item["itemcode"],
        "현재가": item["now_val"]
    }
    print(my_data)
except requests.exceptions.HTTPError as e:
    print(f"HTTP 에러 발생: {e.response.status_code} - {e.response.text}")
except requests.exceptions.ConnectionError as e:
    print(f"네트워크 연결 오류: {e}")
except requests.exceptions.Timeout as e:
    print(f"요청 타임아웃: {e}")
except requests.exceptions.RequestException as e:
    print(f"그 외 요청 오류: {e}")
except json.JSONDecodeError as e:
    print(f"JSON 파싱 오류: {e}. 응답 텍스트: {response.text[:200]}")
except Exception as e:
    print(f"예상치 못한 오류 발생: {e}")

 

 

4️⃣ Selenium과 requests 조합 전략

어떤 상황에서는 Selenium과 requests를 함께 사용하는 것이 가장 효과적인 방법이 될 수 있다.

  • Selenium로 초기 페이지 진입 및 로그인 처리: 복잡한 로그인 과정이나 특정 버튼 클릭을 통해 접근해야 하는 페이지는 Selenium으로 처리한다.
  • API 호출 정보 추출: Selenium으로 로그인 후 개발자 도구의 Network 탭에서 동적으로 로드되는 데이터의 API 호출 정보를 파악한다.
  • requests로 API 직접 호출: 파악된 API URL, 헤더, 파라미터 등을 이용하여 requests로 직접 대량의 데이터를 가져온다. (로그인 후 얻은 세션 쿠키 등을 requests에 전달할 수도 있다.)
  • 데이터 파싱 및 저장: requests로 가져온 JSON 데이터를 파이썬 객체로 변환하고 필요한 정보를 추출하여 저장한다.

이러한 조합은 로그인 등 복잡한 상호작용은 Selenium으로 처리하고, 실제 데이터 추출은 더 빠르고 효율적인 requests로 처리하여 크롤링 효율을 극대화할 수 있다.

 

 

 


2025.07.04 - [[스파르타]내일배움캠프 데이터 분석 트랙] - 38일차 - 데이터 크롤링 1

 

38일차 - 데이터 크롤링 1

튜터김대영 튜터님학습 목표1. 웹의 동작 방식과 주요 구성 요소에 대해 이해합니다.2. HTML과 CSS의 기본적인 구조 및 역할을 파악합니다.3. 웹 개발자 도구의 핵심 기능을 활용하여 웹 페이지를

kminx.tistory.com

2025.07.04 - [[스파르타]내일배움캠프 데이터 분석 트랙] - 38일차 - 데이터 크롤링 2

 

38일차 - 데이터 크롤링 2

튜터김대영 튜터님학습 목표1. requests로 가져온 HTML 데이터를 BeautifulSoup 객체로 변환하는 방법을 익힙니다.2. BeautifulSoup의 다양한 메서드 (find(), find_all(), select(), select_one())를 활용하여 HTML 요소에

kminx.tistory.com