DEV Community

Bugrahan Saka
Bugrahan Saka

Posted on Originally published at scrape.do

SearchPeopleFree Scraper in Python: Profiles, Phones and Emails from a Name Search

SearchPeopleFree.com is one of the more generous US people search sites: type a name and a free profile shows the exact age, current and past addresses, phone numbers, emails, the spouse and relatives, with no signup. That also makes it one of the harder ones to scrape. Here is a Python scraper that turns a name search into a CSV of profiles, and the three things in its way.

The block: Cloudflare, DataDome and a US-only geoblock

import requests

r = requests.get("https://www.searchpeoplefree.com/")
print(r.status_code)  # 403, "Attention Required! | Cloudflare"
Enter fullscreen mode Exit fullscreen mode

A plain request gets HTTP 403 and Cloudflare's "Attention Required!" page. Cloudflare is only the front door: behind it the site runs DataDome, a second bot detection layer that scores fingerprints and behaviour, so passing one is not enough. And the whole site only answers visitors from the United States; datacenter IPs and most VPNs are dropped too.

The fix: one request from a US residential IP

Send the same URL through Scrape.do with super=true (residential IP plus anti-bot handling) and geoCode=us:

import json
import re

import requests
from bs4 import BeautifulSoup

TOKEN = "<your_token>"

def fetch(url):
    response = requests.get("https://api.scrape.do/", params={
        "token": TOKEN, "url": url, "super": "true", "geoCode": "us",
    }, timeout=90)
    response.raise_for_status()
    return BeautifulSoup(response.text, "html.parser")
Enter fullscreen mode Exit fullscreen mode

In my tests every profile and search page came back with HTTP 200 in about 3 seconds, for 10 credits each.

Trap: the emails are not in the HTML

Open a profile in the browser and the emails are on screen. In the page source they look like this:

