Python으로 Bing 검색 결과를 스크랩하는 방법

댓글: 0

웹 분석은 Google에만 국한되지 않습니다. Bing은 SEO 연구, 링크 탐색, 브랜드 모니터링, 경쟁 분석 및 콘텐츠 조사에 유용한 SERP에 대한 대안적인 보기를 제공합니다. Python은 이러한 종류의 자동화에 이상적인 도구로, 성숙한 에코시스템, 간단한 구문, HTML 구문 분석 및 JSON 작업을 위한 강력한 라이브러리를 통해 Bing 검색 결과를 보다 빠르고 편리하게 스크랩할 수 있습니다.

왜 구글이 아닌 빙에 집중해야 할까요?

Bing은 자체 순위 가이드라인과 품질 신호를 사용하므로 결과가 Google과 다른 경우가 많습니다. 이는 자연 검색 및 롱테일 쿼리에서 추가적인 기회를 발견하는 데 유용합니다. 웹마스터 추천에서 Bing은 관련성, 품질/신뢰도, 사용자 참여, 최신성, 지리적 요인, 페이지 속도를 강조하는데, 이는 Google과는 다른 신호의 균형입니다. 그렇기 때문에 일부 페이지가 Bing에서 특히 더 높은 순위를 차지하는 것입니다.

Bing 검색 결과를 스크랩할 때의 실제 사용 사례:

  • 링크 구축 기부자 목록 확장 - 이 엔진은 때때로 Google 상위 10위 안에 들지 않는 사이트도 순위를 올려줍니다.
  • PAA("사람들은 또한 묻습니다") 및 Bing의 범용 SERP 요소(동영상, 캐러셀)를 추적하여 콘텐츠 전략을 조정합니다.

Bing 검색에서 어떤 데이터를 추출할 수 있나요?

"클래식" SERP에서 안정적으로 추출할 수 있습니다:

  • 제목;
  • URL(문서 링크);
  • 스니펫(설명);
  • 결과 내 위치(서수 인덱스);
  • 일부 보편적인 결과: '관련/사람들도 묻는 질문', 임베드된 이미지/비디오 결과(메인 SERP에 직접 포함된 경우).

중요: Bing의 마크업은 주기적으로 변경되므로 아래 코드의 선택기를 조정해야 할 수 있습니다.

스크랩 빙 검색 시 법적 및 윤리적 고려 사항

  • Microsoft의 사용 약관에 따라 웹 데이터에 대한 "공식" 액세스를 위해 Microsoft는 이제 Azure AI 에이전트의 일부로 Bing Search를 통한 Grounding을 제공합니다. 공개 Bing Search API는 2025년 8월 11일에 완전히 일몰되었습니다.
  • Bing Search를 사용한 접지에는 고유한 TOU 및 제약 조건이 있습니다. Azure 에이전트를 통해 사용되며, 결과는 '원시' JSON SERP 데이터가 아닌 에이전트의 응답으로 반환됩니다.
  • robots.txt를 준수하고 호스트에 과부하가 걸리지 않도록 하는 것은 스크래핑 윤리의 기본입니다.

스크래핑을 위한 Python 환경 설정하기

기본 사항을 설치합니다:

pip install requests beautifulsoup4 lxml fake-useragent selenium
  • 요청 - HTTP 클라이언트(사용자 에이전트 등의 헤더를 설정할 수 있음);
  • beautifulsoup4 + lxml - HTML 파싱;
  • 가짜 사용자 에이전트 - 무작위 UA 생성(또는 자체 목록 작성);
  • 셀레늄 - 필요할 때 동적 블록을 렌더링합니다.

방법 1 - 요청 및 BeautifulSoup을 통한 Bing 스크래핑

이를 워크플로우 데모의 기준으로 삼아 GET 요청을 발행하고, 사용자 에이전트를 설정하고, 결과 카드를 구문 분석하고, 제목, URL, 스니펫 및 위치를 수집하겠습니다.

import time
import random
from typing import List, Dict
import requests
from bs4 import BeautifulSoup

BING_URL = "https://www.bing.com/search"

HEADERS_POOL = [
    # You can add more — or use fake-useragent
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15 "
    "(KHTML, like Gecko) Version/17.0 Safari/605.1.15",
]

