GIS数据采集效率低?Scrapy爬虫实战教程(含:反爬策略与地理编码技巧)

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

如果你正在被“GIS数据采集效率低?Scrapy爬虫实战教程(含:反爬策略与地理编码技巧)”这个问题困扰,通常不是因为你不会写爬虫,而是因为采集目标、字段结构、坐标转换、地理编码和数据质量控制没有形成一条稳定流程。本文以 GIS 项目中常见的 POI、地址、行政区划属性采集为例,演示如何用 Scrapy 搭建可维护的数据采集流程,并说明合规的反爬应对、地理编码和结果校验方法。

引言:GIS数据采集为什么总是慢

在很多 GIS 项目里,数据采集往往比空间分析更耗时。你可能需要从公开网页中整理学校、医院、停车场、企业网点、政务设施等信息,再把地址转换成经纬度,最后导入 QGIS、ArcGIS Pro、PostGIS 或 WebGIS 系统。

如果只靠手工复制,问题会很快出现:

  • 字段名称不统一,后期清洗成本高。
  • 地址缺失或格式混乱,无法直接制图。
  • 网页分页多,重复采集和漏采很难发现。
  • 坐标系不明确,导入 GIS 软件后位置偏移。
  • 请求频率过高,容易触发网站限制。

Scrapy 的价值不只是“更快地抓网页”,而是把采集任务拆成请求、解析、清洗、去重、地理编码、导出几个环节,让 GIS 数据采集变成可复用的工程流程。

GIS数据采集效率低 Scrapy爬虫实战教程与地理编码流程
Scrapy 采集 GIS 数据的推荐流程:先结构化字段,再做地理编码与空间校验,最后导入 GIS 平台。

背景:适合用 Scrapy 采集的 GIS 数据类型

Scrapy 适合采集具有稳定页面结构、分页规则清晰、数据字段重复出现的公开网页数据。对于 GIS 用户来说,最常见的是以下几类:

  • POI 数据:名称、类别、地址、联系电话、营业时间。
  • 公共服务设施:学校、医院、派出所、停车场、公园、公厕。
  • 行政管理信息:街道社区、村委会、网格、片区责任单位。
  • 项目公告信息:建设项目名称、位置描述、审批状态、发布时间。
  • 地名地址信息:标准地址、门牌号、小区名称、楼栋信息。

但并不是所有数据都适合爬取。涉及登录权限、个人隐私、付费数据、明确禁止自动化访问的数据,应遵守网站条款和法律要求。GIS 数据采集的目标是提高公开信息整理效率,而不是绕过访问限制。

原理:Scrapy 如何服务 GIS 数据采集

Scrapy 是一个 Python 爬虫框架,核心思想是把网页采集分成几个职责明确的模块:

  • Spider:定义从哪里开始访问、如何翻页、如何解析字段。
  • Request:表示一次网页请求,可以携带 URL、请求头和回调函数。
  • Item:定义结构化字段,例如名称、地址、经度、纬度。
  • Pipeline:对采集结果进行清洗、去重、地理编码和保存。
  • Settings:配置下载延迟、并发数、User-Agent、导出编码等。

对 GIS 项目而言,Scrapy 的关键不只是抓到 HTML,而是把网页内容转换成稳定的空间数据表。至少应包含以下字段:

字段 说明 GIS 用途
name 设施或地点名称 地图标注、检索、去重
category 设施类别 分层制图、符号化
address 标准或半标准地址 地理编码、空间定位
district 区县、街道或行政区 行政区统计、空间连接
lng 经度 生成点图层
lat 纬度 生成点图层
source_url 来源页面 回溯、核验、更新

步骤:Scrapy爬虫实战采集 GIS 地址数据

步骤 1:准备 Python 环境与项目目录

建议使用虚拟环境,避免 Scrapy、GeoPandas、requests 等库与其他项目互相影响。

python -m venv venv
# Windows
venvScriptsactivate
# macOS / Linux
source venv/bin/activate

pip install scrapy pandas requests

创建 Scrapy 项目:

scrapy startproject gis_poi_spider
cd gis_poi_spider
scrapy genspider poi_example example.com

