Scrapy框架真的过时了吗?GIS数据采集实战指南(附:逆向与清洗技巧)

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

很多做空间数据采集的同学都会问:Scrapy框架真的过时了吗?GIS数据采集实战指南(附:逆向与清洗技巧)这个问题到底该怎么判断?如果你的目标是批量采集POI、行政区划、地名地址、公开目录数据、空间元数据或接口返回的GeoJSON,Scrapy并没有过时;真正过时的是“只会写一个简单爬虫、不理解GIS数据结构和反爬限制”的采集方式。

引言:Scrapy在GIS数据采集里还值得用吗

在GIS项目中,数据采集往往不是“抓几个网页”这么简单。你可能需要从公开网站批量获取空间要素、从接口分页下载JSON、把坐标字段转换成点图层、清洗重复POI、校验行政区划归属,最后输出为GeoJSON、Shapefile、GeoPackage或PostGIS表。

Scrapy的优势正好在这些环节:任务调度稳定、并发控制清晰、失败重试方便、管道机制适合做数据清洗和入库。对于GIS数据采集来说,它不是万能工具,但仍然是一个可靠的工程化框架。

提醒:本文讨论的是公开数据、授权数据和合规接口的数据采集流程。不要绕过登录权限、付费墙、验证码、访问控制或违反网站服务条款进行采集。

Scrapy框架GIS数据采集与GIS数据清洗流程图
Scrapy用于GIS数据采集时,重点不只是请求网页,而是把空间字段、坐标、属性和输出格式串成稳定流程。

背景:GIS数据采集为什么比普通网页爬虫更复杂

普通网页爬虫通常关注标题、正文、图片链接等文本信息。GIS数据采集还要处理空间含义,例如经纬度、坐标系、行政区划编码、几何对象、地址标准化、空间范围筛选等。

常见GIS采集场景包括:

  • 采集公开POI列表,并整理为点图层。
  • 从政务公开平台下载行政区划、规划公示、项目位置数据。
  • 从地图服务或业务接口中解析GeoJSON、WKT、经纬度字段。
  • 批量获取空间元数据,例如图层名称、服务地址、范围、坐标系。
  • 抓取公开目录页面,再下载对应的CSV、Excel、GeoJSON或压缩包。

这些任务的难点通常不在“发请求”,而在以下几个地方:

  • 接口分页规则不明确。
  • 坐标字段名称不统一,例如lng、lon、x、longitude混用。
  • 坐标系可能是WGS84、GCJ-02、BD-09或地方投影坐标。
  • 同一POI在多个页面重复出现。
  • 字段缺失、空值、乱码、地址不规范。
  • 采集结果需要进入QGIS、ArcGIS Pro或PostGIS继续分析。

原理:Scrapy适合GIS数据采集的核心机制

Scrapy不是一个简单的requests封装,而是一个异步爬虫框架。它的核心工作流可以理解为:Spider生成请求,下载器获取响应,解析器提取数据,Item承载结构化结果,Pipeline负责清洗、去重、转换和入库。

放到GIS数据采集中,可以这样对应:

Scrapy组件 在GIS采集中的作用 典型处理内容
Spider 定义采集入口和分页逻辑 列表页、接口URL、区域编码、页码参数
Request 控制请求参数 bbox、keyword、page、city_code、token
Item 保存结构化字段 名称、地址、经度、纬度、分类、来源URL
Pipeline 清洗和输出数据 坐标校验、字段标准化、去重、写入GeoJSON或PostGIS
Middleware 处理请求头、代理、重试 User-Agent、Referer、限速、失败重试

因此,Scrapy框架是否“过时”,不能只看它是否流行,而要看你的任务是否需要可维护的采集流程。如果只是临时下载一个接口,requests可能更轻;如果要长期采集、断点重试、批量清洗、入库,Scrapy依然很合适。

步骤:用Scrapy完成一次GIS数据采集实战

步骤1:先确认数据来源是否合法且可复现

开始写代码前,先确认数据源。GIS数据经常来自政府公开平台、开放数据平台、公开地图服务、企业内部授权接口或项目合作方提供的API。

建议记录以下信息:

  • 数据页面或接口地址。
  • 数据许可或使用说明。
  • 字段含义和更新时间。
  • 空间范围,例如城市、行政区、bbox。
  • 坐标系说明,如果没有说明,需要后续验证。
  • 是否允许批量访问,是否有频率限制。