def fetch_serp(query: str, count: int = 10, first: int = 1,
               proxy: str | None = None) -> List[Dict]:
    """
Returns a list of results: title, url, snippet, position.
`first` — starting position (pagination), `count` — how many records to fetch.

    """
    params = {"q": query, "count": count, "first": first}
    headers = {"User-Agent": random.choice(HEADERS_POOL)}
    proxies = {"http": proxy, "https": proxy} if proxy else None

    resp = requests.get(BING_URL, params=params, headers=headers,
                        proxies=proxies, timeout=15)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "lxml")

   # Typical Bing markup: <li class="b_algo"> ... <h2><a href="">Title</a></h2>
    items = []
    for idx, li in enumerate(soup.select("li.b_algo"), start=first):
        a = li.select_one("h2 a")
        if not a:
            continue
        title = a.get_text(strip=True)
        url = a.get("href")
         # Snippet is often in .b_caption p or simply the first <p>
        sn_el = li.select_one(".b_caption p") or li.select_one("p")
        snippet = sn_el.get_text(" ", strip=True) if sn_el else ""
        items.append({
            "position": idx,
            "title": title,
            "url": url,
            "snippet": snippet
        })
    return items

if __name__ == "__main__":
    data = fetch_serp("python web scraping tutorial", count=10)
    for row in data:
        print(f"{row['position']:>2}. {row['title']} -- {row['url']}")
        print(f"   {row['snippet']}\n")

설명:

  • 페이지 매김에 개수/첫 번째 매개변수를 사용합니다.
  • 선택기 li.b_algo h2 a 및 .b_caption p는 기본값이며, 레이아웃은 변경될 수 있습니다(개발 도구에서 검사).
  • 필요한 경우 프록시를 추가하고 요청 간 일시 중지를 조절할 수 있습니다.
  • 이 예제는 현재 상황에서 가장 효과적인 접근 방식이므로 아래에서 조금 더 개선해 보겠습니다.

방법 2 - API를 통한 Bing 검색 결과 스크랩(2025년 상태)

Microsoft의 공용 Bing 스크레이퍼 API는 2025년 8월에 사용 중단되었습니다. Microsoft는 Azure AI 에이전트 내에서 Bing Search를 사용한 Grounding으로 마이그레이션할 것을 권장합니다.

이것이 실제로 의미하는 것

  • 대부분의 개발자는 '원시' JSON SERP 데이터를 사용하는 기존 REST 엔드포인트를 더 이상 사용할 수 없습니다.
  • Bing Search를 통한 접지는 Azure 에이전트 내부의 도구로 연결되며, 에이전트는 웹을 '조회'하고 합성된 답변을 반환할 수 있습니다. 이 서비스에는 고유한 TOU 및 세부 사항이 있으며, 원시 SERP 결과의 대량 추출을 위해 설계되지 않았습니다.

JSON의 원시 SERP에 대한 대안

제목, URL, 스니펫, 위치 등 구조화된 결과를 반환하는 타사 SERP API/플랫폼(예: Apify Bing 검색 스크레이퍼)을 사용하세요.

최소 Apify 요청 예시:

import requests

API_TOKEN = "apify_xxx"  # store in ENV
actor = "tri_angle/bing-search-scraper"
payload = {
    "queries": ["python web scraping tutorial"],
    "countryCode": "US",
    "includeUnfilteredResults": False
}

r = requests.post(
    f"https://api.apify.com/v2/acts/{actor}/runs?token={API_TOKEN}",
    json=payload, timeout=30
)
run = r.json()
# Retrieve dataset items using run['data']['defaultDatasetId']

오가닉 결과, PAA, 관련 쿼리 등에 대한 문서 지원을 제공합니다. 사용 사례가 플랫폼 규칙과 관할 지역의 법률을 준수하는지 확인하세요.

팁: Azure AI 에이전트 스택에서 작업하고 원시 JSON이 아닌 LLM에 대한 접지된 참조만 필요한 경우 다음에 대한 가이드를 읽어보세요. Bing 검색으로 접지하기.

방법 3 - 셀레늄으로 동적 콘텐츠 구문 분석하기

