Scrapy爬虫抓取GIS数据总被封?反反爬策略与代理池实战(附:完整代码)
《Scrapy爬虫抓取GIS数据总被封?反反爬策略与代理池实战(附:完整代码)》这篇文章,面向需要批量采集公开GIS数据目录、元数据、POI页面或瓦片索引信息的同学。很多GIS工程师第一次用 Scrapy 跑采集任务时,会遇到请求变慢、403、429、验证码、IP被封、数据抓不全等问题。本文不讨论绕过登录、破解验证码或采集受限数据,而是讲清楚如何在合法合规的前提下,让 Scrapy 爬虫更稳定地访问公开GIS数据页面,并用代理池、限速、重试和请求头策略降低被误封的概率。

引言:为什么Scrapy爬虫抓取GIS数据容易被封
GIS数据采集和普通网页采集有一个明显区别:很多目标页面并不是单纯的HTML,而是包含地图服务接口、分页接口、GeoJSON接口、ArcGIS REST服务、WMS/WFS服务或瓦片请求。请求数量一多,就很容易触发服务器的频率限制。
常见场景包括:
- 批量抓取政府开放数据平台中的数据集列表和元数据。
- 采集 ArcGIS REST Services Directory 中的图层信息、字段结构、范围和服务地址。
- 抓取公开POI页面、行政区划页面或地名查询结果。
- 批量请求 GeoJSON、WFS、CSV 或分页API。
- 采集地图瓦片索引或服务能力信息,用于数据质量检查。
如果直接使用默认 Scrapy 配置,短时间内大量并发请求会让服务器认为这是异常流量。结果就是 Scrapy爬虫抓取GIS数据总被封,表现为403、429、连接超时、返回空页面、重定向到验证页,或者同一段时间内数据大量缺失。
背景:GIS数据采集常见被封表现
在排查 Scrapy 爬虫被封问题前,先要判断自己到底遇到的是哪一类限制。不同错误对应的解决方法不一样,不能一上来就盲目换代理。
1. HTTP 403 Forbidden
403通常表示服务器拒绝访问。原因可能是请求头太像程序、Referer缺失、User-Agent异常、访问路径不允许,或者IP已经进入限制名单。
2. HTTP 429 Too Many Requests
429通常表示请求频率过高。GIS数据平台的分页接口、ArcGIS REST查询接口和地图服务接口很容易出现这个状态码。此时优先要降速,而不是继续加代理硬冲。
3. 返回HTML验证页而不是数据
有些站点不会直接返回403,而是返回一个验证页面。对Scrapy来说,响应状态码可能仍然是200,但内容已经不是JSON、GeoJSON或目标HTML。
4. 数据不完整但没有报错
这是GIS采集中最容易被忽略的问题。比如你以为抓到了5000条POI,实际上由于接口限流,只返回了前几页;或者某些图层服务返回了空features。必须用数量校验、字段校验和空间范围校验来确认结果。
原理:反反爬不是“绕过限制”,而是模拟合理访问
本文所说的反反爬策略,重点不是突破网站安全机制,而是让 Scrapy 在公开数据允许访问的范围内更加稳定、礼貌、可控。特别是GIS数据采集,很多目标站点属于公共服务平台,更应该控制请求频率,避免影响服务。
一个稳定的 Scrapy GIS采集程序,通常要同时做好以下几件事:
- 限速:控制并发和请求间隔,避免瞬时请求过高。
- 请求头:补充合理的 User-Agent、Accept、Referer 等字段。
- 重试:对超时、429、502、503等临时错误进行有限重试。
- 代理池:在合规前提下使用稳定代理,降低单IP被频率限制的风险。
- 日志:记录状态码、失败URL、代理IP、重试次数,方便回溯。
- 数据校验:用记录数、字段、坐标范围、几何类型检查抓取结果。
重要提醒:采集前请阅读目标网站的robots.txt、服务条款、开放数据授权说明和接口调用限制。不要采集需要登录、付费、受版权保护或明确禁止自动化访问的数据。
步骤:Scrapy爬虫抓取GIS数据的完整代码示例
下面用一个通用示例演示:从公开GIS数据目录页面读取数据集列表,进入详情页解析名称、简介、数据格式、下载链接和空间范围字段。你可以把解析规则替换成自己的目标站点。
1. 创建Scrapy项目
pip install scrapy
scrapy startproject gis_spider
cd gis_spider
scrapy genspider open_data example.com
项目结构大致如下:
gis_spider/
scrapy.cfg
gis_spider/
__init__.py
items.py
middlewares.py
pipelines.py
settings.py
spiders/
open_data.py
2. 定义GIS数据字段
在 items.py 中定义需要保存的字段。GIS元数据建议至少保留名称、来源URL、数据格式、坐标系、空间范围和下载地址。
import scrapy
class GisDatasetItem(scrapy.Item):
title = scrapy.Field()
source_url = scrapy.Field()
description = scrapy.Field()
data_format = scrapy.Field()
crs = scrapy.Field()
bbox = scrapy.Field()
download_url = scrapy.Field()
service_url = scrapy.Field()
3. 编写爬虫解析逻辑
下面代码演示分页列表页和详情页解析。实际使用时,需要把CSS选择器替换成目标GIS开放数据网站的页面结构。
import scrapy
from urllib.parse import urljoin
from gis_spider.items import GisDatasetItem
class OpenDataSpider(scrapy.Spider):
name = "open_data"
allowed_domains = ["example.com"]
start_urls = [
"https://example.com/opendata/catalog?page=1"
]
custom_settings = {
"DOWNLOAD_DELAY": 2,
"CONCURRENT_REQUESTS": 4,
"CONCURRENT_REQUESTS_PER_DOMAIN": 2
}
def parse(self, response):
self.logger.info("List page: %s status=%s", response.url, response.status)
detail_links = response.css("a.dataset-title::attr(href)").getall()
for href in detail_links:
detail_url = urljoin(response.url, href)
yield scrapy.Request(
detail_url,
callback=self.parse_detail,
meta={"source_list": response.url}
)
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield scrapy.Request(
urljoin(response.url, next_page),
callback=self.parse
)
def parse_detail(self, response):
item = GisDatasetItem()
item["title"] = response.css("h2::text").get(default="").strip()
item["source_url"] = response.url
item["description"] = " ".join(
text.strip() for text in response.css(".dataset-description *::text").getall()
if text.strip()
)
item["data_format"] = response.css(".format::text").get(default="").strip()
item["crs"] = response.css(".crs::text").get(default="").strip()
item["bbox"] = response.css(".bbox::text").get(default="").strip()
item["download_url"] = response.css("a.download::attr(href)").get()
item["service_url"] = response.css("a.service-url::attr(href)").get()
if item["download_url"]:
item["download_url"] = urljoin(response.url, item["download_url"])
yield item
4. 配置限速和自动重试
在 settings.py 中加入以下配置。对于GIS数据平台,建议先低并发测试,不要一开始就开很高。
BOT_NAME = "gis_spider"
ROBOTSTXT_OBEY = True
CONCURRENT_REQUESTS = 4
CONCURRENT_REQUESTS_PER_DOMAIN = 2
DOWNLOAD_DELAY = 2
RANDOMIZE_DOWNLOAD_DELAY = True
COOKIES_ENABLED = False
DOWNLOAD_TIMEOUT = 30
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [408, 429, 500, 502, 503, 504, 522, 524]
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 2
AUTOTHROTTLE_MAX_DELAY = 20
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = False
LOG_LEVEL = "INFO"
FEEDS = {
"output/gis_datasets.jsonl": {
"format": "jsonlines",
"encoding": "utf8",
"overwrite": True
}
}
这里的关键不是“跑得越快越好”,而是让请求像正常用户浏览一样平滑。对于容易被封的GIS站点,AUTOTHROTTLE_ENABLED 往往比盲目使用大量代理更有效。
5. 添加请求头中间件
在 middlewares.py 中添加一个简单的请求头中间件。它可以随机切换常见浏览器User-Agent,并补充Accept、Accept-Language等字段。
import random
class RandomHeaderMiddleware:
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/121.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15 "
"(KHTML, like Gecko) Version/16.6 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0 Safari/537.36"
]
def process_request(self, request, spider):
request.headers.setdefault("User-Agent", random.choice(self.USER_AGENTS))
request.headers.setdefault("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,application/json;q=0.8,*/*;q=0.7")
request.headers.setdefault("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
request.headers.setdefault("Connection", "keep-alive")
然后在 settings.py 启用它:
DOWNLOADER_MIDDLEWARES = {
"gis_spider.middlewares.RandomHeaderMiddleware": 400,
}
6. 准备代理池文件
如果目标网站明确允许合理自动化访问,但单IP仍然经常触发频率限制,可以使用代理池。这里用一个简单的本地文件作为代理池示例,每行一个代理。
http://user:password@1.2.3.4:8000
http://user:password@5.6.7.8:8000
http://user:password@9.10.11.12:8000
保存为项目根目录下的 proxies.txt。实际项目中请使用稳定、合规、可追踪的代理服务,不建议使用来源不明的免费代理。
7. 编写代理池中间件
继续在 middlewares.py 中加入代理中间件。它会为每个请求随机分配代理,并在日志中记录当前代理。
import random
from pathlib import Path
class ProxyPoolMiddleware:
def __init__(self, proxy_file):
self.proxy_file = proxy_file
self.proxies = self.load_proxies(proxy_file)
@classmethod
def from_crawler(cls, crawler):
proxy_file = crawler.settings.get("PROXY_FILE", "proxies.txt")
return cls(proxy_file)
def load_proxies(self, proxy_file):
path = Path(proxy_file)
if not path.exists():
return []
proxies = []
for line in path.read_text(encoding="utf8").splitlines():
line = line.strip()
if line and not line.startswith("#"):
proxies.append(line)
return proxies
def process_request(self, request, spider):
if not self.proxies:
return None
proxy = random.choice(self.proxies)
request.meta["proxy"] = proxy
request.meta["proxy_used"] = proxy
spider.logger.debug("Use proxy: %s for %s", proxy, request.url)
return None
在 settings.py 中启用代理中间件:
PROXY_FILE = "proxies.txt"
DOWNLOADER_MIDDLEWARES = {
"gis_spider.middlewares.RandomHeaderMiddleware": 400,
"gis_spider.middlewares.ProxyPoolMiddleware": 410,
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
}
8. 对403和429做更清晰的日志记录
当Scrapy爬虫抓取GIS数据总被封时,日志比猜测更重要。建议增加一个响应检查中间件,专门记录疑似封禁的状态码。
class BlockStatusLoggerMiddleware:
BLOCK_CODES = {403, 429}
def process_response(self, request, response, spider):
if response.status in self.BLOCK_CODES:
spider.logger.warning(
"Possible block: status=%s url=%s proxy=%s",
response.status,
response.url,
request.meta.get("proxy_used", "")
)
return response
启用方式如下:
DOWNLOADER_MIDDLEWARES = {
"gis_spider.middlewares.RandomHeaderMiddleware": 400,
"gis_spider.middlewares.ProxyPoolMiddleware": 410,
"gis_spider.middlewares.BlockStatusLoggerMiddleware": 420,
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
}
9. 输出为GeoJSON或写入PostGIS前的校验
很多GIS采集任务最终不是只要一个HTML字段,而是要进入GeoJSON、PostGIS或空间数据库。即使当前示例只采集元数据,也建议先做字段完整性检查。
在 pipelines.py 中加入基础校验:
class GisDatasetValidationPipeline:
def process_item(self, item, spider):
if not item.get("title"):
spider.logger.warning("Missing title: %s", item.get("source_url"))
if not item.get("download_url") and not item.get("service_url"):
spider.logger.warning("Missing data link: %s", item.get("source_url"))
bbox = item.get("bbox")
if bbox and len(bbox) < 5:
spider.logger.warning("Suspicious bbox: %s url=%s", bbox, item.get("source_url"))
return item
在 settings.py 中启用:
ITEM_PIPELINES = {
"gis_spider.pipelines.GisDatasetValidationPipeline": 300,
}
10. 运行爬虫
scrapy crawl open_data
运行后检查 output/gis_datasets.jsonl,重点看三类问题:
- 记录数量是否和目标网站分页数量大致一致。
- 是否有大量
download_url或service_url为空。 - 日志里是否频繁出现403、429、超时和重试。
常见坑:Scrapy代理池实战中最容易踩的坑
1. 只换代理,不控制并发
这是最常见的错误。代理池不是万能药。如果并发过高、请求太密集,即使用代理也可能很快触发限制。GIS公开数据平台通常资源有限,应该优先使用 DOWNLOAD_DELAY、CONCURRENT_REQUESTS_PER_DOMAIN 和 AutoThrottle。
2. 把地图瓦片当成普通页面暴力抓取
瓦片服务请求量极大,不能随意全量抓取。很多瓦片服务都有明确的使用限制。若只是做数据分析,应优先寻找官方下载数据、WFS、FeatureServer、GeoJSON或开放数据包,而不是抓瓦片。
3. 没有识别“假200”
有些网站被限制后仍返回状态码200,但内容变成验证页、错误页或空JSON。可以在解析前检查响应内容特征,例如是否包含目标字段、是否是合法JSON、features数量是否异常。
def is_valid_geojson(data):
return (
isinstance(data, dict)
and data.get("type") in ["FeatureCollection", "Feature"]
)
4. 免费代理导致数据污染
来源不明的免费代理可能不稳定、速度慢、被大量站点封禁,甚至篡改响应内容。GIS数据采集重视准确性,不建议把免费代理用于正式数据生产。
5. 没有保存失败URL
如果爬虫跑完才发现缺数据,但没有失败URL记录,就很难补采。建议把失败URL、状态码、代理、时间写入日志或单独文件,方便后续小批量重跑。
方法比较:限速、请求头、代理池和官方API怎么选
| 方法 | 适用场景 | 优点 | 限制 |
|---|---|---|---|
| 降低并发与AutoThrottle | 公开GIS目录、分页页面、元数据采集 | 稳定、合规、实现简单 | 采集速度较慢 |
| 随机请求头 | 默认Scrapy请求被拒绝、缺少浏览器字段 | 能减少误判为异常客户端 | 不能解决高频访问问题 |
| 代理池 | 单IP频率限制明显、目标允许合理访问 | 降低单IP压力,适合长任务 | 成本高,需要监控质量 |
| 官方API或批量下载 | 开放数据平台提供下载包、WFS、ArcGIS REST接口 | 数据结构清晰,稳定性最好 | 可能有调用限制或字段不完整 |
| 浏览器自动化 | 页面强依赖JavaScript渲染 | 能处理动态页面 | 慢、资源消耗大,不适合大规模任务 |
对GIS读者来说,优先级建议是:官方下载数据优先,其次官方API,再考虑Scrapy解析页面。只有在确实需要网页元数据、且访问规则允许的情况下,才使用代理池和反反爬策略。
检查清单:上线前确认这些配置
- 是否确认目标数据是公开数据,并允许自动化访问或批量下载。
- 是否开启
ROBOTSTXT_OBEY,并阅读服务条款。 - 是否设置合理的
DOWNLOAD_DELAY和并发数。 - 是否开启
RETRY_ENABLED,但限制了重试次数。 - 是否记录403、429、超时、失败URL和代理信息。
- 是否检查返回内容不是验证页或错误页。
- 是否对数据量、字段完整性、坐标范围和下载链接做校验。
- 是否避免抓取海量地图瓦片。
- 是否准备断点续采或失败URL补采方案。
- 是否能在小样本测试通过后再扩大采集范围。
FAQ:Scrapy爬虫抓取GIS数据被封的常见问题
Q1:Scrapy爬虫抓取GIS数据总被封,是不是必须使用代理池?
不一定。很多情况下,被封的根本原因是并发太高、请求太密集或请求头不完整。建议先降低并发、开启AutoThrottle、检查robots.txt和接口限制,再考虑代理池。
Q2:遇到429应该怎么处理?
429表示请求过多。最直接的处理方式是增加 DOWNLOAD_DELAY,降低 CONCURRENT_REQUESTS_PER_DOMAIN,开启 AUTOTHROTTLE_ENABLED,并对429做有限重试。不要在429出现后继续高频请求。
Q3:GIS数据采集可以直接抓地图瓦片吗?
不建议。瓦片请求量巨大,容易影响服务,也可能违反使用条款。如果目标是分析数据,应优先寻找矢量下载、WFS、FeatureServer、GeoJSON、Shapefile或GeoPackage等数据源。
Q4:代理池里的代理越多越好吗?
不是。代理质量比数量更重要。低质量代理会导致超时、丢包、响应被篡改或频繁失败。正式GIS数据采集更应该选择稳定代理,并记录每条数据的来源URL和采集日志。
Q5:如何判断抓到的GIS数据是否完整?
可以从四个方面检查:记录总数是否符合分页数量,关键字段是否为空,坐标范围是否落在目标区域,几何类型是否符合预期。如果是ArcGIS REST或GeoJSON,还要检查features数量和服务返回的统计信息。
Q6:动态加载的GIS开放数据页面,Scrapy抓不到内容怎么办?
先用浏览器开发者工具查看Network请求,找到真实的JSON、GeoJSON或API接口。很多动态页面只是前端渲染,真实数据接口比HTML更适合采集。只有接口无法直接使用时,才考虑Playwright或Selenium。
结论:稳定采集公开GIS数据,比盲目提速更重要
Scrapy爬虫抓取GIS数据总被封,通常不是单一原因造成的。真正可靠的方案,是把合规访问、限速、请求头、重试、代理池、日志和数据校验组合起来。
对于GIS项目,采集速度不是唯一目标,数据完整性和可追溯性更重要。建议先用小样本验证解析规则和字段质量,再逐步扩大范围;优先使用官方API和开放下载;只有在合理访问仍受单IP限制时,再引入代理池。这样才能让Scrapy在GIS数据采集中既稳定,又不影响目标服务。