Clutch.co 列出了 IT、营销、设计和开发领域超过 150,000 家服务提供商,每个公司档案都携带着潜在客户生成管道、竞争研究仪表板或市场研究真正需要的结构化 B2B 数据:公司名称、星级评分、评价数量、最小项目规模、时薪区间、所在地,以及完整档案的链接。问题在于 Clutch 有严密的机器人防护,因此普通 HTTP 请求往往根本到达不了列表页面。
本指南向你展示如何以可靠的方式构建 Python Clutch.co 爬虫。你将构建一个可运行的小脚本,通过 Crawling API 获取渲染后的类别页面,用 BeautifulSoup 解析每个公司卡片,并写出干净的结构化行数据。整个演练限定在公开的公司列表数据范围内,靠近末尾的法律部分不是样板文字,在将此代码指向任何实际使用量之前请认真阅读。
你将构建什么
一个 Python 脚本,接收一个公开的 Clutch.co 类别 URL,通过 Crawling API 检索渲染后的 HTML,并为页面上的每家公司提取结构化记录。我们以 IT 服务目录为运行示例,并从每张卡片中提取这些字段:
- 公司名称,即提供商列出的企业名称。
- 评分,即卡片上显示的综合星级得分。
- 评价数量,即支撑该评分的客户评价数。
- 最小项目规模,即提供商接受的最小合作规模,例如"$5,000+"。
- 时薪,即列出的时薪区间,例如"$50 - $99 / hr"。
- 所在地,即提供商的主要城市或地区。
- 档案 URL,即公司在 Clutch 上完整档案的链接。
为什么普通请求在 Clutch.co 上会失败
如果你用裸 HTTP 客户端请求 Clutch.co 的类别 URL,通常根本得不到列表。Clutch 运行激进的机器人防护,来自数据中心 IP 的明显自动化请求在任何公司数据到达你之前就会被挑战或返回 403。即使请求侥幸通过,页面的部分内容也是客户端渲染的,因此原始 HTML 可能缺少你需要的那些卡片。
所以可运行的 Clutch 爬虫在一次请求中需要两样东西:真正渲染页面的浏览器,以及平台认为是真实访客的 IP。你可以自己用无头浏览器加上一组轮换住宅代理来组装,但将这些拼合在一起并保持健康才是大部分工作所在。Crawling API 将两者合并为单次调用:你用 JavaScript token 发送 URL,它在受信任的住宅 IP 后面渲染页面,并返回供你解析的完成 HTML。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 先在真实浏览器中渲染页面并通过轮换住宅 IP 路由请求。Clutch 有严密防护且部分是客户端渲染的,所以这里需要 JS token。前 1,000 次请求免费让你上手,无需信用卡。
前置条件
在编写任何代码之前需要准备几样东西,每样都不需要太长时间。
基本 Python 技能。你应该能够编写和运行 Python 脚本,并用 pip 安装包。如果 BeautifulSoup 对你来说是新的,我们的Python 中使用 BeautifulSoup 指南涵盖了本教程所假设的解析基础。
Python 3.8 或更高版本。用 python --version 确认你的版本。如果没有,从 python.org 安装,并确保 Python 在你的系统 PATH 中。
Crawlbase 账户和 JS token。注册,打开你的控制面板,复制你的 JavaScript(JS)token。将 token 视为密码:它对你的请求进行身份验证,因此不要放入版本控制。
设置项目
创建虚拟环境以隔离项目依赖,然后安装爬虫需要的库。
python --version python -m venv clutch_env source clutch_env/bin/activate pip install crawlbase beautifulsoup4 pandas
在 Windows 上,用 clutch_env\Scripts\activate 代替 source 那行来激活环境。三个依赖项完成工作:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML 以便你按 CSS 选择器提取字段,pandas 将收集的行数据最终转为 CSV。
步骤 1:获取渲染后的类别页面
从获取完成的页面开始。导入 CrawlingAPI 类,用你的 JS token 初始化它,并请求类别 URL。在解析之前检查状态码,使失败保持显眼而非静默。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def fetch_html(url): response = api.get(url, {"ajax_wait": "true", "page_wait": 5000}) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": base_url = "https://clutch.co/it-services" html = fetch_html(base_url) print(html[:500] if html else "No HTML returned")
两个等待选项对于像这样有防御性、部分客户端渲染的目标很重要。ajax_wait 告诉 API 等待异步内容完成加载,page_wait 在加载后等待固定毫秒数,使延迟渲染的卡片在页面被捕获前出现。五秒是合理的起点;如果卡片返回为空则增加。用 python scraper.py 运行脚本,你应该能看到真实的提供商标记,而不是普通 requests.get 返回的 403 页面。这在你编写任何选择器之前就确认了请求已成功。
Clutch.co 对裸请求返回 403,所以你需要在受信任 IP 后面的渲染页面,一次调用完成。Crawling API 接收 JS token,在真实浏览器中运行页面,在服务端轮换住宅 IP,并将完成的 HTML 交给你,让你无需自己运行无头集群和代理池。先在免费层级将其指向公开 IT 服务目录试试。
步骤 2:用 BeautifulSoup 解析公司卡片
拿到渲染后的 HTML,将其加载到 BeautifulSoup 中,按选择器提取每个提供商。Clutch 以重复结构排列其列表:每个提供商是 ul.providers__list 内的 li.provider,所以你一次选取所有卡片,然后从每张卡片中读取相同的字段。在浏览器开发者工具(通常是 F12)中检查实时页面以确认当前类名;下面的选择器与撰写时的布局匹配。
import re from bs4 import BeautifulSoup def text_of(card, selector): el = card.select_one(selector) return re.sub(r"\s+", " ", el.get_text(strip=True)) if el else "N/A" def parse_html(html): soup = BeautifulSoup(html, "html.parser") data = [] companies = soup.select("ul.providers__list > li.provider") for company in companies: profile = company.select_one("h3.provider__title a") profile_url = profile["href"] if profile else "N/A" data.append({ "Company Name": text_of(company, "h3.provider__title"), "Rating": text_of(company, "span.sg-rating__number"), "Number of Reviews": text_of(company, "a.sg-rating__reviews"), "Min Project Size": text_of(company, "li.provider__highlights-item.min-project-size span"), "Hourly Rate": text_of(company, "li.provider__highlights-item.hourly-rate span"), "Location": text_of(company, "li.provider__highlights-item.location span.locality"), "Profile URL": profile_url, }) return data
text_of 辅助函数同时做两件有用的事:当元素缺失时返回 "N/A",而不是对空内容调用 .get_text() 时抛出异常;用 re.sub(r"\s+", " ", ...) 折叠空白字符,使类似 "\n 128 reviews " 这样的评价数干净地返回。这使提取在字段缺失时具有弹性,而这种情况很常见,因为不是每个提供商都列出了最小项目规模或时薪。档案 URL 从锚点的 href 而非其文本中读取,所以单独处理。
Clutch 的类名会在不通知的情况下更改。将上面的选择器视为起始模板,而非合同。当某个字段对每张卡片都返回 "N/A" 时,在浏览器开发者工具中重新检查实时列表并更新选择器。周期性选择器维护对任何生产爬虫来说都是正常的,而不是出了问题的信号。
步骤 3:处理翻页
Clutch 将提供商列表分布在多个页面上,并使用 page 查询参数在页面间移动。要收集整个目录,你在循环中遍历页面,通过同一函数获取每个页面,并收集行数据。由于每个页面共享相同的卡片结构,你已经编写的解析器可以在所有页面上正常工作而无需修改。
import time def scrape_clutch_data(base_url, pages): all_data = [] for page in range(1, pages + 1): url = f"{base_url}?page={page}" html = fetch_html(url) if html: all_data.extend(parse_html(html)) time.sleep(3) return all_data
请求之间的 time.sleep(3) 是刻意的。控制节奏使你避免在紧密循环中轰炸 Clutch,这是即使每个请求通过受信任 IP 渲染也会被限速的最快方式。在确认选择器正常工作之前,先用少量页面测试,然后再增加数量。
步骤 4:整合并保存为 CSV
现在将获取、解析和翻页循环连接成一个可运行的脚本,然后将收集的行数据交给 pandas 写入 CSV。平面 CSV 是 B2B 数据最通用的输出:可在任何电子表格中打开,加载到数据库,无需额外工作即可用于 CRM 导入。
import re import time import pandas as pd from bs4 import BeautifulSoup from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def fetch_html(url): response = api.get(url, {"ajax_wait": "true", "page_wait": 5000}) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None def text_of(card, selector): el = card.select_one(selector) return re.sub(r"\s+", " ", el.get_text(strip=True)) if el else "N/A" def parse_html(html): soup = BeautifulSoup(html, "html.parser") data = [] for company in soup.select("ul.providers__list > li.provider"): profile = company.select_one("h3.provider__title a") data.append({ "Company Name": text_of(company, "h3.provider__title"), "Rating": text_of(company, "span.sg-rating__number"), "Number of Reviews": text_of(company, "a.sg-rating__reviews"), "Min Project Size": text_of(company, "li.provider__highlights-item.min-project-size span"), "Hourly Rate": text_of(company, "li.provider__highlights-item.hourly-rate span"), "Location": text_of(company, "li.provider__highlights-item.location span.locality"), "Profile URL": profile["href"] if profile else "N/A", }) return data def scrape_clutch_data(base_url, pages): all_data = [] for page in range(1, pages + 1): html = fetch_html(f"{base_url}?page={page}") if html: all_data.extend(parse_html(html)) time.sleep(3) return all_data def main(): base_url = "https://clutch.co/it-services" data = scrape_clutch_data(base_url, pages=5) df = pd.DataFrame(data) df.to_csv("clutch_data.csv", index=False) print(f"Saved {len(data)} companies to clutch_data.csv") if __name__ == "__main__": main()
用 python scraper.py 运行完整脚本,它将遍历 IT 服务目录的五个页面,解析每页的每个提供商,并写入一个 CSV。将 base_url 替换为任何其他公开类别,例如 https://clutch.co/agencies/digital,并修改 pages 来控制爬取深度。
输出是什么样的
每行是一条干净的结构化记录,可直接在电子表格中打开、加载到数据库或用于 CRM 导入。以下是脚本生成的数据样本,以 JSON 格式显示便于阅读。
[ { "Company Name": "Lorem Software Group", "Rating": "4.9", "Number of Reviews": "128 reviews", "Min Project Size": "$25,000+", "Hourly Rate": "$50 - $99 / hr", "Location": "Austin, TX", "Profile URL": "https://clutch.co/profile/lorem-software-group" }, { "Company Name": "Ipsum Digital Labs", "Rating": "4.7", "Number of Reviews": "54 reviews", "Min Project Size": "$10,000+", "Hourly Rate": "$100 - $149 / hr", "Location": "London, England", "Profile URL": "https://clutch.co/profile/ipsum-digital-labs" } ]
跨类别扩展与保持不被封锁
一个目录是演示;真实任务跨越许多类别运行。形态保持不变:维护一个类别 URL 列表,对每个运行 scrape_clutch_data,并在写入 CSV 之前合并行数据。解析器可以在所有类别上正常工作而无需修改。然而 Clutch 是一个难度高的商业目标,所以一些习惯有助于保持长时间运行的健康。
- 控制请求节奏。在紧密循环中轰炸页面是被限速的最快方式。保持请求之间的延迟,将大型任务分散在时间上,而不是以全速爬取整个目录。
- 依靠轮换。住宅 IP 池将请求分散到多个真实用户地址,使单个地址不会触发速率限制。Crawling API 为你处理这些;如果你自己搭建技术栈,这是需要投入的部分。
-
读取状态码。开始返回
403或其他挑战的运行是在告诉你当前速率太激进。将其视为退让的信号,而非要忽略的噪音。
更广泛的攻略请参阅如何在爬取网站时不被封锁以及关于在网络爬取时如何绕过 CAPTCHA 的深度文章。由于 Clutch 对其页面的部分内容进行客户端渲染,我们关于用 Python 爬取 JavaScript 页面的指南解释了为什么渲染很重要。如果你宁愿通过轮换池路由自己的流量而不使用托管 API,Smart AI Proxy(也称为 AI Proxy)为你提供相同的住宅 IP 轮换作为直接可用的代理端点。
爬取 Clutch.co 合法吗?
爬取 Clutch.co 是否被允许取决于 Clutch 的服务条款、你的司法管辖区,以及你如何使用数据。Clutch 的条款对自动化访问设置了限制,因此无论你的工具多么谨慎,爬取都可能违反这些条款。以上代码不能改变这一点;它只是让技术部分得以运行。阅读 Clutch 服务条款和其 robots.txt,并将两者都视为你采集内容的边界。Clutch 没有为其目录发布公开的开放 API,因此没有比该页面更受认可的端点;这使尊重所述限制变得更加重要,而非更少。
值得坚守的几条原则。只采集公开公司列表数据:任何人在类别页面上无需账户即可看到的公司名称、评分、评价数量、最小项目规模、时薪、所在地和档案链接。保持你的请求量适度,不要给 Clutch 的服务器造成压力,分散运行而非一次拉取整个目录。如果你计划商业性地重用数据,用于外联、转售或产品,请获取许可或正式协议,而不是假设沉默即同意。
本指南有意限定在公开目录和类别页面,因为这是使工作保持可辩护的界限。不涵盖登录后的任何内容、评价者个人数据、未公开列出的联系方式,或你会作为自己内容重新分发的受版权保护的评价文本。仅限公开公司列表数据。如果你的项目需要的不止于此,与 Clutch 的数据合作才是正确的路径,而非更聪明的爬虫。
核心要点
-
Clutch 封锁普通请求。裸
requests.get通常返回 403,所以你必须在解析之前在受信任 IP 后面渲染页面。 -
通过 Crawling API 使用 JS token。一次调用在真实浏览器中渲染页面并轮换住宅 IP;
ajax_wait和page_wait控制等待内容的时长。 -
BeautifulSoup 完成提取。选取
ul.providers__list中的每个li.provider,然后读取公司名称、评分、评价数、最小项目规模、时薪、所在地和档案 URL,并预期选择器会变化。 -
用 page 参数翻页。Clutch 通过
?page=N翻页,所以真实任务循环遍历页面,重用同一解析器,并在请求之间等待。 - 保持在公开数据范围内。尊重 Clutch 的服务条款和 robots.txt,保持适度使用量,并在任何商业重用前获得许可。
常见问题
为什么向 Clutch.co 发出的普通请求返回 403?
Clutch 运行激进的机器人防护。来自数据中心 IP 的明显自动化请求在任何列表数据到达你之前就会被挑战或以 403 封锁。要获取真实数据,你需要一个渲染页面并来自平台认为是真实访客的 IP 的请求,这正是 Crawling API 的 JS token 为你处理的。
Clutch.co 需要普通 token 还是 JS token?
需要 JS token。普通 token 获取静态 HTML 且不运行浏览器,在像 Clutch 这样有防御性、部分客户端渲染的网站上往往返回空内容或被封锁。JS token 在将 HTML 交还之前在真实浏览器中渲染页面并通过轮换住宅 IP 路由,所以当 BeautifulSoup 解析时公司卡片已经存在。
我可以从 Clutch.co 列表中爬取哪些数据?
从公开类别页面,你可以读取每个提供商的公司名称、综合评分、评价数量、最小项目规模、时薪区间、所在地,以及其完整档案的链接。本指南提取的正是这些字段。登录后的任何内容、评价者个人数据或私人联系方式超出范围且不被允许。
如何爬取多页 Clutch.co 结果?
Clutch 用 page 查询参数翻页,所以你将每个 URL 构建为 f"{base_url}?page={page}" 并循环页码。通过同一函数获取每个页面,运行同一解析器,收集行数据。在请求之间添加短暂的 time.sleep,避免轰炸网站,一旦输出看起来正确就增加页数。
我的选择器对每张卡片都返回"N/A"。是什么改变了?
几乎可以肯定是 Clutch 的标记改变了。其类名在不通知的情况下更改,所以上个月有效的选择器可能会失效。在浏览器开发者工具中重新检查实时列表并更新选择器,例如 h3.provider__title 或 span.sg-rating__number。周期性选择器维护对任何生产爬虫来说都是正常的。
如何避免在爬取 Clutch.co 时被封锁?
保持低 per-IP 请求速率,在请求之间用延迟控制节奏,通过轮换住宅 IP 路由,使单个地址不会触发速率限制。Crawling API 为你管理轮换和受信任 IP 池;如果你自己搭建技术栈,这是需要投入的部分。注意状态码,当开始看到 403 响应或其他挑战时立即退让。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
