Scrapy爬虫抓取受阻?GIS数据反爬策略全解析(含:实战代码)

编程与开发
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

《Scrapy爬虫抓取受阻?GIS数据反爬策略全解析(含:实战代码)》这篇文章面向需要采集公开 GIS 数据、地图服务元数据、POI 页面或空间目录信息的读者,重点解决一个具体问题:Scrapy 爬虫在访问 GIS 数据站点时频繁遇到 403、429、验证码、空响应或接口参数校验失败,应该如何判断原因、合规处理,并写出更稳定的采集流程。

说明:本文只讨论公开、允许访问或已获得授权的数据采集场景。对于登录绕过、验证码破解、绕过付费限制、规避访问控制等行为,不建议也不提供实现方法。GIS 数据往往涉及版权、隐私和安全边界,采集前请先确认网站条款、robots 规则、数据许可和接口调用限制。

Scrapy爬虫抓取受阻 GIS数据反爬策略流程图
Scrapy 采集 GIS 数据时常见的受阻环节:请求频率、请求头、接口参数、数据许可和服务端限流。

引言:Scrapy 抓取 GIS 数据为什么更容易受阻

很多同学用 Scrapy 抓取普通网页时问题不大,但一换成 GIS 数据源就频繁失败。常见对象包括行政区划目录、POI 列表、地图瓦片索引、GeoJSON 文件、ArcGIS REST 服务图层信息、WMS/WFS 能力文档,以及城市开放数据平台中的空间数据下载页。

GIS 数据的特点是请求量大、空间范围可切片、接口参数复杂,并且很多服务会被 WebGIS 前端高频调用。因此,服务端通常会配置更严格的反爬策略,例如限速、Referer 校验、User-Agent 校验、Token 校验、IP 频率控制、瓦片请求阈值和下载任务排队。

如果只是简单提高并发、不断重试,结果往往更糟:IP 被临时封禁、返回空数据、坐标字段缺失,甚至采集到重复或不完整的空间数据。正确做法是先判断受阻类型,再采用合规、低频、可恢复的采集策略。

背景:GIS 数据反爬常见表现

Scrapy 爬虫抓取受阻通常不是一个单一错误,而是一组现象。下面这些情况在 GIS 数据采集中尤其常见。

  • HTTP 403:服务器拒绝访问,常见原因是 User-Agent 缺失、Referer 不匹配、接口不允许直接访问或访问权限不足。
  • HTTP 429:请求过于频繁,常见于瓦片服务、POI 分页接口、空间查询接口和开放数据下载接口。
  • HTTP 401 或 498:需要认证或 Token 失效,常见于 ArcGIS REST 服务、业务 WebGIS 平台和带权限的地图服务。
  • 返回 200 但内容为空:参数不完整、空间范围超限、分页参数错误、Cookie 会话失效,或者服务端对异常请求返回伪正常结果。
  • 验证码或人机验证:说明站点明确不希望自动化访问,应停止采集并寻求授权接口或数据下载通道。
  • 瓦片大量缺图:可能是缩放级别、瓦片行列号、坐标系、请求频率或服务访问限制导致。

在 GIS 场景中,还要特别注意一个问题:即使请求成功,也不代表数据正确。例如 WFS 查询可能因为坐标系参数错误而返回空结果,ArcGIS REST 查询可能因为 exceededTransferLimit 只返回部分要素,瓦片服务可能因为 z/x/y 计算错误而下载了错误区域。

原理:反爬不是敌人,很多时候是服务保护机制

反爬策略本质上是服务端对异常访问行为的识别和限制。对 GIS 服务来说,这类保护尤其重要,因为空间数据接口容易产生高负载。

例如,一个地图瓦片请求看起来只是下载一张小图片,但当你按城市范围、多个缩放级别、多个图层批量抓取时,请求数量会迅速增加。一个 POI 接口看起来只是分页查询,但如果按网格、分类、关键字、行政区多维组合,就可能触发频率控制。

常见反爬判断维度包括:

  • 请求频率:单位时间内请求次数过多,触发 429 或临时封禁。
  • 并发连接:同一 IP 同时连接过多,特别容易影响瓦片服务。
  • 请求头异常:缺少 User-Agent、Accept、Referer、Accept-Language 等基础信息。
  • 访问路径异常:直接访问后端接口,但没有先访问页面或缺少必要 Cookie。
  • 参数异常:bbox 过大、分页过深、坐标系参数错误、where 条件过宽。
  • 认证异常:Token 过期、权限不足、会话失效或接口需要授权。
  • 重复请求:同一 URL 高频重复访问,说明缺少缓存或断点续爬机制。

