Scrapy爬虫抓取的数据如何快速转为GIS矢量图层?(附:空间坐标自动匹配脚本)
引言
Scrapy爬虫抓取的数据如何快速转为GIS矢量图层?(附:空间坐标自动匹配脚本)这个问题,常见于把网站POI、公告地址、监测点、项目清单等网页数据采集下来之后,下一步却卡在“怎么放到QGIS或ArcGIS Pro里成图”。
很多Scrapy结果只是CSV、JSON或数据库表,里面可能有经纬度,也可能只有地址字段;坐标字段名称还可能是lng、lon、x、longitude等不同写法。本文给你一套实用流程:先识别字段,再自动匹配空间坐标,最后输出GeoJSON、Shapefile或GeoPackage,直接加载为GIS矢量图层。

背景
Scrapy适合批量抓取结构化网页数据,但它本身不是GIS工具。它输出的数据通常是:
- CSV:适合表格数据交换,QGIS和ArcGIS Pro都能直接读取。
- JSON:适合保留嵌套结构,但需要进一步转换为地理数据格式。
- 数据库表:如SQLite、MySQL、PostgreSQL,用于持续采集和增量更新。
如果爬虫结果中已经包含经纬度字段,把Scrapy数据转GIS矢量图层并不复杂。真正容易出错的是以下几类情况:
- 经度字段名称不统一,例如
lng、lon、x、经度。 - 纬度字段名称不统一,例如
lat、y、latitude、纬度。 - 坐标值被保存为字符串,带有空格、中文符号或单位。
- 经纬度顺序颠倒,导致点位落到错误国家或海上。
- 数据是GCJ-02、BD-09或Web墨卡托坐标,但被误认为WGS84。
因此,快速转图层的关键不只是“导出文件”,而是先判断字段、坐标值和坐标系是否可信。
原理
GIS矢量图层至少需要两部分信息:一是属性字段,二是几何对象。对Scrapy采集的点数据来说,几何对象通常由经度和纬度生成。
在Python GIS处理中,常用做法是:
- 用
pandas读取Scrapy导出的CSV或JSON。 - 自动识别经度字段和纬度字段。
- 把坐标字段转换为数值类型。
- 过滤无效坐标,例如空值、超出范围值。
- 用
GeoPandas生成点几何。 - 指定坐标参考系,通常是
EPSG:4326。 - 导出为GeoJSON、GeoPackage或Shapefile。
其中EPSG:4326指WGS84地理坐标系,是大多数GPS经纬度数据和GeoJSON常用的坐标系。如果你的数据来源是国内互联网地图接口,可能不是WGS84,而是GCJ-02或BD-09,这一点要特别核查。
步骤
步骤一:让Scrapy输出结构化数据
建议先让Scrapy导出为CSV或JSON Lines。对于GIS转换来说,字段越扁平越好,避免把坐标藏在复杂嵌套对象里。
scrapy crawl poi_spider -O output.csv
scrapy crawl poi_spider -O output.jsonl
一个适合转GIS矢量图层的Scrapy结果,至少应包含名称和空间位置字段,例如:
| name | address | lng | lat | category |
|---|---|---|---|---|
| 某某学校 | 某市某区某路 | 116.3912 | 39.9075 | 教育 |
步骤二:安装Python GIS依赖
推荐使用独立环境安装,避免和爬虫项目依赖冲突。
pip install pandas geopandas pyogrio shapely
如果你在Windows环境中安装GeoPandas遇到依赖问题,可以优先使用Conda环境:
conda create -n scrapy_gis python=3.11
conda activate scrapy_gis
conda install -c conda-forge pandas geopandas pyogrio shapely
步骤三:使用空间坐标自动匹配脚本
下面脚本适合处理Scrapy导出的CSV或JSON Lines文件。它会自动查找常见经纬度字段名,清洗坐标值,并输出GeoJSON和GeoPackage。
import re
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
from pathlib import Path
INPUT_FILE = "output.csv"
OUTPUT_GEOJSON = "scrapy_points.geojson"
OUTPUT_GPKG = "scrapy_points.gpkg"
LAYER_NAME = "scrapy_points"
LON_CANDIDATES = [
"lon", "lng", "longitude", "x", "经度", "lon_wgs84", "lng_wgs84",
"location_lon", "location_lng", "gcj_lng", "bd_lng"
]
LAT_CANDIDATES = [
"lat", "latitude", "y", "纬度", "lat_wgs84",
"location_lat", "gcj_lat", "bd_lat"
]
def normalize_col(col):
return str(col).strip().lower().replace(" ", "_")
def read_scrapy_file(path):
path = Path(path)
if path.suffix.lower() == ".csv":
return pd.read_csv(path)
if path.suffix.lower() in [".jsonl", ".jl"]:
return pd.read_json(path, lines=True)
if path.suffix.lower() == ".json":
return pd.read_json(path)
raise ValueError("暂不支持该文件格式,请使用 CSV、JSON 或 JSONL。")
def find_coordinate_fields(df):
original_cols = list(df.columns)
normalized_map = {normalize_col(c): c for c in original_cols}
lon_field = None
lat_field = None
for name in LON_CANDIDATES:
key = normalize_col(name)
if key in normalized_map:
lon_field = normalized_map[key]
break
for name in LAT_CANDIDATES:
key = normalize_col(name)
if key in normalized_map:
lat_field = normalized_map[key]
break
if lon_field and lat_field:
return lon_field, lat_field
numeric_cols = []
for col in original_cols:
s = pd.to_numeric(df[col], errors="coerce")
valid_ratio = s.notna().mean()
if valid_ratio > 0.6:
numeric_cols.append(col)
for lon_col in numeric_cols:
lon_values = pd.to_numeric(df[lon_col], errors="coerce")
if not lon_values.between(-180, 180).mean() > 0.8:
continue
for lat_col in numeric_cols:
if lon_col == lat_col:
continue
lat_values = pd.to_numeric(df[lat_col], errors="coerce")
if lat_values.between(-90, 90).mean() > 0.8:
return lon_col, lat_col
raise ValueError("未能自动识别经纬度字段,请检查字段名或手动指定。")
def clean_number(value):
if pd.isna(value):
return None
text = str(value).strip()
text = text.replace(",", ".")
match = re.search(r"-?d+(.d+)?", text)
if not match:
return None
return float(match.group())
def build_point_layer(input_file):
df = read_scrapy_file(input_file)
lon_field, lat_field = find_coordinate_fields(df)
print(f"识别到经度字段:{lon_field}")
print(f"识别到纬度字段:{lat_field}")
df["_lon"] = df[lon_field].apply(clean_number)
df["_lat"] = df[lat_field].apply(clean_number)
before_count = len(df)
df = df.dropna(subset=["_lon", "_lat"]).copy()
df = df[
(df["_lon"] >= -180) &
(df["_lon"] <= 180) &
(df["_lat"] >= -90) &
(df["_lat"] <= 90)
].copy()
after_count = len(df)
print(f"原始记录数:{before_count}")
print(f"有效坐标记录数:{after_count}")
geometry = [Point(xy) for xy in zip(df["_lon"], df["_lat"])]
gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326")
gdf.to_file(OUTPUT_GEOJSON, driver="GeoJSON")
gdf.to_file(OUTPUT_GPKG, layer=LAYER_NAME, driver="GPKG")
print(f"已输出:{OUTPUT_GEOJSON}")
print(f"已输出:{OUTPUT_GPKG}")
if __name__ == "__main__":
build_point_layer(INPUT_FILE)
运行脚本:
python scrapy_to_gis.py
运行成功后,你会得到两个文件:
scrapy_points.geojson:适合WebGIS、QGIS快速预览和数据交换。scrapy_points.gpkg:适合长期保存,多图层管理能力更好。
步骤四:在QGIS中加载结果
- 打开QGIS。
- 点击“图层”菜单。
- 选择“添加图层”。
- 选择“添加矢量图层”。
- 加载
scrapy_points.geojson或scrapy_points.gpkg。 - 右键图层,查看“属性表”,确认爬虫字段是否完整。
- 添加底图,检查点位是否落在正确位置。
如果点位出现在非预期区域,优先检查经纬度顺序、坐标系来源和是否存在火星坐标偏移。
步骤五:在ArcGIS Pro中加载结果
- 打开ArcGIS Pro项目。
- 在Catalog中连接输出文件所在文件夹。
- 将
scrapy_points.geojson或scrapy_points.gpkg拖入地图。 - 检查图层坐标系是否为WGS 1984。
- 打开属性表,检查字段编码和字段类型。
如果你需要把数据保存到文件地理数据库,可以在ArcGIS Pro中使用“要素类转要素类”工具进行转换。
常见坑
坑一:经纬度顺序写反
Scrapy数据转GIS矢量图层时,经度应该作为X,纬度应该作为Y。很多接口返回的是[lat, lon],但GIS点构造通常需要Point(lon, lat)。
判断方法很简单:如果中国范围内的点,经度通常在73到135之间,纬度通常在18到54之间。如果经度字段出现大量30、40,而纬度字段出现大量110、120,就很可能写反了。
坑二:把GCJ-02或BD-09当成WGS84
国内不少互联网地图数据使用GCJ-02或BD-09坐标。QGIS、GeoJSON和大多数国际GIS工具通常默认使用WGS84。若坐标系不一致,点位会出现几十米到几百米偏移。
如果数据来自高德、腾讯等接口,常见是GCJ-02;如果来自百度地图,常见是BD-09。此时需要先进行坐标转换,再输出GIS矢量图层。
坑三:字段编码导致中文乱码
CSV在不同软件之间传递时容易出现中文乱码。建议优先使用UTF-8编码导出;如果给ArcGIS Pro或旧版软件使用,GeoPackage通常比CSV加Shapefile更稳妥。
坑四:Shapefile字段名被截断
Shapefile字段名长度有限,长字段会被截断。Scrapy抓取的数据字段往往较长,例如business_opening_hours、administrative_division_name。如果字段较多,建议优先输出GeoPackage。
坑五:只有地址没有经纬度
如果Scrapy只抓到了地址,没有坐标,就不能直接生成点图层。你需要先做地理编码,也就是把地址转换为经纬度。地理编码结果还要检查匹配等级,不能把低置信度结果直接用于正式分析。
方法比较
| 方法 | 适用场景 | 优点 | 限制 |
|---|---|---|---|
| CSV加经纬度直接加载 | 少量点数据、临时查看 | 操作简单,QGIS和ArcGIS Pro都支持 | 坐标字段和坐标系容易选错 |
| Python脚本转GeoJSON | WebGIS展示、QGIS快速预览 | 轻量、通用、便于自动化 | 大数据量时文件体积较大 |
| Python脚本转GeoPackage | 正式整理、长期保存、多字段数据 | 支持多图层,字段兼容性更好 | 部分Web前端不能直接加载 |
| PostGIS入库 | 持续采集、空间查询、多人协作 | 适合空间索引和增量更新 | 需要数据库维护能力 |
| 地址地理编码后建图层 | 只有地址没有坐标 | 能把文本地址转换为空间点 | 依赖地理编码服务质量和配额 |
检查清单
在把Scrapy数据转GIS矢量图层之前,建议按下面清单检查一遍:
- 是否有明确的经度字段和纬度字段?
- 经度是否作为X,纬度是否作为Y?
- 坐标值是否为数值,而不是带单位的字符串?
- 坐标是否落在合理范围内?
- 坐标系是否明确,是WGS84、GCJ-02、BD-09还是投影坐标?
- 是否存在重复POI或重复记录?
- 是否存在空坐标、零坐标或明显异常坐标?
- 输出格式是否符合后续软件要求?
- 中文字段和长字段是否能被目标格式正常保存?
- 是否在QGIS或ArcGIS Pro中叠加底图验证过点位?
如果这十项都通过,Scrapy爬虫抓取的数据通常就可以比较可靠地进入GIS分析流程。
FAQ
Scrapy爬虫抓取的数据可以直接变成Shapefile吗?
可以,但不建议作为首选。Shapefile对字段名长度、字段类型和编码都有较多限制。更推荐先输出GeoPackage或GeoJSON。如果必须交付Shapefile,可以在GeoPandas中使用driver="ESRI Shapefile"导出。
空间坐标自动匹配脚本能识别所有字段名吗?
不能。脚本内置了常见字段名,并增加了数值范围判断,但如果你的字段名非常特殊,仍然需要手动添加到LON_CANDIDATES和LAT_CANDIDATES中。自动识别只能提高效率,不能替代人工检查。
为什么导出的GeoJSON在地图上有偏移?
常见原因是坐标系不一致。比如原始数据是GCJ-02或BD-09,但导出时被标记为EPSG:4326。另一个原因是经纬度顺序写反。建议先确认数据来源,再叠加权威底图进行验证。
只有地址字段,没有经纬度,能不能转GIS矢量图层?
可以,但需要先地理编码。地理编码是把地址转换为坐标的过程。你可以使用合规的地理编码服务,得到经纬度后再运行本文脚本生成点图层。注意检查匹配结果,尤其是同名道路、同名小区和模糊地址。
GeoJSON和GeoPackage应该选哪个?
如果用于WebGIS展示或临时交换,GeoJSON更方便;如果用于GIS项目整理、长期保存或字段较多的数据,GeoPackage更稳妥。对于Scrapy采集的大批量POI数据,建议优先保存一份GeoPackage作为主数据。
Scrapy结果中有面或线数据怎么办?
如果网页数据中包含边界坐标串、轨迹点序列或WKT字段,就可以构造线或面几何。点数据使用Point,线数据使用LineString,面数据使用Polygon。但这需要原始数据结构足够规范,不能只靠单个经纬度字段完成。
结论
Scrapy爬虫抓取的数据要快速转为GIS矢量图层,核心流程是:整理结构化输出、识别经纬度字段、清洗坐标值、明确坐标系、生成几何对象、导出GIS格式。
本文提供的空间坐标自动匹配脚本适合处理常见POI、地址点、监测点和项目位置数据。实际项目中,不要只看脚本是否成功运行,更要在QGIS或ArcGIS Pro中叠加底图检查位置是否正确。
如果你的数据来自国内互联网地图接口,尤其要确认是否需要GCJ-02或BD-09到WGS84的转换。只有坐标字段、坐标顺序和坐标系都正确,Scrapy数据转GIS矢量图层才算真正完成。