SERP에 캐러셀, 대화형 블록 또는 자바스크립트로 렌더링된 콘텐츠가 포함된 경우, 셀레늄(헤드리스 크롬/파이어폭스)으로 전환하세요.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options

def selenium_bing(query: str, headless: bool = True):
    opts = Options()
    if headless:
        opts.add_argument("--headless=new")
    opts.add_argument("--disable-gpu")
    opts.add_argument("--no-sandbox")
    with webdriver.Chrome(options=opts) as driver:
        driver.get("https://www.bing.com/")
        box = driver.find_element(By.NAME, "q")
        box.send_keys(query)
        box.submit()

        # Consider adding explicit waits via WebDriverWait
        cards = driver.find_elements(By.CSS_SELECTOR, "li.b_algo h2 a")
        results = []
        for i, a in enumerate(cards, start=1):
            results.append({"position": i, "title": a.text, "url": a.get_attribute("href")})
        return results

if __name__ == "__main__":
    print(selenium_bing("site:docs.python.org requests headers"))

공식 셀레늄 문서 드라이버 설치 및 WebDriverWait 예제를 참조하십시오.

실용적인 솔루션: 구문 분석 전략 및 예제 코드

최종 구현을 위해 HTML에서 직접 Bing 스크래핑을 수행하겠습니다:

  1. https://www.bing.com/search 으로 HTTP 요청을 보냅니다.
  2. 사용자 에이전트를 설정합니다.
  3. BeautifulSoup + lxml을 통해 HTML을 구문 분석하여 제목, URL, 스니펫을 추출합니다.

이렇게 하면 Microsoft 계정이 필요하지 않으며 타사 유료 API에 종속되지 않습니다. 결과 선택에는 Bing의 오가닉 블록에 일반적으로 사용되는 결과 카드 컨테이너 li.b_algo를 사용합니다.

작업 예제(페이지 매김, 지연, 프록시 옵션)

from __future__ import annotations

import argparse
import csv
import dataclasses
import pathlib
import random
import sys
import time
from typing import List, Optional, Tuple

import requests
from bs4 import BeautifulSoup, FeatureNotFound

BING_URL = "https://www.bing.com/search"

# Pool of user agents
UA_POOL = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
]

@dataclasses.dataclass
class SerpItem:
    position: int
    title: str
    url: str
    snippet: str


def build_session(proxy: Optional[str] = None) -> requests.Session:
    """Create a session with baseline headers and an optional proxy."""
    s = requests.Session()
    s.headers.update(
        {
            "User-Agent": random.choice(UA_POOL),
            "Accept-Language": "uk-UA,uk;q=0.9,en;q=0.8",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        }
    )
    if proxy:
        # Requests proxy dict format: {'http': 'http://host:port', 'https': 'http://host:port'}
        s.proxies.update({"http": proxy, "https": proxy})
    return s


def _soup_with_fallback(html: str) -> BeautifulSoup:
    """Parse HTML with a forgiving fallback chain: lxml -> html.parser -> html5lib (if available)."""
    for parser in ("lxml", "html.parser", "html5lib"):
        try:
            return BeautifulSoup(html, parser)
        except FeatureNotFound:
            continue
    # If none are installed, bs4 will raise; let it propagate
    return BeautifulSoup(html, "html.parser")


def parse_serp_html(html: str, start_pos: int) -> List[SerpItem]:
    """Extract organic results from Bing SERP HTML."""
    soup = _soup_with_fallback(html)
    items: List[SerpItem] = []

    # Organic blocks typically look like <li class="b_algo"> with h2>a and a snippet under .b_caption p or the first <p>.
    for i, li in enumerate(soup.select("li.b_algo"), start=start_pos):
        a = li.select_one("h2 > a")
        if not a:
            continue
        title = (a.get_text(strip=True) or "").strip()
        url = a.get("href") or ""
        p = li.select_one(".b_caption p") or li.select_one("p")
        snippet = (p.get_text(" ", strip=True) if p else "").strip()
        items.append(SerpItem(position=i, title=title, url=url, snippet=snippet))

    return items


