当你大规模爬取时,工作中最慢的部分是等待。同步爬虫发送请求,阻塞直到受到严密防护的页面渲染并返回,解析,然后才移向下一个 URL。将数千个 URL 排在这种模式后面,你的大部分运行时间都会是空闲的。异步模式将其翻转:你将一批 URL 交给 crawler,它在自己的基础设施上完成缓慢的渲染工作,并将每个完成的结果推送回你控制的 webhook。本指南正是构建这样一个系统,一个接收来自 Crawlbase 异步 Crawler 结果并将其存储在 MySQL 中的 Flask 回调服务器

示例目标是 LinkedIn,但请先阅读下方的注意事项,因为你存储什么比如何存储更重要。本教程刻意将范围限定在公开的、非个人数据:公司页面字段和公开招聘信息文本,而不是个人会员资料。这里真正的教学价值在于架构,即异步爬取加回调服务器加数据库,这种模式无论你指向什么公开来源都同样适用。

构建前请先阅读

LinkedIn 的用户协议严格限制自动化访问,而且大多数 LinkedIn 数据是关于可识别个人的敏感个人数据。本教程仅收集公开的、非个人字段(公司名称、公开公司描述、公开招聘信息文本),绝不涉及会员资料、人脉关系或任何需要登录的内容。如果涉及个人数据,GDPR 和 CCPA 适用:你需要合法依据并必须处理删除请求。对于任何真实或商业用途,正确途径是 LinkedIn 的官方 API 和合作伙伴计划,而不是爬虫。请在将此应用于任何真实场景前阅读结尾的完整合法性说明。

你将构建什么

一个用 Python 编写的小型异步爬取系统,包含三个活动部分和一个共享的 MySQL 数据库。工作被拆分,使缓慢的爬取在你的机器之外进行,结果随着完成而到达,而不是一个阻塞的脚本:

  • 异步 Crawler 请求脚本:将一批公开 URL 推送给 Crawlbase 异步 Crawler,并为每个请求记录一个请求 ID(RID)。
  • Flask 回调服务器:以 HTTP POST 方式接收每个完成的爬取结果,解压并保存原始有效载荷。
  • 处理器:按计划读取已保存的有效载荷,提取公开字段并写入结构化行。
  • MySQL 模式:包含一个请求跟踪表和用于存储你保留的公开字段的表。

请注意模式中故意缺少的内容:没有个人姓名、没有职位描述、没有个人资料摘要、没有人脉数据。我们存储公司公开的身份信息和公开的招聘信息,这些是公司自行发布的非个人信息。

为什么使用异步,为什么需要回调服务器

LinkedIn 公司页面或公开招聘信息在客户端渲染,并受到严密的机器人防护,因此单次请求速度缓慢且经常遭受挑战。对于一长串 URL 同步执行这种操作意味着你的脚本在每一个上都会依次阻塞。异步 Crawler 接受你的 URL,立即返回请求 ID,然后在自己的基础设施上进行缓慢的渲染和重试。当它最终获得干净的响应时,它会以 POST 方式将结果推送到你的 webhook。

这就是为什么你需要回调服务器的原因。你的端点不进行轮询也不等待;它只是随时准备好,每个结果在完成时就以爬取完成的顺序到达。Crawler 引擎以 gzip 压缩方式发送主体,因此你的端点必须在读取之前先解压。将请求、接收和处理解耦到三个脚本中,使系统能够处理大批量请求而不会有任何单一步骤阻塞其他步骤。如果你想了解引擎本身的更多背景,请参阅我们关于如何使用 Crawlbase Crawler 提取数据的指南。

前提条件

需要准备几样东西,都不需要太长时间。

Python 3.8 或更高版本。使用 python3 --version 确认。如果没有,请从 python.org 安装。

MySQL 8。一个你可以本地连接的运行中的 MySQL 服务器。官方安装手册涵盖每个平台。

Crawlbase 账号和普通(TCP)token。注册,打开控制台,复制你的 token。LinkedIn 由普通请求 Crawler 提供服务,因此这里使用 TCP token,而不是 JavaScript token。请像对待密码一样保管 token,不要纳入版本控制。

