XPath 크롤링 완전 가이드 2026: 웹 데이터 정밀 추출의 핵심

XPath로 웹 크롤링하는 방법을 처음부터 실전까지 완전히 정리했습니다. Python lxml/requests 예제, CSS Selector와 비교, 주요 표현식 레퍼런스까지 2026년 기준으로 작성했습니다.

28
XPath 크롤링 완전 가이드 2026: 웹 데이터 정밀 추출의 핵심

웹 크롤링을 처음 시작할 때 대부분 CSS Selector를 먼저 배웁니다. 그런데 실전에서 조금 복잡한 페이지를 만나면 CSS Selector만으로는 원하는 요소를 집어내기 어려운 상황이 생깁니다. 특정 텍스트를 포함한 요소만 선택하거나, 특정 자식 요소를 가진 부모 노드를 역으로 찾거나, 조건부 필터링을 해야 할 때입니다.

이럴 때 필요한 것이 XPath입니다.

XPath는 XML/HTML 문서에서 특정 노드를 탐색하는 표준 언어입니다. CSS Selector보다 표현력이 강력하고, 일부 경우에는 CSS로는 아예 불가능한 선택을 할 수 있습니다. 2026년 현재도 Python lxml, Scrapy, Playwright, Selenium 등 주요 크롤링 도구가 모두 XPath를 지원합니다.

이 글에서는 XPath의 기본 개념부터 실전 크롤링 코드, 디버깅 팁까지 한 번에 정리합니다.


1. XPath란?

XPath(XML Path Language)는 W3C 표준으로, XML과 HTML 문서의 트리 구조를 탐색하기 위한 쿼리 언어입니다. 1999년 처음 발표된 이후 웹 표준의 핵심 기술로 자리 잡았습니다.

웹 페이지의 HTML은 트리 구조입니다. html 루트에서 시작해 body, 각종 div, span, a 태그들이 부모-자식 관계로 연결됩니다. XPath는 이 트리 구조를 파일 시스템 경로처럼 표현합니다.

예를 들어, 파일 시스템에서 /home/user/documents/report.txt처럼 경로를 지정하듯, HTML에서는 /html/body/div/p 형태로 노드를 특정합니다.

CSS Selector vs XPath: 언제 무엇을 쓸까?

기능 CSS Selector XPath
기본 요소 선택 간결 가능
특정 텍스트 포함 요소 선택 불가 contains(text(), '키워드')
부모 노드 역방향 선택 불가 .. 또는 parent::
형제 노드 접근 제한적 following-sibling::, preceding-sibling::
조건부 필터링 기본적인 것만 복잡한 조건 가능
학습 난이도 쉬움 중간
브라우저 성능 빠름 약간 느림

일반적인 웹 크롤링에서는 CSS Selector를 기본으로 쓰되, CSS로 표현이 안 될 때 XPath를 보조로 사용하는 전략이 효과적입니다.


2. XPath 핵심 문법 레퍼런스

기본 경로 표현식

/html/body/div          # 절대 경로 (루트부터 전체 경로)
//div                   # 문서 전체에서 모든 div 요소
//div[@class='title']   # class가 'title'인 div
//a[@href]              # href 속성이 있는 모든 a 태그

//(이중 슬래시)가 핵심입니다. 실전에서는 절대 경로(/html/body/...)보다 //를 사용한 상대 경로를 훨씬 많이 씁니다. 페이지 구조가 바뀌어도 비교적 안정적이기 때문입니다.

속성 선택자

//div[@id='main']                    # id가 'main'인 div
//input[@type='submit']              # type이 'submit'인 input
//a[contains(@class, 'btn')]         # class에 'btn'이 포함된 a
//img[@src and @alt]                 # src와 alt 속성 모두 있는 img
//a[not(@href)]                      # href 없는 a 태그

contains()는 CSS의 [class*="btn"]과 유사하지만, 텍스트 내용에도 적용할 수 있어 훨씬 유연합니다.

텍스트 선택자

//h2[text()='이벤트 공지']                    # 정확히 '이벤트 공지' 텍스트인 h2
//p[contains(text(), '할인')]                  # '할인' 텍스트를 포함하는 p
//button[normalize-space(text())='구매하기']   # 공백 제거 후 '구매하기'인 버튼

