Selenium Wire Tutorial: Intercept Background Requests
Tags: python, headlessbrowsers, selenium, tools
Published: 2024-04-01 05:46:56

---
title: Selenium Wire Tutorial: Intercept Background Requests
published: true
date: 2024-04-01 04:52:19 UTC
tags: Python,HeadlessBrowsers,Selenium,Tools
canonical_url: https://scrapfly.io/blog/how-to-intercept-background-requests-with-selenium-wire/
---

![Selenium Wire Tutorial: Intercept Background Requests](https://scrapfly.io/blog/content/images/2024/04/inspect-background-requests-selenium-wire-light-1.svg)

Background XHR calls often contain valuable data loaded when navigating web pages. But what about inspecting and modifying while scraping?

In this guide, we'll explore web scraping with Selenium Wire. We'll define what it is, how to install it, and how to use it to inspect and manipulate background requests. Let's get started!

## What is Selenium Wire?

The [selenium-wire](https://github.com/wkeeling/selenium-wire) library is a Python binding that extends the [Selenium headless browser library](https://scrapfly.io/blog/web-scraping-with-selenium-and-python/). It uses the regular Selenium API, making the implementation code the same.

Selenium Wire **captures all the outgoing background requests**, allowing for:

- Inspecting the requests along with their configuration and responses.
- Manipulating the requests by modifying their configurations or limiting them.

## What Are XHR Calls and Why Intercept Them?

Background XHR calls are HTTP requests activated while navigating websites using web browsers through different actions, such as:

- Scrolling down.
- Clicking on a specific button or link.
- Clicking on the next pagination button.
- Filtering the data using filter buttons.
- Searching for particular data.

The above actions activate specific API endpoints to load the requested data. In most cases, the **data returned from these requests are in JSON**. Therefore, it's more convenient to **scrape the data directly from the XHR call response** objects rather than parsing it from the HTML using selectors like [XPath](https://scrapfly.io/blog/parsing-html-with-xpath/) or [CSS](https://scrapfly.io/blog/parsing-html-with-css/).

[Background requests](https://scrapfly.io/blog/web-scraping-background-requests-with-headless-browsers-and-python/) are regular API calls, commonly known as [hidden APIs](https://scrapfly.io/blog/how-to-scrape-hidden-apis/), with [headers](https://scrapfly.io/blog/how-to-avoid-web-scraping-blocking-headers/), body, [cookies](https://scrapfly.io/blog/how-to-handle-cookies-in-web-scraping/), and [local storage](https://scrapfly.io/blog/what-is-local-storage-and-how-to-scrape-it/). These APIs can be called directly by any HTTP client. However, they often contain **complicated or comprehensive request configurations that are hard to replicate** , like [GraphQL](https://scrapfly.io/blog/web-scraping-graphql-with-python/). Therefore, using selenium-wire as a request interceptor by activating these requests from the browser makes it easier to get the data directly or copy their configuration.

## How to Install Selenium Wire?

To install the selenium-wire library, all we have to do is execute the below `pip` command:

```shell
pip install selenium-wire
```

The above command will install the Selenium4 binaries along with the extra APIs required to inspect requests. It will also install the required driver binaries for the different browser types:

```python
from seleniumwire import webdriver

# choose a specific browser driver
driver = webdriver.Chrome()
driver = webdriver.Firefox()
driver = webdriver.Edge()
```

## Inspecting Response Objects

Let's start by creating a selenium-wire response interceptor. For this, we'll use [web-scraping.dev/testimonials](https://web-scraping.dev/testimonials) as our target website:

![Selenium Wire Tutorial: Intercept Background Requests](https://scrapfly.io/blog/content/images/2024/02/web-scraping.dev-review-data-1.webp)
_Reviews on web-scraping.dev_

The above review data are loaded dynamically through background XHR calls, which get activated through scroll actions. First, we'll use a selenium-wire scraping code that requests the above page and scroll it:

```python
from seleniumwire import webdriver
import time

options = webdriver.ChromeOptions()
options.add_argument("log-level=3") # disable logs
driver = webdriver.Chrome(options=options)

driver.get("https://web-scraping.dev/testimonials")

def scroll(driver: webdriver):
    for i in range(0, 6):
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(1)

scroll(driver)

driver.quit()
```

Here, we use selenium-wire to initialize a headless Chrome instance, request the target web page, and [scroll down with Selenium](https://scrapfly.io/blog/how-to-scroll-to-the-bottom-with-selenium/).

The above code will activate the background XHR calls, yet we haven't inspected them. To inspect responses with selenium-wire, we can use the `driver.requests` method:

```python
from seleniumwire import webdriver
import time

#....
scroll(driver)

# iterate over all the recorded XHR requests
for request in driver.requests:
    print(request)

driver.quit()
```

In the above code, we iterate over the recorded XHR requests and print them:

```shell
https://accounts.google.com/ListAccounts?gpsia=1&source=ChromiumBrowser&json=standard
https://web-scraping.dev/testimonials
https://web-scraping.dev/assets/css/main.css
https://web-scraping.dev/assets/css/bootstrap-icons.css
https://web-scraping.dev/assets/css/bootstrap.min.css
https://web-scraping.dev/assets/css/highlight-nord.css
https://web-scraping.dev/assets/js/cash.min.js
https://web-scraping.dev/assets/js/bootstrap.js
https://web-scraping.dev/assets/js/main.js
https://web-scraping.dev/assets/js/htmx.js
https://cdn.jsdelivr.net/npm/minidenticons@3.1.2/minidenticons.min.js
https://web-scraping.dev/assets/media/icon.png
https://web-scraping.dev/api/testimonials?page=2
https://web-scraping.dev/api/testimonials?page=3
https://optimizationguide-pa.googleapis.com/v1:GetModels?key=AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw
https://web-scraping.dev/api/testimonials?page=4
https://web-scraping.dev/api/testimonials?page=5
```

We can see that selenium-wire **captured all the requests sent from the browser**, including the ones for assets, which aren't useful. Next, let's have a look at filtering these responses and returning the details of each response.


### Filtering
Since we are web scraping data from the HTML, we aren't interested in third-party or asset requests. Let's filter the inspected responses to only match what we are looking for:

```python
from seleniumwire import webdriver
import time

#....

# iterate over all the recorded XHR requests
for request in driver.requests:
    if "/testimonials" in request.url:
        print(request.url)

driver.quit()
```

Here, we filter the captured XHR calls to only return the ones for review data:

```shell
https://web-scraping.dev/testimonials
https://web-scraping.dev/api/testimonials?page=2
https://web-scraping.dev/api/testimonials?page=3
https://web-scraping.dev/api/testimonials?page=4
https://web-scraping.dev/api/testimonials?page=5

```

Great! We have all the responses in place. Next, let's return the details of each XHR call response:

```python
from seleniumwire import webdriver
import time
import json

#....

def format_response(response):
    formatted_response = {
        "status_code": response.status_code,
        "reason": response.reason,
        "headers": dict(response.headers),
        "date": response.date.strftime("%Y-%m-%d %H:%M:%S"),
        "body": response.body[:50].decode('utf-8') # decode the body as it's encoded
    }
    return formatted_response

# iterate over all the recorded XHR requests
for request in driver.requests:
    if "/testimonials" in request.url:
        formatted_response = format_response(request.response)
        print(json.dumps(formatted_response, indent=2))

driver.quit()
```

Here, we return the **response properties provided by selenium-wire**. Let's break them down:

| Attribute | Description |
| --- | --- |
| `status_code` | The response status code. |
| `reason` | A text representation of the status code. |
| `headers` | The response headers. |
| `date` | The response date, when it was returned. |
| `body` | The response body, HTML, JSON, etc. |

Here's a sample output of the returned response details:

```json
{
  "status_code": 200,
  "reason": "",
  "headers": {
    "content-type": "text/html; charset=utf-8",
    "date": "Mon, 25 Mar 2024 23:32:15 GMT",
    "permissions-policy": "fullscreen=(self), autoplay=*, geolocation=(), camera=()",
    "referrer-policy": "strict-origin-when-cross-origin",
    "server": "uvicorn",
    "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
    "x-content-type-options": "nosniff",
    "x-xss-protection": "1; mode=block",
    "content-length": "28266"
  },
  "date": "2024-03-26 01:32:17",
  "body": "\n<!doctype html>\n<html lang=\"en\">\n <head>\n <me"
}
```

The above details can make the web scraping process much easier. For example, we can **parse each response body** to obtain the data. Let's have a look!


### Parsing

In this section, we'll parse the HTML found in each response body inspected by selenium-wire to retrieve the full review data using [Parsel](https://pypi.org/project/parsel/):

```python
from seleniumwire import webdriver
from parsel import Selector
import time
import json

options = webdriver.ChromeOptions()
options.add_argument("log-level=3") # disable logs
driver = webdriver.Chrome(options=options)

driver.get("https://web-scraping.dev/testimonials")

def scroll(driver: webdriver):
    for i in range(0, 5):
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(1)

scroll(driver)

def parse_xhr_html(html: str):
    """parse review data from each xhr response body"""
    data = []
    selector = Selector(html)
    for review in selector.css("div.testimonial"):
        data.append({
            "rate": len(review.css("span.rating > svg").getall()),
            "text": review.css("p.text::text").get()            
        })
    return data

# iterate over all the recorded XHR requests and parse each response body
data = []
for request in driver.requests:
    if "/testimonials" in request.url:
        xhr_data = parse_xhr_html(request.response.body.decode('utf-8'))
        data.extend(xhr_data)

print(json.dumps(data, indent=2, ensure_ascii=False))

driver.quit()
```

The above code is pretty straightforward. We use selenium-wire to inspect the responses of the background requests and parse each response body using CSS selectors to retrieve the review data. Here is what the results we got look like:

```json
[
  {
    "rate": 5,
    "text": "We've been using this utility for years - awesome service!"
  },
  {
    "rate": 5,
    "text": "This Python app simplified my workflow significantly. Highly recommended."
  },
  {
    "rate": 4,
    "text": "Had a few issues at first, but their support team is top-notch!"
  },
  {
    "rate": 5,
    "text": "A fantastic tool - it has everything you need and more."
  },
  {
    "rate": 5,
    "text": "The interface could be a little more user-friendly."
  },
  ....
]
```

Parsing data from XHR calls eliminates the need to replicate complex HTTP requests while scraping. **The browser manages all the required configurations** for us, such as headers or private API keys.

## Inspecting Request Objects

Just like how we intercepted the responses of the background XHR calls, we can use the same approach to inspect the outgoing requests along with their configurations:

```python
from seleniumwire import webdriver
import time
import json

options = webdriver.ChromeOptions()
options.add_argument("log-level=3") # disable logs
driver = webdriver.Chrome(options=options)

driver.get("https://web-scraping.dev/testimonials")

def scroll(driver: webdriver):
    for i in range(0, 5):
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(1)

scroll(driver)

def format_response(request):
    formatted_request = {
        "url": request.url,
        "method": request.method,
        "date": request.date.strftime("%Y-%m-%d %H:%M:%S.%f"),
        "ws_messages": request.ws_messages,
        "querystring": request.querystring,
        "params": request.params,
        "path": request.path,
        "host": request.host,
        "response": {
            "status_code": request.response.status_code,
            "reason": request.response.reason,
            "headers": dict(request.response.headers),
            "date": request.response.date.strftime("%Y-%m-%d %H:%M:%S") if request.response.date else None,
            "body": request.response.body[:50].decode('utf-8') if request.response.body else None
        }
    }
    return formatted_request

# inspect the outgoing XHR requests
data = []
for request in driver.requests:
    if "/testimonials" in request.url:
        formatted_request = format_response(request)
        print(json.dumps(formatted_request, indent=2))

driver.quit()
```

Here, we inspect the outgoing XHR requests and return their configurations. The full request properties are the following:

| Attribute | Description |
| --- | --- |
| `url` | The full request URL. |
| `method` | The HTTP method used by the request, GET, POST, etc. |
| `date` | The request date, when it was sent. |
| `cert` | The target website SSL certificate details. |
| `ws_messages` | Applicable for WebSocket requests, the messages sent and recieved. |
| `querystring` | The request query paremeters represented as a string. |
| `params` | The request query paremeters represented as an object. |
| `body` | The request body. |
| `host` | The request URL path. |
| `host` | The request URL host. |
| `response` | The full response object of the request. |

Here's a sample output of the inspected requests retrieved:

```josn
{
  "url": "https://web-scraping.dev/api/testimonials?page=2",
  "method": "GET",
  "date": "2024-03-26 03:19:14.208584",
  "ws_messages": [],
  "querystring": "page=2",
  "params": {
    "page": "2"
  },
  "body": "",
  "path": "/api/testimonials",
  "host": "web-scraping.dev",
  "response": {
    "status_code": 200,
    "reason": "",
    "headers": {
      "content-type": "text/html; charset=utf-8",
      "date": "Tue, 26 Mar 2024 01:19:13 GMT",
      "permissions-policy": "fullscreen=(self), autoplay=*, geolocation=(), camera=()",
      "referrer-policy": "strict-origin-when-cross-origin",
      "server": "uvicorn",
      "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
      "x-content-type-options": "nosniff",
      "x-xss-protection": "1; mode=block",
      "content-length": "12366"
    },
    "date": "2024-03-26 03:19:14",
    "body": "\n \n <div class=\"testimonial\">\n \n <identicon-"
  }
}
```

When it comes to web scraping, the reason why XHR calls are inspected can vary based on the use case:

- Inspecting requests  
It **comes in handy when replicating an HTTP request independently**, such as obtaining specific header keys required for the requests.  
  
- Inspecting responses  
It's **useful for retrieving the data directly from the body** without managing the requests ourselves. It's particularly convenient when the requests' configuration is complex.

## Intercepting Requests

So far, we have only inspected recorded XHR calls. However, selenium-wire also enables **modifying background requests**, including their responses.

For this, selenium-wire provides two interceptor functions: `driver.request_interceptor` and `driver.response_interceptor`. Once the request or response reaches selenium-wire, these functions get invoked.

Let's explore intercepting requests through examples.

### Modifying Reuqest Headers

Headers play a vital role in every HTTP request. A common trick used by web scrapers is rotating request headers, such as the [User-Agent](https://scrapfly.io/blog/user-agent-header-in-web-scraping/). Here's how to manipulate background request headers using the `interceptor` function

```python
from seleniumwire import webdriver
from parsel import Selector
import json

driver = webdriver.Chrome()

# define the request interceptor
def interceptor(request):
    """Modify headers of the outgoing background calls"""
    request.headers['Cookie'] = 'key1=value1;key2=value2;' # add a new request header
    # replacing a request header
    del request.headers['User-Agent'] # remove the header first (it's automatically provided)
    request.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0' # add a new header value

driver.request_interceptor = interceptor
driver.get("https://httpbin.dev/headers")
 
# get the used request header values from the response
selector = Selector(driver.page_source)
response = json.loads(selector.xpath("//pre/text()").get())
print(json.dumps(response, indent=2))

driver.quit()
```

In the above code, we define a request `interceptor` function. It observes all the outgoing requests sent from the browser and modifies their headers. From the response, we'll find the new header values used:

```json
{
  "headers": {
    ....
    "Cookie": [
      "key1=value1;key2=value2;",
    "User-Agent": [
      "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0"
    ]
  }
}

```

### Modifying Response Headers

Similar to the above section, we can create a response interceptor function to modify the response headers:

```python
from seleniumwire import webdriver
import json

driver = webdriver.Chrome()

# define the request interceptor
def interceptor(request, response):
    """Modify headers of the outgoing background calls"""
    response.headers['Cookie'] = 'key1=value1;key2=value2;' # add a new request header
    # replacing a request header
    del response.headers['User-Agent'] # remove the header first (it's automatically provided)
    response.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0' # add a new header value

driver.response_interceptor = interceptor
driver.get("https://httpbin.dev/headers")
 
# review the xhr response changes
for request in driver.requests:
    if request.url == "https://httpbin.dev/headers":
        print(json.dumps(dict(request.response.headers), indent=2))

driver.quit()
```

Here, we create a selenium-wire request interceptor to **modify the response headers**. The same approach can also be used to manipulate other response properties, such as the HTML found in the response body.


### Blocking Requests

Another advantage of using selenium-wire interceptors is blocking specific background requests from getting executed:

```python
from seleniumwire import webdriver

driver = webdriver.Chrome()

# define the request interceptor blocker
def interceptor(request):
    """Block specific requests"""
    # Block image requests
    if request.path.endswith(('.png', '.jpg', '.gif', '.webp', '.mp4', '.mp3')):
        request.abort()
    for partial_host in ['analytics', 'facebook', 'google', 'google-analytics', 'optimizationguide']:
        if partial_host in request.host:
            request.abort()

driver.request_interceptor = interceptor
driver.get("https://web-scraping.dev/products")

driver.quit()
```

The above code defines a request interceptor to **block certain background requests**, such as images, videos, or requests sent to third-party services like Google Analytics.

This can significantly optimize the [web scraper speed](https://scrapfly.io/blog/web-scraping-speed/) by eliminating unnecessary requests while also saving bandwidth, which is beneficial when using [proxies](https://scrapfly.io/blog/introduction-to-proxies-in-web-scraping/) to reduce cost.

### Limiting Requests Capture

When using selenium-wire, background requests are redirected to an internal server where they get captured. This can throttle down the performance when navigating complex websites.

For this, selenium-wire allows **limiting the driver scope to only capture background requests on a specific host or domain** :

```python
from seleniumwire import webdriver

driver = webdriver.Chrome()

# define the driver score
driver.scopes = [
    '.*web-scraping.dev.*'
]

driver.get("https://web-scraping.dev/products")

# only requests sent to the host "web-scraping.dev" will get captured
for request in driver.requests:
    print(request)

driver.quit()
```

## Avoiding Selenium Wire Scraping Blocking

Websites use protection mechanisms to block automated requests, such as web scrapers, from accessing their pages. Let's explore two tricks we can use to avoid selenium-wire [blocking](https://scrapfly.io/blog/how-to-scrape-without-getting-blocked-tutorial/)!

### Using Proxies With Selenium Wire

The IP address is an essential property that's sent with every HTTP request. Websites and protection services can access each request's IP address. Once the **requesting rate exceeds a specific threshold**, the IP address gets blocked for a certain period of time.

Hence, using a **proxy server distributes the requesting traffic across multiple IP addresses**, which makes it harder to detect the IP address origin, preventing its blocking!

Selenium Wire allows changing the headless browser IP address through proxies: **HTTP** and **SOCKS5**. It also supports an exciting feature: **changing proxy on the fly**. Let's have a look!

#### HTTP Proxies

To use HTTP proxies with selenium-wire, we can add the proxy URLs to the driver options:

```python
from seleniumwire import webdriver

options = {
    'proxy': {
        'http': 'http://123.12.12.12:1234', # HTTP
        'https': 'https://123.12.12.12:1234', # HTTPS
        'https': 'https://user:pass@123.12.12.12:1234', # authenticated proxies
    }
}

driver = webdriver.Chrome(seleniumwire_options=options)
```

#### SOCKS Proxies

Similar to the above snippet, we can use SOCKS proxies with selenium-wire by adding them to the driver options:

```python
options = {
    'proxy': {
        'http': 'socks4://123.12.12.12:1234', # SOCKS4
        'https': 'socks5://123.12.12.12:1234', # SOCKS5
        'https': 'socks5://user:pass@123.12.12.12:1234', # authenticated SOCKS proxies
    }
}

driver = webdriver.Chrome(seleniumwire_options=options)
```

#### Changing Proxies Dynamically

Headless browsers often execute time-consuming tasks, such as navigating different page links in one session. Hence, maintaining the same IP address for such a long period isn't optimal in terms of blocking.

Selenium Wire allows switching proxies with running driver instances using the `driver.proxy` attribute:

```python
# A driver using initial proxy
driver.get(...)

# Switch proxy
driver.proxy = {
    'https': 'https://123.12.12.12:1234',
}

# Driver will use the new proxy
driver.get(...) 

```

* * *

We have only touched the surface of avoiding IP address blocking. For further details, refer to our [dedicated guide](https://scrapfly.io/blog/how-to-avoid-web-scraping-blocking-ip-addresses/).

### Using Undedected ChromeDriver With Selenium Wire

The [Undetected ChromeDriver](https://scrapfly.io/blog/web-scraping-without-blocking-using-undetected-chromedriver/) is a modified version of the regular Selenium Web Driver meant to avoid the protections of popular anti-bot services. It **mimics regular browser behavior** through various techniques, such as:

- Changing Selenium's variable names to appear normal.
- Randomizing User-Agent strings.
- Adding randomized delays between requests.
- Maintaining cookies and sessions correctly.
- Adding random mouse clicks and moves.

Selenium Wire integrates with the Undetected ChromeDriver to initiate the browser. The first step is **installing the Undetected ChromeDriver** using the following pip command:

```shell
pip install undetected-chromedriver
```

Here's how to use the UndetectedChrome Driver with selenium-wire. We'll request [nowsecure.nl](https://nowsecure.nl/), a web page with a [Cloudflare](https://scrapfly.io/blog/how-to-bypass-cloudflare-anti-scraping/) challenge:

```python
import seleniumwire.undetected_chromedriver as uc

chrome_options = uc.ChromeOptions()

# Initialize a selenium-wire driver
driver = uc.Chrome(
    options=chrome_options, # UndetectedChrome driver options
    seleniumwire_options={} # selenium-wire options
)

driver.get("https://www.nowsecure.nl/")
driver.quit()
```

🙋‍ Note that the Cloudflare challenge on the above target website requires the SSL certificate to be available in the browser. However, selenium-wire uses a self-signed certificate, preventing access to the page with a "Not Secure" message. Therefore, we have to import a root certificate. For more details, refer to the [official GitHub docs](https://github.com/wkeeling/selenium-wire/tree/master#certificates) and this [Stack Overflow answer](https://stackoverflow.com/a/72267931).

The Undetected ChromeDriver can help avoid bot detection with its modified browser configurations. However, websites use [TLS fingerprinting](https://scrapfly.io/blog/how-to-avoid-web-scraping-blocking-tls/) techniques to detect selenium-wire.

For example, let's use the above selenium-wire code to access [G2](https://scrapfly.io/blog/how-to-scrape-g2-company-data-and-reviews/):

```python
import seleniumwire.undetected_chromedriver as uc

chrome_options = uc.ChromeOptions()
driver = uc.Chrome()

driver.get("https://www.nowsecure.nl/")
driver.quit()
```

From the response, we can see that the browser couldn't bypass the challenge:

![Selenium Wire Tutorial: Intercept Background Requests](https://scrapfly.io/blog/content/images/2023/12/G2-cloudflare-challenge-2.webp)
_selenium-wire blocking_

Let's have a look at a better alternative for bypassing scraping blocking!

## Powering Up With ScrapFly

ScrapFly is a web scraping API that allows for scraping at scale by providing:

- [Anti-scraping protection bypass](https://scrapfly.io/docs/scrape-api/anti-scraping-protection) - For scraping any website without getting blocked.
- [Residential and Mobile proxies](https://scrapfly.io/docs/scrape-api/proxy) in over 50 countries - For scraping from almost any geographical location while also preventing IP address throttling and blocking.
- [JavaScript rendering](https://scrapfly.io/docs/scrape-api/javascript-rendering) - For scraping dynamic web pages through cloud headless browsers wihtout running them yourself.
- [JavaScript scenarios](https://scrapfly.io/docs/scrape-api/javascript-scenario) - For automating headless browsers for scrolling, waiting, clicking buttons, and filling forms using ready-to-use commands.
- Easy to use [Python](https://scrapfly.io/docs/sdk/python) and [Typescript](https://scrapfly.io/docs/sdk/typescript) SDKs, as well as [Scrapy integration](https://scrapfly.io/docs/sdk/scrapy).
- [And much more!](https://scrapfly.io/docs/scrape-api/getting-started)

![Selenium Wire Tutorial: Intercept Background Requests](https://scrapfly.io/blog/content/images/common_scrapfly-api.svg)
_ScrapFly service does the heavy lifting for you!_

To avoid scraping blocking with ScrapFly. All we have to do is replace the HTTP client with the ScrapFly client, enable the `asp` parameter, and select a proxy country. **ScrapFly also saves all the background XHR requests** with each response. Here's how we can inspect them:

```python
# standard web scraping code
import requests
from parsel import Selector

response = requests.get("https://web-scraping.dev/testimonials")
selector = Selector(response.text)

# in ScrapFly becomes this 👇
from scrapfly import ScrapeConfig, ScrapflyClient

# replaces your HTTP client (httpx in this case)
scrapfly = ScrapflyClient(key="Your ScrapFly API key")

response = scrapfly.scrape(ScrapeConfig(
    url="https://web-scraping.dev/testimonials",
    asp=True, # enable the anti scraping protection to bypass blocking
    proxy_pool="public_residential_pool", # select a proxy pool
    country="US", # set the proxy location to a specfic country
    render_js=True, # enable rendering JavaScript (like headless browsers) to scrape dynamic content if needed
    # execute custom JavaScript code to scroll down till the end
    js="for (let i = 0; i < 6; i++) setTimeout(() => window.scrollTo(0, document.body.scrollHeight), i * 2000);"
))

# use the built in Parsel selector
selector = response.selector

# access the HTML content
html = response.scrape_result['content']

# access the browser background XHR requests
xhr_calls = response.scrape_result["browser_data"]["xhr_call"]
for call in xhr_calls:
    if "/testimonials" in call["url"]:
        print(call["url"])
        "https://web-scraping.dev/api/testimonials?page=2"
        # remaining pagination requests
```

[Try for FREE!](https://scrapfly.io/register)

[More on Scrapfly](https://scrapfly.io/docs)


## FAQ

To wrap up this guide on intercepting background requests with selenium-wire for web scraping, let's have a look at some frequently asked questions.

#### What is the difference between Welenium Wire and Selenium?

[Selenium](https://scrapfly.io/blog/web-scraping-with-selenium-and-python/) is an automation tool used to run and manipulate web browsers. Selenium Wire is a Python binding for Selenium that allows it to inspect and modify background requests made by the browser driver.

#### Are there alternatives for selenium-wire?

Yes, other headless browsers like [Playwright](https://scrapfly.io/blog/how-to-capture-xhr-requests-playwright/) and [Puppeteer](https://scrapfly.io/blog/how-to-capture-xhr-requests-puppeteer/) enable capturing background requests natively. Refer to our guide on [web scraping background requests](https://scrapfly.io/blog/web-scraping-background-requests-with-headless-browsers-and-python/) for more details.

## Summary

In this guide, we explained how to use selenium-wire to capture background requests, specifically for web scraping. We started by exploring how to:

- Inspect requests and responses, filter and parsing them.
- Create interceptor functions to request configurations.
- Optimize selenium wire by blocking or limiting certain domains.

Then, we explored how to avoid selenium-wire scraping blocking by:

- Adding proxies to split the request traffic across multiple IPs.
- Using the Undetected ChromeDriver to mimic normal browsers.