暴露 localhost 的方法。Crawler 向公开 URL 发布,因此在开发期间你需要 ngrok 这样的隧道来访问你的本地 Flask 应用。

设置项目

创建一个隔离的虚拟环境,然后安装系统所需的库。

bash
python3 -m venv .venv
source .venv/bin/activate

pip install Flask mysql-connector-python pyyaml requests SQLAlchemy

在 Windows 上,使用 .venv\Scripts\activate 代替 source 行。四个库各司其职:Flask 是 webhook 服务器,SQLAlchemy 配合 mysql-connector-python 处理数据库,requests 发送爬取请求,pyyaml 从配置文件读取你的 token。在你的脚本旁边创建一个 settings.yml 来保存 token 和你的 Crawler 名称。

yaml
token: YOUR_CRAWLBASE_TOKEN
crawler: linkedin-public-crawler

步骤 1:设计 MySQL 模式

模式有两个职责:跟踪每个爬取请求的生命周期,以及保存你保留的公开字段。创建用户、数据库和表。在 MySQL 命令行客户端中运行这些语句。

sql
CREATE USER 'linkedincrawler'@'localhost' IDENTIFIED BY 'linked1nS3cret';
CREATE DATABASE linkedin_crawler_db;
GRANT ALL PRIVILEGES ON linkedin_crawler_db.* TO 'linkedincrawler'@'localhost';
USE linkedin_crawler_db;

接下来是表。crawl_requests 表是整个异步过程的控制表:你推送的每个 URL 都有一行,通过 status 跟踪,从 waitingreceived 再到 processedcrawlbase_rid 列将行与 Crawler 返回的请求 ID 关联,这是你将传入回调与触发它的请求匹配的唯一键。

sql
CREATE TABLE IF NOT EXISTS `crawl_requests` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `url` TEXT NOT NULL,
  `status` VARCHAR(30) NOT NULL,
  `crawlbase_rid` VARCHAR(255) NOT NULL
);

CREATE INDEX `idx_crawl_requests_status` ON `crawl_requests` (`status`);
CREATE INDEX `idx_crawl_requests_rid` ON `crawl_requests` (`crawlbase_rid`);

目标表仅保存公开的、非个人的公司数据。每个公司页面一行,加上该页面链接的公开招聘信息子表。任何地方都没有个人姓名、职位或个人资料文本的列。这是模式本身具体体现的隐私边界。

sql
CREATE TABLE IF NOT EXISTS `company_pages` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `crawl_request_id` INT NOT NULL,
  `company_name` VARCHAR(255),
  `industry` VARCHAR(255),
  `description` TEXT,
  FOREIGN KEY (`crawl_request_id`) REFERENCES `crawl_requests`(`id`)
);

CREATE TABLE IF NOT EXISTS `company_job_postings` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `company_page_id` INT NOT NULL,
  `title` VARCHAR(255),
  `location` VARCHAR(255),
  `description` TEXT,
  FOREIGN KEY (`company_page_id`) REFERENCES `company_pages`(`id`)
);

步骤 2:定义 ORM

使用 SQLAlchemy 将这些表映射到 Python 类,这样其余代码就可以操作对象而不是原始 SQL。将其保存为 lib/database.py。这些类精确反映了模式:用于跟踪的 CrawlRequest、用于公开公司字段的 CompanyPage,以及每个公开招聘信息的 JobPosting 子类。

python
from typing import List
from sqlalchemy import ForeignKey, create_engine
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column, relationship

class Base(DeclarativeBase):
    pass

class CrawlRequest(Base):
    __tablename__ = 'crawl_requests'
    id: Mapped[int] = mapped_column(primary_key=True)
    url: Mapped[str]
    status: Mapped[str]
    crawlbase_rid: Mapped[str]
    company_page: Mapped['CompanyPage'] = relationship(back_populates='crawl_request')

class CompanyPage(Base):
    __tablename__ = 'company_pages'
    id: Mapped[int] = mapped_column(primary_key=True)
    company_name: Mapped[str]
    industry: Mapped[str]
    description: Mapped[str]
    crawl_request_id: Mapped[int] = mapped_column(ForeignKey('crawl_requests.id'))
    crawl_request: Mapped['CrawlRequest'] = relationship(back_populates='company_page')
    job_postings: Mapped[List['JobPosting']] = relationship(back_populates='company_page')

