포털 검색 결과 상위 N 건을 긁어 분석하려고 requests + BeautifulSoup 으로 코드를 짰는데 결과 리스트가 빈 배열로 나온다. 선택자를 바꿔 봐도 아무것도 걸리지 않는다.
import requests
from bs4 import BeautifulSoup
url = "https://search.naver.com/search.naver?where=blog&query=" + keyword
soup = BeautifulSoup(requests.get(url).text, "html.parser")
items = soup.select(".sh_blog_top") # 아무것도 안 걸린다
블로그나 오래된 예제에 나오는 CSS 클래스는 그 시점의 마크업이다. 포털은 클래스 이름을 수시로 바꾸고, 지금은 api_subject_bx · lst_view · title_link 같은 전혀 다른 이름을 쓴다. 선택자가 맞지 않으면 예외 없이 빈 리스트를 돌려주므로 코드가 조용히 아무것도 하지 않는다.
이유는 넷이다.
마크업이 바뀌었다. 가장 흔하다.
내용이 자바스크립트로 그려진다. requests 가 받는 것은 초기 HTML 뿐이라 이후 스크립트가 채우는 영역은 비어 있다.
봇으로 판정돼 다른 페이지를 받았다. User-Agent 가 기본값이면 차단 페이지나 축약 페이지가 온다.
로그인·지역·기기에 따라 다른 결과가 온다.
먼저 무엇을 실제로 받았는지 확인한다. 추측하지 않는다.
html = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}).text
open("dump.html", "w", encoding="utf-8").write(html)
print(len(html))
받은 파일을 열어 원하는 텍스트가 들어 있는지 찾는다. 없다면 선택자 문제가 아니라 애초에 그 내용이 응답에 없는 것이다.
for a in soup.select("a[href*='blog.naver.com']"):
print(a.get_text(strip=True), a["href"])
클래스 이름 대신 덜 바뀌는 특징에 건다. 링크의 도메인, 구조적 위치, 텍스트 패턴이 클래스 이름보다 오래간다. 그래도 언젠가는 깨지므로 결과 건수를 검증하는 코드를 함께 둔다.
if not items:
raise RuntimeError("선택자가 더 이상 맞지 않는다. 마크업 확인 필요")
빈 결과를 정상으로 넘기지 않는 것이 핵심이다. 그래야 깨진 시점을 안다.
네이버는 검색 API 를 제공한다. 마크업 변경과 무관하고 JSON 으로 온다.
import os
import requests
res = requests.get(
"https://openapi.naver.com/v1/search/blog.json",
params={"query": keyword, "display": 10, "sort": "sim"},
headers={
"X-Naver-Client-Id": os.environ["NAVER_CLIENT_ID"],
"X-Naver-Client-Secret": os.environ["NAVER_CLIENT_SECRET"],
},
timeout=10,
)
res.raise_for_status()
for item in res.json()["items"]:
print(item["title"], item["link"])
주의할 점이 있다. API 결과 순서는 화면의 검색 결과 순서와 같지 않다. "상위 노출 순위" 를 재는 것이 목적이라면 API 로는 그 값을 얻을 수 없다. 목적이 순위 측정인지 문서 수집인지 먼저 정한다.
호출 한도와 이용 약관도 확인한다.
정말로 렌더링된 화면이 필요하면 헤드리스 브라우저를 쓴다.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(url, wait_until="networkidle")
html = page.content()
browser.close()
비용이 크다. 실행 시간과 메모리가 몇 배로 늘고 브라우저 바이너리를 함께 배포해야 한다. 정적 HTML 로 되는지 먼저 확인한 뒤에 선택한다.
원 대화에서는 선택자를 여러 번 바꿔 봤지만 결과를 얻지 못한 채로 끝났다. 지역 · 시술 · 표현 세 축의 키워드로 상위 10건을 모아 텍스트 마이닝한다는 목표 자체는 남아 있다.
남은 판단은 둘이다. 순위가 꼭 필요하면 스크래핑 외에 방법이 없고, 그렇다면 마크업 변경에 따라 주기적으로 고치는 비용을 감수해야 한다. 순위가 아니라 문서 본문이 목적이면 공식 API 로 바꾸는 편이 유지보수가 훨씬 싸다.
어느 쪽이든 수집 대상 사이트의 robots.txt 와 이용 약관을 먼저 확인한다.