이것이 XPath의 핵심 강점입니다. CSS Selector는 텍스트 내용으로 요소를 선택할 수 없지만, XPath는 text()contains()를 조합해 자유롭게 텍스트 기반 선택이 가능합니다.

축(Axis) 탐색

//span[@class='price']/parent::div          # 특정 span의 부모 div
//td[contains(@class, 'total')]/../td[1]    # 형제 td 첫 번째
//h3/following-sibling::p                   # h3 다음에 오는 형제 p 태그
//li[last()]                                # 마지막 li 항목
//li[position() <= 5]                       # 첫 5개 li 항목

부모 역방향 탐색 은 XPath에서만 가능한 기능입니다. 예를 들어 특정 클래스의 버튼을 포함하는 카드 컨테이너 전체를 가져오고 싶을 때 유용합니다.


3. Python으로 XPath 크롤링 (lxml)

기본 설치 및 구조

import requests
from lxml import html

url = "https://example.com/products"
response = requests.get(url, headers={
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
})

tree = html.fromstring(response.content)

# XPath로 데이터 추출
titles = tree.xpath('//h2[@class="product-title"]/text()')
prices = tree.xpath('//span[@class="price"]/text()')
links = tree.xpath('//a[@class="product-link"]/@href')

for title, price, link in zip(titles, prices, links):
    print(f"{title} | {price} | {link}")

lxml.html.fromstring()으로 HTML을 파싱한 뒤, .xpath() 메서드에 XPath 표현식을 전달하면 됩니다. 반환값은 리스트입니다.

텍스트 vs @속성 선택

# 요소 텍스트 추출: /text()
titles = tree.xpath('//h1/text()')

# 속성값 추출: /@속성명
hrefs = tree.xpath('//a/@href')
images = tree.xpath('//img/@src')

# 요소 객체 자체를 가져올 때 (하위 탐색 필요 시)
product_cards = tree.xpath('//div[@class="product-card"]')
for card in product_cards:
    title = card.xpath('.//h2/text()')  # 점(.)은 현재 요소 기준
    price = card.xpath('.//span[@class="price"]/text()')
    print(title[0] if title else "N/A", price[0] if price else "N/A")

반드시 알아야 할 것: 요소 객체를 loop로 순회할 때 내부 .xpath()에는 점(.)을 붙여 현재 요소 기준으로 탐색해야 합니다. 점 없이 //h2/text()를 쓰면 전체 문서에서 다시 찾습니다.

contains()로 유연한 선택

# class에 특정 단어가 포함된 경우
items = tree.xpath('//*[contains(@class, "item")]')

# 텍스트에 특정 단어가 포함된 링크
discount_links = tree.xpath('//a[contains(text(), "할인")]/@href')

# 여러 조건 조합 (and/or)
main_buttons = tree.xpath('//button[contains(@class, "btn") and @type="submit"]')

4. 동적 페이지 크롤링: Playwright + XPath

정적 HTML은 requests + lxml로 충분하지만, JavaScript로 렌더링되는 동적 페이지는 Playwright나 Selenium이 필요합니다. 두 도구 모두 XPath를 지원합니다.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://target-site.com")

    # XPath로 요소 선택
    # locator 방식 (권장)
    price = page.locator('xpath=//span[@class="final-price"]').first.text_content()

    # evaluate로 XPath 직접 실행
    result = page.evaluate("""
        () => {
            const el = document.evaluate(
                '//h1[@class="product-name"]/text()',
                document,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
            ).singleNodeValue;
            return el ? el.nodeValue : null;
        }
    """)

    print(price, result)
    browser.close()

Playwright의 locator('xpath=...')는 XPath 표현식 앞에 xpath= 접두어를 붙이면 됩니다.


5. XPath 디버깅: 브라우저에서 바로 테스트

크롤링 코드를 짜기 전에 XPath가 올바른지 브라우저에서 먼저 확인할 수 있습니다.

Chrome 개발자 도구 사용법:
1. F12 또는 오른쪽 클릭 → 검사
2. Console 탭 선택
3. 아래 코드 입력:

// 단일 요소 찾기
$x('//h1[@class="title"]')

// 모든 매칭 요소 텍스트 출력
$x('//span[@class="price"]').map(e => e.textContent)

// 첫 번째 매칭 텍스트
$x('//div[@class="product-name"]')[0]?.textContent