class JobPosting(Base):
    __tablename__ = 'company_job_postings'
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    location: Mapped[str]
    description: Mapped[str]
    company_page_id: Mapped[int] = mapped_column(ForeignKey('company_pages.id'))
    company_page: Mapped['CompanyPage'] = relationship(back_populates='job_postings')

def create_database_session():
    url = 'mysql+mysqlconnector://linkedincrawler:linked1nS3cret@localhost:3306/linkedin_crawler_db'
    engine = create_engine(url, echo=True)
    return Session(engine)

create_database_session 返回其他每个脚本导入的会话。连接字符串携带你在步骤 1 中设置的用户、密码、主机和数据库;如果你的配置不同,请在这里修改。

步骤 3:向异步 Crawler 推送 URL

此脚本读取公开 URL 列表,将每个发送给异步 Crawler,并以 waiting 状态记录返回的 RID。关键参数是 callback=true(告诉 Crawler 将结果以 POST 方式发回,而不是内联返回)和 crawler=(命名你将在控制台创建的 Crawler)。将其保存为 crawl.py,并在 urls.txt 中每行放一个公开公司和招聘信息 URL。

python
import requests
import urllib.parse
import json
import yaml
from json import JSONDecodeError
from lib.database import CrawlRequest, create_database_session

settings = yaml.safe_load(open('settings.yml'))
token = settings.get('token')
crawler = settings.get('crawler')

if not token or not crawler:
    print('Set your token and crawler name in settings.yml')
    exit()

urls = open('urls.txt', 'r').readlines()
api = 'https://api.crawlbase.com?token={0}&callback=true&crawler={1}&url={2}&autoparse=true'
session = create_database_session()

for url in urls:
    url = url.strip()
    if not url:
        continue
    encoded = urllib.parse.quote(url, safe='')
    api_url = api.format(token, crawler, encoded)
    print(f'Requesting crawl for {url}')
    try:
        response = requests.get(api_url)
        rid = json.loads(response.text)['rid']
        request_row = CrawlRequest(url=url, crawlbase_rid=str(rid), status='waiting')
        session.add(request_row)
        session.commit()
    except JSONDecodeError:
        print(f'Could not decode response for {url}')

print('Done pushing crawl requests.')

每次调用返回一个类似 {"rid": 12341234} 的小型 JSON 主体。脚本将 RID 以 waiting 状态存储在 crawl_requests 中,并立即移向下一个 URL,而不阻塞实际的爬取。autoparse=true 参数要求 Crawler 返回结构化字段而非原始 HTML,这就是步骤 6 中处理器所读取的内容。这正是异步模型的意义所在:推送一百个 URL 只需几秒钟,繁重的工作在其他地方进行。

Crawlbase LinkedIn Scraper

你刚刚写的推送请求在几秒钟内返回 RID,因为缓慢的部分(在可信住宅 IP 后渲染受到严密防护的 LinkedIn 页面并重试直到获得干净的 200 响应)发生在 Crawlbase 基础设施上,而不是你的机器上。异步 Crawler 将你的批次排入队列,在服务端进行渲染和轮换,并将每个完成的结果发布到你的 webhook,所以你永远不需要运行无头浏览器集群或代理池。从免费套餐开始。

步骤 4:构建 Flask 回调服务器

这是系统的核心。Crawler 将每个完成的结果发布到单个路由。你的工作是验证请求、解压主体并保存有效载荷,以便处理器可以提取它。Crawler 在名为 rid 的头中发送 RID,并发送两个状态头,PC-Status(Crawlbase 状态)和 Original-Status(目标网站的状态)。你只保留两者都为 200 的结果。将其保存为 callback_server.py

python
import gzip
import os
from flask import Flask, request
from lib.database import CrawlRequest, create_database_session

app = Flask(__name__)
session = create_database_session()
os.makedirs('./data', exist_ok=True)

def header_status(name):
    value = request.headers.get(name)
    return int(value.split(',')[0]) if value else None

