Scrapy爬虫采集GIS数据太慢?教你配置异步并发与代理(含:反爬策略)
《Scrapy爬虫采集GIS数据太慢?教你配置异步并发与代理(含:反爬策略)》这篇教程,面向需要批量采集公开 GIS 数据的同学和工程师:例如下载行政区划 GeoJSON、采集开放地图服务元数据、批量请求 WFS/WMS 图层信息,或者抓取带空间坐标的 POI 数据。本文重点解决一个具体问题:Scrapy 爬虫采集 GIS 数据太慢时,如何合理配置异步并发、下载延迟、代理和反爬策略,同时避免把目标站点压垮或触发封禁。

引言:为什么 Scrapy 爬虫采集 GIS 数据会特别慢
普通网页采集慢,通常是页面多、网络慢或解析复杂;但 GIS 数据采集慢,还有几个更典型的原因:空间范围大、分页多、单个响应体积大、地图服务限流严格、坐标或属性字段解析耗时。
例如你用 Scrapy 批量请求某个开放平台的 GeoJSON 接口,每个行政区划网格都要请求一次;或者按图层 ID 批量读取 WFS Feature,结果发现运行几个小时仍然只采集了一小部分。这种场景下,只靠“多开几个爬虫进程”并不一定有效,甚至可能更快触发 403、429 或验证码。
正确做法是先定位慢在哪里,再按目标站点承受能力配置 Scrapy 异步并发、下载延迟、重试、代理和限速策略。
背景:GIS 数据采集常见的慢速场景
在 GIS 项目中,Scrapy 常用于采集以下公开或授权数据:
- 开放数据平台中的 GeoJSON、Shapefile 下载链接和元数据。
- ArcGIS REST Services 的图层列表、字段信息、Feature 查询结果。
- OGC 服务中的 WMS GetCapabilities、WFS GetFeature 响应。
- 带经纬度字段的 POI、地址、设施点位和专题统计数据。
- 瓦片服务的元数据、图层样式、范围和坐标系信息。
这些数据采集慢,通常不是单一原因造成的。常见瓶颈包括:
- 请求数量多:按行政区、网格、分页、时间段切分后,请求数可能成千上万。
- 响应体积大:GeoJSON 和 WFS 返回的空间对象较大,尤其是面数据和复杂属性表。
- 服务端限流:地图服务常限制同一 IP 的访问频率,过快请求会返回 429、403 或空数据。
- 解析耗时:JSON 解析、坐标转换、字段清洗、写入数据库都会拖慢整体速度。
- 代理不稳定:劣质代理会导致超时、重试和重复请求,反而比不用代理更慢。
提醒:采集前请确认数据来源的授权、网站 robots 规则、开放 API 使用条款和访问频率限制。本文讨论的是合规场景下的性能优化,不建议绕过登录、验证码或访问控制采集受保护数据。
原理:Scrapy 异步并发、下载延迟和代理如何影响采集速度
Scrapy 基于 Twisted 异步网络框架,并不是“一个请求结束后才发下一个请求”。它可以同时维护多个请求连接,所以合理配置并发数后,采集效率会明显提升。
但 GIS 数据采集不能盲目把并发调得很高。因为地图服务往往比普通静态网页更重:一次查询可能触发空间索引、属性过滤、坐标转换和要素序列化。如果并发过高,服务端压力增加,你本地也会出现超时、失败重试和数据缺失。
Scrapy 中最关键的几个速度参数
| 参数 | 作用 | GIS 采集建议 |
|---|---|---|
CONCURRENT_REQUESTS |
全局最大并发请求数 | 先从 8 或 16 开始测试,不要一开始设到 100 |
CONCURRENT_REQUESTS_PER_DOMAIN |
单个域名最大并发数 | 采集同一个 GIS 服务时更关键,建议 4 到 8 起步 |
DOWNLOAD_DELAY |
同域名请求间隔 | 公开地图服务建议保留 0.3 到 2 秒延迟 |
RANDOMIZE_DOWNLOAD_DELAY |
随机化下载延迟 | 建议开启,避免请求节奏过于机械 |
AUTOTHROTTLE_ENABLED |
根据响应延迟自动限速 | 强烈建议用于 WFS、ArcGIS REST 等接口 |
DOWNLOAD_TIMEOUT |
请求超时时间 | 大 GeoJSON 或复杂空间查询可适当调高 |
为什么代理不是越多越快
代理的主要作用是提高请求稳定性、分散访问压力,或者满足企业内网、区域访问的网络要求。它不是万能加速器。对于 GIS 数据采集,如果代理质量差,会出现以下问题:
- 连接慢,导致下载器长时间等待。
- 频繁超时,触发 Scrapy 重试。
- 返回异常 HTML 页面,解析器误判为有效数据。
- 代理 IP 被目标服务封禁,导致大量 403。
- 同一个空间分页重复请求,造成数据不完整或重复。
因此,代理配置的核心不是“数量”,而是“可用率、延迟、失败剔除和错误识别”。
步骤:配置 Scrapy 异步并发采集 GIS 数据
步骤 1:先用低并发确认接口和数据结构
不要一上来就开高并发。先用少量 URL 测试接口是否稳定,确认响应格式、分页字段、坐标字段和异常返回格式。
import scrapy
import json
class GisFeatureSpider(scrapy.Spider):
name = "gis_feature_spider"
start_urls = [
"https://example.com/arcgis/rest/services/demo/FeatureServer/0/query"
"?where=1%3D1&outFields=*&f=json&resultOffset=0&resultRecordCount=1000"
]
def parse(self, response):
data = json.loads(response.text)
if "features" not in data:
self.logger.warning("未发现 features 字段,可能被限流或接口返回异常:%s", response.url)
return
for feature in data["features"]:
yield {
"attributes": feature.get("attributes"),
"geometry": feature.get("geometry")
}
如果低并发时都经常失败,问题通常不在 Scrapy 并发,而在接口参数、网络连通性、授权、分页逻辑或目标服务稳定性。
步骤 2:在 settings.py 中配置基础并发
下面是一套适合公开 GIS 接口测试的保守配置。它不会追求极限速度,而是优先保证稳定、可恢复和不误伤目标服务。
BOT_NAME = "gis_data_crawler"
ROBOTSTXT_OBEY = True
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 6
DOWNLOAD_DELAY = 0.5
RANDOMIZE_DOWNLOAD_DELAY = True
DOWNLOAD_TIMEOUT = 60
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 522, 524, 408]
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 10
AUTOTHROTTLE_TARGET_CONCURRENCY = 3.0
AUTOTHROTTLE_DEBUG = False
COOKIES_ENABLED = False
DEFAULT_REQUEST_HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}
这组配置适合 ArcGIS REST、WFS、开放数据 JSON 接口等场景。对于大型 GeoJSON 下载,建议进一步降低单域名并发,并提高超时时间。
步骤 3:按分页或空间网格生成请求
GIS 数据通常不是一个 URL 就能全部拿完。常见切分方式有两种:按分页请求,或按空间范围请求。
以 ArcGIS REST 查询分页为例,常见参数包括 resultOffset 和 resultRecordCount。需要注意,不同服务的最大返回记录数可能有限制,例如一次最多返回 1000 或 2000 条。
import scrapy
import json
from urllib.parse import urlencode
class ArcgisRestSpider(scrapy.Spider):
name = "arcgis_rest_features"
base_url = "https://example.com/arcgis/rest/services/demo/FeatureServer/0/query"
custom_settings = {
"CONCURRENT_REQUESTS_PER_DOMAIN": 6,
"DOWNLOAD_DELAY": 0.5,
}
def start_requests(self):
page_size = 1000
for offset in range(0, 50000, page_size):
params = {
"where": "1=1",
"outFields": "*",
"f": "json",
"returnGeometry": "true",
"resultOffset": offset,
"resultRecordCount": page_size
}
url = self.base_url + "?" + urlencode(params)
yield scrapy.Request(
url=url,
callback=self.parse,
meta={"offset": offset},
dont_filter=True
)
def parse(self, response):
try:
data = json.loads(response.text)
except json.JSONDecodeError:
self.logger.warning("JSON 解析失败:%s", response.url)
return
if "error" in data:
self.logger.warning("接口返回错误:%s %s", response.url, data["error"])
return
features = data.get("features", [])
self.logger.info("offset=%s 获取 %s 条", response.meta["offset"], len(features))
for feature in features:
yield {
"objectid": feature.get("attributes", {}).get("OBJECTID"),
"attributes": feature.get("attributes"),
"geometry": feature.get("geometry")
}
如果接口支持按 bbox 查询,可以把研究区切成多个网格,逐格请求。但要注意网格边界可能造成重复要素,需要用唯一 ID 去重。
步骤 4:增加代理中间件
如果目标服务明确允许高频 API 访问,或者你在企业授权环境中需要通过代理访问,可以在 Scrapy 中配置代理。下面是一个简单的代理中间件示例。
import random
class ProxyMiddleware:
def __init__(self, proxies):
self.proxies = proxies
@classmethod
def from_crawler(cls, crawler):
proxies = crawler.settings.getlist("PROXY_LIST")
return cls(proxies)
def process_request(self, request, spider):
if not self.proxies:
return None
proxy = random.choice(self.proxies)
request.meta["proxy"] = proxy
spider.logger.debug("使用代理:%s", proxy)
return None
在 settings.py 中启用:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyMiddleware": 350,
}
PROXY_LIST = [
"http://user:password@proxy1.example.com:8000",
"http://user:password@proxy2.example.com:8000",
]
这只是基础版本。实际项目中更建议维护一个代理池,记录每个代理的成功率、平均延迟、失败次数和最近封禁时间。
步骤 5:识别反爬和限流响应
GIS 接口被限流时,不一定总是返回明确的 429。有些服务会返回 HTML 登录页、空 JSON、错误码对象,或者字段结构变化。采集时必须做内容校验。
def parse(self, response):
content_type = response.headers.get("Content-Type", b"").decode("utf-8").lower()
if response.status in [403, 429]:
self.logger.warning("可能触发反爬或限流:status=%s url=%s", response.status, response.url)
return
if "text/html" in content_type and "json" not in content_type:
self.logger.warning("预期 JSON,实际返回 HTML,可能是验证码或错误页:%s", response.url)
return
try:
data = json.loads(response.text)
except json.JSONDecodeError:
self.logger.warning("不是合法 JSON:%s", response.url)
return
if "features" not in data and "error" in data:
self.logger.warning("接口错误:%s", data["error"])
return
for feature in data.get("features", []):
yield feature
如果发现 403、429 明显增多,不要继续提高并发。应降低并发、增加延迟、开启 AutoThrottle,并检查请求头、接口参数和访问授权。
步骤 6:把数据写入 GeoJSON、CSV 或 PostGIS
Scrapy 的速度也可能被写入环节拖慢。对于小规模数据,可以直接导出 JSON Lines:
scrapy crawl arcgis_rest_features -O output.jsonl
对于较大规模的 GIS 数据,更推荐先写入 JSON Lines 或临时文件,再用 GeoPandas、ogr2ogr 或数据库脚本批量入库。不要在每个 item 中都单独连接数据库。
如果要写入 PostGIS,建议在 pipeline 中复用连接,并批量提交。点数据可以用经纬度构造 geometry;面数据则需要确认坐标结构、环方向和 SRID。
ITEM_PIPELINES = {
"myproject.pipelines.PostgisBatchPipeline": 300,
}
POSTGIS_DSN = "dbname=gisdb user=postgres password=postgres host=localhost port=5432"
写入前建议记录唯一 ID,例如 OBJECTID、id 或业务编码,避免分页重试时产生重复数据。
常见坑:Scrapy 爬虫采集 GIS 数据太慢的真正原因
坑 1:只调高并发,不看失败率
如果并发从 8 提到 64 后,看似请求更多,但 429、超时和重试也一起增加,实际有效采集速度可能下降。判断速度不能只看请求数,要看有效要素数、失败率和重复率。
坑 2:把地图瓦片当作矢量数据接口采集
瓦片服务通常用于地图显示,不适合当作原始 GIS 数据源。大量请求瓦片不仅效率低,也难以还原属性和拓扑关系。若目标是矢量数据,应优先寻找 WFS、ArcGIS REST FeatureServer、GeoJSON 下载或官方开放数据包。
坑 3:没有处理分页上限
很多 ArcGIS REST 服务有最大返回记录数限制。如果只请求一次 where=1=1,可能只拿到前 1000 条。你以为采集很快,实际上数据不完整。
坑 4:空间网格切分后没有去重
按 bbox 请求时,跨网格边界的要素可能被重复返回。尤其是道路、河流、行政区面等跨区域对象,必须用唯一 ID 或几何哈希去重。
坑 5:代理池质量差导致更慢
代理不可用会带来连接超时和重试。GIS 数据响应体较大,对代理稳定性要求更高。建议先测试代理延迟和成功率,再接入正式爬虫。
坑 6:忽略坐标系和字段单位
采集到的数据如果包含 x、y、rings、paths 等几何字段,要确认坐标系。ArcGIS REST 常见空间参考字段为 spatialReference,例如 wkid: 4326 或 wkid: 3857。坐标系判断错误,会导致后续入库或制图位置偏移。
方法比较:异步并发、代理、限速和批量下载怎么选
| 方法 | 适合场景 | 优点 | 风险 |
|---|---|---|---|
| 提高 Scrapy 并发 | 接口稳定、响应较快、请求数量多 | 最直接提升吞吐量 | 过高会触发限流和超时 |
| 开启 AutoThrottle | ArcGIS REST、WFS 等服务延迟波动大 | 自动根据响应速度调节访问频率 | 速度不一定最高,但更稳 |
| 配置代理池 | 授权代理访问、网络出口受限、多区域访问 | 提高网络可达性和稳定性 | 代理差会显著拖慢采集 |
| 按分页采集 | 有明确 offset、page、limit 参数的接口 | 逻辑简单,便于断点续采 | 需要处理最大返回条数 |
| 按空间网格采集 | 支持 bbox 或 geometry 查询的 GIS 服务 | 适合大范围空间数据 | 需要处理重复和边界遗漏 |
| 直接下载数据包 | 官方提供 Shapefile、GeoPackage、GeoJSON 文件 | 最快、最完整、最合规 | 更新频率可能不如接口实时 |
如果官方提供完整数据包,优先下载数据包,而不是用爬虫逐页抓接口。Scrapy 更适合处理没有统一下载入口、需要批量遍历目录或接口分页的场景。
检查清单:优化 Scrapy GIS 爬虫速度前先看这 12 项
- 是否确认目标数据允许采集和使用?
- 是否遵守 robots 规则、API 文档和访问频率限制?
- 是否先用低并发验证接口稳定性?
- 是否检查了响应是否为真正的 JSON、GeoJSON 或 XML?
- 是否处理了 403、429、500、502、503、504 等异常状态?
- 是否开启了
AUTOTHROTTLE_ENABLED? - 是否设置了合理的
CONCURRENT_REQUESTS_PER_DOMAIN? - 是否处理分页上限和总记录数?
- 是否为 bbox 或网格采集设计了去重逻辑?
- 是否记录了失败 URL,方便断点续采?
- 是否确认几何字段的坐标系和 SRID?
- 是否避免在每条 item 中频繁打开数据库连接?
FAQ:Scrapy 爬虫采集 GIS 数据并发与代理常见问题
Scrapy 爬虫采集 GIS 数据太慢,第一步应该改什么?
第一步不是马上加代理,而是检查慢在哪里。先看请求成功率、平均响应时间、重试次数、单个响应大小和写入耗时。如果成功率高但吞吐低,可以逐步提高并发;如果失败率高,应先降低并发并开启 AutoThrottle。
Scrapy 并发数设置多少比较合适?
没有固定值。采集单个 GIS 服务时,可以先设置 CONCURRENT_REQUESTS=16、CONCURRENT_REQUESTS_PER_DOMAIN=4 到 8,观察 10 到 30 分钟。如果 429 和超时很少,再小幅增加。不要在不了解服务限制时直接设置到几十甚至上百。
采集 ArcGIS REST 服务时为什么只拿到 1000 条?
很多 ArcGIS REST FeatureServer 配置了最大返回记录数。即使条件是 where=1=1,服务也可能只返回前 1000 条或 2000 条。需要使用分页参数,或先查询总数,再按 offset 分批请求。
代理能解决 Scrapy 采集 GIS 数据太慢吗?
代理不一定能加速。只有在网络出口不稳定、目标服务允许代理访问、代理质量较高时,代理才可能提升稳定性。劣质代理会增加超时和重试,让 Scrapy 爬虫采集 GIS 数据更慢。
遇到 429 Too Many Requests 怎么办?
429 表示请求过于频繁。应降低 CONCURRENT_REQUESTS_PER_DOMAIN,增加 DOWNLOAD_DELAY,开启 AUTOTHROTTLE_ENABLED,并检查是否有重复 URL 或异常重试风暴。不要用更高并发硬顶。
采集 GeoJSON 很慢,是 Scrapy 的问题吗?
不一定。GeoJSON 文件可能很大,解析和写入也会耗时。如果官方提供压缩包或静态下载链接,直接下载通常比接口分页采集更快。若必须采集接口,可以减少返回字段、按空间范围分块,并把写入环节改成批处理。
如何判断采集到的 GIS 数据是否完整?
可以从三个层面验证:记录数是否与接口 count 或官方说明一致;空间范围是否覆盖研究区;抽样检查字段、坐标系和几何类型是否正确。对于分页或网格采集,还要检查重复 ID 和遗漏区块。
结论:稳定比盲目提速更重要
Scrapy 爬虫采集 GIS 数据太慢时,真正有效的优化路径是:先确认数据源合规和接口结构,再逐步配置异步并发、AutoThrottle、下载延迟、重试策略和代理池。对于 ArcGIS REST、WFS、GeoJSON 这类 GIS 数据接口,过高并发往往会带来限流、超时和数据缺失。
建议你在实际项目中采用“低并发验证、小步调参、记录失败、批量写入、结果校验”的流程。这样既能提升采集效率,也能保证 GIS 数据的完整性、坐标正确性和后续分析可用性。