Як спарсити результати пошуку Bing за допомогою Python

Коментарі: 0

Веб-аналітика не обмежується Google: Bing дає альтернативний зріз пошукової видачі, корисної для SEO-досліджень, збору посилань, моніторингу брендів, конкурентного аналізу й контент-ресерчу. Python — ідеальний інструмент для такої автоматизації: багата екосистема, зрозумілий синтаксис і зрілі бібліотеки для html-парсингу та роботи з json роблять парсинг результатів пошуку Bing більш зручним і швидким.

Чому варто парсити саме Bing, а не Google?

Bing має власні настанови ранжування й сигналів якості, тож результати часто відрізняються від Google, що корисно для виявлення додаткових можливостей у органіці та «довгих хвостів» (long tail-запити). У рекомендаціях для вебмайстрів Bing підкреслює релевантність, якість/довіру, залученість користувачів, свіжість, гео-фактори та швидкість сторінки — інший баланс сигналів, ніж у Google. Це пояснює, чому деякі сторінки показуються вище саме в Bing

Практичні кейси парсингу результатів пошуку Bing:

  • Розширення списку донорів для лінкбілдингу — ця пошукова система інколи забирає у ТОП сайти, яких немає у ТОП-10 Google.
  • Трекінг PAA-блоків («People also ask») і універсальних елементів видачі Bing (відео, каруселі), щоб адаптувати контент-стратегію.

Які дані можна отримати з пошуку Bing

Із «звичайної» SERP реально дістати:

  • Заголовок (title);
  • URL (посилання на документ);
  • Сніпет (опис);
  • Позицію у видачі (порядковий номер);
  • Деякі елементи універсальної видачі: блоки «Related/People also ask», результати зображень/відео (коли вони вбудовані в загальну SERP).

Важливо: верстка Bing періодично змінюється, тож селектори у коді нижче можуть потребувати поправок.

Правові та етичні аспекти парсингу результатів пошуку Bing

  • Дотримуйтеся умов використання Microsoft: для «офіційного» доступу до веб-даних Microsoft тепер пропонує Grounding with Bing Search у складі Azure AI Agents. Публічні Bing Search API були повністю виведені з експлуатації 11 серпня 2025 року.
  • Grounding with Bing Search має власні умови (TOU) й обмеження: сервіс використовується через агентів Azure, а результати повертаються у відповідях агента, а не як «сирий» json серп-даних.
  • Поважайте robots.txt і не перевантажуйте хости: дотримання robots — базова етика парсингу.

Налаштування оточення Python для парсингу

Встановіть базові пакети:


pip install requests beautifulsoup4 lxml fake-useragent selenium
  • requests — HTTP-клієнт (додаєте заголовки, наприклад User-Agent).
  • beautifulsoup4 + lxml — парсинг html.
  • fake-useragent — генерація випадкового UA (або зберіть власний список).
  • selenium — рендеринг динамічних блоків, коли потрібно.

Метод 1 — парсинг Bing через Requests і BeautifulSoup