项目生成后,重点关注这几个文件:

  • spiders/poi_example.py:写采集逻辑。
  • items.py:定义字段。
  • pipelines.py:写清洗、去重、地理编码逻辑。
  • settings.py:控制请求频率、导出格式和管道启用。

步骤 2:定义 GIS 数据字段

items.py 中定义一个适合 GIS 项目的 Item。字段不需要一开始就很多,但要保证后续能定位、能核验、能导入 GIS 软件。

import scrapy

class GisPoiItem(scrapy.Item):
    name = scrapy.Field()
    category = scrapy.Field()
    address = scrapy.Field()
    district = scrapy.Field()
    phone = scrapy.Field()
    lng = scrapy.Field()
    lat = scrapy.Field()
    source_url = scrapy.Field()

步骤 3:编写 Spider 解析列表页和详情页

下面示例使用通用写法。实际项目中,你需要根据目标网页的 HTML 结构调整 CSS 选择器或 XPath。建议先在浏览器开发者工具中确认字段所在节点,再写解析规则。

import scrapy
from gis_poi_spider.items import GisPoiItem

class PoiExampleSpider(scrapy.Spider):
    name = "poi_example"
    allowed_domains = ["example.com"]
    start_urls = [
        "https://www.example.com/poi/list?page=1"
    ]

    def parse(self, response):
        cards = response.css(".poi-card")

        for card in cards:
            detail_url = card.css("a::attr(href)").get()
            if detail_url:
                yield response.follow(detail_url, callback=self.parse_detail)

        next_page = response.css("a.next::attr(href)").get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

    def parse_detail(self, response):
        item = GisPoiItem()
        item["name"] = response.css("h1::text").get()
        item["category"] = response.css(".category::text").get()
        item["address"] = response.css(".address::text").get()
        item["district"] = response.css(".district::text").get()
        item["phone"] = response.css(".phone::text").get()
        item["source_url"] = response.url
        yield item

如果网页中已经包含经纬度,优先直接提取。常见位置包括页面脚本、地图参数、接口返回 JSON 或隐藏字段。直接坐标通常比地址地理编码更稳定,但仍然要检查坐标系。

步骤 4:配置合规的反爬应对策略

这里的“反爬策略”不是绕过验证码、登录权限或付费限制,而是让 Scrapy 以更温和、可控、可追踪的方式访问公开页面,减少对网站造成压力,也降低被限制的概率。

settings.py 中建议配置:

ROBOTSTXT_OBEY = True

DOWNLOAD_DELAY = 2
RANDOMIZE_DOWNLOAD_DELAY = True

CONCURRENT_REQUESTS = 4
CONCURRENT_REQUESTS_PER_DOMAIN = 2

DEFAULT_REQUEST_HEADERS = {
    "User-Agent": "GIS data collection research bot; contact: your_email@example.com",
    "Accept-Language": "zh-CN,zh;q=0.9",
}

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 2
AUTOTHROTTLE_MAX_DELAY = 10
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

RETRY_ENABLED = True
RETRY_TIMES = 2
HTTPERROR_ALLOWED_CODES = [429, 500, 502, 503, 504]

如果遇到 429 Too Many Requests,应降低并发、增加延迟,或暂停任务。不要通过高频代理池、验证码识别、伪装登录等方式强行采集,这类做法不仅不稳定,也可能违反网站规则。

步骤 5:在 Pipeline 中清洗字段

GIS 数据采集效率低,很多时候是因为前期没有做字段清洗。建议至少清理空格、换行、重复符号和无效地址。

import re

class CleanPoiPipeline:
    def process_item(self, item, spider):
        for key in ["name", "category", "address", "district", "phone"]:
            value = item.get(key)
            if value:
                value = re.sub(r"s+", " ", value).strip()
                value = value.replace("地址:", "").replace("电话:", "")
                item[key] = value

        if not item.get("name") or not item.get("address"):
            raise ValueError("缺少 name 或 address,跳过该记录")

        return item

settings.py 启用 Pipeline:

ITEM_PIPELINES = {
    "gis_poi_spider.pipelines.CleanPoiPipeline": 300,
}

步骤 6:加入地理编码技巧

地理编码是把地址转换为经纬度的过程。GIS 项目中,不建议把所有地址直接丢给地理编码服务,而应先做地址标准化。