$x()는 Chrome과 Firefox 모두에서 사용 가능한 내장 XPath 테스트 함수입니다. 코드 작성 전에 원하는 요소가 올바르게 선택되는지 반드시 확인하세요.


6. 실전 예제: 이커머스 상품 데이터 추출

import requests
from lxml import html

def scrape_product_list(url):
    headers = {"User-Agent": "Mozilla/5.0 (compatible; DataCollector/1.0)"}
    resp = requests.get(url, headers=headers, timeout=10)
    tree = html.fromstring(resp.content)

    # 상품 카드 전체 목록 가져오기
    cards = tree.xpath('//li[contains(@class, "product-item")]')

    results = []
    for card in cards:
        name = card.xpath('.//strong[@class="product-name"]/text()')
        price = card.xpath('.//span[contains(@class, "price")]/text()')
        rating = card.xpath('.//span[@class="rating"]/text()')
        reviews = card.xpath('.//span[@class="review-count"]/text()')
        link = card.xpath('.//a[@class="product-link"]/@href')

        results.append({
            "name": name[0].strip() if name else None,
            "price": price[0].strip() if price else None,
            "rating": rating[0].strip() if rating else None,
            "reviews": reviews[0].strip() if reviews else None,
            "url": link[0] if link else None,
        })

    return results

products = scrape_product_list("https://example-shop.com/category/shoes")
print(f"수집된 상품 수: {len(products)}")

7. 자주 발생하는 오류와 해결법

IndexError: list index out of range

# 잘못된 방법
title = tree.xpath('//h1/text()')[0]  # 요소 없으면 에러 발생

# 올바른 방법
titles = tree.xpath('//h1/text()')
title = titles[0] if titles else None

요소는 보이는데 XPath로 안 잡힐 때

원인 대부분은 네임스페이스 또는 동적 렌더링 문제입니다.

# 네임스페이스가 있는 경우
namespaces = {'ns': 'http://www.w3.org/1999/xhtml'}
result = tree.xpath('//ns:div[@id="content"]', namespaces=namespaces)

# 실제로 화면에는 있지만 XPath로 안 잡힐 때 → 동적 렌더링 확인
# requests로 받은 HTML에는 없고, 브라우저에서만 보이는 경우
# → Playwright/Selenium으로 전환 필요

텍스트 추출 시 공백 문제

# normalize-space()로 앞뒤 공백 및 연속 공백 제거
clean_text = tree.xpath('normalize-space(//h1/text())')

# Python에서 후처리
texts = [t.strip() for t in tree.xpath('//p/text()') if t.strip()]

8. XPath의 한계와 대안

XPath가 강력하긴 하지만 한계도 있습니다.

구조 변경에 취약: 절대 경로(/html/body/div[2]/div[3]/ul/li)를 사용하면 사이트 리디자인 한 번에 전체 XPath가 무용지물이 됩니다. 가능하면 @id, @class 속성 기반 XPath를 사용하세요.

JavaScript 렌더링: XPath는 HTML 파싱 도구이지, 렌더링 도구가 아닙니다. 동적으로 로딩되는 컨텐츠는 requests + lxml 조합으로는 수집 불가능합니다.

대규모 크롤링: 수천 개 이상의 페이지를 수집할 때는 lxml 단독보다 Scrapy를 활용하는 것이 훨씬 효율적입니다. Scrapy는 XPath와 CSS Selector를 모두 지원하고 비동기 처리, 미들웨어, 파이프라인을 갖추고 있습니다.



정리

  • XPath는 CSS Selector로 해결 안 되는 복잡한 선택 에 사용합니다
  • 텍스트 기반 선택, 부모 역방향 탐색, 조건부 필터링 이 핵심 강점입니다
  • Python: lxml + .xpath() 조합이 가장 보편적
  • 동적 페이지: Playwright locator('xpath=...') 사용
  • 디버깅: Chrome 콘솔 $x()로 빠르게 테스트
  • 대규모 크롤링은 Scrapy 같은 프레임워크 활용을 검토하세요

이 글이 도움이 됐다면, Playwright 크롤링 완전 가이드도 함께 읽어보세요.

댓글

댓글 작성

이메일은 공개되지 않으며, 답글 알림에만 사용됩니다.

이어서 읽어보세요

새 글 알림 받기

해시스크래퍼 기술 블로그의 새 글이 발행되면 이메일로 알려드립니다.

이메일은 새 글 알림에만 사용됩니다.