Візьмімо цей варіант за основу для демонстрації алгоритму роботи: виконання GET-запитів, підміна User-Agent, парсинг карток результатів і збір title, 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 = [
    # Можна додати більше -- або використати 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]:
    """
    Повертає список результатів: title, url, snippet, position.
    first -- з якої позиції починати (пагінація), count -- скільки записів.
    """
    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")

    # Типова розмітка Bing: <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")
        # Сніпет часто у .b_caption p або просто перший <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")

Пояснення:

  • Використовуємо параметри count/first для пагінації.
  • Селектори li.b_algo h2 a та .b_caption p — базові; інколи верстка змінюється (перевіряйте у DevTools).
  • За потреби додайте проксі і регулюйте паузи між запитами.
  • Важливо: трошки нижче ми покращимо та розширимо цей приклад, так як він є найефективніший для наших потреб та за поточних умов.

Метод 2 — парсинг результатів пошуку Bing через API (актуальний стан у 2025)

Публічний парсер Bing у вигляді Bing Web Search API від Microsoft було скасовано 11 серпня 2025. Microsoft офіційно рекомендує міграцію на Grounding with Bing Search у складі Azure AI Agents.

Що це означає на практиці

  • Класичний REST-ендпойнт з «чистим» json SERP-даних більше недоступний для більшості розробників.
  • Grounding with Bing Search підключається як інструмент усередині агента Azure; агент може «підглянути» у веб і повернути узагальнену відповідь. Сервіс має окремі TOU і специфіку: не призначений для «масового знімання» сирих результатів.

Альтернатива для сирого SERP у json

Скористайтеся стороннім SERP-API/платформами (напр., Apify Bing Search Scraper), які повертають структуровані результати: заголовок, URL, сніпет, позицію тощо. Приклад мінімального запиту на Apify:


import requests

API_TOKEN = "apify_xxx"  # збережіть у 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()
# Отримати дані з key-value store (по run['data']['defaultDatasetId'])

Apify задокументовано підтримує органічні результати, PAA, пов’язані запити тощо. Переконайтеся, що ваш use-case відповідає правилам майданчика і законам вашої юрисдикції.

Важливо: Якщо працюєте у стеку Azure AI Agents і вам достатньо «довідкових» відповідей для LLM (а не сирого json), читайте гайд по Grounding with Bing Search.

Метод 3 — парсинг динамічного контенту за допомогою Selenium

Коли у видачі з’являються каруселі, інтерактивні блоки або контент рендериться JavaScript-ом, підключайте Selenium (Headless Chrome/Firefox).


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()

        # Очікування можна додати через 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"))

Документація Selenium з прикладами інсталяції драйверів і WebDriverWait знаходиться в офіційних гайдах.

Вирішення на практиці: розбір і приклад коду

Підхід котрий ми вибрали для остаточної реалізації задачі виконує парсинг Bing напряму з HTML:

  1. Надсилаємо HTTP-запити до https://www.bing.com/search.
  2. Підставляємо User-Agent.
  3. Розбираємо html через BeautifulSoup + lxml і дістаємо заголовки, URL та сніпети.

Завдяки такому підходу ми не реєструємося у сервісах Microsoft і не залежимо від сторонніх платних API. Для вибірки результатів використовуємо контейнер картки видачі li.b_algo, який зазвичай використовують під час обробки SERP Bing.

Робочий приклад коду (пагінація, затримки, опція проксі)


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"

# Пул юзер-агентів
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:
    """Створює сесію з базовими заголовками та необов’язковим проксі"""
    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: {'http': 'http://host:port', 'https': 'http://host:port'}
        s.proxies.update({"http": proxy, "https": proxy})
    return s


def _soup_with_fallback(html: str) -> BeautifulSoup:
    """Розбір HTML із «поблажливим» ланцюжком запасних парсерів: lxml -> html.parser -> html5lib (за наявності)."""
    for parser in ("lxml", "html.parser", "html5lib"):
        try:
            return BeautifulSoup(html, parser)
        except FeatureNotFound:
            continue
    # Якщо жодного немає, bs4 згенерує виняток; пропускаємо його далі
    return BeautifulSoup(html, "html.parser")


def parse_serp_html(html: str, start_pos: int) -> List[SerpItem]:
    """Витягуємо органічні результати з HTML видачі Bing"""
    soup = _soup_with_fallback(html)
    items: List[SerpItem] = []

    # Органічні блоки зазвичай мають вигляд <li class="b_algo"> з h2>a та фрагментом під .b_caption p або першим <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]:
    """Завантажує одну сторінку видачі та повертає розібрані елементи."""
    params = {
        "q": query,
        "count": count,   # 10, 15, 20...
        "first": first,   # 1, 11, 21...
        "cc": cc,         # код країни для результатів
        "setlang": setlang,  # мова інтерфейсу/сніпетів
    }
    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]:
    """Перегортає сторінки видачі та повертає агрегований список результатів"""
    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))  # ввічлива затримка
        first += count
    return all_items


def _normalize_cell(s: str) -> str:
    """Необов’язково: стискає внутрішні пропуски, щоб у простих переглядачах комірки були в один рядок."""
    # Перетворює таби/переноси рядка/множинні пробіли на один пробіл
    return " ".join((s or "").split())


