SuperPages 是美国最大的在线企业目录之一,收录了数百万家按行业和地区索引的公司。每条房源都包含销售和营销团队在构建潜在客户列表时所需的公开结构化信息:公司名称、所属类别、街道地址、公开电话号码,通常还有公司自己网站的链接。对于构建服务提供商的区域数据集或启动 B2B 外拓活动,这些公开目录数据正是你所需要的原材料。
本指南将向你展示如何可靠地使用 Python 抓取 SuperPages 企业列表。你通过 Crawling API 获取渲染后的搜索结果页面,使用 BeautifulSoup 解析每条结果以提取名称、类别、地址、电话、网站和详情页链接,然后遍历分页以覆盖完整结果集,并将记录导出为 JSON 或 CSV 格式。这里的所有内容仅限于公开的企业目录数据,文末的合法性章节涵盖了与 B2B 潜在客户数据相关的义务,请在将其应用于实际大批量采集前务必阅读。
你将构建什么
一个小型 Python 爬虫,接受搜索关键词和地点,通过 Crawling API 获取渲染后的 SuperPages 搜索结果页面,并为页面上的每家企业提取一条结构化记录。运行示例是加利福尼亚州洛杉矶的"家政服务"企业,每条房源提取以下字段:
- Business name 主要识别标识,用于对潜在客户分组。
- Category 房源所属的行业,用于对潜在客户进行细分。
- Address 公开街道地址,包含城市、州和邮政编码。
- Phone 房源卡片上显示的公开联系电话。
- Website 企业自有网站的链接(如有)。
- Detail page link 该企业在 SuperPages 上专属页面的 URL。
为什么普通请求在 SuperPages 上会失败
你可以用 requests 库访问 SuperPages 的搜索 URL,运气好的时候能得到 HTML。问题在批量请求时才会显现。SuperPages 部署了反爬虫防护:按 IP 限速、向看起来像自动化的流量提供 CAPTCHA,并封锁以机器特征紧密循环请求页面的数据中心地址。从你的笔记本电脑发出一次请求可能会成功;从同一 IP 发出数百次就不行了。
因此,真正能完成任务的爬虫需要被视为受信任 IP 的真实访客发出的请求。你可以自己搭建轮换住宅代理池并维护其正常运行,但这才是主要工作量。Crawling API 将这些整合为一次调用:你向它发送 URL,它在服务器端通过住宅 IP 路由请求并处理反爬虫层,然后返回供你解析的 HTML。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript (JS) token 首先在真实浏览器中渲染页面。SuperPages 在初始 HTML 中提供其房源数据,因此普通 token 是正确选择,且每次请求费用更低。只有当目标开始在客户端渲染房源时,才需要切换到 JS token。
前提条件
首先需要准备几样东西,都不会花太长时间。
基础 Python 知识。 你应该熟悉运行脚本和使用 pip 安装包。如果你对选择器不熟悉,如何在 Python 中使用 BeautifulSoup 的入门指南深入讲解了解析部分。
Python 3.8 或更高版本。 使用 python --version 确认。如果尚未安装,请从 python.org 或通过 Anaconda 等发行版安装。
Crawlbase 账户和 token。 注册后,打开控制台,从账户文档页面复制你的普通 token。前 1,000 次请求免费,无需信用卡。请像对待密码一样保管 token,不要将其提交到版本控制系统中。
设置项目
创建虚拟环境以隔离依赖,然后安装爬虫所需的两个库。
python --version python -m venv superpages_env source superpages_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,使用 superpages_env\Scripts\activate 替代 source 命令激活环境。两个依赖完成主要工作:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML,让你可以通过 CSS 选择器提取每个字段。
步骤 1:获取渲染后的搜索页面
首先获取一个结果页面。根据查询关键词和地点构建搜索 URL,导入 CrawlingAPI 类,使用你的 token 初始化它,并请求该 URL。在解析之前检查状态码,可以让失败情况清晰可见而非悄无声息。
from urllib.parse import urlencode from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def build_url(query, location, page=1): base = "https://www.superpages.com/search?" params = {"search_terms": query, "geo_location_terms": location, "page": page} return base + urlencode(params) def crawl(page_url): response = api.get(page_url) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['pc_status']}") return None if __name__ == "__main__": url = build_url("Home Services", "Los Angeles, CA") html = crawl(url) print(html[:500] if html else "No HTML returned")
注意状态检查读取的是响应头中的 pc_status,这是请求的 Crawlbase 状态,与上游 HTTP 状态码不同。运行 python scraper.py,你应该能看到真实的结果标记而非挑战页面。这证明在你编写任何选择器之前,获取路径已经正常工作。
SuperPages 按 IP 限速并对类似爬虫的流量发出挑战,正是你在获取步骤中看到的问题所在。Crawling API 在服务器端通过轮换住宅 IP 路由每次请求,处理 CAPTCHA 和封锁,并返回可直接解析的 HTML,省去了自行运行无头浏览器集群和代理池的麻烦。先在免费层级指向一个公开搜索页面试用。
步骤 2:使用 BeautifulSoup 解析房源
拿到结果页面,将其加载到 BeautifulSoup 中并遍历结果卡片。每家企业都是可预测容器下的独立卡片,其中的名称、地址、电话、网站和详情页链接各有对应的选择器。防御性地读取每个字段(当元素缺失时返回空字符串),可以避免因单个缺失值导致运行崩溃。
from bs4 import BeautifulSoup BASE = "https://www.superpages.com" def extract_listings(html): soup = BeautifulSoup(html, "html.parser") listings = [] for business in soup.select("div.search-results > div.result"): name_el = business.select_one("a.business-name span") category_el = business.select_one("div.categories") address_el = business.select_one("span.street-address") phone_el = business.select_one("a.phone.primary") website_el = business.select_one("a.weblink-button") link_el = business.select_one("a.business-name") listings.append({ "name": name_el.text.strip() if name_el else "", "category": category_el.text.strip() if category_el else "", "address": address_el.text.strip() if address_el else "", "phone": phone_el.text.strip() if phone_el else "", "website": website_el["href"] if website_el else "", "detail_page_link": BASE + link_el["href"] if link_el else "", }) return listings
选择器直接来自 SuperPages 卡片标记:公司名称位于 a.business-name 锚点内的 span 中,地址位于 span.street-address,电话位于 a.phone.primary,外部网站位于 a.weblink-button。详情页链接复用同一个 a.business-name 锚点,为相对路径,因此需要加上 BASE 主机名使其成为完整 URL。每个字段都使用 if ... else "" 进行防护,确保缺失元素在记录中留下空字符串而非抛出异常。
上述类名(result、business-name、street-address、phone primary、weblink-button)反映了当前的 SuperPages 标记,该标记可能会无通知地发生变化。将这些选择器视为起始模板,而非永久合同。当某个字段在所有房源中都返回空值时,在浏览器开发者工具中重新检查实时结果页面并更新选择器。对于任何生产级爬虫,定期维护选择器都是正常操作。
步骤 3:处理结果页面的分页
单页只是演示;实际的潜在客户列表需要覆盖完整结果集。SuperPages 通过 URL 中的 page 参数公开结果页面,因此遍历页面就是对整数范围进行循环。相同的 build_url 和 extract_listings 函数无需修改即可继续使用,因此分页只是一个控制自身请求速率的外层循环。
import time def scrape_all_pages(query, location, max_pages): all_listings = [] for page in range(1, max_pages + 1): print(f"Scraping page {page}...") url = build_url(query, location, page) html = crawl(url) if not html: print(f"Stopping at page {page}: no HTML") break listings = extract_listings(html) if not listings: print(f"No results on page {page}; reached the end") break all_listings.extend(listings) time.sleep(2) return all_listings
两个细节使这个循环适合生产使用。当某页面没有返回任何房源时,它会提前停止,避免在最后一个真实页面之后浪费请求;它在请求之间休眠两秒,使运行不会以一个紧密突发的形式出现。根据你的量需调整 max_pages 和休眠时间;速度越慢,引起的关注越少。
步骤 4:整合并导出
现在将获取、解析和分页整合到一个可运行的脚本中,然后将记录写入 JSON 文件和 CSV 文件,使潜在客户列表可以直接导入电子表格或 CRM 系统。
import csv import json import time from urllib.parse import urlencode from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) BASE = "https://www.superpages.com" FIELDS = ["name", "category", "address", "phone", "website", "detail_page_link"] def build_url(query, location, page=1): base = "https://www.superpages.com/search?" params = {"search_terms": query, "geo_location_terms": location, "page": page} return base + urlencode(params) def crawl(page_url): response = api.get(page_url) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['pc_status']}") return None def extract_listings(html): soup = BeautifulSoup(html, "html.parser") listings = [] for business in soup.select("div.search-results > div.result"): name_el = business.select_one("a.business-name span") category_el = business.select_one("div.categories") address_el = business.select_one("span.street-address") phone_el = business.select_one("a.phone.primary") website_el = business.select_one("a.weblink-button") link_el = business.select_one("a.business-name") listings.append({ "name": name_el.text.strip() if name_el else "", "category": category_el.text.strip() if category_el else "", "address": address_el.text.strip() if address_el else "", "phone": phone_el.text.strip() if phone_el else "", "website": website_el["href"] if website_el else "", "detail_page_link": BASE + link_el["href"] if link_el else "", }) return listings def scrape_all_pages(query, location, max_pages): all_listings = [] for page in range(1, max_pages + 1): print(f"Scraping page {page}...") html = crawl(build_url(query, location, page)) if not html: break listings = extract_listings(html) if not listings: break all_listings.extend(listings) time.sleep(2) return all_listings def save_json(data, filename="superpages_listings.json"): with open(filename, "w") as f: json.dump(data, f, indent=4) def save_csv(data, filename="superpages_listings.csv"): with open(filename, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=FIELDS) writer.writeheader() writer.writerows(data) def main(): rows = scrape_all_pages("Home Services", "Los Angeles, CA", max_pages=5) save_json(rows) save_csv(rows) print(f"Saved {len(rows)} listings") if __name__ == "__main__": main()
由于每条记录共享相同的六个键,CSV 列能整齐对齐,csv.DictWriter 无需额外映射即可写入。修改底部的查询关键词和地点可以针对不同行业或城市,增加 max_pages 可以更深入地搜索某个查询。
输出结果示例
运行完整脚本 python scraper.py,你将得到一份整洁的结构化记录列表,可以写入 JSON、CSV 或数据库。JSON 文件如下所示:
[ { "name": "Evergreen Cleaning Systems", "category": "House Cleaning", "address": "3325 Wilshire Blvd Ste 622, Los Angeles, CA 90010", "phone": "213-375-1597", "website": "https://www.evergreencleaningsystems.com", "detail_page_link": "https://www.superpages.com/los-angeles-ca/bpp/evergreen-cleaning-systems-540709574" }, { "name": "Any Day Anytime Cleaning Service", "category": "House Cleaning", "address": "27612 Cherry Creek Dr, Santa Clarita, CA 91354", "phone": "661-297-2702", "website": "", "detail_page_link": "https://www.superpages.com/santa-clarita-ca/bpp/any-day-anytime-cleaning-service-513720439" } ]
未认领网站的房源返回 "website": "",这是预期行为,也正是解析器防御性读取每个字段而非假设所有键都存在的原因。从这里开始,数据已准备好进行去重、数据丰富化,或导入你的外拓工具。关于将这些记录转化为营销活动的整体工作流程,请参阅网络爬取用于潜在客户生成指南。
扩展至更多查询和地点
单次搜索只覆盖一个行业的一个城市。实际的潜在客户数据集通常需要跨越多个行业和城市,因此下一步自然是用查询和地点对的列表驱动爬虫,而非硬编码的字符串。
searches = [ ("Home Services", "Los Angeles, CA"), ("Plumbers", "San Diego, CA"), ("Electricians", "Phoenix, AZ"), ] all_rows = [] for query, location in searches: all_rows.extend(scrape_all_pages(query, location, max_pages=3)) save_json(all_rows) save_csv(all_rows)
extend 调用将所有搜索结果追加到一个扁平列表中,因此导出步骤保持不变。当查询和地点的矩阵变得庞大时,将工作从单个同步循环迁移到队列。异步 Crawler 可以批量接受 URL 并在完成时推回结果,这比在数千次搜索时逐页阻塞更适合。
保持畅通运行
即使 Crawling API 处理了 IP 轮换和反爬虫层,一些良好习惯仍可让运行保持健康,这些习惯适用于任何目录目标。
- 控制请求速率。 两秒的休眠不是装饰。紧密循环是最快被限速的方式;分散请求看起来更像正常流量。
- 善用轮换。 住宅 IP 池将请求分散到多个真实用户地址,使任何单个地址都不会触发速率限制。Crawling API 为你处理这一切;如果你自建栈,这正是需要做好的部分。
- 关注状态码。 运行开始返回挑战或错误,说明当前速率过于激进。将其视为需要退后的信号,而非可以忽略的噪音。
更广泛的操作手册,请参阅如何在不被封锁的情况下抓取网站。如果你想将同样的方法应用于相邻目录,抓取 Yellow Pages和抓取本地企业列表的教程使用不同的选择器遵循相同的获取-解析-分页模式。
抓取 SuperPages 合法吗?
抓取 SuperPages 是否被允许,取决于网站的服务条款、你所在的司法管辖区以及你对数据的用途。这里的代码并不改变这一点,它只是让技术部分得以实现。请阅读 SuperPages 服务条款及其 robots.txt,并将两者视为你采集内容及速率的边界。本指南中的所有内容仅限于公开的 B2B 企业目录数据:公司名称、类别、公开街道地址、公开电话号码以及其自有网站的链接。这些信息是任何访客无需登录即可看到的,且描述的是企业而非个人。
当你基于这些数据采取行动时,法律权重会发生变化。企业联系方式在许多地区仍受隐私和反垃圾邮件法律的约束。根据 GDPR,小型企业中有姓名的联系人可能被视为个人数据,因此你需要合法依据来存储和处理这些数据,且相关人员保有反对和被删除的权利。在美国,CAN-SPAM 法案规范商业邮件:你必须诚实地表明身份、避免欺骗性主题行,并及时响应退订请求。电话外拓规则和禁止来电登记册在同样的精神下适用于电话推广。采集数据是一回事,用于外拓则是这些义务真正生效之处,因此从一开始就将退订处理和抑制列表纳入构建,而非事后补充。
同样重要的是,本方法不涵盖哪些内容。它不涉及任何登录后才能访问的内容,也不绕过任何身份验证或访问控制来获取受限内容;这超出了本指南的范围,且违反网站条款。如果 SuperPages 为你所需的量提供了官方 API 或授权数据源,请优先使用:官方来源完全消除了歧义。当不确定聚合联系数据集的商业用途时,请查阅适用于你的规则,而非认为公开意味着不受限制。
核心要点
-
SuperPages 是结构化的 B2B 目录。 每条搜索结果都是包含公司名称、类别、公开地址、公开电话、可选网站和详情页链接的卡片,由
search_terms和geo_location_termsURL 参数驱动。 - 批量请求时普通获取会遇到问题。 速率限制、CAPTCHA 和 IP 封锁会阻止简单的循环;Crawling API 通过住宅 IP 路由并在一次调用中返回可直接解析的 HTML。
- BeautifulSoup 负责提取。 将名称、类别、地址、电话、网站和链接映射到当前选择器,防御性地读取每个字段,并预期这些选择器会漂移。
-
分页是对
page参数的循环。 跨页面复用相同的解析器,在空页面时提前停止,请求之间休眠,并导出为 JSON 和 CSV。 - 合规外拓是你的责任。 数据是公开的,但 GDPR 和 CAN-SPAM 仍然适用于你联系这些企业的方式,因此你需要合法依据并必须响应退订请求。
常见问题
抓取 SuperPages 需要普通 token 还是 JS token?
需要普通 token。SuperPages 在初始 HTML 中提供其房源数据,因此普通 token 获取即可返回可解析的标记,且每次请求费用更低。JS token 首先在真实浏览器中渲染页面,只有在目标在页面到达后才在客户端加载房源时才需要。从普通 token 开始,只有当所有字段都返回空时才切换。
如何处理 SuperPages 的分页?
SuperPages 通过 URL 中的 page 参数公开结果页面,因此你循环一个整数范围,为每页构建 URL,并对每页运行相同的解析器。当某页面返回零条房源时停止,这标志着结果集的结束,并在请求之间休眠几秒,使运行不会以一个突发形式出现。
我的选择器返回空值。发生了什么变化?
几乎可以肯定是 SuperPages 的标记发生了变化。像 result、business-name、street-address 和 weblink-button 这样的类名会在没有通知的情况下发生变化,因此上个月还有效的选择器可能会失效。在浏览器开发者工具中重新检查实时结果页面并更新选择器。对于任何生产级爬虫,定期维护选择器都是正常操作,而非出了问题的迹象。
如何将潜在客户导出为 CSV 或 Excel?
爬虫已经使用 csv.DictWriter 写入 CSV,因为每条记录共享相同的键。对于 Excel,pandas 可以用两行代码将相同的字典列表转换为电子表格:pd.DataFrame(rows).to_excel("superpages_listings.xlsx", index=False)。列能整齐对齐,因为字段集是固定的。
我可以同时抓取各企业详情页面吗?
可以。每条记录都包含 detail_page_link,因此你可以将这些 URL 反馈给相同的 crawl 函数,并解析专属页面以获取额外字段,如营业时间或更详细的联系信息。以相同的方式控制第二次通过的速率,因为这会使请求量翻倍,并将范围限于页面上的公开企业信息。
如何保持外拓合规?
将抓取的列表视为起点,而非绿灯信号。根据你所在地区的规则确认你有联系每家企业的合法依据,在每条消息中诚实表明自己身份,并从第一天起就将退订处理和抑制列表纳入你的发送流程。GDPR 和 CAN-SPAM 义务附加于外拓行为,而非采集行为,因此合规工作体现在你如何使用数据上。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
