TruePeopleSearch is one of the most used free people search sites: search a name and you get an age, addresses, phone numbers and public records, no sign-up. Point a script at it, though, and you rarely get a single byte of profile HTML back.
This post walks through why, how to get through with one request, and how to pull ten structured fields from a profile without fragile HTML parsing.
Every name, phone number and address below is fictional. Swap the placeholder URL for a profile URL you get from a normal search on the site.
Two walls in front of every profile
1. A US-only geofence. Request the site from outside the United States and there is no error page and no loading screen. The connection is simply cut off.
2. Cloudflare. Inside the US, requests and httpx still stall on JavaScript challenges, fingerprint checks and, sometimes, a CAPTCHA. Send requests too quickly and Cloudflare adds rate limiting, then escalates to harder challenges once it sees an automated pattern.
There is no public TruePeopleSearch API, so the request itself has to look like a real visitor from the US.
Getting through: two parameters
Routing the request through Scrape.do with two parameters handles both walls:
| Parameter | What it does |
|---|---|
geoCode=us |
keeps the exit IP inside the United States, which clears the geofence |
super=true |
uses a residential IP instead of a datacenter one, so the request looks like a home user |
No proxy rotation, no header tuning, no headless browser.
pip install requests beautifulsoup4
import requests
import urllib.parse
from bs4 import BeautifulSoup
token = "<your_token>"
target_url = "https://www.truepeoplesearch.com/find/person/jane-doe"
encoded_url = urllib.parse.quote_plus(target_url)
api_url = f"https://api.scrape.do/?token={token}&url={encoded_url}&super=true&geoCode=us"
response = requests.get(api_url)
soup = BeautifulSoup(response.text, "html.parser")
print(response) # <Response [200]>
A 200 means the real profile HTML is in soup.
Name and age: read attributes, not text
The profile header is a div#personDetails that carries the values as data attributes. Reading attributes skips every formatting problem, because you get the values the way the site stores them rather than the way it displays them:
person = soup.find("div", id="personDetails")
name = f"{person['data-fn']} {person['data-ln']}"
age = person["data-age"]
# Jane Doe, 23
Address and phone: schema.org item props
The current address sits in a link marked data-link-to-more="address", with street, city and state in their own itemprop spans. The phone works the same way. Targeting itemprop instead of class names keeps the selectors stable through a restyle:
addr = soup.find("a", {"data-link-to-more": "address"})
address = addr.find("span", itemprop="streetAddress").text.strip()
city = addr.find("span", itemprop="addressLocality").text.strip()
state = addr.find("span", itemprop="addressRegion").text.strip()
phone = soup.find("a", {"data-link-to-more": "phone"}).find("span", itemprop="telephone").text.strip()
That gives you the basics. The page holds far more, and the best part isn't in the visible HTML at all.
The shortcut: one JSON-LD block
Every profile embeds a <script type="application/ld+json"> block of type ProfilePage. Its mainEntity is the person, and every field is a dictionary lookup:
import json
person_data = None
for script in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(script.string)
if isinstance(data, dict) and data.get("@type") == "ProfilePage":
person_data = data.get("mainEntity", {})
break
except (json.JSONDecodeError, TypeError):
pass
address_data = person_data.get("address", {})
zipcode = address_data.get("postalCode", "")
phones = person_data.get("telephone", [])
emails = person_data.get("email", [])
aliases = person_data.get("alternateName", [])
relatives = [rel.get("name", "") for rel in person_data.get("relatedTo", [])]
On the sample profile:
ZIP: 20001
Phones: 8 found, primary: (202) 555-5555
Emails: 6 found, primary: janedoe@example.com
Aliases: Jane Marie Doe, Jane D Smith, Jane M Johnson
Relatives (24): John Doe, Mary Doe, Robert Smith, Emily Doe, Thomas Doe
Three things worth knowing about this block:
-
telephoneandemailare arrays, so you get every number and address on the profile, not only the one shown first. The first entry is usually the most recent. -
relatedTolists every relative as aPersonobject. Some profiles list dozens. - The JSON-LD keeps full middle names that the visible page abbreviates.
Combined with the name and age attributes, one request returns ten structured fields: name, age, address, city, state, ZIP, phones, emails, aliases and relatives.
Skip the boilerplate: a ready-made package
The same logic is packaged as an open-source library, truepeoplesearch-scraper on GitHub and PyPI:
pip install truepeoplesearch-scraper
export SCRAPEDO_TOKEN=your_token
truepeoplesearch-scraper "https://www.truepeoplesearch.com/find/person/<profile-id>" --format csv -o profiles.csv
from truepeoplesearch_scraper import TruePeopleSearchScraper
person = TruePeopleSearchScraper().get_profile(profile_url)
print(person.name, person.age, person.phones, person.relatives)
It prefers the JSON-LD values, falls back to the visible itemprop spans, retries server errors and returns JSON or CSV.
Before you run it at scale
- Space requests out. Cloudflare escalates when traffic arrives too fast. Residential IPs rotate automatically, but pacing still helps.
- Use the data responsibly. This is data about real people. Keep it out of hiring, tenant screening, credit and insurance decisions, which the Fair Credit Reporting Act regulates in the US. Honor opt-out requests and follow the privacy laws that apply to you. (Not legal advice.)
Source: TruePeopleSearch.com Scraping: Extract Data Without Blocks
Top comments (0)