@app.route('/crawlbase_crawler_callback', methods=['POST'])
def crawlbase_crawler_callback():
    rid = request.headers.get('rid')
    encoding = request.headers.get('Content-Encoding')

    if rid is None:
        return ('', 204)
    if rid == 'dummyrequest':
        print('Callback server is working')
        return ('', 204)
    if header_status('PC-Status') != 200 or header_status('Original-Status') != 200:
        return ('', 204)

    crawl_request = session.query(CrawlRequest).filter_by(crawlbase_rid=rid, status='waiting').first()
    if crawl_request is None:
        print(f'No waiting request for rid {rid}')
        return ('', 204)

    body = request.data
    if encoding == 'gzip':
        try:
            body = gzip.decompress(body)
        except OSError:
            pass

    with open(f'./data/{rid}.json', 'wb') as f:
        f.write(body)

    crawl_request.status = 'received'
    session.commit()
    print(f'Received rid {rid}')
    return ('', 201)

if __name__ == '__main__':
    app.run(port=5000)

逐一检查这些验证逻辑,因为每个都很重要。缺少 rid 意味着请求不是来自 Crawler,因此被丢弃。dummyrequest RID 是平台发送的测试 ping 以确认你的端点可访问;你记录它并提前返回。状态检查忽略两端都不是干净 200 的任何内容。然后你以 waiting 状态在 crawl_requests 中查找 RID:如果不存在这样的行,回调与你发出的请求不对应,它被忽略。只有在所有这些之后,你才解压并保存主体,然后将行翻转为 received。端点永远不会阻塞;它写入文件并立即返回,即使在回调洪流中也能保持响应。

保护你的 webhook

当隧道打开时,你的回调 URL 是公开的。加强安全:只接受 POST,在你每次请求都会验证的自定义头或 URL 参数中要求一个秘密 token,并确认预期的 ridPC-StatusOriginal-Status 头都存在。避免使用 IP 白名单,因为源地址会轮换且可能随时更改。

步骤 5:暴露服务器并注册 Crawler

Crawler 需要一个公开 URL 来发布内容。Flask 应用在端口 5000 上运行时,打开隧道。

bash
python callback_server.py
ngrok http 5000

ngrok 打印一个公开 HTTPS URL。你的完整回调路由是该 URL 加上路径,例如 https://your-subdomain.ngrok.io/crawlbase_crawler_callback。在涉及 Crawler 之前,先用测试 ping 确认端点是否存活。

bash
curl -i -X POST 'http://localhost:5000/crawlbase_crawler_callback' \
  -H 'rid: dummyrequest' \
  -H 'Content-Type: gzip/json' \
  -H 'Content-Encoding: gzip'

你应该在 Flask 日志中看到 Callback server is working。现在转到你的 Crawlbase 控制台,打开创建 Crawler 页面,给 Crawler 与 settings.yml 中相同的名称,并粘贴你的完整 ngrok 回调 URL。LinkedIn 由普通请求(TCP)Crawler 提供服务,因此选择该类型。保存后,Crawler 就知道在哪里推送结果了。

步骤 6:将接收到的有效载荷处理为结构化行

回调服务器只保存原始有效载荷。一个单独的处理器按计划运行,提取状态为 received 的所有内容,提取公开字段,写入结构化行,并将请求标记为 processed。将接收与处理分离意味着缓慢的数据库写入永远不会阻塞 webhook。将其保存为 process.py

python
import json
import sched
import time
from lib.database import CrawlRequest, CompanyPage, JobPosting, create_database_session

INTERVAL_SECONDS = 60
BATCH_LIMIT = 10

def process():
    session = create_database_session()
    received = session.query(CrawlRequest).filter_by(status='received').limit(BATCH_LIMIT).all()

    if not received:
        print('No received requests to process.')
        return

    for req in received:
        with open(f'./data/{req.crawlbase_rid}.json') as f:
            data = json.load(f)

        page = CompanyPage(
            company_name=data.get('name'),
            industry=data.get('industry'),
            description=data.get('description'),
        )
        page.crawl_request_id = req.id
        session.add(page)

        for job in data.get('jobs', []):
            posting = JobPosting(
                title=job.get('title'),
                location=job.get('location'),
                description=job.get('description'),
            )
            posting.company_page = page
            session.add(posting)

        req.status = 'processed'

    session.commit()

def process_and_reschedule():
    process()
    scheduler.enter(INTERVAL_SECONDS, 1, process_and_reschedule)

