Mobile Emulation and Geolocation Spoofing
Many sites serve different content to mobile users, geo-target by IP and JS, and gate features by user-agent. Emulating these correctly opens scrapes you'd otherwise be locked out of.
What you’ll learn
- Emulate a real mobile device with consistent UA, viewport, screen, touch, and devicePixelRatio.
- Spoof geolocation via the Geolocation API and align it with timezone/locale.
- Combine device emulation with proxy-based IP geo to look consistent end-to-end.
- Apply mobile emulation as a workaround when the desktop site is anti-bot-protected and the mobile site isn't.
The same URL can return wildly different content depending on what device or location you appear to be using. Restaurant prices vary by city. App store listings vary by country. News sites geo-block, language-switch, and personalise based on signals you control. Mobile emulation and geolocation spoofing are how you steer those signals deliberately.
Why emulate a mobile device
Three common reasons:
- Mobile-only or mobile-first content. Some sites serve a richer mobile experience (more listings per page, simpler markup, sometimes no anti-bot). Apps and instant-articles flows often have less protection than desktop web.
- The mobile site is easier to scrape. Less JS, simpler DOM, more SSR. Many "modern" sites have a
/m/ormobile.subdomain that's a static-scraping dream. - Geo-locked content. Mobile-app-style endpoints sometimes leak GPS coordinates the desktop site only inferred from IP. Spoofing the lat/lng can unlock features.
Playwright's device descriptors
Playwright ships pre-baked device profiles:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
iphone = p.devices["iPhone 14"]
browser = p.chromium.launch()
context = browser.new_context(**iphone)
page = context.new_page()
page.goto("https://practice.scrapingcentral.com/")
print(page.evaluate("navigator.userAgent"))
browser.close()
p.devices is a dictionary of presets, "iPhone 14", "Pixel 7", "iPad Mini", etc. Each profile sets:
user_agent, realistic iOS Safari or Android Chrome string.viewport, physical dimensions.device_scale_factor, devicePixelRatio (2 or 3 on retina).is_mobile, sets(max-device-width)CSS to treat the device as mobile.has_touch, enables touch events.screen, overall screen size.
Spread the dict into new_context and you have a consistent emulated device.
Custom mobile device
When the presets don't match what you need:
context = browser.new_context(
user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
viewport={"width": 390, "height": 844},
device_scale_factor=3,
is_mobile=True,
has_touch=True,
screen={"width": 390, "height": 844},
)
Make sure the UA matches the device dimensions, an "iPhone" UA with desktop viewport is detectable.
Geolocation spoofing
The Geolocation API:
navigator.geolocation.getCurrentPosition(pos => console.log(pos.coords));
Override the coordinates Playwright reports:
context = browser.new_context(
geolocation={"latitude": 37.7749, "longitude": -122.4194, "accuracy": 10},
permissions=["geolocation"],
locale="en-US",
timezone_id="America/Los_Angeles",
)
permissions=["geolocation"] is critical, without it, the page's request to read location is denied and your override never gets queried.
Pages calling navigator.geolocation.getCurrentPosition will receive the lat/lng you specified.
Aligning all geo signals
A user "in San Francisco" should be consistent across:
- IP address (handled via proxy)
- Browser timezone (
timezone_id) - Locale (
locale="en-US") - Geolocation API (lat/lng)
- Accept-Language header
- Currency / language in cookies (some apps store these)
Mismatched geo signals (US IP + Tokyo timezone + Russian locale) is a high-confidence bot pattern. Build the geo profile carefully:
SF_PROFILE = {
"geolocation": {"latitude": 37.7749, "longitude": -122.4194, "accuracy": 50},
"permissions": ["geolocation"],
"locale": "en-US",
"timezone_id": "America/Los_Angeles",
"extra_http_headers": {"Accept-Language": "en-US,en;q=0.9"},
}
context = browser.new_context(**iphone, **SF_PROFILE,
proxy={"server": "http://us-west-residential-proxy:8080"})
Combined with a US-West proxy, the visitor looks coherent end-to-end.
When mobile emulation helps with anti-bot
A trick that works on some sites: the desktop version has aggressive bot protection, the mobile version is lighter. Reasons:
- Mobile pages are usually a separate codebase, sometimes maintained by a different team.
- Cloudflare/Akamai rule sets can be configured per-route or per-UA.
- Mobile apps use different APIs (often gRPC or proto-JSON) that are sometimes simpler to talk to than the desktop GraphQL endpoint.
Run the three-test diagnostic (Lesson 2.1) on both the desktop and mobile versions of any target. If the mobile version is server-rendered, you might skip the browser entirely.
Catalog108 example: locations
/locations accepts a ?near= parameter or reads geolocation. With spoofed coords:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(
geolocation={"latitude": 37.7749, "longitude": -122.4194, "accuracy": 10},
permissions=["geolocation"],
locale="en-US",
timezone_id="America/Los_Angeles",
)
page = context.new_page()
page.goto("https://practice.scrapingcentral.com/locations")
page.wait_for_selector(".location-card")
for card in page.locator(".location-card").all():
print(card.inner_text())
browser.close()
The map filters to nearby locations based on the spoofed coordinates. Change the lat/lng to scrape different regions.
Touch event emulation
has_touch=True enables touch events, but Playwright doesn't drive a "real" finger. To simulate taps and swipes:
page.tap("button.submit") # touchstart + touchend
# Manual swipe via pointer events:
page.touchscreen.tap(100, 200)
page.touchscreen.tap(100, 600)
# Multi-touch is more involved, use the lower-level CDP if you need pinch/zoom
For most mobile scraping, taps are enough. Pinch/zoom and complex gestures are rare scrape targets.
Detecting the emulation
Sites can fingerprint emulation:
- Touch events without touchstart-touchend pairs. Real fingers leave a sequence; lazy emulators only fire one.
- Sensor APIs (gyroscope, accelerometer) returning null. Real phones expose orientation data.
navigator.maxTouchPoints === 0despite "mobile" UA. Real phones have ~5.
Playwright handles some but not all. For premium anti-bot targets, the gap remains.
Geo restrictions and content variations
A non-exhaustive list of behaviours you can unlock with geo + UA control:
- App stores, different listings per country.
- News sites, different language editions.
- Restaurant/delivery sites, different menus/prices per neighbourhood.
- Streaming catalogs, different content per region (where not also IP-checked).
- E-commerce, different currencies, taxes, product availability.
For data products that depend on regional variation, this is the lever.
Putting it together: a regional scraper
REGIONS = [
{"name": "sf", "lat": 37.7749, "lng": -122.4194, "tz": "America/Los_Angeles", "proxy": "us-west"},
{"name": "ny", "lat": 40.7128, "lng": -74.0060, "tz": "America/New_York", "proxy": "us-east"},
{"name": "ldn", "lat": 51.5074, "lng": -0.1278, "tz": "Europe/London", "proxy": "uk"},
{"name": "tk", "lat": 35.6762, "lng": 139.6503, "tz": "Asia/Tokyo", "proxy": "jp"},
]
for r in REGIONS:
context = browser.new_context(
geolocation={"latitude": r["lat"], "longitude": r["lng"], "accuracy": 50},
permissions=["geolocation"],
timezone_id=r["tz"],
proxy={"server": f"http://{r['proxy']}-proxy:8080"},
)
page = context.new_page()
page.goto("https://practice.scrapingcentral.com/locations")
# ... scrape regional results ...
context.close()
Per-region contexts under one browser, one isolated session per region. Lesson 2.26 scales this up with bounded concurrency.
Hands-on lab
Open /locations. Use Playwright with custom geolocation to scrape it from three different "cities". Verify the returned locations actually differ. Then switch to the iPhone 14 device profile and re-scrape, note any differences in markup or available data between mobile and desktop. That's the breadth of "what device + geo control unlocks."
Hands-on lab
Practice this lesson on Catalog108, our first-party scraping sandbox.
Open lab target →/locationsQuiz, check your understanding
Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.