推荐做法:

  • 把省、市、区县字段与详细地址拼接,减少重名地址误匹配。
  • 保留原始地址字段,不要只保存清洗后的地址。
  • 记录地理编码服务返回的置信度、匹配级别或匹配地址。
  • 对批量请求设置延迟,遵守服务商配额和使用条款。
  • 同一地址先查本地缓存,避免重复请求。

下面给出一个通用地理编码函数结构。实际使用时,请替换为你有合法授权的地理编码服务,并按官方文档填写参数。

import time
import requests

def geocode_address(address, api_key):
    url = "https://api.example-geocoder.com/geocode"
    params = {
        "address": address,
        "key": api_key
    }

    response = requests.get(url, params=params, timeout=10)
    response.raise_for_status()
    data = response.json()

    if not data.get("results"):
        return None, None, None

    result = data["results"][0]
    lng = result["location"]["lng"]
    lat = result["location"]["lat"]
    matched_address = result.get("formatted_address")

    time.sleep(0.2)
    return lng, lat, matched_address

如果你采集的是中国大陆互联网地图服务返回的坐标,常见可能是 GCJ-02 坐标;GPS 和大多数国际 GIS 数据常用 WGS84;部分服务还可能使用 BD-09。导入 QGIS 或 ArcGIS Pro 前,必须确认坐标系,否则会出现几十米到数百米的位置偏移。

步骤 7:导出 CSV 并转换为空间数据

Scrapy 可以直接导出 CSV:

scrapy crawl poi_example -O output/poi.csv

CSV 中如果已经有 lnglat 字段,可以在 QGIS 中按以下步骤加载:

  1. 打开 QGIS。
  2. 选择“图层”中的“添加分隔文本图层”。
  3. 选择 poi.csv
  4. X 字段选择 lng,Y 字段选择 lat
  5. 根据坐标来源选择正确 CRS,例如 WGS84 对应 EPSG:4326
  6. 加载后叠加底图或行政区边界检查位置是否合理。

如果要用 Python 转成 GeoJSON,可以使用 GeoPandas:

import pandas as pd
import geopandas as gpd
from shapely.geometry import Point

df = pd.read_csv("output/poi.csv")

df = df.dropna(subset=["lng", "lat"])
df["lng"] = df["lng"].astype(float)
df["lat"] = df["lat"].astype(float)

geometry = [Point(xy) for xy in zip(df["lng"], df["lat"])]
gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326")

gdf.to_file("output/poi.geojson", driver="GeoJSON")

步骤 8:导入 PostGIS 进行空间查询

如果数据后续要给 WebGIS 或空间分析使用,建议导入 PostGIS。可先用 GeoPandas 写入数据库,也可以用 ogr2ogr。导入后要建立空间索引。

CREATE INDEX poi_geom_gix
ON public.poi
USING GIST (geom);

常用核验 SQL:

-- 检查空几何
SELECT COUNT(*) FROM public.poi WHERE geom IS NULL;

-- 检查重复名称和地址
SELECT name, address, COUNT(*)
FROM public.poi
GROUP BY name, address
HAVING COUNT(*) > 1;

-- 检查是否落在目标行政区范围内
SELECT p.*
FROM public.poi p
LEFT JOIN public.district d
ON ST_Within(p.geom, d.geom)
WHERE d.id IS NULL;

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

坑 1:只采集名称,不保留来源链接

GIS 数据经常需要复核。如果没有 source_url,后期发现某个点位置异常时,很难回到原网页确认原因。建议任何采集结果都保留来源 URL 和采集时间。

坑 2:地址可以显示在地图上,不代表坐标是对的

地理编码可能返回同名道路、同名小区或城市中心点。尤其是“某某学校”“某某医院”这类名称,在不同区县可能重复。建议拼接行政区字段,并把匹配地址、匹配级别一起保存。

坑 3:忽略坐标系导致整体偏移

GIS 项目里最常见的坐标问题是 WGS84、GCJ-02、BD-09 混用。表现为点位和底图有固定偏移,或者在不同底图上位置不一致。处理方式是先确认数据来源坐标系,再决定是否转换。

坑 4:并发设置过高

Scrapy 默认性能很强,但 GIS 数据采集通常不追求极限速度。过高并发会增加失败率,也不利于长期更新。对于公开网页,建议低并发、带延迟、可恢复。