所以,解决 Scrapy 爬虫抓取受阻的关键不是“强行绕过”,而是让采集行为更接近合规客户端:明确授权、降低频率、补齐请求上下文、减少重复请求、缩小空间查询范围,并对服务端限制保持尊重。

步骤:Scrapy GIS 数据采集合规排查流程

步骤 1:先确认数据是否允许采集

在写代码前,先做这几项检查:

  • 网站是否提供正式数据下载入口,例如 Shapefile、GeoJSON、CSV、GPKG 或开放 API。
  • 是否有 robots 规则、服务条款、数据许可说明。
  • 接口是否需要登录、Token、API Key 或申请授权。
  • 数据是否涉及个人位置、敏感设施、内部业务数据或受版权保护的数据。
  • 是否存在更合适的官方接口,例如 ArcGIS REST、OGC WFS、WMS GetCapabilities、开放数据平台 API。

如果官方已提供下载包,优先使用下载包,不要通过高频分页或瓦片方式还原数据。对于 GIS 学习和项目交付来说,数据来源清晰比“能抓下来”更重要。

步骤 2:用浏览器开发者工具定位真实请求

很多 GIS 网站前端会加载多个资源:页面 HTML、JS、瓦片、图层配置、空间查询接口、属性表接口。不要只看页面地址,要在浏览器开发者工具的 Network 面板中观察真实请求。

  1. 打开目标地图页面或数据目录页面。
  2. 筛选 Fetch、XHR、Img、Doc 请求。
  3. 查看请求 URL、请求方法、Query String、Request Headers、Response Headers。
  4. 确认返回数据格式:JSON、GeoJSON、PBF、XML、图片瓦片或压缩包。
  5. 记录是否存在分页参数、bbox 参数、坐标系参数、Token 参数。

对于 ArcGIS REST 服务,常见接口形态包括图层信息、查询接口和导出地图接口。对于 OGC 服务,常见接口包括 WMS GetCapabilities、WFS GetFeature、WMTS GetTile。不同接口的请求边界和限制不同,不能一概而论。

步骤 3:先用单请求验证,不要一开始就上并发

在 Scrapy 项目里,建议先用一个最小请求验证响应是否正确。下面是一个基础 Spider 示例,用于访问公开的 JSON 或 GeoJSON 接口。请把 URL 替换为你有权访问的目标接口。

import json
import scrapy


class GisApiSpider(scrapy.Spider):
    name = "gis_api_spider"

    custom_settings = {
        "DOWNLOAD_DELAY": 2,
        "CONCURRENT_REQUESTS": 2,
        "CONCURRENT_REQUESTS_PER_DOMAIN": 1,
        "RETRY_TIMES": 2,
        "ROBOTSTXT_OBEY": True,
        "DEFAULT_REQUEST_HEADERS": {
            "User-Agent": "GIS-Learning-Spider/1.0 (+contact: your_email@example.com)",
            "Accept": "application/json,text/plain,*/*",
            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        },
    }

    def start_requests(self):
        url = "https://example.com/open-gis/api/features?page=1&size=50"
        yield scrapy.Request(
            url=url,
            callback=self.parse,
            errback=self.errback,
            meta={"page": 1},
        )

    def parse(self, response):
        self.logger.info("status=%s url=%s", response.status, response.url)

        if response.status != 200:
            self.logger.warning("非 200 响应:%s", response.text[:300])
            return

        try:
            data = json.loads(response.text)
        except json.JSONDecodeError:
            self.logger.warning("返回内容不是 JSON:%s", response.text[:300])
            return

        features = data.get("features", [])
        for feature in features:
            yield {
                "id": feature.get("id"),
                "geometry": feature.get("geometry"),
                "properties": feature.get("properties"),
            }

        next_page = data.get("next_page")
        if next_page:
            yield response.follow(
                next_page,
                callback=self.parse,
                errback=self.errback,
                meta={"page": response.meta["page"] + 1},
            )

    def errback(self, failure):
        self.logger.error("请求失败:%s", failure)

这个示例的重点不是“伪装浏览器”,而是降低并发、遵守 robots、设置明确的 User-Agent,并对状态码和响应格式进行判断。对于公开数据采集,这是更稳妥的起点。

步骤 4:处理 403,先检查请求头和访问权限

遇到 403 时,不要马上加代理。请先判断是否属于以下情况:

  • 接口本来就不允许直接访问。
  • 数据需要登录或授权 Token。
  • 服务要求 Referer 来源于指定页面。
  • 请求头过于异常,例如默认 Scrapy User-Agent 被拦截。
  • 目标路径是临时下载地址,已过期或绑定会话。