def fetch_bing_page(
    session: requests.Session,
    query: str,
    first: int = 1,
    count: int = 10,
    cc: str = "UA",
    setlang: str = "uk",
    timeout: int = 20,
) -> List[SerpItem]:
    """Download one results page and return parsed items."""
    params = {
        "q": query,
        "count": count,   # 10, 15, 20...
        "first": first,   # 1, 11, 21...
        "cc": cc,         # country code for results
        "setlang": setlang,  # interface/snippet language
    }
    r = session.get(BING_URL, params=params, timeout=timeout)
    r.raise_for_status()
    return parse_serp_html(r.text, start_pos=first)


def search_bing(
    query: str,
    pages: int = 1,
    count: int = 10,
    pause_range: Tuple[float, float] = (1.2, 2.7),
    proxy: Optional[str] = None,
    cc: str = "UA",
    setlang: str = "uk",
    timeout: int = 20,
) -> List[SerpItem]:
    """Iterate over pages and return an aggregated list of results."""
    session = build_session(proxy=proxy)
    all_items: List[SerpItem] = []
    first = 1
    for _ in range(pages):
        items = fetch_bing_page(
            session, query, first=first, count=count, cc=cc, setlang=setlang, timeout=timeout
        )
        all_items.extend(items)
        time.sleep(random.uniform(*pause_range))  # polite delay
        first += count
    return all_items


def _normalize_cell(s: str) -> str:
    """Optional: collapse internal whitespace so simple viewers show one‑line cells."""
    # Convert tabs/newlines/multiple spaces to a single space
    return " ".join((s or "").split())


def save_csv(
    items: List[SerpItem],
    path: str,
    excel_friendly: bool = False,
    normalize: bool = False,
    delimiter: str = ",",
) -> int:
    """
Write results to CSV.
— excel_friendly=True -> write UTF‑8 with BOM (utf‑8‑sig) so Excel auto‑detects Unicode.
— normalize=True -> collapse whitespace inside string fields.
— delimiter -> change if your consumer expects ';', etc.
Returns the number of rows written (excluding header).

    """
    p = pathlib.Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)

    encoding = "utf-8-sig" if excel_friendly else "utf-8"

    # newline='' is required so Python's csv handles line endings correctly on all platforms
    with p.open("w", newline="", encoding=encoding) as f:
        writer = csv.DictWriter(
            f,
            fieldnames=["position", "title", "url", "snippet"],
            delimiter=delimiter,
            quoting=csv.QUOTE_MINIMAL,
        )
        writer.writeheader()
        for it in items:
            row = dataclasses.asdict(it)
            if normalize:
                row = {k: _normalize_cell(v) if isinstance(v, str) else v for k, v in row.items()}
            writer.writerow(row)
    return len(items)


def main() -> int:
    ap = argparse.ArgumentParser(description="Bing SERP scraper (Requests + BS4)")
    ap.add_argument("-q", "--query", required=True, help="Search query")
    ap.add_argument("--pages", type=int, default=1, help="Number of pages (x count)")
    ap.add_argument("--count", type=int, default=10, help="Results per page")
    ap.add_argument("--cc", default="UA", help="Country code for results (cc)")
    ap.add_argument("--setlang", default="uk", help="Interface/snippet language (setlang)")
    ap.add_argument("--proxy", help="Proxy, e.g. http://user:pass@host:port")
    ap.add_argument("--csv", help="Path to CSV to save results")
    ap.add_argument(
        "--excel-friendly",
        action="store_true",
        help="Add BOM (UTF‑8‑SIG) so Excel opens the file correctly",
    )
    ap.add_argument(
        "--normalize-cells",
        action="store_true",
        help="Remove line breaks and extra spaces in cells",
    )
    ap.add_argument(
        "--delimiter",
        default=",",
        help="CSV delimiter (default ','); e.g.: ';'",
    )
    args = ap.parse_args()

    try:
        items = search_bing(
            args.query,
            pages=args.pages,
            count=args.count,
            proxy=args.proxy,
            cc=args.cc,
            setlang=args.setlang,
        )
    except requests.HTTPError as e:
        print(f"[ERROR] HTTP error: {e}", file=sys.stderr)
        return 2
    except requests.RequestException as e:
        print(f"[ERROR] Network error: {e}", file=sys.stderr)
        return 2

    if args.csv:
        try:
            n = save_csv(
                items,
                args.csv,
                excel_friendly=args.excel_friendly,
                normalize=args.normalize_cells,
                delimiter=args.delimiter,
            )
            print(f"Saved {n} rows to {args.csv}")
        except OSError as e:
            print(f"[ERROR] Could not write CSV to {args.csv}: {e}", file=sys.stderr)
            return 3
    else:
        for it in items:
            print(f"{it.position:>2}. {it.title} -- {it.url}")
            if it.snippet:
                print("   ", it.snippet[:180])

    return 0