if __name__ == '__main__':
    scheduler = sched.scheduler(time.monotonic, time.sleep)
    process_and_reschedule()
    scheduler.run()

处理器从解析后的有效载荷中只读取非个人的公司和招聘字段:公司名称、行业、公开描述,以及每个公开招聘信息的职位、地点和描述。即使有效载荷中碰巧存在个人层面的字段,它也不会触及。将提取列表保持得如此严格,是继模式之后的第二道隐私边界。sched 循环每 60 秒重新运行一次 process,每次最多处理十个请求,这在大量积压下保持了内存的稳定。

运行完整管道

注册了 Crawler 之后,在各自的终端中运行三个部分,每个终端都激活了虚拟环境。顺序很重要:回调服务器和处理器必须在你推送请求之前启动,否则早期的回调到来时无处落地。

bash
# terminal 1: webhook (already running, plus ngrok)
python callback_server.py

# terminal 2: scheduled processor
python process.py

# terminal 3: push the batch
python crawl.py

随着 crawl.py 运行,crawl_requests 中出现状态为 waiting 的行。几分钟后,随着 Crawler 完成每个页面,回调服务器将其翻转为 received 并在 ./data 下写入 JSON 文件。在下一次执行时,处理器读取这些文件,填充 company_pagescompany_job_postings,并将请求标记为 processed。你可以从控制台的 Crawler 监控标签实时观察这个过程,它显示每个请求的实时状态。

存储数据的样子

完整运行后,目标表保存干净的、非个人的公司记录。以 JSON 形式读取时,一个已处理的公司页面如下所示。

json
{
  "company_name": "Example Robotics",
  "industry": "Industrial Automation",
  "description": "We design warehouse automation systems.",
  "job_postings": [
    {
      "title": "Backend Engineer",
      "location": "Remote, EU",
      "description": "Build and operate our ingestion services."
    }
  ]
}

那里的每个字段都是公司自己发布的内容。没有个人,没有联系方式,没有个人资料。这是有意为之的,也是使数据集站得住脚的原因。

扩展规模和发送额外上下文

这个架构无需结构性变化即可扩展:更大的 urls.txt 意味着更多 waiting 行,Crawler 吸收队列,回调随着爬取完成而到达。为了将有效载荷与你自己的上下文匹配,在推送请求时使用 callback_headers 参数附加数据。Crawler 在回调时将这些头回显,因此你可以携带例如批次 ID 而无需将其存储在 URL 中。

python
raw_headers = f'BATCH-ID:{batch_id}|SOURCE:public-company-page'
encoded_headers = urllib.parse.quote(raw_headers, safe='')
# append &callback_headers={encoded_headers} to the api url

在接收端,将它们作为普通请求头读回:request.headers.get('BATCH-ID')。有关在受防护目标上保持大型运行健康的更深入内容,请参阅我们关于如何在不被封禁的情况下抓取网站以及构建可扩展网络数据管道的指南。

抓取 LinkedIn 合法吗?

这是你在编写生产代码之前需要厘清的部分,而不是之后。LinkedIn 的用户协议和其禁止软件及扩展政策明确禁止抓取和自动化数据收集,LinkedIn 会执行这些条款。无论你的工具多么谨慎,这一立场都不会改变。本指南中的代码使技术层面的工作得以实现;它不使抓取 LinkedIn 符合 LinkedIn 的条款。阅读用户协议和 LinkedIn 的 robots.txt,并将两者视为你行为的边界。

数据维度同样重要。大多数 LinkedIn 内容是关于可识别个人的个人数据:姓名、工作经历、职位标题、人脉关系和帖子。在欧洲的 GDPR 和加利福尼亚的 CCPA 下,处理个人数据需要合法依据,且人们拥有权利,包括要求删除其数据的权利。这里也有真实的判例法:在 hiQ Labs v. LinkedIn 案中,美国法院根据《计算机欺诈和滥用法》审查了对公开资料的抓取,但该诉讼范围狭窄,仅针对特定司法管辖区,并不认可一般性抓取行为,也不凌驾于 LinkedIn 的合同条款或数据保护法之上。合法性取决于数据、方法、司法管辖区以及你所受约束的协议,因此对于"公开意味着可以随意使用"这样的笼统说法要保持怀疑。