如果该接口是公开页面正常调用的开放接口,可以在合规前提下补齐基础请求头。示例如下:

headers = {
    "User-Agent": "GIS-Learning-Spider/1.0 (+contact: your_email@example.com)",
    "Accept": "application/json,text/plain,*/*",
    "Referer": "https://example.com/open-data/map",
}

yield scrapy.Request(
    url="https://example.com/open-gis/api/layers",
    headers=headers,
    callback=self.parse,
)

如果 403 来自登录态、付费授权或内部系统权限,不应通过脚本绕过。正确做法是申请 API Key、使用官方 SDK、联系数据提供方,或者改用公开数据集。

步骤 5:处理 429,控制频率比重试更重要

429 表示 Too Many Requests,也就是请求过快。GIS 采集中最容易触发 429 的场景是瓦片下载、网格化 POI 查询和分页拉取要素。

推荐的 Scrapy 配置如下:

custom_settings = {
    "DOWNLOAD_DELAY": 3,
    "RANDOMIZE_DOWNLOAD_DELAY": True,
    "CONCURRENT_REQUESTS": 4,
    "CONCURRENT_REQUESTS_PER_DOMAIN": 1,
    "AUTOTHROTTLE_ENABLED": True,
    "AUTOTHROTTLE_START_DELAY": 2,
    "AUTOTHROTTLE_MAX_DELAY": 20,
    "AUTOTHROTTLE_TARGET_CONCURRENCY": 0.5,
    "RETRY_HTTP_CODES": [500, 502, 503, 504, 522, 524, 408, 429],
    "RETRY_TIMES": 2,
}

如果服务端返回 Retry-After 响应头,应尊重这个等待时间。可以在下载中间件中读取该字段并延迟重试。

import time
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message


class RespectRetryAfterMiddleware(RetryMiddleware):
    def process_response(self, request, response, spider):
        if response.status == 429:
            retry_after = response.headers.get("Retry-After")
            if retry_after:
                try:
                    wait_seconds = int(retry_after.decode("utf-8"))
                    spider.logger.warning("收到 429,等待 %s 秒后重试:%s", wait_seconds, request.url)
                    time.sleep(min(wait_seconds, 60))
                except ValueError:
                    pass

            reason = response_status_message(response.status)
            return self._retry(request, reason, spider) or response

        return response

注意:在高并发项目中,直接 time.sleep 会阻塞下载线程,示例只用于说明逻辑。生产环境可结合队列、调度器或异步延迟机制做更细的控制。

步骤 6:为 GIS 分页和空间范围增加断点续爬

GIS 数据接口经常按页、按行政区、按 bbox 或按瓦片行列号采集。如果中途失败,不能从头开始重复请求,否则容易触发反爬,也浪费服务资源。

Scrapy 可以配合 JOBDIR 做简单断点续爬:

custom_settings = {
    "JOBDIR": "jobs/gis_api_spider",
    "DUPEFILTER_DEBUG": False,
    "HTTPCACHE_ENABLED": True,
    "HTTPCACHE_EXPIRATION_SECS": 86400,
    "HTTPCACHE_IGNORE_HTTP_CODES": [401, 403, 429, 500, 502, 503],
}

对于按空间网格采集的任务,建议先生成任务清单,把每个 bbox、行政区编码或页码作为一个独立任务,并记录状态。这样即使中断,也可以只补采失败部分。

import scrapy


class BboxTaskSpider(scrapy.Spider):
    name = "bbox_task_spider"

    def start_requests(self):
        tasks = [
            {"id": "grid_001", "bbox": "116.30,39.90,116.35,39.95"},
            {"id": "grid_002", "bbox": "116.35,39.90,116.40,39.95"},
        ]

        for task in tasks:
            url = (
                "https://example.com/open-gis/api/search"
                "?bbox={bbox}&outFields=*&f=geojson"
            ).format(bbox=task["bbox"])

            yield scrapy.Request(
                url=url,
                callback=self.parse_grid,
                meta={"task_id": task["id"], "bbox": task["bbox"]},
                dont_filter=False,
            )

    def parse_grid(self, response):
        yield {
            "task_id": response.meta["task_id"],
            "bbox": response.meta["bbox"],
            "status": response.status,
            "body_sample": response.text[:200],
        }

步骤 7:ArcGIS REST 查询要处理要素数量限制