坑 5:不做去重

分页、分类页和搜索页可能指向同一个详情页。如果不去重,后续统计会偏大。可以用 source_urlname + address 或空间距离阈值做多级去重。

方法比较:Scrapy、requests、浏览器插件和手工采集怎么选

方法 适合场景 优点 限制
手工采集 几十条以内、一次性任务 简单、无需开发 慢、易漏、难复现
浏览器插件 表格结构简单的网页 上手快、适合临时提取 复杂翻页和详情页较弱
requests + BeautifulSoup 小型静态页面采集 代码轻、依赖少 工程化、调度和重试能力有限
Scrapy 分页多、字段多、需要定期更新 结构清晰、支持 Pipeline、扩展性好 需要理解框架结构
官方 API 有正规数据接口的项目 稳定、字段规范、合规性好 可能有配额、费用或权限要求

如果目标网站提供官方 API,优先使用 API。只有在没有合适接口、且数据为公开可访问内容时,再考虑 Scrapy。对于 GIS 工程项目,稳定性和合规性通常比短时间采集速度更重要。

检查清单:采集完成后如何确认数据可用于 GIS

  • 是否保留了 name、address、source_url 等基本字段?
  • 地址字段是否去除了换行、标签、无效前缀?
  • 是否保存了原始地址和地理编码后的匹配地址?
  • 经纬度字段是否为数值类型,而不是字符串?
  • 是否确认坐标系是 WGS84、GCJ-02 还是 BD-09?
  • 是否检查了点位是否落在目标城市或目标行政区内?
  • 是否去除了重复记录?
  • 是否记录了采集时间,方便后续增量更新?
  • 是否遵守 robots.txt、网站条款和地理编码服务配额?
  • 是否能在 QGIS 或 ArcGIS Pro 中正常加载并叠加底图检查?

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

Scrapy 适合采集所有 GIS 数据吗?

不适合。Scrapy 适合采集公开网页中结构较稳定的信息。如果数据有官方 API、开放数据平台或标准下载入口,应优先使用这些渠道。涉及权限、隐私、付费或禁止自动化访问的数据,不建议用爬虫采集。

地理编码后的点位偏移怎么办?

先确认坐标系。WGS84、GCJ-02、BD-09 混用会造成偏移。其次检查地址是否完整,是否缺少城市、区县、街道信息。最后抽样与权威底图或已有控制点对比,判断是单点错误还是整体坐标系错误。

Scrapy 爬到的数据如何导入 QGIS?

最简单的方法是导出 CSV,确保包含 lnglat 字段,然后在 QGIS 中使用“添加分隔文本图层”。如果要保留空间格式,可以用 GeoPandas 转为 GeoJSON、GeoPackage 或 Shapefile。

如何处理网站的反爬限制?

合规做法是遵守 robots.txt,降低并发,设置下载延迟,使用清晰的 User-Agent,必要时联系数据方申请接口或授权。不要绕过验证码、登录权限、付费墙或明确禁止的访问限制。

GIS 数据采集时要不要直接入库 PostGIS?

小任务可以先导出 CSV 或 GeoJSON,检查无误后再入库。定期更新、多人协作或 WebGIS 服务建议直接进入 PostGIS,并建立空间索引、唯一约束和更新时间字段。

地址没有门牌号还能地理编码吗?

可以,但精度会下降。没有门牌号时,地理编码可能返回道路中心、街道中心或行政区中心。对于精度要求高的项目,应记录匹配级别,并对低置信度结果进行人工复核。

结论:把 Scrapy 当成 GIS 数据工程的一环

GIS数据采集效率低,根本原因往往不是采集速度慢,而是缺少从网页字段到空间数据的完整流程。Scrapy 可以帮助你把公开网页数据结构化,再通过清洗、去重、地理编码和坐标校验,变成可以进入 QGIS、ArcGIS Pro、PostGIS 或 WebGIS 的数据资产。

实际项目中,建议坚持三个原则:第一,优先使用官方 API 或开放数据;第二,采集过程保持低频、合规、可追踪;第三,任何经纬度结果都必须经过坐标系和空间位置校验。这样做,Scrapy 才能真正提升 GIS 数据采集效率,而不是制造更多难以修复的数据质量问题。