Scrapy爬虫抓取受阻?GIS数据反爬策略全解析(含:实战代码)
《Scrapy爬虫抓取受阻?GIS数据反爬策略全解析(含:实战代码)》这篇文章面向需要采集公开 GIS 数据、地图服务元数据、POI 页面或空间目录信息的读者,重点解决一个具体问题:Scrapy 爬虫在访问 GIS 数据站点时频繁遇到 403、429、验证码、空响应或接口参数校验失败,应该如何判断原因、合规处理,并写出更稳定的采集流程。
说明:本文只讨论公开、允许访问或已获得授权的数据采集场景。对于登录绕过、验证码破解、绕过付费限制、规避访问控制等行为,不建议也不提供实现方法。GIS 数据往往涉及版权、隐私和安全边界,采集前请先确认网站条款、robots 规则、数据许可和接口调用限制。

引言: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 面板中观察真实请求。
- 打开目标地图页面或数据目录页面。
- 筛选 Fetch、XHR、Img、Doc 请求。
- 查看请求 URL、请求方法、Query String、Request Headers、Response Headers。
- 确认返回数据格式:JSON、GeoJSON、PBF、XML、图片瓦片或压缩包。
- 记录是否存在分页参数、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 数据使用的专业要求。