简介全国旅游景区数据集收录了约12000条景区记录时间节点为2022年6月覆盖1A至5A等级景点字段包含景点名称、所在城市、详细地址、景区等级、经度和纬度可满足旅游数据分析、地图可视化、行程规划、景点检索等应用需求适合数据从业者、产品经理及旅游爱好者使用。压缩包内共2个文件一份xlsx表格便于在Excel中筛选、排序和手工核对一份json文件适合程序批量读取和二次开发整体大小仅1.26MB下载后即可打开使用。目前已有4710人学习下载数据字段结构清晰无需额外清洗即可直接用于景点等级分布统计、城市旅游热度对比或经纬度打点展示。json格式保留了完整的数据层级xlsx则方便快速浏览记录两类文件互为补充能帮助读者高效搭建景点数据库、制作可视化大屏或开发旅游类小程序原型是一份轻量且实用的基础数据集。1. 全国旅游景区数据集能做什么先判断这份数据值不值得复用全国旅游景区数据集含json、excel格式文件这种资源通常是把全国景区的基础信息名称、等级、所在省市区、经纬度、简介、开放时间等整理成同一份数据的两种载体2022-06-03 一般是数据抓取或整理的时间快照。拿到手之后最常见的三个用途按省份或等级做统计报表、在底图上画景区分布点、给旅游推荐类项目做冷启动基础数据。适合的人群也很明确——做GIS展示的数据分析师、做旅游产品原型的学生、想快速验证一个想法但没有爬虫条件的一线工程师。我是建议你动手前先花十分钟想清楚两件事。第一这份数据的经纬度是什么坐标系直接拿去做地图展示会不会偏移第二json和excel两个文件字段是否完全对齐哪个才是你后续流程里的主数据源。这两件事定下来后面处理就能少返工。下面章节按结构拆解、格式转换、清洗修正、踩坑排查、快速出图这条线往下走。2. 拆开 JSON 与 Excel字段分层与读取姿势2.1 一个数据集的两种格式JSON 数组与 Excel 工作表这类数据集最常见的组织方式是一个 JSON 文件里装着景区对象的数组数组的每一项就是一个景区Excel 文件里则是把同样内容摊成一张宽表一行一个景区列是字段。JSON 的优势是能表达嵌套信息比如一个景区的相册是字符串数组、开放时间是一个带工作日和节假日的字典这些在 Excel 里只能要么拆列要么拼成长文本。我拿到 JSON 文件后的第一件事是把它当作一个 json 数组而不是单个对象来读。很多人直接用 pandas 的read_json结果发现数组变成了行索引不对的 DataFrame其实更可控的做法是用json.load读出原始结构再交给json_normalize去展平。这一步是为了避免把嵌套字段当成只有一行的大字符串后面统计和筛选取值都会很痛苦。2.2 用 pandas 读取两种格式最小可跑代码下面这段代码是我处理这类文件的标准开头两个格式都读先看结构和列名再做决定。import json import pandas as pd # 读取 JSON先load成Python对象确认它是list还是dict with open(scenic_spot.json, encodingutf-8) as f: data json.load(f) print(JSON 顶层类型:, type(data)) print(景区记录数:, len(data) if isinstance(data, list) else 非数组) # 如果是list of dict用json_normalize展开成DataFrame df_json pd.json_normalize(data) print(JSON 展平后的列:, df_json.columns.tolist()) # 读取 Excel用dtypestr避免数字ID被读成科学计数法 df_excel pd.read_excel(scenic_spot.xlsx, dtypestr) print(Excel 形状:, df_excel.shape) print(Excel 列:, df_excel.columns.tolist())这段代码里json_normalize是关键它会自动把嵌套的字典按层级展开例如location.lng变成一列、location.lat变成一列如果嵌套的是数组它不会展开这一点后面会专门说。Excel 读取时用dtypestr是一个我保留多年的习惯因为景区 ID、电话这类字段一旦是纯数字Excel 源文件里可能已经是文本但 pandas 默认会尝试转成 int64转完再写回 Excel 就变成科学计数法显示很难看也容易丢精度。如果json_normalize展开后列名带点号例如location.lng我习惯立刻统一改名后续写 SQL 或改列表头都会省事。现场判断数据是否值得继续投入就看两件事记录的经纬度有没有明显缺失以及省份字段里是不是混入了空白和异体写法。2.3 两个文件不一致时的对齐策略标题是含 json、excel 格式文件实际拿到的两个文件往往不是同一天导出的或者字段名有差异。常见做法是用景区名称加省份作为组合主键做对齐。先给代码再解释。# 假设JSON里的字段是英文Excel里是中文先统一列名 df_json df_json.rename(columns{ name: 景区名称, province: 省份, level: 等级 }) # 去除两边的首尾空格避免“北京”和“北京 ”对不上 df_json[景区名称] df_json[景区名称].str.strip() df_excel[景区名称] df_excel[景区名称].str.strip() # 用merge的indicator检查哪些行只出现在一边 merged df_json.merge( df_excel, on[景区名称, 省份], howouter, indicatorTrue ) # 看一看哪些记录只在JSON里有哪些只在Excel里有 print(merged[_merge].value_counts()) print(merged[merged[_merge] left_only].head())这里把how设为outer再通过indicatorTrue自动生成_merge列就能把两边匹配不上的记录显式暴露出来。出现left_only或right_only不一定是数据错了可能是部分景区在某个文件里被剔除了但你得知道这个差异否则后面做统计时两边的总数对不上会花很长时间找原因。3. 格式互转不掉坑JSON 转 Excel 与反向转换的脚本3.1 为什么需要互转以及工具选型json格式文件适合程序消费excel格式文件适合给业务同事核对所以这个数据集最常见的加工动作就是格式互转。有人图省事会直接改扩展名或者用在线工具上传转换我都不太建议原因有两个在线工具面对嵌套 JSON 时经常把数组自动抹掉转换过程丢失字符集的情况很常见。自己动手写脚本大概二十行内能解决而且可以反复用。我的工具选型非常固定Python 3 加 pandas。读 Excel 用openpyxl引擎做正向写回读旧版.xls则用xlrd但 2022 年以后的景区数据几乎都是.xlsx所以我默认你在处理新格式。下面两个小节分别给出两个方向的脚本。3.2 JSON 转 Excel先把嵌套字段做拍平再落盘import json import pandas as pd with open(scenic_spot.json, encodingutf-8) as f: data json.load(f) rows [] for item in data: flat { 景区名称: item.get(name), 等级: item.get(level), 省份: item.get(province), 城市: item.get(city), 经度: item.get(lng), 纬度: item.get(lat), 简介: item.get(description), } # 开放时间如果是字典拆成多个列避免单元格里塞JSON open_hours item.get(open_hours) if isinstance(open_hours, dict): for k, v in open_hours.items(): flat[f开放时间_{k}] v # 图片这类数组字段用分号拼接成单行文本 images item.get(images) if isinstance(images, list): flat[图片列表] ;.join(images) rows.append(flat) df pd.DataFrame(rows) df.to_excel(scenic_spot_converted.xlsx, indexFalse, engineopenpyxl) print(导出完成共, df.shape[0], 行, df.shape[1], 列)这段脚本的核心思路是在循环里手动构造扁平字典。有人会问为什么不直接用json_normalize理由是嵌套的数组字段比如图片列表在json_normalize下不会被展开成多行而是变成整列的列表对象写进 Excel 后就变成了[a.jpg, b.jpg]这种很难看的文本。手动处理的好处是你能明确决定数组字段的去向要么拉平要么用分隔符合并。第一版用不到图片的话把图片列表那一段注释掉即可。to_excel的三个参数值得说明indexFalse不写行号避免导出后 Excel 里多一列莫名其妙的数据engineopenpyxl明确指定写入引擎避免老 pandas 版本默认走xlsxwriter时报依赖错误如果单元格里有空值pandas 会写成空单元格不会写 nan 字符串只有当你预先填充过nan才会出事。3.3 Excel 转 JSON注意中文字符与空值的类型反向转换在很多场景是刚需比如业务同事在 Excel 里维护了一批景区联系方式需要导回给后端程序用。最稳妥的方式是读出来再序列化并且把空值处理成null而不是空字符串这样下游接口能正确判断没填和填了空内容的区别。import json import pandas as pd df pd.read_excel(scenic_spot.xlsx, dtypestr) # 把空字符串统一为None序列化后变成JSON里的null df df.where(df.notna(), None) records df.to_dict(orientrecords) with open(scenic_spot_from_excel.json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2) print(已导出 JSON记录数:, len(records))这里两个参数是每次都要检查的ensure_asciiFalse让中文字符直接以 UTF-8 写入文件不加的话你会看到一个\u666f\u533a这样的转义海洋通用性和可读性都很差indent2是让 JSON 文件格式化方便用文本编辑器直接比对字段。df.where(df.notna(), None)是很多人不知道的写法它把 DataFrame 里的缺失值统一替换为 None而不是读 Excel 时自动读到的NaN。Python 的NaN序列化进 JSON 会变成NaN这个非法值虽然json.dump默认能写出来但很多严格校验的解析器会直接报json parse error。如果你的数据量大到 Excel 都装不下另一个常见做法是把 Excel 整理成规范的 CSV用pandas.read_csv读进来再批量写库效果一致。遇到上百万行的极端情况Excel 本身就不是合适载体了我一般会建议把表格导入数据库再做查询相关内容可以放后面小节里展开。3.4 批量处理和入库的延展这类数据集通常几千条起步Excel 打开还流畅但如果你要做多版本合并比如把 2022-06-03 这份和另一份更新数据合并就要把流程脚本化而不是靠手动另存为。我常用的套路是统一读成 DataFrame、按主键去重、再决定是写回 Excel 还是写进 SQLite。import sqlite3 df pd.read_excel(scenic_spot.xlsx, dtypestr) df df.drop_duplicates(subset[景区名称, 省份]) conn sqlite3.connect(scenic.db) df.to_sql(scenic_spot, conn, if_existsreplace, indexFalse) conn.close() print(导入完成SQLite 表 scenic_spot 已生成)to_sql是 pandas 里非常实用的接口它的原理是按块拼接INSERT语句几千行数据几秒钟就能导入。if_existsreplace会直接覆盖旧表适合每次全量更新的场景如果你要做增量可以改为if_existsappend。这里仍然保留dtypestr是为了让所有字段在数据库里都以文本存储避免电话号码或景区 ID 被 SQLite 自动推断成整数而丢前面的零。这一节的目的是告诉你用 json 和 excel 共同组织的景区数据最终流向往往是数据库或者地图所以中间的字段对齐和类型控制要尽早做。4. 清洗坐标和省份字段让景区数据能直接画图4.1 省份名称归一化别让广西和广西壮族自治区各算一栏景区数据里省份字段的脏数据属于不算坏但很烦的问题。有人写北京有人写北京市同样的道理广西壮族自治区、内蒙古自治区有时被简写成广西、内蒙古。直接groupby(省份)做统计结果里同一地区会被拆成两行地图上也会出现两个重叠的图斑。我的做法是先构造一份小规模别名映射再用map统一。完整覆盖全国所有别名不现实但把最常见的几种简写和全称、以及末尾带空格的情况处理掉统计结果就基本干净了。province_alias { 北京: 北京市, 北京市: 北京市, 上海: 上海市, 上海市: 上海市, 广西: 广西壮族自治区, 广西壮族自治区: 广西壮族自治区, 内蒙古: 内蒙古自治区, 内蒙古自治区: 内蒙古自治区, 新疆: 新疆维吾尔自治区, 新疆维吾尔自治区: 新疆维吾尔自治区, } df[省份_归一] df[省份].astype(str).str.strip().map( lambda x: province_alias.get(x, x) ) # 验证是否还有未归一化的零散值 print(df.groupby(省份_归一).size().sort_values(ascendingFalse).head(20))如果province_alias里没覆盖的写法还很多不要急着无限加映射先打印出唯一值列表人工过一遍比猜更快。这种归一化脚本是典型的主动防御花二十分钟写完后面每次统计都能用。我在多个项目里发现真正容易出问题的不是省份本身而是名称里夹了全角空格或换行符所以上面的代码里加了str.strip()这一行能解决不少看似诡异的分组不符问题。等级字段也一样。如果有人把5A级景区填成5A、5a、5A级统一处理成5A是一个必要步骤否则筛选时漏掉一部分记录最后画出来的地图明显缺一块。这类问题不属于算法难题但属于不做就会在结果里翻车的部分。4.2 经纬度的坐标系陷阱GCJ-02 与 WGS-84 怎么分辨这是整个数据集里最容易踩的坑也是很多人画完图发现点全部偏移到几百米外还找不到原因的地方。中国大陆公开地图服务高德、腾讯用的坐标是 GCJ-02俗称火星坐标而野外 GPS 设备、Google Earth 的原始数据多是 WGS-84。景区数据如果是通过网页爬虫抓的坐标大概率已经是 GCJ-02如果最初来自 GPS 轨迹或测绘成果则可能是 WGS-84。判断方法很直接任取一个你已经知道精确位置的景区对照坐标放在高德地图和 Google 地图上各看一次。如果在某一家地图上是准的、另一家偏了几百米那就有坐标系嫌疑。更简单的做法是看字段名——lng、lat这类短字段通常来自接口爬取极大概率是 GCJ-02而longitude、latitude这种完整拼写则可能是 WGS-84。这只是一种经验不是铁律但能帮你快速建立直觉。如果确认是 GCJ-02 而要画到 WGS-84 底图或导出到其他 GIS 工具就需要做转换。下面的代码用迭代逼近的方式反算 WGS-84 坐标精度在几米内对展示级应用完全够用。import math def _out_of_china(lng, lat): return not (72.004 lng 137.8347 and 0.8293 lat 55.8271) def _transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y ret 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * math.pi) 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * math.pi) 320.0 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(x, y): ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * math.pi) 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * math.pi) 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 return ret def wgs84_to_gcj02(lng, lat): a 6378245.0 ee 0.00669342162296594323 d_lat _transform_lat(lng - 105.0, lat - 35.0) d_lng _transform_lng(lng - 105.0, lat - 35.0) rad_lat lat / 180.0 * math.pi magic math.sin(rad_lat) magic 1 - ee * magic * magic sqrt_magic math.sqrt(magic) d_lat (d_lat * 180.0) / ((a * (1 - ee)) / (magic * sqrt_magic) * math.pi) d_lng (d_lng * 180.0) / (a / sqrt_magic * math.cos(rad_lat) * math.pi) return lng d_lng, lat d_lat def gcj02_to_wgs84(lng, lat): if _out_of_china(lng, lat): return lng, lat wgs_lng, wgs_lat lng, lat for _ in range(2): gcj_lng, gcj_lat wgs84_to_gcj02(wgs_lng, wgs_lat) wgs_lng - gcj_lng - lng wgs_lat - gcj_lat - lat return wgs_lng, wgs_lat # 示例把高德系的GCJ-02坐标转成WGS-84 print(gcj02_to_wgs84(116.397428, 39.90923))这段公式是公开的坐标转换经验值其中wgs84_to_gcj02是正向转换gcj02_to_wgs84用两次迭代做反向逼近。迭代两轮是因为正向公式已经比较平滑两轮后残差降到米级如果你要更高的亚米级精度需要调用更严格的测绘转换服务而不是自己维护这段公式。对景区分布图来说这个精度足够因为点的展示半径通常就几像素。4.3 缺失与越界的经纬度过滤景区数据里偶尔会出现经纬度为 0、为 999 或者明显在大陆范围之外的点。这些值不能直接删除因为你可能还想保留这条景区记录做统计只是不在地图上展示。所以我习惯的做法是在 DataFrame 上新增一个是否可绘图的布尔列而不是直接dropna。df[经度_数值] pd.to_numeric(df[经度], errorscoerce) df[纬度_数值] pd.to_numeric(df[纬度], errorscoerce) valid_lng df[经度_数值].between(72, 137) valid_lat df[纬度_数值].between(3, 54) df[可绘图] valid_lng valid_lat # 用可绘图列做地图用完整表做统计 map_df df[df[可绘图]].copy() print(f全部记录 {len(df)}可绘图 {len(map_df)})pd.to_numeric配合errorscoerce会把非数字内容变成NaN避免拿到 106.5E 这种带字母的字符串时直接报错。between函数的范围我按中国粗略边界写的能滤掉为 0 的点和明显错位的点但不排除部分东经 72 度以西的边境区域会被误杀所以这个范围可以按实际数据微调。我见过最离谱的脏数据是把省份填到经度列里这类问题上面的代码能直接拦下来。5. 踩坑排查清单乱码、偏移和字段丢失的根因以下五条是我在反复处理类似数据集时积累下的高频问题每条按现象、原因、解决展开。遇到同样问题可以直接照着定位。5.1 Excel 打开 JSON 文件乱码数字列还变成科学计数法现象用 Excel 双击打开 json 文件中文全是乱码ID 变成1.23E18这种样子。原因JSON 文件一般以 UTF-8 无 BOM 格式保存Excel 在 Windows 下默认按 ANSIGBK猜测编码于是中文解读失败。ID 过长则是 Excel 对超过 11 位的数字自动转科学计数法显示原始数据其实没坏。解决不要用 Excel 直接打开 JSON改用 VS Code、Notepad 等文本编辑器或走一段 pandas 脚本转成 Excel。如果确实需要发给同事在 Excel 里看可以在写入 Excel 时把 ID 列设为文本格式pandas 里可以用xlsxwriter的set_column实现。最简单的土办法是导入 CSV 时选择 UTF-8 编码但最可靠的是用上一章的转换脚本生成.xlsx。5.2 地图上所有景区点整体偏移几百米现象用 folium 或 QGIS 绘图后景区点整齐地偏向实际位置的西北或东南方向所有点偏移方向和距离一致。原因数据坐标是 GCJ-02而你画的底图是 WGS-84 或 Web 墨卡托。这种偏移不是随机噪点而是坐标系差异带来的系统性位移在经纬度上可能只差 0.00x 度但在城市尺度上已经明显可见。解决先用 4.2 的gcj02_to_wgs84做整体转换再绘图。转换前后的数据各保留一列方便随时回滚核对不要原地覆盖原始经纬度。我吃过一次亏直接在原列上做转换后来想核对原始值只能重新下载数据后悔药是没有的。5.3 导出 CSV 后列错位景区地址里的逗号惹的祸现象CSV 在 Excel 里打开地址字段被拆成两列后面的列全部向右错位。原因CSV 用逗号做分隔符但不少景区地址里本身就含逗号比如XX路,XX号这种写法。解决导出时用df.to_csv(scenic.csv, sep;, indexFalse)改成分号分隔或者在写入前把地址里的逗号替换成全角逗号。我一般优先考虑导出 Excel 而不是 CSV因为 Excel 格式内部有严格的列分隔规则不会出现这类错列问题。如果下游系统必须吃 CSV就用sep;并在导入端同步说明。5.4 按省份统计总数对不上 Excel 透视表的结果现象pandasgroupby统计算出来某个省份有 120 个景区Excel 透视表看同一个省份却是 118 个而且两边反复核对不一致。原因省份列里有不可见字符最常见的是全角空格、制表符和换行。pandas 读取后肉眼看不出来但分组时把它当成不同的字符串透视表则可能由于 Excel 的严格程度不同或用户手动排序时顺带修改了单元格导致结果不同。解决在全流程早期跑一次df[省份] df[省份].str.strip().replace({\t: , \n: , \u3000: })把各类空白和全角空格清掉。清理后再做一次唯一值列表人工扫一遍看有没有北京和北京 并存的情况。这个坑属于典型的不报错但结果偏的隐患不对比透视表很难发现。5.5 2022-06-03 被当成年份或日期字段混进统计现象数据里明明只有一个日期 2022-06-03数据快照时间但在读入后它出现在 DataFrame 的数值列里或者被 pandas 自行解析成Timestamp后续过滤时报类型错误。原因pandas 在读取时会对形如日期的字符串做类型推断自动转成datetime64这本来是好意但如果这个字段只是数据集的生成标记并不代表每条记录的业务时间就会被误用。解决读取时统一加dtypestr把所有列先当文本处理之后按需手动转换真正需要的列。如果你已经读进来了可以用df[日期] df[日期].astype(str)还原字符串再决定是否要丢弃这一列。景区数据里的时间是资料快照时间不是客流或开放时间的字段保留但不要参与统计。6. 半小时出图用 pandas 和 folium 做 5A 景区分布地图走到这一步前面的清洗工作可以合并成一段连贯的脚本直接产出可交付的地图。我选择 folium 而不选更重的 GIS 工具因为它不需要安装额外软件生成的 HTML 文件在任何浏览器打开对非技术同事也友好。import pandas as pd import folium # 复用 4.2 的 gcj02_to_wgs84 函数这里不再重复粘贴 df pd.read_excel(scenic_spot.xlsx, dtypestr) df[经度] pd.to_numeric(df[经度], errorscoerce) df[纬度] pd.to_numeric(df[纬度], errorscoerce) df[等级] df[等级].astype(str).str.strip() df_5a df[df[等级].str.contains(5A, naFalse)].dropna(subset[经度, 纬度]) m folium.Map(location[35.86, 104.19], zoom_start4) for _, row in df_5a.iterrows(): lng, lat gcj02_to_wgs84(row[经度], row[纬度]) folium.CircleMarker( location[lat, lng], radius3, popuprow[景区名称], color#c0392b, fillTrue, fill_opacity0.7, ).add_to(m) m.save(scenic_5a_map.html) print(已生成 scenic_5a_map.html共标记, len(df_5a), 个5A景区)CircleMarker的radius我默认设成 3数据量大时不用调大因为聚合后密集区域的点会自然堆叠成热力效果如果只要看概览可以把zoom_start提到 5 减少视野内的点密度。这里的关键点是gcj02_to_wgs84必须在画图前调用如果你是直接从接口拿的数据这一行能省下大量为什么点偏了的排查时间。画完图建议顺手做一张各省 5A 景区数量的统计表这一步能验证前面的清洗是否到位——如果省份归一化有遗漏表格里会立刻出现两个同名不同写的省份行。我自己的习惯是先把统计表发给业务同事看数字是否合理再决定要不要正式交付地图因为地图上视觉上少几个点很难被发现而表格里省一级的数字差异一眼就能看出来。希望这份处理路径和踩坑清单帮到你后面再做同类地理位置数据集时不用再从零趟一遍。本文还有配套的精品资源点击获取