这就是为什么本教程的范围如此设定。它只存储公开的、非个人的公司信息:公司名称、行业、公开描述,以及公司自行发布的公开招聘信息文本。它从不构建个人资料,从不触碰登录后的任何内容,也从不收集会员个人数据。对于任何真实或商业需求,正确途径是 LinkedIn 的官方 API 和合作伙伴计划,这些提供了在 LinkedIn 条款内经过认可的结构化访问。如果你的项目需要会员级别的数据,那条路,或者正式的数据协议,才是答案,而不是爬虫。如果你对自己的具体用途有疑问,请咨询有资质的律师。有关公开数据方法的更多概述,请参阅我们关于如何抓取 LinkedIn的指南。

回顾

核心要点

  • 异步在大规模场景下优于同步。向 Crawler 推送 URL 在几秒钟内返回 RID;缓慢的渲染在你的机器之外进行,结果随着完成而到达。
  • 回调服务器是一个精简的、受保护的接收器。验证 RID 和两个状态头,解压 gzip 主体,保存它,并立即返回以确保 webhook 永远不阻塞。
  • 在 MySQL 中跟踪状态。crawl_requests 表将每个请求经历 waitingreceivedprocessed 三个阶段,这就是接收和处理保持解耦的原因。
  • 只存储公开的、非个人的数据。模式和处理器都只保留公司页面和公开招聘信息字段,从不涉及会员资料或个人数据。
  • 对于任何真实用途,优先使用官方途径。LinkedIn 的条款限制抓取,其大部分数据属于个人数据;使用 LinkedIn 的官方 API 和合作伙伴计划,并遵守 GDPR 和 CCPA。

常见问题

为什么使用异步 crawler 而不是同步脚本?

同步脚本在每个 URL 上都会阻塞,等待受防护的页面渲染并返回,因此长列表的运行大部分时间是空闲的。异步 Crawler 接受你的 URL,立即返回请求 ID,并在自己的基础设施上进行缓慢的渲染和重试,然后将完成的结果发布到你的 webhook。推送一个大批次只需几秒钟,结果随着完成而流回,而不是一次一个缓慢的请求。

Flask 回调服务器实际上做什么?

它暴露一个 POST 路由,Crawler 对每个完成的结果调用此路由。处理程序读取 rid 头,检查 PC-StatusOriginal-Status 都为 200,确认 RID 与仍处于 waiting 状态的请求匹配,解压 gzip 主体,将有效载荷保存到磁盘,并将请求翻转为 received。它立即返回且永不阻塞,因此即使在回调突发情况下也能保持响应。

为什么将接收和处理分成两个脚本?

这样缓慢的数据库写入就不会拖慢 webhook。回调服务器唯一的工作是快速接收和保存。一个单独的计划处理器分小批量读取已保存的有效载荷,提取公开字段,写入结构化行,并将每个请求标记为 processed。将两者解耦使系统能够处理大量回调,而两端都不会产生背压。

我需要 JavaScript token 还是普通 token?

普通请求(TCP)token。LinkedIn 由普通请求 Crawler 提供服务,因此你在控制台选择该 Crawler 类型,并在 settings.yml 中使用你的 TCP token。异步 Crawler 仍然在后台处理轮换和重试;token 类型只是告诉它针对目标使用哪种请求路径。

如何保持 webhook 安全?

只接受 POST 请求,在你每次调用都验证的自定义头或 URL 参数中要求一个秘密 token,并确认预期的 ridPC-StatusOriginal-Status 头在你信任有效载荷之前都存在。避免使用 IP 白名单,因为源地址会轮换且可能随时更改。示例中的状态和 RID 检查是起点,而不是全部。

以这种方式存储 LinkedIn 数据安全吗?

只有当你只保留公开的、非个人数据时才安全,就像本教程所做的那样:公司名称、行业、公开描述和公开招聘信息文本。存储会员资料、姓名、人脉关系或其他个人数据会引发 LinkedIn 用户协议以及 GDPR 和 CCPA 等法律,这超出了本教程的范围。对于会员级别或商业用途,请使用 LinkedIn 的官方 API 和合作伙伴计划,而不是爬虫。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量