A fast async Python scraper that enriches business records by finding matching Yelp profile URLs, then writing results back to CSV.
It uses multiple search strategies (direct site search, DuckDuckGo, Bing, and website backlink checks), plus verification logic (website domain match + name similarity) to improve accuracy.
- What this script does
- How it works (high level)
- Requirements
- Installation
- How to run
- Input and output format
- Configuration
- Step-by-step code explanation
- Logging
- Troubleshooting
- Notes and limitations
- Suggested improvements
Given an input CSV of businesses, the script:
- Reads each business record (
name,city,state,website, etc.). - Checks if
bbband/oryelpcolumns are missing. - Searches for missing profiles using several strategies.
- Verifies likely matches using:
- website domain matching, and/or
- company name similarity scoring.
- Writes updated records to an output CSV.
It is optimized to search BBB and Yelp concurrently per record when both are missing.
For each business row:
- If BBB missing → run BBB search pipeline.
- If Yelp missing → run Yelp search pipeline.
- If both missing → run both in parallel (
asyncio.gather) using separate browser pages. - Save found URLs in
bbbandyelpcolumns. - Keep existing URLs unchanged if already present.
- Python 3.9+ (recommended)
- Internet connection
- Playwright Chromium browser binaries
Python packages used:
playwrightbeautifulsoup4lxml
git clone https://github.com/mujtabaalmas/scraper.git
cd scraperWindows (PowerShell):
python -m venv .venv
.venv\Scripts\Activate.ps1macOS/Linux:
python3 -m venv .venv
source .venv/bin/activatepip install playwright beautifulsoup4 lxmlplaywright install chromiumMake sure the input file exists (default: bussiness_records.csv), then run:
python yelp_bbb_scraper.pyThe script writes results to:
bussiness_records_yelp_bbb.csv- and logs to
yelp_bbb_scraper.log
Must include at least:
namecitystatewebsitebbb(can be empty/null/none)yelp(can be empty/null/none)
Example:
name,city,state,website,bbb,yelp
Acme Plumbing,Houston,TX,acmeplumbing.com,,
Best Dental Clinic,Dallas,TX,bestdental.com,null,noneSame columns as input, with bbb and/or yelp populated where found.
Top-of-file constants control behavior:
INPUT_CSV = "bussiness_records.csv"OUTPUT_CSV = "bussiness_records_yelp_bbb.csv"TEST_LIMIT = 10- Only first 10 records processed by default.
HEADLESS = True- Set
Falseto watch browser.
- Set
NAVIGATION_TIMEOUT = 25000MIN_DELAY = 0.5,MAX_DELAY = 1.5MAX_CANDIDATES_TO_CHECK = 5PROXIES = []- Add proxy URLs if needed.
The script imports async/browser tools, HTML parsing, URL parsing, regex, logging, CSV, and matching helpers.
Important modules:
playwright.async_apifor browser automationbs4.BeautifulSoupfor parsing HTMLSequenceMatcherfor fuzzy name similarity
Constants define:
- input/output files
- speed, delays, timeout
- headless mode
- max candidate URLs checked per search
These are foundational helpers used everywhere:
-
extract_domain(url)
Normalizes URL to domain (www.removed, lowercased, port stripped). -
domains_match(url1, url2)
Returns true if normalized domains are equal. -
similarity(a, b)
Computes fuzzy ratio after alphanumeric cleanup. -
clean_company_name(name)
Removes suffixes like Inc, LLC, Corp,.comto improve matching/search. -
is_valid_bbb_profile(url)
True if URL containsbbb.organd/profile/. -
is_valid_yelp_profile(url)
True if URL containsyelp.com/biz/. -
clean_yelp_url(url)
Drops query string (?params). -
clean_bbb_url(url)
Drops hash fragment (#...). -
decode_bing_url(bing_href)
Decodes Bing redirect wrappers (bing.com/ck/,aclick) to actual destination. -
name_in_url(company_name, url)
Checks whether most business name words appear in URL slug. -
random_delay(min_s, max_s)
Adds randomized sleep for anti-bot pacing.
Injects JS properties to reduce automation fingerprints:
- hides
navigator.webdriver - sets fake
navigator.plugins - sets
navigator.languages - patches permission query behavior
- chooses random user-agent
- launches Chromium with anti-automation flags
- optionally applies proxy config
- creates browser context with locale/timezone
Creates page and blocks heavy resource files (images/fonts/video) for speed.
Class: BBBScraper
Tries strategies in order:
- DuckDuckGo HTML search (direct links)
- Direct BBB site search
- Bing search (with redirect decode)
- Company website backlink check
Returns first verified BBB URL or None.
_search_duckduckgoparses DDG results and redirect params (uddg)_search_bbb_directparses page links, API responses, and Next.js hydration JSON_search_bingparses normal links +<cite>url text_check_websitescans company website for BBB links via regex and anchors
_verify_candidates:- Fast path: URL slug contains company name
- Strong path: visit profile page, extract business website, compare domain
- Fallback: compare profile business name to company name (threshold
> 0.55)
_extract_website_from_bbb_profilefinds business site from page links/text/embedded JSON_extract_name_from_bbb_profilegets business name from<h1>orog:title_extract_bbb_links,_extract_from_api,_extract_from_nextjs_data,_find_profile_urlsrecursively harvest candidate URLs
Class: YelpScraper
Tries strategies in order:
- Direct Yelp search page
- DuckDuckGo search
- Bing search
- Company website backlink check
_search_yelp_direct: parse/biz/links from Yelp search result page_search_duckduckgo: parse DDG links and decodeuddg_search_bing: parse Bing links + decode redirect + parse<cite>URLs_check_website: scrape company website for yelp.com/biz links_extract_yelp_links: normalize and filter invalid/non-profile Yelp URLs
_verify_candidates visits candidate pages and validates using:
- website domain match (best signal), otherwise
- profile name similarity score (
> 0.6)
_extract_website_from_yelp_profile:- handles
/biz_redir?url=...extraction - checks nearby “business website” labels
- checks JSON-LD scripts
- checks raw JSON-like patterns
- handles
_extract_name_from_yelp_profile: from<h1>orog:title
get_proxy_config(proxy_url) converts a proxy URL into Playwright proxy dict:
server- optional
username - optional
password
If PROXIES has entries, first one is used.
Function: process_record(...)
For each CSV row:
- reads name/location/website
- decides if BBB/Yelp are needed (
empty,null,none) - if both needed:
- creates 2 pages
- runs BBB + Yelp searches concurrently via
asyncio.gather
- if only one needed:
- runs only that scraper
- closes pages in
finally - returns tuple
(found_bbb, found_yelp)as counters
Function: main()
- Start timer and log header.
- Read input CSV via
csv.DictReader. - Apply
TEST_LIMITto number of rows processed. - Count already-existing BBB/Yelp values.
- Create scraper instances.
- Start Playwright context (with optional proxy + stealth setup).
- Iterate records:
- call
process_record - accumulate found counts
- delay between records
- call
- Close browser/context.
- Write all rows to output CSV.
- Print summary (processed, found, totals, elapsed time, output file).
Entrypoint:
if __name__ == "__main__":
asyncio.run(main())Logs are written to:
- Console (stdout)
- File:
yelp_bbb_scraper.log
Log includes:
- per-record status
- search strategy used
- candidate verification details
- domain mismatches
- summary stats
Install dependencies again:
pip install playwright beautifulsoup4 lxml
playwright install chromiumTry:
- updating Playwright
- reinstalling Chromium binaries
- setting
HEADLESS = Falseto inspect behavior
- Increase
MAX_CANDIDATES_TO_CHECK(e.g., 10) - Raise
NAVIGATION_TIMEOUT - Set
TEST_LIMIT = Noneto process full file - Ensure website domains in input are correct
- Add valid proxies in
PROXIES - Increase delays (
MIN_DELAY,MAX_DELAY) - Rotate user agents (already enabled)
- Search engines and target sites can change markup, which may break selectors/patterns.
- Aggressive scraping may trigger anti-bot protections.
- Domain matching depends on clean/accurate website values in CSV.
TEST_LIMITdefaults to 10, so full dataset is not processed unless you change it.
- THIS IS CURRENTLY EXTRACTING YELP PROFILE ONLY BBB IS NOT FETECHING RIGHT NOW