如果数据源没有明确授权,或者需要登录、验证码、签名密钥、付费权限,不建议用爬虫绕过限制。对于GIS项目,数据合规性和可追溯性往往比技术实现更重要。

步骤2:创建Scrapy项目和Spider

假设我们要采集一个公开接口返回的POI JSON数据,字段包含name、address、lng、lat和category。可以先创建项目:

scrapy startproject gis_poi_crawler
cd gis_poi_crawler
scrapy genspider poi_spider example.com

项目结构中重点关注三个文件:

  • spiders/poi_spider.py:写请求和解析逻辑。
  • items.py:定义POI字段。
  • pipelines.py:做坐标校验、去重和输出。

步骤3:定义GIS字段结构

在GIS数据采集中,字段设计不要只照搬网页字段。建议提前定义清晰的标准字段,方便后续导入QGIS、ArcGIS Pro或PostGIS。

import scrapy

class PoiItem(scrapy.Item):
    name = scrapy.Field()
    address = scrapy.Field()
    category = scrapy.Field()
    longitude = scrapy.Field()
    latitude = scrapy.Field()
    source_url = scrapy.Field()
    source_id = scrapy.Field()
    city = scrapy.Field()

这里把lng和lat统一成longitude和latitude,是为了避免后续不同数据源字段混乱。source_url和source_id也很重要,它们可以帮助你追溯数据来源和去重。

步骤4:编写接口采集逻辑

很多GIS数据页面背后并不是纯HTML,而是通过接口返回JSON。可以在浏览器开发者工具中查看Network面板,找到公开请求地址、分页参数和返回字段。

一个简化版Spider如下:

import json
import scrapy
from gis_poi_crawler.items import PoiItem

class PoiSpider(scrapy.Spider):
    name = "poi_spider"
    allowed_domains = ["example.com"]

    def start_requests(self):
        base_url = "https://example.com/api/poi"
        for page in range(1, 6):
            url = f"{base_url}?city=hangzhou&page={page}&size=20"
            yield scrapy.Request(url=url, callback=self.parse_api)

    def parse_api(self, response):
        data = json.loads(response.text)
        records = data.get("records", [])

        for row in records:
            item = PoiItem()
            item["name"] = row.get("name")
            item["address"] = row.get("address")
            item["category"] = row.get("category")
            item["longitude"] = row.get("lng")
            item["latitude"] = row.get("lat")
            item["source_id"] = row.get("id")
            item["source_url"] = response.url
            item["city"] = "杭州"
            yield item

这段代码只适合作为结构示例。真实项目中,你需要根据目标数据源的字段、分页规则、返回状态码和数据许可进行调整。

步骤5:在Pipeline中做坐标清洗和去重

GIS数据采集不能只保存原始文本。经纬度字段必须经过基本校验,否则导入GIS软件后很容易出现点位跑到海里、坐标落在国外、图层无法显示等问题。

class PoiCleanPipeline:
    def __init__(self):
        self.seen = set()

    def process_item(self, item, spider):
        name = self.clean_text(item.get("name"))
        address = self.clean_text(item.get("address"))
        lon = self.to_float(item.get("longitude"))
        lat = self.to_float(item.get("latitude"))

        if not name:
            raise DropItem("缺少名称字段")

        if lon is None or lat is None:
            raise DropItem("缺少坐标字段")

        if not self.is_valid_wgs84(lon, lat):
            raise DropItem("经纬度范围异常")

        unique_key = f"{name}_{address}_{round(lon, 6)}_{round(lat, 6)}"
        if unique_key in self.seen:
            raise DropItem("重复POI")
        self.seen.add(unique_key)

        item["name"] = name
        item["address"] = address
        item["longitude"] = lon
        item["latitude"] = lat
        return item

    def clean_text(self, value):
        if value is None:
            return ""
        return str(value).strip().replace("n", " ").replace("t", " ")

    def to_float(self, value):
        try:
            return float(value)
        except (TypeError, ValueError):
            return None

    def is_valid_wgs84(self, lon, lat):
        return -180 <= lon <= 180 and -90 <= lat <= 90

如果在Pipeline中使用DropItem,需要在文件顶部导入:

from scrapy.exceptions import DropItem

并在settings.py启用Pipeline:

ITEM_PIPELINES = {
    "gis_poi_crawler.pipelines.PoiCleanPipeline": 300,
}

步骤6:输出为GeoJSON,方便直接进入GIS软件

Scrapy可以直接导出JSON、CSV等格式。如果要在QGIS或WebGIS中使用,GeoJSON更方便。可以在Pipeline中把POI写成FeatureCollection。

import json

class GeoJsonWriterPipeline:
    def open_spider(self, spider):
        self.features = []

    def process_item(self, item, spider):
        feature = {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [item["longitude"], item["latitude"]]
            },
            "properties": {
                "name": item.get("name"),
                "address": item.get("address"),
                "category": item.get("category"),
                "city": item.get("city"),
                "source_id": item.get("source_id"),
                "source_url": item.get("source_url")
            }
        }
        self.features.append(feature)
        return item

    def close_spider(self, spider):
        geojson = {
            "type": "FeatureCollection",
            "features": self.features
        }
        with open("poi_result.geojson", "w", encoding="utf-8") as f:
            json.dump(geojson, f, ensure_ascii=False, indent=2)

settings.py中可以同时启用清洗和输出:

ITEM_PIPELINES = {
    "gis_poi_crawler.pipelines.PoiCleanPipeline": 300,
    "gis_poi_crawler.pipelines.GeoJsonWriterPipeline": 400,
}

运行采集:

scrapy crawl poi_spider

生成poi_result.geojson后,可以直接拖入QGIS检查点位分布。如果点位整体偏移,优先怀疑坐标系或加密坐标问题。

步骤7:用QGIS验证采集结果

数据采集完成后,不要只看文件是否生成。GIS数据必须做空间验证。

  1. 打开QGIS。
  2. 将poi_result.geojson拖入地图窗口。
  3. 设置项目坐标参考系为EPSG:4326或常用Web墨卡托底图环境。
  4. 叠加公开底图或行政区边界。
  5. 检查点位是否落在目标城市范围内。
  6. 随机抽查名称、地址、分类字段是否对应。
  7. 使用“按位置选择”或空间连接检查行政区归属是否异常。

如果点位整体偏移几十米到几百米,可能是GCJ-02或BD-09坐标。如果点位完全跑到另一个国家,可能是经纬度顺序反了,或者投影坐标被当成经纬度使用。

常见坑:Scrapy做GIS数据采集最容易出错的地方

坑1:把x、y当成经纬度

很多接口字段叫x和y,但它们不一定是经度和纬度。x、y可能是投影坐标,也可能是屏幕坐标,还可能是瓦片坐标。判断方法很简单:如果数值类似120.123、30.456,通常是经纬度;如果数值是几十万或几百万,通常是投影坐标。

坑2:经纬度顺序写反

GeoJSON的点坐标顺序是[longitude, latitude],不是[latitude, longitude]。这是GIS初学者非常常见的问题。写反后,点位会出现在错误位置,甚至完全不显示。

坑3:只清洗文本,不清洗空间字段

GIS数据清洗至少包括字段清洗和空间清洗两部分。字段清洗处理空格、乱码、重复值;空间清洗处理坐标范围、坐标系、几何合法性和空间范围。

坑4:忽略分页终止条件

有些接口最后一页会返回空数组,有些会返回total字段,有些会固定返回成功状态但没有records。写分页逻辑时要根据实际响应判断终止条件,否则容易重复请求或漏采数据。

坑5:过度并发导致请求失败

GIS公开数据平台有时服务器性能有限。并发太高会导致请求失败、封禁IP或影响对方服务。应设置合理下载延迟和并发数。

CONCURRENT_REQUESTS = 4
DOWNLOAD_DELAY = 1
RETRY_TIMES = 3
AUTOTHROTTLE_ENABLED = True

坑6:没有保留原始来源字段

做GIS数据项目时,source_url、source_id、采集时间、数据来源名称非常关键。后期如果发现某批点位异常,需要靠这些字段回溯问题来源。

方法比较:Scrapy、requests、Selenium和地图服务下载怎么选

