资讯详情

全国火车站站点位置GIS:从脏数据到空间分析

📅 2026/10/27 20:23:45 | 华诺云谱 👁 阅读
全国火车站站点位置GIS:从脏数据到空间分析
简介这份全国火车站站点位置 GIS 数据包面向地理信息、交通规划与空间分析学习者提供覆盖全国的火车站点位矢量数据可用于地图制图、站点分布查询、区域可达性分析与铁路线路规划等场景。压缩包共 8 个文件约 405KB包含 shp 矢量主体、shx 与 sbn/sbx 空间索引、prj 坐标参考、dbf 属性表、cpg 编码说明及 xml 元数据分别承担几何位置、快速检索、投影定义、字段信息与数据说明等作用结构完整且便于在常见 GIS 软件中直接加载。已有 396 人学习下载。读者可据此完成全国火车站的空间定位与属性查询围绕站点间最短路径、指定区域站点筛选和线路规划做练习也可作为企业系统集成或教学演示的基础数据快速验证空间分析与制图流程。1. 全国火车站站点位置 GIS一张点表背后到底难在哪拿到「全国火车站站点位置 GIS」这个题目很多人第一反应是找一份现成的点数据往地图上一丢就完事。真动手就会发现最难的不是画点而是判断一个点该不该存在同一座城市可能有三个站名相近的车站有的车站已经停办客运有的货场在公开数据里也挂着「站」字还有大量站点只有名称没有坐标或者坐标落在街道中心甚至另一个区。更麻烦的是坐标系问题从不同渠道拿到的经纬度可能属于不同基准直接混用会出现几十米到几百米的整体偏移铁路这种线性地物对位置精度又格外敏感。这篇内容讲的就是如何从零构建一份可用的全国火车站站点位置 GIS数据从哪里来、字段怎么设计、坐标怎么统一、如何做质检与去重、最后怎么落到地图服务和空间分析里。适合做交通规划、物流选址、地理信息开发和大屏可视化的从业者也适合手里只有一份脏表格、想把它们变成可用空间数据的人。2. 站点数据的来源判断与字段设计先定骨架再找肉做站点位置 GIS第一件事不是打开地图而是想清楚这张表要承载什么。很多项目翻车就翻在字段一开始没设计好等到要做客流分析、线路串联、缓冲区计算时才发现缺了所属线路、车站等级、客运状态这些关键属性回头补数据成本极高。我一般把站点数据拆成三层标识层站名、别名、行政编码、位置层经纬度、坐标基准、定位精度、业务层线路、等级、客货运状态、启用停用时间。三层分开管后面任何一层出问题都好修补。2.1 常见数据来源与可信度排序全国火车站点的来源大致分几类可信度和用途差别很大。第一类是官方发布的铁路统计资料和公开的年鉴类信息站名、线路、等级比较权威但通常没有坐标。第二类是开放地图数据里的铁路站点要素坐标齐全但站名规范性和状态更新不一定及时有些支线小站早就停用了还留在库里。第三类是地名地址库和行政区划数据用来做空间约束和归属校验非常合适。第四类是各类开放数据集与社区整理的点表方便起步但必须当作「待验证线索」而不是结论。我的做法是以官方站名清单为骨架用开放地图要素补坐标用地名地址库做归属校验社区数据只用来查漏。之所以这样排序是因为站名和线路属性错了会直接影响业务结论而坐标错了至少还能通过多源比对发现反过来如果以坐标数据为骨架站名一乱后面所有关联都会跟着乱。常见做法是把权威清单作为主表其余来源作为「候选坐标池」一个站名可以对应多个候选点最后靠规则打分选出最可信的那个。还有一点容易被忽略站名的别名问题。同一个车站在不同资料里可能带「站」字或不带可能有旧称、俗称、方位词变体。比如方向词前置后置的差异、同名站加行政前缀的情况都很普遍。字段设计时必须留一个别名字段用竖线或数组形式存多个写法否则做名称匹配时会大量漏配。这个字段在后面去重和坐标回填时价值极高。2.2 表结构设计与最小可用字段集下面这张表是我常用的最小字段集够支撑绝大多数站点 GIS 场景。字段类型按 PostgreSQL/PostGIS 的习惯写换成其他数据库也能对应上。字段名类型说明是否必填station_idvarchar(32)业务主键建议用行政区划码序号是namevarchar(64)标准站名是aliasestext别名多个用竖线分隔否adcodevarchar(12)行政区划编码是lonnumeric(10,6)经度是latnumeric(10,6)纬度是sridint坐标基准标识是geom_typevarchar(16)定位类型站房/站场/中心点否accuracy_mint估计定位精度米否line_namestext所属线路多个用竖线分隔否gradevarchar(16)车站等级否statusvarchar(16)运营状态客运/货运/停用否sourcevarchar(64)坐标来源标记否updated_atdate最后核验日期否建表语句可以直接落到 PostGIS 里注意几何字段单独生成不要和经纬度双写冲突。-- 站点主表经纬度与几何字段并存便于不同工具消费 CREATE TABLE rail_station ( station_id varchar(32) PRIMARY KEY, name varchar(64) NOT NULL, aliases text, adcode varchar(12) NOT NULL, lon numeric(10,6) NOT NULL, lat numeric(10,6) NOT NULL, srid int NOT NULL DEFAULT 4326, geom_type varchar(16) DEFAULT center, accuracy_m int, line_names text, grade varchar(16), status varchar(16) DEFAULT unknown, source varchar(64), updated_at date ); -- 生成几何列统一按 WGS84 存储展示时再按需投影 ALTER TABLE rail_station ADD COLUMN geom geometry(Point, 4326); UPDATE rail_station SET geom ST_SetSRID(ST_MakePoint(lon, lat), 4326); CREATE INDEX idx_rail_station_geom ON rail_station USING GIST (geom);这段 SQL 的关键决定是「经纬度列 几何列并存」。经纬度列方便导出成 CSV 给非 GIS 同事用几何列方便做空间查询和索引。srid字段记录原始坐标基准即使入库统一成 4326也保留溯源信息将来发现某批数据偏移时能快速定位是哪个来源的问题。accuracy_m看起来可有可无但在做缓冲区分析和多源融合时它就是权重精度 50 米和精度 500 米的点不该同等对待。geom_type区分站房点和站场中心点两者可能差几百米做步行可达性分析时用站房点做线路走向分析时用站场点混用会得出莫名其妙的结论。2.3 坐标系与投影别让偏移毁掉整张图坐标系是站点 GIS 里最容易埋雷的地方。常见情况是从 A 渠道拿到的经纬度是 WGS84从 B 渠道拿到的是国内加密坐标系两者看起来都是「经纬度」混在一起整体偏几百米而且偏得很有规律肉眼在省市级尺度上几乎看不出来一放大到街区就露馅。血泪经验是凡是来源不明的坐标先做小范围比对再入库别一次性全导进去。判断方法很朴素挑几个你确定位置的站比如自己常去的车站把候选坐标和影像叠一下看偏差方向和量级。如果整体朝一个方向偏移几百米基本就是基准不一致如果偏移随机、量级几十米那更可能是定位点选取不同站房中心 vs 站场中心。两种情况处理方式完全不同前者要做坐标转换后者要做的是统一定位口径。入库统一用 4326 存做距离计算和缓冲区时再投影到适合的平面坐标系。全国范围做面积或距离统计直接用地理坐标算会有误差常见做法是投影到等积或等距投影再做计算或者直接用 PostGIS 的地理类型函数它们内部按椭球计算精度够用但性能差一些。数据量大时我倾向于先投影到局部平面坐标算完再转回来。3. 从脏表到可用点层清洗、去重与坐标回填有了表结构接下来是把一堆来源不同的脏数据塞进去并且保证质量。这一步最耗时间也最能体现功底。核心任务有三件站名标准化、重复站点合并、缺失坐标回填。三件事有先后依赖顺序错了会反复返工。3.1 站名标准化与别名匹配站名标准化的目标是让「同一座车站在任何来源里都能被认出来」。做法分四步去空白和全半角差异、统一「站」字后缀策略、抽取方位词、建立别名索引。方位词东、西、南、北、新、老在站名里既是区分符也是干扰项处理时要保留但不能当作独立匹配依据否则「东站」和「东广场站」会被误配。import re from typing import List SUFFIX re.compile(r(火车站|车站|站)$) DIRECTION re.compile(r^(.*?)([东南西北新老])(?:站)?$) def normalize_name(raw: str) - str: 把站名压成可比较的规范形式仅用于匹配不覆盖原值 if not raw: return s raw.strip().replace(\u3000, ) s s.replace(, ().replace(, )) s re.sub(r\s, , s) # 去所有空白 s SUFFIX.sub(, s) # 去掉通用后缀 # 括号内容保留为别名信息主名先剥离 s re.sub(r\(.*?\), , s) return s def build_keys(name: str, aliases: str ) - List[str]: 生成用于匹配的键集合包含原名、去后缀名、方位词拆解 keys set() for item in [name] (aliases or ).split(|): if not item: continue n normalize_name(item) if not n: continue keys.add(n) m DIRECTION.match(n) if m and m.group(2): keys.add(m.group(1)) # 去掉方位词的主体名 return sorted(keys)这段代码的作用是生成「匹配键」而不是改写原数据。normalize_name去掉后缀和空白是为了让「北京南」和「北京南站」落到同一个键build_keys额外产出主体名是为了让带方位词的站点和主体名建立弱关联但注意这只是弱关联不能直接合并否则「南京」和「南京南」会被当成同一站。实际使用中匹配键用于召回候选最终是否同站要靠坐标距离和行政区划双重确认这个设计能显著降低误合并率。参数上SUFFIX的顺序有讲究长的放前面否则「火车站」会被「车站」先吃掉一部分虽然结果一样但可读性差。3.2 重复站点识别与合并策略重复通常有三种形态完全重复名称坐标都接近、名称重复但坐标差很远同名不同站或一方坐标错、坐标重复但名称不同一个站多个站名。处理策略要分开定不能一刀切。完全重复最好办按匹配键分组组内坐标距离小于阈值就合并保留字段最全的一条其余字段做并集填充。同名不同站要谨慎先看行政区划编码编码不同基本可以判定是两个站编码相同再看距离超过一定阈值就要人工复核很可能是某个来源的坐标错得离谱。坐标重复名称不同通常是主站和货场、编组站的关系业务上未必该合并建议保留但加关联标记。-- 找出疑似重复组同行政区划内匹配键相同且距离过近 WITH candidate AS ( SELECT a.station_id AS id_a, b.station_id AS id_b, a.name AS name_a, b.name AS name_b, ST_Distance(a.geom::geography, b.geom::geography) AS dist_m FROM rail_station a JOIN rail_station b ON a.adcode b.adcode AND a.station_id b.station_id ) SELECT * FROM candidate WHERE dist_m 300 ORDER BY dist_m;这段查询用地理类型计算真实距离a.station_id b.station_id是为了避免同一对出现两次。阈值 300 米是我常用的起点因为站房和站场中心点的合理差距一般在这个量级内如果业务上要求更严可以降到 150 米但会漏掉一些站场较大的车站。查出来的结果不要直接删先落一张station_merge_log表记录合并依据和操作人将来出问题能回滚。后悔药这种东西做数据清洗时一定要提前备好。还有一种隐性重复是「站名带行政前缀」造成的比如同一车站在跨省资料里被加上不同前缀。这种靠匹配键召回后用坐标距离判定最稳不要迷信字符串相似度编辑距离在中文站名上误判率不低。3.3 缺失坐标的回填与人工核验闭环没有坐标的站点怎么补是这类项目里最费人力的环节。全自动方案不存在务实做法是「自动候选 人工抽检」。候选生成可以按优先级排先用别名在其他来源里找同名点再用行政区划中心点做兜底最后把实在找不到的单独列出来人工核。import math def pick_candidate(cands, adcode_center): 从候选坐标里选最可信的一个返回坐标与置信度 if not cands: # 兜底行政区划中心点置信度最低 return (*adcode_center, 0.2, fallback_adcode) scored [] for c in cands: score 0.0 score 0.5 if c[name_exact] else 0.0 score 0.3 if c[adcode_match] else 0.0 # 精度字段越小越好归一到 0~0.2 if c.get(accuracy_m): score max(0.0, 0.2 - c[accuracy_m] / 2500) scored.append((score, c)) scored.sort(keylambda x: x[0], reverseTrue) best scored[0][1] return best[lon], best[lat], round(scored[0][0], 2), best[source] def haversine(lon1, lat1, lon2, lat2): 粗略距离单位米仅用于候选合理性判断 r 6371000 p1, p2 math.radians(lat1), math.radians(lat2) dp p2 - p1 dl math.radians(lon2 - lon1) a math.sin(dp/2)**2 math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2 return 2 * r * math.asin(math.sqrt(a))pick_candidate的打分逻辑里站名完全匹配权重最高行政区划匹配次之精度作为微调项。这样设计是因为站名和归属是硬约束精度只是软信息不能让一个「精度很高但站名只是相似」的候选压过「站名完全一致但精度一般」的候选。兜底方案给 0.2 的置信度是为了让这类点在下游使用时能被过滤掉——做全国尺度分布图可以用做站点周边 500 米分析就不该用。haversine在这里不是用来算精确距离而是做合理性检查比如候选点距离行政区划中心超过 100 公里基本可以判定配错了。人工核验环节建议做成闭环可疑点导出成带影像底图的表格核验人逐条确认后回写source和accuracy_m同时更新updated_at。这套流程听起来笨但比反复返工强。我见过太多项目因为省了这一步最后大屏上一个站标到了河里被业务方一句话问穿。4. 空间分析怎么用站点 GIS 的三个典型落地场景数据建好只是起点真正体现价值的是拿它做分析。站点位置 GIS 的常见用途集中在覆盖分析、网络构建和可视化三类每类对数据精度和字段完整度的要求不同理解这个差异能帮你判断手里的数据够不够用。4.1 服务覆盖与缓冲区分析缓冲区分析是最直接的用法以站点为圆心看某个半径内覆盖了多少人口、多少路网、多少竞争设施。这里有个常被忽略的点——用站房点还是站场点。站房点反映旅客实际到达位置做步行可达性必须用它站场点反映铁路线位做线路缓冲用它是合理的。两者混用会让分析结果在城区站点上偏差很大因为大站的站房和站场可能隔着几条街。-- 统计每个站点 5 公里范围内的人口栅格加权值示意 SELECT s.station_id, s.name, SUM(p.pop_value) AS pop_5km FROM rail_station s JOIN population_grid p ON ST_DWithin(s.geom::geography, p.geom::geography, 5000) WHERE s.status 客运 GROUP BY s.station_id, s.name ORDER BY pop_5km DESC;用ST_DWithin配合 geography 类型距离参数直接写米省去投影转换的麻烦。WHERE s.status 客运这个过滤非常关键货运站和停用站纳入覆盖分析会虚高指标。半径 5000 米不是固定值高铁站枢纽可以放到 10 公里看辐射城际小站 3 公里更贴合实际步行和接驳范围。如果数据量到百万级这个查询会慢常见优化是预先物化一张站点缓冲表或者把人口栅格转成点后建空间索引。提示缓冲区半径的选择要和交通方式绑定步行、骑行、驾车三种模式的合理半径差好几倍直接用同一个值做对比分析会得出误导性结论。4.2 基于站点的铁路网络构建把站点连成网络是进阶用法难点在于「哪些站之间有直连」。站点表里如果只有line_names只能知道某站属于哪条线无法直接得到邻接关系。解决办法是引入线路的线状几何按线做点线关系判断再按里程排序生成邻接边。from shapely.geometry import LineString, Point def build_edges(stations_on_line, line_geom): 按线路几何给站点排序生成相邻站点边 # 把每个站点投影到线位上取里程 ordered [] for st in stations_on_line: pt Point(st[lon], st[lat]) m line_geom.project(pt) # 沿线里程 ordered.append((m, st[station_id])) ordered.sort(keylambda x: x[0]) edges [] for i in range(len(ordered) - 1): a, b ordered[i][1], ordered[i1][1] seg line_geom.interpolate(ordered[i1][0]) - line_geom.interpolate(ordered[i][0]) # 距离用两站实际坐标算更贴近业务口径 d Point(stations_on_line[i][lon], stations_on_line[i][lat]).distance( Point(stations_on_line[i1][lon], stations_on_line[i1][lat])) edges.append({from: a, to: b, line: st_line_name}) return edges st_line_name 示例线这段逻辑的关键是project它把站点映射到线位上的里程位置按里程排序就得到沿线顺序。为什么不用直线距离排序因为铁路有弯道和绕行直线排序在环线或大弯道上会出错。距离字段我建议用站点实际坐标算而不是线位里程差前者更贴近旅客感知的站间距离。这段代码只处理单条线实际项目中同一条线可能被拆成多段几何需要先做线位合并再投影否则站点会落到错误的段上。参数上注意坐标系要和线位一致一个 4326 一个投影坐标混着算结果会离谱。4.3 可视化落点从点表到可交互地图可视化不是把点丢上去就完了站点密度在全国尺度上极不均匀东部密集到糊成一片西部稀疏到看不见。常见处理是按缩放级别做聚合低层级显示聚合计数高层级显示单站。颜色编码建议按grade或status分不要按经纬度随便上色那样除了好看没有任何信息量。如果要做线路走向图站点和线位要分层渲染线在下点在上有遮挡关系才自然。站点标注在低缩放级别应该关闭否则几万个标签会把地图压垮。性能上全国站点量级不算大但加上线位和底图后前端仍要注意按视口加载。这些经验都是被大屏卡顿教育出来的早期版本一次性全量渲染滚动地图像放幻灯片。5. 站点位置数据的避坑与排查五条踩出来的经验这一章单独讲踩坑因为前面几章的方法论再完整真到落地时出问题的往往是这些细节。每条按现象、原因、解决三段写都是我实际遇到过的。第一条地图上站点整体偏移几百米。现象是全国点位在同一方向上系统性偏移放大到街区级别尤其明显。原因是坐标基准混用部分来源是加密坐标系但被当成普通经纬度入库。解决办法是先做小样本比对定位问题来源把可疑批次单独隔离统一转换后再合并同时把source和srid字段用起来以后每批数据入库前先做基准校验。这项校验写成脚本自动跑比事后人工找强得多。第二条同名站点被错误合并。现象是分析时发现某个城市只剩一个站但实际有两个。原因是清洗阶段只按名称去重没考虑行政归属和距离。解决是把合并逻辑改成三级判定名称匹配召回、行政区划确认、距离阈值把关三级都过才合并同时保留合并日志发现误合并可以快速回滚。第三条停用站点混进客运分析。现象是覆盖人口指标虚高某些偏远区域出现不合理的覆盖热点。原因是status字段大面积缺失或默认填了客运。解决是给status设默认值unknown分析时显式过滤并建立停用站清单定期核对。把「未知」当「客运」用是最危险的默认行为。第四条坐标落在站场中心而非站房。现象是做步行可达性分析时站点到周边设施的路径明显不合理有时要绕行很远。原因是坐标来源是线位投影点不是旅客出入口。解决是靠geom_type字段区分做旅客相关分析时只取站房点缺失站房点的站点标记出来单独补。第五条空间查询慢到不可用。现象是缓冲区统计跑一次要几分钟。原因是没有空间索引或者用了会破坏索引的函数写法。解决是给几何列建 GIST 索引查询条件尽量用ST_DWithin而不是ST_Distance x后者通常不走索引。这项优化往往能把查询从分钟级压到秒级。注意以上五条里前三条属于数据质量问题后两条属于使用方式问题。排错时先判断属于哪类数据问题回源头修使用问题改查询或改字段口径别混在一起查。6. 精度评估与增量维护让这张点表活得久站点数据不是一次性的新站开通、老站停用、站名调整都会发生一份没有维护机制的点表两年后基本就不可信了。这一章讲两件具体的事怎么评估手里的数据到底准不准以及怎么用低成本方式持续更新。先说精度评估。很多人拿到数据直接就用从不做精度量化结果出了问题说不清是数据错还是方法错。我一般会抽一批站点做人工核验规模不用大按区域和车站等级分层抽一两百个就够用影像底图逐点确认位置偏差记录偏差距离。然后算两个指标中位偏差和 90 分位偏差。中位数反映整体水平90 分位反映最差那批点的情况。如果 90 分位超过 500 米说明有一批点的定位口径明显不对需要回头查来源如果中位数在 100 米以内做市级尺度的分析基本够用做街区级分析还差点意思。评估指标计算方式参考阈值超标时的动作中位偏差抽样点偏差距离的中位数≤100 米可支撑市级分析90 分位偏差抽样点偏差的 90 分位≤500 米排查定位口径坐标缺失率无坐标站点数 / 总数≤5%补录或标记兜底状态未知率status 为 unknown 的占比≤10%核对运营状态重复率疑似重复组数 / 总数≤1%复核合并逻辑再说增量维护。务实做法是建一张变更表任何新增、修改、停用都记一条带变更类型、变更依据和日期。更新频率不用追求实时季度级对绝大多数业务场景足够。更新来源优先用官方站名清单的增量坐标变更则靠定期抽样复核发现。不要试图每次全量重建全量重建成本高且容易引入新错误增量更新加定期全量校验的组合更稳。-- 站点变更日志表支撑增量维护与回溯 CREATE TABLE rail_station_change ( change_id bigserial PRIMARY KEY, station_id varchar(32) NOT NULL, change_type varchar(16) NOT NULL, -- insert/update/deactivate field_name varchar(32), old_value text, new_value text, reason text, changed_at date NOT NULL DEFAULT current_date ); -- 每次更新站点主表后把变更写进日志便于回溯 INSERT INTO rail_station_change (station_id, change_type, field_name, old_value, new_value, reason) VALUES (示例站点ID, update, status, 客运, 停用, 年度核验);这张日志表的价值在于「可解释」。业务方问某个站为什么从图上消失了你能翻出变更记录说清楚是哪天、依据什么改的。没有这张表数据一多就变成黑匣子谁也不敢动。最后分享一个我自己的习惯每次更新完数据先在地图上随机点开十个站看一圈再跑一遍上一章的五个排查项。这个动作花不了十分钟但能挡住大部分低级错误。数据这行谨慎比聪明重要。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