How to Build a Reliable Contact Scraper for B2B Lead Generation
Let’s dive in!
What is a Contact Scraper?
A contact scraper is a tool that automatically extracts contact information (such as email addresses, phone numbers, names, and company details) from publicly available online sources. These sources could include company directories, social media platforms, review sites, and business listings. The main benefit of using a contact scraper is that it automates lead gathering, enabling businesses to scale their outreach efforts without manual work.
Contact scrapers are particularly valuable for B2B lead generation because they enable businesses to target decision-makers in specific industries, which is key to successful marketing and sales outreach. However, scraping data is not as simple as just downloading information from websites. Many sites use anti-bot measures to prevent automated scraping, and overcoming these challenges requires a well-designed scraper.
Understanding the Basics of Contact Scraping
Before diving into the technical aspects of building a contact scraper, it’s important to understand the fundamental components of scraping:
- Data Sources: The websites or online platforms from which contact data is extracted. Popular sources for B2B lead generation include company directories, social media sites like LinkedIn, review sites like Yelp, and niche databases like Crunchbase.
- Scraping Tools: The software or scripts used to automate the scraping process. Scraping tools send requests to target websites and parse the returned HTML data to extract the required contact information.
- Data Parsing: Once data is fetched, it must be parsed and structured for use. Parsing involves identifying the specific elements on a webpage that contain contact details, such as email addresses, names, or phone numbers.
- Anti-Bot Measures: Many websites deploy anti-bot techniques like CAPTCHAs, JavaScript rendering, and IP blocking to prevent automated scraping. Overcoming these measures is a key challenge in building a reliable scraper.
Step-by-Step Guide to Building a Reliable Contact Scraper
There are two primary approaches to contact scraping: code-based and no-code. Depending on your technical skills and needs, you can choose the approach that works best for you.
Step 1: Choosing the Right Data Sources for Contact Scraping
The first step in building a reliable contact scraper is choosing the right data sources. Depending on your lead generation goals, the best source will vary. Here are some common sources for B2B lead generation:
- LinkedIn: A powerful platform for professional networking, LinkedIn allows you to scrape data based on job titles, company names, industries, and more. It’s ideal for targeting specific decision-makers in organizations. Check out my article of the best LinkedIn scrapers.
- Crunchbase: Crunchbase is a popular database for tech companies, startups, and investors. It’s a great source for finding decision-makers in high-growth companies, including their contact information. You can also use Bright Data’s Crunchbase Scraper or an Apify one.
- AngelList: AngelList is another resource for identifying startup founders and key employees, particularly for seed investment, partnerships, or recruitment purposes.
- Yelp & Yellow Pages: These local business directories are great for sourcing contact information for small and medium-sized businesses (SMBs), particularly in specific geographical regions.
- Trustpilot & Glassdoor: These platforms provide valuable insights into company reviews, which can be used to source HR professionals, recruitment leads, or customer insights.
Step 2: Overcoming Anti-Bot Measures
One of the main obstacles when scraping contact data is dealing with anti-bot measures. Websites often block or restrict scraping to protect their data and prevent server overload. Some standard anti-bot techniques include:
- CAPTCHAs: These are puzzle-like challenges that require human intervention to solve. They are used to prevent bots from accessing websites.
- IP Blocking: Many websites block IP addresses that send too many requests within a short period.
- JavaScript Rendering: Some websites rely heavily on JavaScript to load content, making it difficult for simple scrapers to retrieve the data.
To overcome these challenges, you will need to use more advanced scraping tools that can bypass these anti-bot measures. For instance, some tools use rotating proxies to avoid IP blocking, while others offer headless browsers that can execute JavaScript to render the content as if a real user is browsing the site.
Tip: Using a tool like Bright Data can be a game-changer in this regard. It provides features such as rotating proxies, headless browser support, and JavaScript rendering, allowing you to scrape data reliably without getting blocked.
Step 3: Building a Code-Based Scraper Using Python
If you’re comfortable with coding, Python is one of the most popular languages for building web scrapers. It has powerful libraries like requests and BeautifulSoup for fetching and parsing web content. Here's a comprehensive example of how to build a scraper using Python and Bright Data proxies.
Step 3.1: Set Up Your Python Environment
- Install Python on your machine if you haven’t already. Download from python.org.
- Install the necessary libraries by running the following commands:
pip install requests
pip install beautifulsoup4
pip install lxml
Step 3.2: Configure Bright Data Proxies
First, you’ll need to set up your Bright Data proxy credentials. Log in to your Bright Data account and retrieve your proxy credentials from the zone overview:
- Host:
brd.superproxy.io - Port:
33335(or your specific port) - Username: Your Bright Data username (e.g.,
brd-customer-[ACCOUNT_ID]-zone-[ZONE_NAME]) - Password: Your Bright Data proxy zone password
import requests
from bs4 import BeautifulSoup
import time
import csv
import re
from urllib.parse import urljoin
# Bright Data Proxy Configuration
PROXY_HOST = "brd.superproxy.io"
PROXY_PORT = "33335"
PROXY_USERNAME = "brd-customer-[ACCOUNT_ID]-zone-[ZONE_NAME]" # Replace with your credentials
PROXY_PASSWORD = "[YOUR_PASSWORD]" # Replace with your password
# Construct proxy dictionary
PROXIES = {
"http": f"http://{PROXY_USERNAME}:{PROXY_PASSWORD}@{PROXY_HOST}:{PROXY_PORT}",
"https": f"http://{PROXY_USERNAME}:{PROXY_PASSWORD}@{PROXY_HOST}:{PROXY_PORT}"
}
# Request headers to mimic a real browser
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "text/html,application/xhtml xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
}
Step 3.3: Create Helper Functions for Data Validation
def is_valid_email(email):
"""Validate email address format"""
pattern = r'^[a-zA-Z0-9._% -] @[a-zA-Z0-9.-] .[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def clean_phone(phone):
"""Clean and format phone number"""
if not phone:
return None
# Remove all non-digit characters except at the start
cleaned = re.sub(r'[^d ]', '', phone)
return cleaned if len(cleaned) >= 10 else None
def decode_cloudflare_email(encoded_str):
"""Decode Cloudflare-protected email addresses"""
try:
key = int(encoded_str[:2], 16)
email = "".join(
chr(int(encoded_str[i:i 2], 16) ^ key)
for i in range(2, len(encoded_str), 2)
)
return email if is_valid_email(email) else None
except (ValueError, IndexError):
return None
def extract_email_from_href(href):
"""Extract email from mailto: links"""
if not href:
return None
if href.startswith('mailto:'):
email = href.replace('mailto:', '').split('?')[0].strip()
return email if is_valid_email(email) else None
return None
Step 3.4: Create the Main Scraping Function
def fetch_page(url, retries=3):
"""Fetch page content with retry logic and error handling"""
for attempt in range(retries):
try:
response = requests.get(
url,
proxies=PROXIES,
headers=HEADERS,
timeout=30
)
response.raise_for_status()
return response
except requests.exceptions.RequestException as e:
print(f"Attempt {attempt 1} failed for {url}: {str(e)}")
if attempt < retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
else:
print(f"Failed to fetch {url} after {retries} attempts")
return None
return None
def scrape_listing_urls(listing_page_url, link_selector=".business-name a"):
"""Extract company listing URLs from a directory page"""
print(f"Fetching listing page: {listing_page_url}")
response = fetch_page(listing_page_url)
if not response:
return []
soup = BeautifulSoup(response.content, 'lxml')
company_links = []
# Find all links matching the selector
for link in soup.select(link_selector):
href = link.get('href')
if href:
# Convert relative URLs to absolute URLs
absolute_url = urljoin(listing_page_url, href)
company_links.append(absolute_url)
# Remove duplicates while preserving order
unique_links = list(dict.fromkeys(company_links))
print(f"Found {len(unique_links)} unique company listings")
return unique_links
def scrape_contact_data(company_url):
"""Extract contact information from a company page"""
print(f"Scraping: {company_url}")
response = fetch_page(company_url)
if not response:
return None
soup = BeautifulSoup(response.content, 'lxml')
contact_data = {
'url': company_url,
'name': None,
'email': None,
'phone': None,
'address': None
}
# Extract company name
name_elem = soup.select_one('h1.business-name, h1[class*="business"], h1')
if name_elem:
contact_data['name'] = name_elem.get_text(strip=True)
# Extract email - check multiple sources
# 1. Look for mailto: links
email_links = soup.select('a[href^="mailto:"]')
for link in email_links:
email = extract_email_from_href(link.get('href'))
if email:
contact_data['email'] = email
break
# 2. Look for Cloudflare-protected emails
if not contact_data['email']:
cf_email = soup.select_one('a.__cf_email__')
if cf_email and cf_email.get('data-cfemail'):
decoded = decode_cloudflare_email(cf_email.get('data-cfemail'))
if decoded:
contact_data['email'] = decoded
# 3. Search for email patterns in text
if not contact_data['email']:
text_content = soup.get_text()
email_pattern = r'b[A-Za-z0-9._% -] @[A-Za-z0-9.-] .[A-Z|a-z]{2,}b'
emails = re.findall(email_pattern, text_content)
for email in emails:
if is_valid_email(email):
contact_data['email'] = email
break
# Extract phone number
phone_elem = soup.select_one('a[href^="tel:"], .phone, [class*="phone"]')
if phone_elem:
phone_text = phone_elem.get_text(strip=True)
contact_data['phone'] = clean_phone(phone_text)
# Extract address
address_elem = soup.select_one('.address, [class*="address"], [itemprop="address"]')
if address_elem:
contact_data['address'] = address_elem.get_text(strip=True)
return contact_data
Step 3.5: Implement the Main Scraping Workflow
def scrape_business_directory(base_url, max_pages=5, delay=2):
"""
Main function to scrape business directory
Args:
base_url: Starting URL of the directory listing
max_pages: Maximum number of listing pages to scrape
delay: Delay between requests in seconds
"""
all_contacts = []
# Scrape listing pages
for page_num in range(1, max_pages 1):
# Adjust URL format based on your target site's pagination
if page_num == 1:
listing_url = base_url
else:
listing_url = f"{base_url}?page={page_num}"
print(f"n--- Processing page {page_num} ---")
company_urls = scrape_listing_urls(listing_url)
if not company_urls:
print(f"No listings found on page {page_num}, stopping...")
break
# Scrape each company's contact page
for idx, company_url in enumerate(company_urls, 1):
print(f"[{idx}/{len(company_urls)}]", end=" ")
contact_data = scrape_contact_data(company_url)
if contact_data:
all_contacts.append(contact_data)
# Rate limiting - be respectful to the target site
time.sleep(delay)
# Delay between pages
time.sleep(delay * 2)
return all_contacts
def save_to_csv(contacts, filename='b2b_leads.csv'):
"""Save scraped contacts to CSV file"""
if not contacts:
print("No contacts to save")
return
keys = contacts[0].keys()
with open(filename, 'w', newline='', encoding='utf-8') as output_file:
dict_writer = csv.DictWriter(output_file, fieldnames=keys)
dict_writer.writeheader()
dict_writer.writerows(contacts)
print(f"nSuccessfully saved {len(contacts)} contacts to {filename}")
# Main execution
if __name__ == "__main__":
# Example: Scraping Yellow Pages for electricians in San Francisco
target_url = "https://www.yellowpages.com/san-francisco-ca/electricians"
print("Starting B2B contact scraper...")
print(f"Target: {target_url}n")
# Scrape the directory
contacts = scrape_business_directory(
base_url=target_url,
max_pages=3, # Adjust based on your needs
delay=2 # Delay between requests in seconds
)
# Display results
print(f"n{'='*50}")
print(f"Scraping completed!")
print(f"Total contacts scraped: {len(contacts)}")
print(f"{'='*50}n")
# Show sample of scraped data
if contacts:
print("Sample data (first 3 entries):")
for i, contact in enumerate(contacts[:3], 1):
print(f"n{i}. {contact['name']}")
print(f" Email: {contact['email']}")
print(f" Phone: {contact['phone']}")
print(f" Address: {contact['address']}")
print(f" URL: {contact['url']}")
# Save to CSV
save_to_csv(contacts)
Step 3.6: Advanced Configuration for Different Sources
Different websites have different HTML structures. Here’s how to customize the scraper for different sources:
# Configuration for different data sources
SCRAPER_CONFIGS = {
'yellowpages': {
'listing_selector': '.business-name a',
'name_selector': 'h1.business-name',
'email_selector': 'a[href^="mailto:"]',
'phone_selector': '.phone',
'address_selector': '.address'
},
'yelp': {
'listing_selector': 'a[href*="/biz/"]',
'name_selector': 'h1[class*="heading"]',
'email_selector': 'a[href^="mailto:"]',
'phone_selector': '[class*="phone"]',
'address_selector': 'address'
},
# Add more configurations as needed
}
def scrape_with_config(url, source_type='yellowpages'):
"""Scrape using predefined configuration for specific source"""
config = SCRAPER_CONFIGS.get(source_type)
if not config:
raise ValueError(f"Unknown source type: {source_type}")
# Use config selectors for scraping
# Implementation here...
pass
Step 4: No-Code Contact Scraping with Clay
If you prefer not to write code, you can use no-code platforms like Clay to automate the scraping process. Clay integrates with Bright Data to provide powerful scraping capabilities without coding.
Step 4.1: Set Up Your Clay Workflow
- Create a new table: Sign up for a Clay account at clay.com and create a new table for your B2B leads.
- Add columns: Set up the following columns in your table:
- Company URL
- Company Name
- Phone Number
- Address
- Industry (optional)
- Notes (optional)
Import initial data: You can manually add company URLs, import from a CSV, or use Clay’s “Find Companies” enrichment to discover leads based on criteria like industry, location, or company size.
Step 4.2: Integrate Bright Data with Clay
- Add the “HTTP API” action: In your Clay table, click “Add enrichment” and search for “HTTP API” or look for Bright Data-specific integrations.
- Configure the scraping request:
- Set the method to GET
- Add the company URL from your column as the target
- Configure Bright Data proxy settings if using the HTTP API action
Alternative: Use Clay’s built-in web scraper:
- Search for “Scrape website” in Clay’s enrichments
- Select the column containing your target URLs
- Clay will automatically use proxies to scrape the data
Step 4.3: Extract Data with Clay’s AI
- Use “Extract from website” enrichment: This AI-powered feature can automatically identify and extract contact information from web pages.
- Define what to extract: Tell Clay what information you need:
- “Find the primary email address”
- “Extract the phone number”
- “Get the company’s physical address”
Run the enrichment: Clay will process each URL and extract the requested information automatically.
Step 4.4: Validate and Enrich Data
- Email validation: Use Clay’s email validation enrichment to verify email addresses are valid and deliverable.
- Phone validation: Add phone validation to ensure phone numbers are properly formatted.
- Additional enrichment: Layer on more data using Clay’s integrations:
- Company information from Clearbit or Similar Web
- Social profiles from LinkedIn
- Technographic data
Step 4.5: Export and Use Your Data
- Review results: Check the enriched data in your Clay table.
- Export options:
- Download as CSV for use in Excel or Google Sheets
- Integrate directly with your CRM (Salesforce, HubSpot, etc.)
- Connect to your email outreach tool (Lemlist, Instantly, etc.)
Automate the workflow: Set up Clay to automatically process new leads as they’re added to your table.
Step 5: Storing and Using the Scraped Data
After successfully scraping contact data, you need to store and use it effectively. Here are the best practices and options:
Option 1: CSV/Excel Files
Best for small to medium datasets (up to 10,000 records).
import pandas as pd
# Load scraped data
df = pd.DataFrame(contacts)
# Clean and deduplicate
df = df.drop_duplicates(subset=['email'])
df = df.dropna(subset=['email']) # Remove entries without email
# Save to Excel with formatting
df.to_excel('b2b_leads.xlsx', index=False, engine='openpyxl')
Option 2: Database Storage
Best for large datasets and when you need to query and update data frequently.
import sqlite3
import pandas as pd
def save_to_database(contacts, db_name='b2b_leads.db'):
"""Save contacts to SQLite database"""
conn = sqlite3.connect(db_name)
df = pd.DataFrame(contacts)
# Create or replace table
df.to_sql('contacts', conn, if_exists='replace', index=False)
# Create index on email for faster queries
cursor = conn.cursor()
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email ON contacts(email)')
conn.commit()
conn.close()
print(f"Saved {len(contacts)} contacts to database: {db_name}")
def query_contacts(db_name='b2b_leads.db', city=None):
"""Query contacts from database"""
conn = sqlite3.connect(db_name)
if city:
query = f"SELECT * FROM contacts WHERE address LIKE '%{city}%'"
else:
query = "SELECT * FROM contacts"
df = pd.read_sql_query(query, conn)
conn.close()
return df
Option 3: Direct CRM Integration
Best for immediate use in sales workflows.
import requests
def export_to_hubspot(contacts, api_key):
"""Export contacts directly to HubSpot CRM"""
url = "https://api.hubapi.com/contacts/v1/contact/batch/"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
# Format contacts for HubSpot
formatted_contacts = []
for contact in contacts:
if contact.get('email'): # Only export if email exists
formatted_contacts.append({
"email": contact['email'],
"properties": [
{"property": "company", "value": contact.get('name', '')},
{"property": "phone", "value": contact.get('phone', '')},
{"property": "address", "value": contact.get('address', '')},
{"property": "website", "value": contact.get('url', '')}
]
})
# Send in batches of 100
batch_size = 100
for i in range(0, len(formatted_contacts), batch_size):
batch = formatted_contacts[i:i batch_size]
response = requests.post(url, headers=headers, json=batch)
if response.status_code == 200:
print(f"Successfully exported batch {i//batch_size 1}")
else:
print(f"Error exporting batch: {response.text}")
Best Practices and Legal Considerations
1. Respect Robots.txt
Always check the website’s robots.txt file to see what's allowed:
import requests
from urllib.parse import urljoin
def check_robots_txt(base_url, user_agent='*'):
"""Check if scraping is allowed by robots.txt"""
robots_url = urljoin(base_url, '/robots.txt')
try:
response = requests.get(robots_url, timeout=5)
if response.status_code == 200:
print(f"Robots.txt content:n{response.text}")
return response.text
except:
print("No robots.txt found or unable to fetch")
return None
2. Implement Rate Limiting
Don’t overload target servers:
import time
from datetime import datetime
class RateLimiter:
def __init__(self, max_requests_per_minute=30):
self.max_requests = max_requests_per_minute
self.requests = []
def wait_if_needed(self):
now = datetime.now()
# Remove requests older than 1 minute
self.requests = [req_time for req_time in self.requests
if (now - req_time).seconds < 60]
if len(self.requests) >= self.max_requests:
sleep_time = 60 - (now - self.requests[0]).seconds
print(f"Rate limit reached. Waiting {sleep_time} seconds...")
time.sleep(sleep_time)
self.requests.append(now)
# Usage
limiter = RateLimiter(max_requests_per_minute=30)
for url in urls:
limiter.wait_if_needed()
scrape_contact_data(url)
3. Data Privacy and Compliance
- GDPR Compliance: If scraping EU citizens’ data, ensure compliance with GDPR
- CAN-SPAM Act: Follow regulations when using scraped emails for marketing
- Terms of Service: Always review and respect website terms of service
- Data Minimization: Only collect data you actually need
- Secure Storage: Encrypt sensitive contact information
4. Error Handling and Logging
import logging
from datetime import datetime
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(f'scraper_{datetime.now().strftime("%Y%m%d")}.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def scrape_with_logging(url):
"""Scrape with comprehensive logging"""
try:
logger.info(f"Starting scrape of {url}")
data = scrape_contact_data(url)
if data and data.get('email'):
logger.info(f"Successfully scraped {url} - Found email: {data['email']}")
else:
logger.warning(f"No email found for {url}")
return data
except Exception as e:
logger.error(f"Error scraping {url}: {str(e)}", exc_info=True)
return NoneConclusion
Conclusion
Creating a reliable contact scraper for B2B lead generation can significantly improve how efficiently you gather leads and drive sales. By following the best practices outlined in this guide, you can:
- Choose the right data sources for quality lead generation
- Overcome anti-bot measures using Bright Data’s proxy network
- Build robust scrapers with proper error handling and validation
- Respect legal and ethical boundaries while scraping
- Store and utilize data effectively in your sales workflow
Whether you choose the code-based approach for maximum flexibility or the no-code Clay solution for ease of use, contact scraping becomes a powerful tool to support your marketing and sales goals. Automating this process not only saves time but also provides a consistent flow of valuable leads for your business.
Remember to always:
- Respect website terms of service and robots.txt files
- Implement proper rate limiting
- Validate and clean your data
- Ensure compliance with data privacy regulations
- Use the scraped data ethically and responsibly
Start building your contact scraper today and transform your B2B lead generation process!