方法 适合场景 优点 局限
Scrapy 批量采集公开网页、接口、目录数据 结构清晰、并发稳定、适合清洗入库 学习成本高于简单脚本
requests 少量接口请求、一次性下载 简单直接、调试方便 任务复杂后代码容易混乱
Selenium或Playwright 页面强依赖浏览器渲染 能处理动态页面交互 速度慢、资源消耗大,不适合大量数据
GDAL或OGR 标准地图服务和空间文件下载 GIS格式支持强 不适合复杂网页解析
PostGIS导入管道 采集后直接空间分析 适合去重、索引和空间查询 需要数据库环境

如果你的GIS数据采集任务只是下载一个公开GeoJSON文件,不需要Scrapy。如果你要采集多个城市、多个分类、多个分页接口,并且要清洗坐标、去重、入库,Scrapy框架仍然非常实用。

检查清单:GIS数据采集上线前必须确认

  • 数据来源是否公开、授权或符合使用条款。
  • 是否记录了source_url、source_id、采集时间等追溯字段。
  • 分页逻辑是否会漏采或重复采。
  • 经纬度顺序是否符合GeoJSON规范。
  • 坐标范围是否落在合理空间范围内。
  • 坐标系是否明确,是否存在GCJ-02、BD-09或投影坐标问题。
  • 字段编码是否为UTF-8,中文是否正常。
  • 重复POI是否已经处理。
  • 输出格式是否能被QGIS、ArcGIS Pro或PostGIS正确读取。
  • 是否设置了合理并发、延迟和失败重试。
  • 是否保留原始响应样本,方便后续排错。
  • 是否用地图叠加方式抽查了空间位置。

FAQ:Scrapy框架与GIS数据采集常见问题

Scrapy框架真的过时了吗?

没有。对于简单接口请求,Scrapy可能显得偏重;但对于批量GIS数据采集、分页抓取、失败重试、字段清洗、坐标校验和入库流程,Scrapy仍然很适合。它的问题不是过时,而是需要按照工程化方式使用。

GIS数据采集一定要用Scrapy吗?

不一定。如果只是下载一个公开文件,用浏览器、wget、requests或GDAL就够了。如果你要长期采集多个来源,并且要做数据清洗、去重和结构化输出,Scrapy更容易维护。

如何判断采集到的坐标是不是WGS84?

先看字段说明。如果没有说明,可以检查数值范围、叠加底图验证位置,并抽查几个已知点。如果点位整体有固定偏移,可能不是标准WGS84;如果数值是几十万或几百万,可能是投影坐标。

Scrapy能不能直接写入PostGIS?

可以。常见做法是在Pipeline中使用psycopg或SQLAlchemy连接PostgreSQL,把经纬度生成geometry字段,再写入PostGIS表。写入前建议先做坐标校验和唯一键去重。

接口需要逆向分析时要注意什么?

只分析公开页面正常加载时产生的请求,重点看URL、请求方法、分页参数、返回JSON结构和字段含义。不要绕过登录、验证码、签名权限、付费限制或访问控制。逆向的目的应是理解公开数据加载方式,而不是突破限制。

采集POI后为什么在QGIS里不显示?

常见原因包括:GeoJSON格式错误、coordinates顺序写反、坐标为空、坐标范围异常、图层坐标系识别错误,或者地图缩放范围不在点位附近。建议先打开属性表,再用“缩放到图层”检查。

Scrapy采集速度越快越好吗?

不是。GIS公开平台往往不是为高频爬取设计的。速度过快可能导致失败、数据缺失或影响对方服务。应设置合理并发、下载延迟和自动限速,并遵守数据源访问规则。

结论:Scrapy没有过时,过时的是粗糙的数据采集流程

对于GIS读者来说,Scrapy框架的价值不在于“能不能抓网页”,而在于能否把公开数据采集、接口分页、字段解析、坐标清洗、去重、GeoJSON输出和PostGIS入库组织成稳定流程。

如果你的任务只是一次性下载少量数据,requests或GDAL可能更轻便;如果你的任务涉及长期维护、多数据源、多城市、多分类和空间质量控制,Scrapy依然是GIS数据采集中的实用选择。

真正可靠的GIS数据采集,不只是把数据抓下来,还要说明数据从哪里来、坐标是否正确、字段是否可用、结果能否在地图上验证。做到这些,Scrapy就不是过时工具,而是一套可维护的数据工程入口。