Scrapy爬虫采集GIS数据太慢?教你配置异步并发与代理(含:反爬策略)

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

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

Scrapy爬虫采集GIS数据太慢 异步并发与代理配置流程
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 查询分页为例,常见参数包括 resultOffsetresultRecordCount。需要注意,不同服务的最大返回记录数可能有限制,例如一次最多返回 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,例如 OBJECTIDid 或业务编码,避免分页重试时产生重复数据。

常见坑: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:忽略坐标系和字段单位

采集到的数据如果包含 xyringspaths 等几何字段,要确认坐标系。ArcGIS REST 常见空间参考字段为 spatialReference,例如 wkid: 4326wkid: 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=16CONCURRENT_REQUESTS_PER_DOMAIN=48,观察 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 数据的完整性、坐标正确性和后续分析可用性。