<a href="/cdn-cgi/l/email-protection" class="__cf_email__"
   data-cfemail="5a303b343f3e353f1a3f223b372a363f74393537">[email&#160;protected]</a>
Enter fullscreen mode Exit fullscreen mode

That is Cloudflare's email obfuscation: the first byte is a key and every following byte is XORed with it; JavaScript decodes it after the page loads. You could decode it yourself:

def decode_cfemail(hex_string):
    data = bytes.fromhex(hex_string)
    return bytes(b ^ data[0] for b in data[1:]).decode()

decode_cfemail("5a303b343f3e353f1a3f223b372a363f74393537")  # 'janedoe@example.com'
Enter fullscreen mode Exit fullscreen mode

But there is a better source.

The data: one JSON-LD block of type Person

Every profile embeds a <script type="application/ld+json"> block of type Person (schema.org), the structured data the site publishes for search engines. It has the name, every phone and email (in plain text, no cipher), the current address under contentLocation.address, past addresses under homeLocation, aliases, the spouse and the relatives. One test profile listed 37 phone numbers, 30 emails, 54 relatives and 27 past addresses, far more than the visible page shows.

The one field missing from it is the age. It sits in the profile summary, <article class="current-bg">, as "Age 41", so a regex picks it up:

def person_jsonld(soup):
    for script in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(script.string)
        except (json.JSONDecodeError, TypeError):
            continue
        if isinstance(data, dict) and data.get("@type") == "Person":
            return data
    return None

def names(values):
    return [v["name"] if isinstance(v, dict) else v for v in values or []]

def parse_profile(soup, url):
    person = person_jsonld(soup)
    if person is None:
        raise ValueError("No Person JSON-LD: blocked page or not a profile")
    address = person.get("contentLocation", {}).get("address", {})
    summary = soup.find("article", class_="current-bg")
    age = re.search(r"Age\s+(\d+)", summary.get_text(" ")) if summary else None
    return {
        "name": person.get("name"),
        "age": int(age.group(1)) if age else None,
        "address": address.get("streetAddress"),
        "city": address.get("addressLocality"),
        "state": address.get("addressRegion"),
        "zip": address.get("postalCode"),
        "phones": person.get("telephone", []),
        "emails": person.get("email", []),
        "spouse": names(person.get("spouse")),
        "relatives": names(person.get("relatedTo")),
        "aliases": person.get("additionalName", []),
        "address_history": [
            ", ".join(filter(None, [a.get("streetAddress"), a.get("addressLocality"), a.get("addressRegion")]))
            for a in (place.get("address", {}) for place in person.get("homeLocation", []))
        ],
        "url": url,
    }
Enter fullscreen mode Exit fullscreen mode

On a fictional test page (no real person) the output looks like this:

{
  "name": "Jane Doe",
  "age": 41,
  "address": "2912 Northern Ave",
  "city": "Washington",
  "state": "DC",
  "zip": "20001",
  "phones": ["(202) 555-0147", "(202) 555-0199"],
  "emails": ["janedoe@example.com", "jane.m.doe@example.org"],
  "spouse": ["John Doe"],
  "relatives": ["Mary Doe", "Robert Smith", "Emily Doe", "Thomas Doe", "Anna Roe", "Peter Roe"],
  "aliases": ["Jane M Doe", "Jane Marie Doe"],
  "address_history": ["2912 Northern Ave, Washington, DC", "44 Elm St, Arlington, VA"]
}
Enter fullscreen mode Exit fullscreen mode

From a name to a CSV

A name search lives at /find/<first-last>, with an optional two-letter state (/find/john-smith/tx). Each results page lists about 10 people, the next page adds /p-2, and the page carries an ItemList JSON-LD block with every profile link in order:

import csv

def search_url(name, state="", page=1):
    url = "https://www.searchpeoplefree.com/find/" + "-".join(name.lower().split())
    if state:
        url += "/" + state.lower()
    if page > 1:
        url += f"/p-{page}"
    return url

def profile_links(soup):
    for script in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(script.string)
        except (json.JSONDecodeError, TypeError):
            continue
        if isinstance(data, dict) and data.get("@type") == "ItemList":
            return [item["url"] for item in data.get("itemListElement", [])]
    return []

rows = []
for url in profile_links(fetch(search_url("John Smith", "TX")))[:5]:
    rows.append(parse_profile(fetch(url), url))

with open("people.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
    writer.writeheader()
    for row in rows:
        writer.writerow({k: " | ".join(v) if isinstance(v, list) else v for k, v in row.items()})
print(f"Saved {len(rows)} profiles")
Enter fullscreen mode Exit fullscreen mode

One search page plus three profiles was 4 requests and 40 credits.

Skip the boilerplate

The same scraper is open source, with retries, the email decoder as a fallback, aliases and past addresses, and a CLI:

pip install searchpeoplefree-scraper
export SCRAPEDO_TOKEN=your_token
searchpeoplefree-scraper --name "John Smith" --state TX --limit 5 -f csv -o john-smith.csv
searchpeoplefree-scraper "https://www.searchpeoplefree.com/find/<name>/<id>"
Enter fullscreen mode Exit fullscreen mode

Source: github.com/bugrahansss/searchpeoplefree-scraper. There is also a no-code version on Apify (SearchPeopleFree Scraper) and a browser demo on Hugging Face.

Use it responsibly

This is personal data about real people. SearchPeopleFree is not a consumer reporting agency, so don't use it for decisions about employment, housing, credit or insurance (the FCRA regulates those in the US). Honor opt-out requests, follow the privacy laws that apply to you, and keep request rates modest.

Run it at scale with Scrape.do

Every page here is one Scrape.do call through a US residential IP: no proxy pool to manage, no CAPTCHA solving, no headless browser to keep alive against Cloudflare and DataDome. Get 1000 free credits and start scraping with Scrape.do.

Full walkthrough on the Scrape.do blog: SearchPeopleFree.com Scraping: Extract Emails, Phones, and Family Data

Top comments (0)