一、项目背景
很多人做采集做到中后期,都会遇到一个绕不开的问题——“多用户共用平台怎么隔离权限?”
一开始也许只是想做个简单的管理后台,大家都能登录提交爬取任务。但慢慢你会发现:
- 不同客户的数据混在一起,查的时候要手动过滤;
- 某个团队的代理IP被封了,影响到了别人;
- 某个任务出错竟然把别的租户任务也卡死了……
这就是典型的 权限隔离和多租户(SaaS)问题。
于是,我就做了这样一个实战项目:构建一个支持多租户、任务隔离、代理独立的采集平台。让每个租户(公司或部门)都能在自己的空间里提交任务、查看结果、用自己的代理池,不干扰别人。
二、数据目标
我们以一个具体案例来展开。
假设有几家汽车经销商客户,他们都想抓取Autohome 上的汽车品牌、型号和价格数据,用于市场分析。
要求很明确:
- 每个客户能自定义关键词(比如“新能源SUV”、“丰田”等);
- 每个客户的数据独立保存;
- 抓取任务之间互不影响;
- 每个客户使用自己的代理身份,避免被封号或数据串线。
换句话说,这个平台要实现一个带有“隐形边界”的多租户采集系统,每个租户既能自由爬,又互不打扰。
三、技术选型
为了实现这样的系统,我选用了这套技术组合:
- FastAPI:轻量、快、天然适合做多租户API层;
- Celery + Redis:分布式任务调度系统,用来分发不同租户的抓取任务;
- PostgreSQL:多 schema 架构,每个租户单独一个 schema 实现逻辑隔离;
- Requests + 代理IP:为每个租户配置独立的代理身份;
- JWT + RBAC:用户身份认证 + 角色权限控制;
- Docker Compose:快速部署和环境隔离。
目标很清晰:前端提交任务 → 后端认证租户 → Celery 分发任务 → 采集模块带上独立代理执行 → 数据落入对应 schema。
四、模块实现
1. 用户与租户模型
每个用户都属于某个租户。登录后系统会根据 JWT 自动识别所属租户,从而决定他能访问的数据范围。
# models.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
class Tenant(Base):
__tablename__ = "tenants"
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
users = relationship("User", back_populates="tenant")
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String)
password = Column(String)
tenant_id = Column(Integer, ForeignKey("tenants.id"))
tenant = relationship("Tenant", back_populates="users")
这一层主要负责身份绑定——系统永远知道请求是谁的。
2. 任务调度与权限隔离
接着是任务调度。Celery 负责异步任务的分发,每个租户的任务通过独立参数区分,避免冲突。
# tasks.py
from celery import Celery
from crawler import fetch_car_data
celery = Celery("crawler", broker="redis://localhost:6379/0")
@celery.task
def run_crawl_task(tenant_id, keyword):
"""
每个租户独立运行的任务
"""
data = fetch_car_data(keyword, tenant_id)
save_to_tenant_schema(tenant_id, data)
不同租户的任务在执行层面上是逻辑隔离的,比如租户A在抓汽车,租户B在抓价格,他们都走不同的 Celery 任务通道。
3. 抓取核心模块
这是整个系统的灵魂。每个租户都会使用独立的代理凭证,从网络层就实现隔离。这里用的是 爬虫代理 举例。
# crawler.py
import requests
from fake_useragent import UserAgent
def fetch_car_data(keyword, tenant_id):
"""
抓取汽车品牌与价格信息
每个租户使用独立的代理IP和UA
"""
ua = UserAgent()
headers = {
"User-Agent": ua.random,
"Cookie": "your_cookie_here",
}
# 16YUN代理示例配置
proxy_host = "proxy.16yun.cn"
proxy_port = "3100"
proxy_user = f"tenant_{tenant_id}_user"
proxy_pass = "tenant_specific_password"
proxy_meta = f"http://{proxy_user}:{proxy_pass}@{proxy_host}:{proxy_port}"
proxies = {
"http": proxy_meta,
"https": proxy_meta,
}
url = f"https://www.autohome.com.cn/{keyword}/"
response = requests.get(url, headers=headers, proxies=proxies, timeout=10)
if response.status_code == 200:
# 模拟解析逻辑
return {
"keyword": keyword, "price": "12.5万", "brand": "Toyota"}
else:
return {
"error": f"Request failed: {response.status_code}"}
这里最关键的是:不同租户的代理身份是分开的,不仅能防止封禁波及,也保证了任务独立。
4. 数据隔离(多 Schema)
数据库层我采用了 PostgreSQL 的多 schema 架构。每个租户独立一个 schema,这样数据上就彻底隔离了。
# database.py
from sqlalchemy import create_engine
def get_engine(tenant_id):
"""
根据租户ID连接对应schema
"""
db_url = f"postgresql://user:pass@localhost:5432/crawler"
engine = create_engine(db_url, connect_args={
"options": f"-c search_path=tenant_{tenant_id}"})
return engine
这样做的好处是显而易见的——哪怕某个租户的表炸了,其他租户的数据完全不受影响。
5. API 层实现(FastAPI)
API 层是整个系统的入口。用户带着 JWT 令牌请求任务接口,后端自动判断租户身份并分发任务。
# main.py
from fastapi import FastAPI, Depends
from auth import get_current_user
from tasks import run_crawl_task
app = FastAPI()
@app.post("/crawl")
def crawl(keyword: str, user=Depends(get_current_user)):
"""
发起抓取任务(基于当前用户租户)
"""
tenant_id = user.tenant_id
run_crawl_task.delay(tenant_id, keyword)
return {
"msg": f"爬取任务已提交(租户 {tenant_id})"}
这样,一个租户发的任务永远不会被另一个租户干扰,整个过程对用户是透明的。
五、总结
多租户采集平台最难的地方,其实不在“爬”,而在“分”。
要解决的是——
- 不同租户之间的 任务独立;
- 不同网络层的 代理隔离;
- 不同存储层的 数据分区;
- 不同访问层的 权限控制。
简单回顾一下本项目的分层方案:
- 网络层:独立代理(爬虫代理)
- 存储层:PostgreSQL 多 schema
- 应用层:JWT + 权限验证
- 任务层:Celery 异步队列
进一步扩展的话,还可以:
- 加上限速和优先级控制;
- 用 Kubernetes 给不同租户分配资源;
- 加入 Prometheus 做抓取性能监控。
做完这个项目,你会真正理解一个“企业级采集平台”要面对的复杂度。而最有趣的是,当这些机制搭好之后,你的系统会变得异常稳健——再多租户都能轻松扩展。