def save_csv(
    items: List[SerpItem],
    path: str,
    excel_friendly: bool = False,
    normalize: bool = False,
    delimiter: str = ",",
) -> int:
    """
Записує результати у CSV.
  — excel_friendly=True -> записує UTF-8 з BOM (utf-8-sig), щоб Excel автоматично розпізнав Unicode.
  — normalize=True -> стискає пропуски всередині рядкових полів.
  — delimiter -> змініть, якщо ваш споживач очікує ';' тощо.
Повертає кількість записаних рядків (без заголовка).
    """
    p = pathlib.Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)

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

    # Параметр newline='' потрібен, щоб модуль csv у Python коректно обробляв переведення рядків на всіх платформах
    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="Пошуковий запит")
    ap.add_argument("--pages", type=int, default=1, help="Кількість сторінок (x count)")
    ap.add_argument("--count", type=int, default=10, help="Результатів на сторінку")
    ap.add_argument("--cc", default="UA", help="Код країни результатів (cc)")
    ap.add_argument("--setlang", default="uk", help="Мова інтерфейсу/сніпетів (setlang)")
    ap.add_argument("--proxy", help="Проксі, напр. http://user:pass@host:port")
    ap.add_argument("--csv", help="Шлях до CSV (щоб зберегти результати)")
    ap.add_argument(
        "--excel-friendly",
        action="store_true",
        help="Додати BOM (UTF-8-SIG), щоб Excel коректно відкрив файл",
    )
    ap.add_argument(
        "--normalize-cells",
        action="store_true",
        help="Прибрати переноси рядків та зайві пробіли у комірках",
    )
    ap.add_argument(
        "--delimiter",
        default=",",
        help="Роздільник CSV (за замовчуванням ','); напр.: ';'",
    )
    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" --pages 3 --csv out.csv \
  --proxy "http://username:password@proxy:port"

Що робить скрипт:

  1. Надсилає GET-запити до Bing із керованими параметрами (q, count, first) та мовно-регіональними налаштуваннями (cc, setlang).
  2. Підміняє User-Agent і додає Accept-Language для більш стабільних сніпетів.
  3. Розбирає html через BeautifulSoup(..., "lxml"), шукає картки результатів li.b_algo і дістає title, url, snippet. Селектори .select() — стандартний і гнучкий спосіб у BS4.
  4. Підтримує проксі (опційно). Правильне задання ip-проксі у Requests — словник протоколів → URL проксі.

Поради для стабільності:

  • Встановлюйте таймаути та робіть рандомні паузи між запитами. (Requests підтримує timeout, сесії та власні заголовки).
  • За потреби додайте повтори/бекоф або зміну User-Agent у кожному циклі.
  • Якщо розмітка b_algo зміниться, перевірте DOM через DevTools і оновіть селектори (на кшталт #b_results h2 > a чи альтернативні блоки SERP).

Де подивитися деталі по інструментах

Порада: якщо вам потрібна проксі-інфраструктура для стійкішого збору, перегляньте кращий проксі для Bing.

Як уникнути блокувань при парсингу Bing

Базові принципи, щоб ваш парсер не «вмер» у першому циклі:

  • Додавайте паузи (рандомізуйте інтервали між запитами).
  • Ротуйте User-Agent (динамічно або зі свого списку); правильну установку UA в requests описано у документації, та ми використовуємо це у нашому робочому прикладі.
  • Використовуйте проксі/IP-ротацію (з повагою до правил сервісів).
  • Обмежуйте загальну кількість запитів і переглядайте відповіді на предмет капчі.
  • За складних сценаріїв розгляньте керовані SERP-API (Apify, ін.) з антибот-інфраструктурою.

Висновок

Скрейпінг Bing доречний, коли потрібно розширити дослідження за межі Google, зібрати додаткові домени-донори, відстежити інші SERP-фічі й отримати незалежну картину. Для стабільної та «офіційної» інтеграції Microsoft просуває Grounding with Bing Search в Azure AI Agents; це безпечніше з точки зору правил, але не надає «сирих» SERP у json. Якщо завдання — «зняти» структуру результатів, оберіть парсинг пошуку Bing напряму через Requests/BS4 або Selenium, або використайте спеціалізовані SERP-API. Обирайте інструмент під ціль: швидкий парсинг Bing HTML для прототипів, «агенти» — для LLM-відповідей, SERP-API — для масштабованого збору.

Коментарії:

0 Коментаріїв