很多 ArcGIS REST 图层查询接口会限制单次返回数量。常见表现是响应里出现 exceededTransferLimit,表示结果没有完全返回。此时不应简单认为采集完成,而要使用分页、objectIds 分批或空间切片。

下面示例展示一种 objectIds 分批查询思路。实际字段名和接口参数请以目标服务文档为准。

import json
import scrapy
from urllib.parse import urlencode


class ArcgisRestSpider(scrapy.Spider):
    name = "arcgis_rest_spider"

    layer_url = "https://example.com/arcgis/rest/services/open/FeatureServer/0"

    custom_settings = {
        "DOWNLOAD_DELAY": 2,
        "CONCURRENT_REQUESTS_PER_DOMAIN": 1,
    }

    def start_requests(self):
        params = {
            "where": "1=1",
            "returnIdsOnly": "true",
            "f": "json",
        }
        yield scrapy.Request(
            self.layer_url + "/query?" + urlencode(params),
            callback=self.parse_ids,
        )

    def parse_ids(self, response):
        data = json.loads(response.text)
        object_ids = data.get("objectIds", [])

        batch_size = 100
        for i in range(0, len(object_ids), batch_size):
            batch = object_ids[i:i + batch_size]
            params = {
                "objectIds": ",".join(map(str, batch)),
                "outFields": "*",
                "returnGeometry": "true",
                "f": "geojson",
            }
            yield scrapy.Request(
                self.layer_url + "/query?" + urlencode(params),
                callback=self.parse_features,
                meta={"batch_start": i},
            )

    def parse_features(self, response):
        data = json.loads(response.text)
        for feature in data.get("features", []):
            yield feature

如果服务需要 Token 或权限认证,请使用官方授权方式,不要绕过认证。对于学习场景,建议优先选择开放的 FeatureServer 示例或政府开放数据服务。

步骤 8:瓦片数据不要盲目全量下载

地图瓦片是最容易把请求量放大的 GIS 数据类型。一个区域在多个缩放级别下可能对应成千上万张瓦片。对于瓦片服务,建议遵循三条原则:

  • 只请求实际需要的范围和缩放级别。
  • 尊重服务条款,不批量镜像不允许下载的底图。
  • 能使用官方离线包、矢量切片包或缓存服务,就不要自行高频抓取。

如果只是为了检查某个区域是否有瓦片,可以先计算少量 z/x/y 样例进行验证,而不是直接全量下载。

import math


