简介:全国旅游景区数据集收录了约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", encoding="utf-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:用dtype=str避免数字ID被读成科学计数法 df_excel = pd.read_excel("scenic_spot.xlsx", dtype=str) print("Excel 形状:", df_excel.shape) print("Excel 列:", df_excel.columns.tolist())这段代码里json_normalize是关键,它会自动把嵌套的字典按层级展开,例如location.lng变成一列、location.lat变成一列;如果嵌套的是数组,它不会展开,这一点后面会专门说。Excel 读取时用dtype=str是一个我保留多年的习惯,因为景区 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=["景区名称", "省份"], how="outer", indicator=True ) # 看一看哪些记录只在JSON里有,哪些只在Excel里有 print(merged["_merge"].value_counts()) print(merged[merged["_merge"] == "left_only"].head())这里把how设为outer,再通过indicator=True自动生成_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", encoding="utf-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", index=False, engine="openpyxl") print("导出完成,共", df.shape[0], "行,", df.shape[1], "列")这段脚本的核心思路是在循环里手动构造扁平字典。有人会问为什么不直接用json_normalize,理由是嵌套的数组字段(比如图片列表)在json_normalize下不会被展开成多行,而是变成整列的列表对象,写进 Excel 后就变成了['a.jpg', 'b.jpg']这种很难看的文本。手动处理的好处是你能明确决定数组字段的去向:要么拉平,要么用分隔符合并。第一版用不到图片的话,把图片列表那一段注释掉即可。
to_excel的三个参数值得说明:index=False不写行号,避免导出后 Excel 里多一列莫名其妙的数据;engine="openpyxl"明确指定写入引擎,避免老 pandas 版本默认走xlsxwriter时报依赖错误;如果单元格里有空值,pandas 会写成空单元格,不会写 "nan" 字符串,只有当你预先填充过"nan"才会出事。
3.3 Excel 转 JSON:注意中文字符与空值的类型
反向转换在很多场景是刚需,比如业务同事在 Excel 里维护了一批景区联系方式,需要导回给后端程序用。最稳妥的方式是读出来再序列化,并且把空值处理成null而不是空字符串,这样下游接口能正确判断"没填"和"填了空内容"的区别。
import json import pandas as pd df = pd.read_excel("scenic_spot.xlsx", dtype=str) # 把空字符串统一为None,序列化后变成JSON里的null df = df.where(df.notna(), None) records = df.to_dict(orient="records") with open("scenic_spot_from_excel.json", "w", encoding="utf-8") as f: json.dump(records, f, ensure_ascii=False, indent=2) print("已导出 JSON,记录数:", len(records))这里两个参数是每次都要检查的:ensure_ascii=False让中文字符直接以 UTF-8 写入文件,不加的话你会看到一个\u666f\u533a这样的转义海洋,通用性和可读性都很差;indent=2是让 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", dtype=str) df = df.drop_duplicates(subset=["景区名称", "省份"]) conn = sqlite3.connect("scenic.db") df.to_sql("scenic_spot", conn, if_exists="replace", index=False) conn.close() print("导入完成,SQLite 表 scenic_spot 已生成")to_sql是 pandas 里非常实用的接口,它的原理是按块拼接INSERT语句,几千行数据几秒钟就能导入。if_exists="replace"会直接覆盖旧表,适合每次全量更新的场景;如果你要做增量,可以改为if_exists="append"。这里仍然保留dtype=str,是为了让所有字段在数据库里都以文本存储,避免电话号码或景区 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(ascending=False).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["经度"], errors="coerce") df["纬度_数值"] = pd.to_numeric(df["纬度"], errors="coerce") 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配合errors="coerce"会把非数字内容变成NaN,避免拿到 "106.5E" 这种带字母的字符串时直接报错。between函数的范围我按中国粗略边界写的,能滤掉为 0 的点和明显错位的点,但不排除部分东经 72 度以西的边境区域会被误杀,所以这个范围可以按实际数据微调。我见过最离谱的脏数据是把省份填到经度列里,这类问题上面的代码能直接拦下来。
5. 踩坑排查清单:乱码、偏移和字段丢失的根因
以下五条是我在反复处理类似数据集时积累下的高频问题,每条按"现象、原因、解决"展开。遇到同样问题可以直接照着定位。
5.1 Excel 打开 JSON 文件乱码,数字列还变成科学计数法
现象:用 Excel 双击打开 json 文件,中文全是乱码,ID 变成1.23E+18这种样子。
原因:JSON 文件一般以 UTF-8 无 BOM 格式保存,Excel 在 Windows 下默认按 ANSI(GBK)猜测编码,于是中文解读失败。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=";", index=False)改成分号分隔,或者在写入前把地址里的逗号替换成全角逗号。我一般优先考虑导出 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,这本来是好意,但如果这个字段只是数据集的生成标记,并不代表每条记录的业务时间,就会被误用。
解决:读取时统一加dtype=str,把所有列先当文本处理,之后按需手动转换真正需要的列。如果你已经读进来了,可以用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", dtype=str) df["经度"] = pd.to_numeric(df["经度"], errors="coerce") df["纬度"] = pd.to_numeric(df["纬度"], errors="coerce") df["等级"] = df["等级"].astype(str).str.strip() df_5a = df[df["等级"].str.contains("5A", na=False)].dropna(subset=["经度", "纬度"]) m = folium.Map(location=[35.86, 104.19], zoom_start=4) for _, row in df_5a.iterrows(): lng, lat = gcj02_to_wgs84(row["经度"], row["纬度"]) folium.CircleMarker( location=[lat, lng], radius=3, popup=row["景区名称"], color="#c0392b", fill=True, fill_opacity=0.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 景区数量的统计表,这一步能验证前面的清洗是否到位——如果省份归一化有遗漏,表格里会立刻出现两个同名不同写的省份行。我自己的习惯是先把统计表发给业务同事看数字是否合理,再决定要不要正式交付地图,因为地图上视觉上少几个点很难被发现,而表格里省一级的数字差异一眼就能看出来。希望这份处理路径和踩坑清单帮到你,后面再做同类地理位置数据集时,不用再从零趟一遍。
本文还有配套的精品资源,点击获取