跳转至

账号 / Cookie 池

需要登录才能抓、或者想用一批账号轮流抓、被封了自动换号 —— 用账号池。

爬虫里覆写两个方法即可,调度器会自动把池子挂到下载链上:

import netspy as mw


class MallSpider(mw.Spider):
    def user_pool(self):
        return mw.LocalUserPool(
            users=[
                {"username": "u1", "password": "p1"},
                {"username": "u2", "password": "p2"},
            ],
            login=self.login,          # 账号没 cookie 时调用
        )

    def login(self, user):
        resp = mw.Request(
            "https://mall.com/api/login",
            method="POST",
            data={"user": user.username, "pwd": user.password},
        ).download()
        return dict(resp.cookies)      # 返回 cookies dict

    def check_login(self, response):
        return "请先登录" not in response.text   # False -> 拉黑当前账号,换号重试

    def start_requests(self):
        yield mw.Request("https://mall.com/orders", callback=self.parse)

    def parse(self, request, response):
        ...

每个请求下载前自动挂上某个账号的 cookie;下载后 check_login 判断是否掉登录, 掉了就 report_bad(默认拉黑 30 分钟、清掉缓存的 cookie)并换一个账号重试。

池子空了不会匿名发出去

账号全被拉黑(正是站点在封你的时候)会让池子取不到号。这时框架不会 匿名把请求发出去 —— 需要登录的站会回一张登录墙,而那张页面会被当成数据入库。

池空时先等 USER_POOL_WAIT_TIMEOUT(默认 10 秒,游客池能现登、拉黑也会到期), 仍拿不到才抛 UserUnavailableError,走正常重试;重试耗尽后落进 failed_requests.jsonlnetspy retry --requests 只是探活(重新下载看状态码,不跑回调、不入库);要把数据真正抓回来,开 RETRY_FAILED_ON_START 重跑一次爬虫。 这个错误不计入熔断 —— 没号可用是自己这边的事,不是站点挂了。

check_login 判断的是响应,和有没有挂账号无关,所以匿名发出的请求 (允许匿名时)照样会被检查。早先的版本在这里先看有没有账号, 没有就直接放行 —— 于是最需要检查的时候反而不检查了。

实测:账号被封后 12 行落库里 10 行是登录墙页面,而用户写的 check_login 一次都没被调用。修复后登录墙 0 行,那 10 个请求落进了 failed_requests

确实想要「有号更好、没号也能抓」,把 USER_POOL_ALLOW_ANONYMOUS = True 打开。

几种池子

场景
LocalUserPool 单机。给一批账号,轮流用,被封的拉黑一段时间
GuestUserPool 无账号,只要匿名 cookie。login() 拿一份游客 cookie,维护固定数量的游客会话
RedisUserPool 多进程 / 多机共享一批账号 —— 不会两个节点同时用同一个号,cookie 缓存在 Redis
# 游客
def user_pool(self):
    return mw.GuestUserPool(login=self.get_guest_cookie, size=5)

# 分布式共享账号
def user_pool(self):
    return mw.RedisUserPool(
        "MallSpider",
        accounts=[{"username": "u1", "password": "p1"}, ...],
        login=self.login,
        cookie_ttl=3600,
    )

手动用

pool = mw.LocalUserPool([{"username": "u1"}], login=my_login)
user = pool.get()
try:
    resp = mw.Request(url, cookies=user.cookies).download()
    pool.report_ok(user)
except SomethingBad:
    pool.report_bad(user, block_seconds=600)

单个请求也能直接指定 cookie:mw.Request(url, cookies={"sid": "..."})