if __name__ == "__main__":
    sys.exit(main())

추가 매개변수와 프록시를 사용한 사용 예시입니다:

python bing_scraper.py -q "Python web scraping" --pages 3 --csv out.csv \
  --proxy "http://username:password@proxy:port"

스크립트의 기능

  1. 제어된 매개변수(q, count, first) 및 로캘 설정(cc, setlang)을 사용하여 Bing에 GET 요청을 보냅니다.
  2. 보다 안정적인 스니펫을 위해 User-Agent를 재정의하고 Accept-Language를 추가합니다.
  3. BeautifulSoup(..., "lxml")을 통해 HTML을 구문 분석하고 결과 카드 li.b_algo를 찾은 다음 제목, URL, 스니펫을 추출합니다. BS4의 .select() CSS 선택기는 표준적이고 유연한 접근 방식입니다.
  4. 선택적 프록시를 지원합니다. 요청의 경우 올바른 프록시 형식은 프로토콜→URL 매핑입니다.

안정성 팁:

  • 일시 중지 추가(요청 간격을 무작위로 지정).
  • 사용자-에이전트 회전(동적으로 또는 목록에서). 요청에서 헤더를 올바르게 설정하는 방법을 보여드리며, 작업 예제에서는 이 작업을 수행합니다.
  • 필요한 경우 프록시 인프라/IP 로테이션을 사용하여 플랫폼 제한 내에서 효과적으로 확장하세요.
  • 전체 요청량을 적정 수준으로 유지하고 캡차 프롬프트에 대한 응답을 확인합니다.
  • 복잡한 시나리오의 경우 안티봇 인프라가 포함된 관리형 SERP API(Apify 등)를 고려하세요.

도구에 대해 자세히 알아볼 수 있는 곳

팁: 보다 안정적인 데이터 수집을 위해 프록시 인프라가 필요한 경우, 다음을 확인하세요 Bing을 위한 최고의 프록시.

Bing 스크래핑 시 차단을 피하는 방법

스크레이퍼가 첫 번째 주기 동안 "죽지 않도록" 하는 핵심 원칙입니다:

  • 지연을 추가합니다(요청 간격을 무작위로 지정).
  • 사용자 에이전트(동적으로 또는 자체 목록에서)를 회전하고 요청에서 헤더를 설정하는 올바른 방법은 문서에 설명되어 있으며, 작업 예제에서도 동일한 접근 방식을 사용합니다.
  • 프록시 또는 IP 로테이션 사용(서비스 이용 약관을 준수).
  • 전체 요청 수를 제한하고 캡차 프롬프트에 대한 응답을 모니터링합니다.
  • 복잡한 작업의 경우 안티봇 인프라가 내장된 관리형 SERP API(Apify 등)를 고려하세요.

결론

Bing 스크래핑은 Google을 넘어 연구를 확장하고, 추가 제공자 도메인을 수집하고, 대체 SERP 기능을 추적하고, 환경에 대한 독립적인 시각을 얻고자 할 때 유용합니다. 안정적이고 "공식적인" 통합을 위해 Microsoft는 Azure AI 에이전트에서 Bing Search와의 접목을 권장하며, 이는 서비스 약관 관점에서 더 안전하지만 원시 JSON SERP 데이터를 반환하지 않습니다. 구조화된 결과를 추출해야 하는 작업이라면 Requests/BS4 또는 Selenium을 통한 직접 HTML 구문 분석을 선택하거나 특수 SERP API를 사용하세요. 프로토타입을 위한 빠른 HTML 구문 분석, LLM 기반 답변을 위한 에이전트, 대규모 수집을 위한 SERP API 등 작업에 적합한 도구를 선택하세요.

댓글:

0 댓글