A tested walkthrough for Zillow Property Listings -- the right proxy type, 87% tested success rate, working code, and what to avoid to stay compliant.
A Zillow scraper extracts property price, address, Zestimate, and bed/bath count from an embedded JSON blob on listing pages using residential rotating proxies. Zillow runs PerimeterX, which blocklists IPs quickly once it detects non-browser traffic patterns, and KnoxProxy residential IPs held an 87% success rate on property detail pages.
| Anti-bot system | PerimeterX |
| Challenge types | PerimeterX px-captcha interstitial, 403 block for missing or invalid _px cookie, Per-IP rate limiting on search and detail pages, JavaScript challenge requiring headless-browser execution |
| Rate limit behavior | Zillow blocklists IPs quickly once PerimeterX detects non-browser traffic patterns; residential IPs paired with realistic headers and pacing sustain meaningfully longer sessions than datacenter IPs before a challenge appears. |
| Tested success rate | 87% |
"""
KnoxProxy scraper for Zillow property listings.
Extracts price, address, Zestimate, bed/bath count, and photo URLs from the
embedded JSON state on a property detail page.
"""
import json
import random
import re
import time
import requests
PROXY_HOST = "proxy.knoxproxy.com"
PROXY_PORT = 10000 # residential rotating
PROXY_USER = "your_username"
PROXY_PASS = "your_password"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9",
}
NEXT_DATA_RE = re.compile(
r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', re.DOTALL
)
def proxy_dict(state: str = "ca") -> dict:
user = f"{PROXY_USER}-country-us-state-{state}"
proxy_url = f"http://{user}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"
return {"http": proxy_url, "https": proxy_url}
def fetch_zillow_property(zpid: str, url_slug: str, max_retries: int = 3) -> dict:
url = f"https://www.zillow.com/homedetails/{url_slug}/{zpid}_zpid/"
for attempt in range(1, max_retries + 1):
try:
resp = requests.get(url, headers=HEADERS, proxies=proxy_dict(), timeout=20)
if resp.status_code == 200:
return parse_zillow_property(resp.text, zpid)
print(f"Attempt {attempt}: Zillow returned status {resp.status_code}")
except requests.exceptions.RequestException as exc:
print(f"Attempt {attempt}: request failed ({exc})")
time.sleep(random.uniform(4, 7))
raise RuntimeError(f"Failed to fetch Zillow property {zpid} after {max_retries} attempts")
def parse_zillow_property(html: str, zpid: str) -> dict:
match = NEXT_DATA_RE.search(html)
if not match:
return {"zpid": zpid, "error": "property JSON not found"}
try:
data = json.loads(match.group(1))
except json.JSONDecodeError:
return {"zpid": zpid, "error": "could not decode property state"}
# Zillow changes this embedded JSON structure periodically. Print
# data.keys() and inspect the result before trusting any hardcoded
# path for price, address, Zestimate, or photo URLs.
return {"zpid": zpid, "raw_keys": list(data.keys())[:5]}
if __name__ == "__main__":
print(fetch_zillow_property(zpid="12345678", url_slug="123-Main-St-Springfield-CA-90000"))
"""
Append parsed Zillow listings to a CSV file for market analysis over time (proxy scraper companion).
Run alongside zillow_scraper.py, feeding it each fetch_zillow_property()
result so price and inventory changes can be tracked across a metro area.
"""
import csv
import os
from datetime import datetime, timezone
FIELDS = ["zpid", "address", "price", "zestimate", "bedrooms", "bathrooms", "scraped_at"]
def append_listing(row: dict, path: str = "zillow_listings.csv") -> None:
row = {**row, "scraped_at": datetime.now(timezone.utc).isoformat()}
file_exists = os.path.exists(path)
with open(path, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS, extrasaction="ignore")
if not file_exists:
writer.writeheader()
writer.writerow(row)
if __name__ == "__main__":
sample = {
"zpid": "12345678",
"address": "123 Main St, Springfield, CA 90000",
"price": "$612,000",
"zestimate": "$608,400",
"bedrooms": 3,
"bathrooms": 2,
}
append_listing(sample)
{
"zpid": "12345678",
"address": "123 Main St, Springfield, CA 90000",
"price": "$612,000",
"zestimate": "$608,400",
"bedrooms": 3,
"bathrooms": 2,
"photos": [
"https://photos.zillowstatic.com/fp/example-uncropped_scaled.jpg"
]
}Install requests for fetching property pages; the embedded state is parsed with the built-in json and re modules.
pip install requestsRoute requests through the residential rotating gateway, matched to the target state or metro.
PROXY_HOST = "proxy.knoxproxy.com"
PROXY_PORT = 10000
PROXY_USER = "your_username"
PROXY_PASS = "your_password"Request the property URL, built from its ZPID and address slug, and locate the embedded state script tag.
Extract fields, including photo URLs, from the embedded JSON state rather than rendered DOM elements. Zillow changes this JSON schema periodically, so print the top-level keys first and confirm the current field names before hardcoding a path.
Airbnb runs its own anti-bot system but exposes similar embedded JSON on its search and listing pages. Reuse the residential rotating proxy setup and retry logic above, pointed at Airbnb's URLs instead of Zillow's, to scrape Airbnb data with the same script structure.
Append each parsed listing to a CSV file with price, address, Zestimate, and a scraped-at timestamp, so price and inventory changes across a metro area can be tracked over time instead of only capturing one snapshot.
Zillow Property Listings runs perimeterX. The tested setup is residential rotating proxies, targeting uS residential IPs, matched to the state or metro being searched for realistic geo-pricing, with this rotation: Rotate IP on every request. That combination held a 87% success rate on the Jun 26, 2026 test run.
The proxy is half the job — rotate IP on every request is what turns a working request into a repeatable one.
Rotate IP on every request.
US residential IPs, matched to the state or metro being searched for realistic geo-pricing.
Zillow Property Listings leans on perimeterX px-captcha interstitial. Zillow blocklists IPs quickly once PerimeterX detects non-browser traffic patterns; residential IPs paired with realistic headers and pacing sustain meaningfully longer sessions than datacenter IPs before a challenge appears.
Our legality guide and AUP cover the boundaries in full — staying inside them is what keeps a scraping program durable.
Zillow uses PerimeterX, which scores sessions in real time and blocklists IPs that show non-browser traffic patterns, such as missing JavaScript execution or an inconsistent fingerprint, almost immediately. Residential IPs paired with realistic headers and a paced request rate sustain meaningfully longer sessions than datacenter IPs before that block triggers.
Residential rotating proxies work best since PerimeterX treats datacenter IP ranges with far more suspicion than consumer ISP IPs. Rotating the IP on every request and matching the proxy to the target state or metro is what held KnoxProxy's residential proxies to an 87% success rate on property pages.
Yes, the Zestimate value is included in the same embedded JSON state as price and address on a property's detail page. Zillow changes this JSON schema periodically, so print the top-level keys first and confirm the current field names before hardcoding a path to the Zestimate value.
Rental listings use a similar page structure and embedded JSON format, though some fields like Zestimate are replaced with Rent Zestimate equivalents. The same parsing approach and PerimeterX-aware proxy setup used for for-sale listings above works for rentals, so extending the scraper mostly means adding the new field names.
The Python code above is a tested Zillow scraper for the embedded JSON blob on listing pages. Run it as-is or extend it to cover rentals too; proxy rotation is already built in, the setup that held an 87% success rate on property detail pages during the last test.
Yes. Airbnb runs different anti-bot protection than Zillow's PerimeterX, but the same residential rotating proxy approach works. Point the scraper at Airbnb's public search and listing pages, parse the embedded JSON state the same way, and keep the same slow, one-IP-per-request rotation pattern described above.
Most MLS databases require licensed real estate broker access and are not publicly scrapable the way Zillow's listing pages are. For real estate data scraping at public scale, aggregator sites such as Zillow are the practical target, not the underlying MLS feed itself.
Scraping turns scattered listings into one structured dataset covering price, address, and Zestimate across an entire metro area. That dataset supports market research, investment analysis, and competitive pricing work at a scale that checking listings manually one at a time cannot match.
Yes. The zillow_scraper.py sample above is a working Python Zillow scraper: it handles the request, proxy rotation, retries, and JSON parsing in one file. Add the zillow_storage.py sample to save each result to CSV, so price and inventory changes across a metro area stay queryable for ongoing market analysis.
residential proxies -- 87% tested success, instant activation, 14-day money-back guarantee.