def lonlat_to_tile(lon, lat, z):
    lat_rad = math.radians(lat)
    n = 2 ** z
    x = int((lon + 180.0) / 360.0 * n)
    y = int((1.0 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2.0 * n)
    return x, y


lon, lat, z = 116.391, 39.907, 12
x, y = lonlat_to_tile(lon, lat, z)
print(z, x, y)

还要确认瓦片方案是 Web Mercator、TMS、XYZ、WMTS 还是自定义切片矩阵。行列号规则不同,会导致请求成功但图像位置错误。

常见坑:Scrapy GIS 数据采集失败的高频原因

坑 1:只看状态码,不检查数据完整性

GIS 接口返回 200 并不代表结果完整。你还要检查:

  • 要素数量是否符合预期。
  • 是否存在 exceededTransferLimit 或 next 参数。
  • geometry 是否为空。
  • 坐标值是否在合理范围内。
  • 分页是否重复返回第一页。
  • 字段是否缺失或被脱敏。

坑 2:bbox 坐标系写错

很多空间查询接口要求 bbox 使用特定坐标系。例如前端地图可能是 Web Mercator,但接口参数可能要求 WGS84 经纬度,也可能要求传入 inSR 和 outSR。坐标系错误会导致空结果,容易被误判为反爬。

排查时可以先用一个已知点或小范围 bbox 测试,确认接口返回的 geometry 坐标是否符合预期。

坑 3:分页参数和空间切片重复叠加

有些接口既支持 page,又支持 bbox。如果你对每个 bbox 再分页,但没有处理边界重复,就会采集到大量重复要素。建议使用唯一 ID 去重,例如 objectid、fid、行政区划代码或业务主键。

坑 4:把验证码当成普通反爬处理

如果目标站点出现验证码、人机验证或登录安全校验,说明自动化访问已经超出普通公开接口使用范围。此时应停止脚本,改用授权接口、数据申请流程或官方下载方式。

坑 5:代理不是万能解法

很多新手遇到 403 或 429 就马上加代理。实际上,代理只会掩盖问题,不会解决数据许可、参数错误、频率过高和接口权限问题。对于合规 GIS 项目,更推荐限速、缓存、断点续爬和官方 API。

方法比较:不同 GIS 数据采集方式怎么选

方式 适用场景 优点 风险与限制
官方数据下载包 行政区划、道路、兴趣点、专题图层等公开数据 来源清晰,结构稳定,适合分析入库 更新频率可能较低,字段可能有限
开放 API 开放数据平台、政务数据接口、授权业务接口 调用规范,有文档,适合自动化 通常有配额、Token、频率限制
ArcGIS REST 或 OGC 服务 图层查询、空间范围查询、服务元数据读取 GIS 语义明确,支持空间参数 可能有返回数量限制、坐标系要求和权限限制
Scrapy 抓取公开页面 数据目录、元数据、下载链接、公开列表页 适合整理数据索引和批量发现资源 页面结构易变,需遵守 robots 和访问频率
瓦片请求 少量预览、服务可用性检测、授权缓存 直观,便于地图展示 请求量大,版权和服务压力风险高,不适合盲目全量下载

如果你的目标是空间分析,优先选择矢量数据下载包、GeoJSON、GPKG、Shapefile、PostGIS 数据表或正式 API。瓦片更适合展示,不适合直接还原高精度矢量数据。

检查清单:Scrapy 爬虫抓取受阻时逐项排查

  • 合法性:是否允许采集?是否需要授权?是否有数据许可说明?
  • 接口类型:目标是 HTML、JSON、GeoJSON、ArcGIS REST、WFS、WMS 还是瓦片?
  • 状态码:403、429、401、500 分别代表什么?是否记录了响应片段?
  • 请求头:User-Agent、Accept、Referer 是否合理且真实说明用途?
  • 频率控制:是否设置 DOWNLOAD_DELAY、AutoThrottle、低并发?
  • 重复请求:是否启用缓存、去重、断点续爬?
  • 空间参数:bbox、坐标系、inSR、outSR、瓦片 z/x/y 是否正确?
  • 分页限制:是否处理 next、offset、objectIds、exceededTransferLimit?
  • 数据完整性:要素数量、geometry、字段、坐标范围是否验证?
  • 失败处理:是否把 403、429、空响应、解析失败分别记录到日志?

FAQ:Scrapy GIS 数据反爬常见问题

Q1:Scrapy 抓取 GIS 数据遇到 403 是不是一定要换代理?

不是。403 更常见的原因是权限不足、请求头异常、Referer 校验或接口不允许直接访问。应先确认数据许可和接口规则,再检查请求头、Token、访问路径和参数。代理不能解决未授权访问问题。

Q2:Scrapy 抓取地图瓦片时如何避免 429?

首先减少请求量,只请求必要范围和缩放级别;其次降低并发,启用 DOWNLOAD_DELAY 和 AutoThrottle;最后检查服务条款是否允许批量访问。对于不允许离线下载的底图,不应使用脚本批量抓取。

Q3:ArcGIS REST 返回 exceededTransferLimit 怎么办?

这通常表示单次查询结果被截断。可以使用 objectIds 分批查询、resultOffset 分页,或者按 bbox 切片查询。采集后要用唯一 ID 去重,并检查要素数量是否完整。

Q4:返回 200 但 GeoJSON 没有 features,是反爬吗?

不一定。可能是 bbox 坐标系错误、查询条件过窄、分页参数错误、Token 失效,也可能是服务端返回了空结果。建议先在浏览器中复制同样请求验证,再用已知范围的小 bbox 测试。

Q5:能不能用 Scrapy 自动处理验证码?

不建议。验证码是明确的人机验证和访问控制信号。GIS 数据采集遇到验证码时,应停止自动化请求,改为申请授权、使用官方 API 或下载开放数据包。

Q6:Scrapy 适合采集哪些 GIS 信息?

Scrapy 更适合采集公开数据目录、元数据、下载链接、图层列表、服务描述、开放 API 分页结果等。对于大规模空间分析数据,优先使用官方下载包、数据库导出、OGC 服务或授权 API。

结论:稳定的 GIS 采集来自合规、限速和验证

Scrapy 爬虫抓取受阻并不罕见,尤其是在 GIS 数据场景中。真正可靠的解决思路不是盲目提高并发、不断换代理,而是先确认数据许可,再识别受阻类型,最后用低频请求、合理请求头、缓存、断点续爬和数据完整性校验来完成采集。

对于 GIS 学习和工程实践,建议把采集流程设计成可解释、可恢复、可验证的任务:每个请求为什么发出、每个空间范围是否正确、每批数据是否完整、失败原因是否记录清楚。这样写出来的 Scrapy 项目,才适合长期维护,也更符合 GIS 数据使用的专业要求。