恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
全国旅游景点数据清洗与可视化:从7z到SQLite与热力图
首页
资讯中心
/
全国旅游景点数据清洗与可视化:从7z到SQLite与热力图
全国旅游景点数据清洗与可视化:从7z到SQLite与热力图
发布时间:2026/10/12 17:15:03
简介这是一份全国旅游景点结构化SQL数据包面向需要批量景点数据做网站开发、移动应用或旅游数据分析的开发者与分析人员可直接解决景点信息采集难、清洗成本高的问题。压缩包采用7z格式仅包含1个SQL文件大小约11.01MB解压后可直接导入MySQL使用。表中含324498条景点记录覆盖景点名称、联系电话、地址、类型、行政区编号、POI标识及GCJ与GPS两种经纬度坐标等字段方便按区域、类型或坐标范围筛选。数据示例包括北京动物园等真实景点字段类型定义完整无论做景区检索、行程规划还是数据可视化都可快速复用。目前已有4653人学习下载是一份省时省力、可直接落地的旅游基础数据尤其适合快速搭建原型或补充已有数据库。1. 32万条全国旅游景点数据打开压缩包之前先想清楚三件事打开这份32万条全国旅游景点数据之前先提个醒量级不代表质量。这类数据集大多是从公开渠道聚合采集的字段口径、坐标系、编码格式经常不一致直接拿原始文件统计或画图大概率翻车。这篇笔记要解决的是从拿到 7z 包到产出可用分析表的完整链路压缩包校验、字段与坐标识别、清洗去重、入库查询和一张全国景点密度热力图。适合做旅游数据分析、地图可视化或数据产品原型的人按步骤做完你会得到一份能直接查询的全国景点数据表。2. 先校验再解压用7z和Python给景点数据做体检这类数据落到手里大多是 7z 压缩包压缩率确实高但传输过程损坏、多次二次打包、内部文件编码不一致三个问题任何一个都会让后续清洗白跑。所以我的习惯是先在不解压的前提下做一轮体检看包内清单、做 CRC 校验、抽样看字段。这一步花不了五分钟却能把后面大部分不确定性挡在门外。2.1 用7z命令先看包内清单别急着全部解压如果操作系统里有 7-Zip最省事的方式是直接用命令行做两层检查。# 1) 校验压缩包是否完整不实际解压 7z t 32万条全国旅游景点数据.7z # 2) 查看包内文件清单看里面到底有什么 7z l 32万条全国旅游景点数据.7z # 3) 只平铺解压CSV文件到指定目录-o后无空格 7z e 32万条全国旅游景点数据.7z -oD:\data\tourism *.csv读一下结果7z t输出 Everything is Ok 说明压缩包 CRC 校验通过如果输出 Data Error说明包不完整或在传输中损坏这时候重新下载或让对方重传比解压更快。7z l列出的是包内文件名和原始大小重点看两件事一是里面是不是只有一个 CSV还是有多个分表二是文件名是否是乱码。若包内嵌套了另一个压缩包后续需要解压两轮这一步就能提前发现。最后一条命令7z e是平铺解压把包内所有文件释放到指定目录。需要注意-o参数后没有空格目录不存在时 7-Zip 不会自动创建先mkdir一下更稳。若想保留包内目录结构改成7z x而不是7z e。2.2 用Python做完整性校验和抽样预览如果不想装命令行工具或者后续步骤本身就要用 Python直接把 py7zr 接进数据处理流程更顺。py7zr 是开源跨平台的 7z 解压库纯 Python 实现pip 安装即可用。import py7zr archive_path rC:\downloads\32万条全国旅游景点数据.7z with py7zr.SevenZipFile(archive_path, r) as z: ok z.test() # 校验所有文件CRCTrue表示完整 names z.getnames() # 包内文件列表解压前先看 print(校验结果, ok) print(文件数量, len(names)) print(names[:10]) # 先打印前10个看命名和类型 z.extractall(pathrD:\data\tourism)z.test()是 py7zr 提供的 CRC 完整性校验接口返回 True 才建议继续getnames()在解压前先打印文件名能提前发现嵌套压缩包或乱码文件名extractall指定输出目录后所有文件被释放到该目录。整个过程不依赖系统是否安装 7-Zip跨 Windows、Linux 和 macOS 表现一致。拿到 CSV 后第一件事不是分析而是确认编码。国内聚合数据的 CSV 常混着两种编码GB18030 和 UTF-8-SIG。用编码轮询读前 500 行哪次不报UnicodeDecodeError就用哪个。import pandas as pd path rD:\data\tourism\attractions.csv for enc in [gb18030, gbk, utf-8-sig]: try: df pd.read_csv(path, nrows500, encodingenc) print(使用的编码, enc) break except UnicodeDecodeError: continue print(df.head()) print(df.shape, df.dtypes)nrows500只读部分数据预览阶段没必要全量加载一旦确定编码后续read_csv都固定用这个值避免每次重复探测。编码判断多少带点玄学但遵循一个原则先 GB18030 后 UTF-8-SIG绝大多数国内聚合数据都能覆盖。如果两种都不行再用chardet做一次概率检测不过这种情况很少见。2.3 字段粗查哪些列值得当主键解压、读入都成功后先别急着写清洗逻辑花两分钟看字段的完整性。用 pandas 的count和nunique快速做一个体检表。summary pd.DataFrame({ 非空个数: df.count(), 缺失个数: df.isna().sum(), 唯一值个数: df.nunique(), }) print(summary) print(完全重复行数, df.duplicated().sum())关键看三列name、province、坐标列。它们如果非空率接近 100%且组合后重复率低就是天然的候选主键如果某列缺失率超过 60%后续清洗成本可能高于它带来的价值先记下来不急着决定删不删。duplicated()统计整行完全重复的数量这类重复在后续去重里是最好处理的直接删即可。以这类聚合数据最常见的结构来看列名不一定完全一致但含义基本跑不掉这几种常见字段名大致含义落地时注意name景点名称同一景点的不同写法需要规范化province / city / county行政区划可能有缺失也存在旧区划名lng / lat坐标坐标系是重点下一章专门讲level景区等级5A/4A/3A 与未评级混存score / rating评分5分制和10分制可能并存ticket_price门票价格免费暂无起等文本混入这一章做完你应该能回答三个问题包是否完整、文件用什么编码、哪些列可当主键。回答不了任何一个都先别往下走。3. 看懂数据结构和坐标系字段拆解与GCJ-02偏移判断预览只能看到有没有值看不到值是否能用。景点数据最特殊的地方在于两套坐标系并存WGS-84 是 GPS 原始坐标GCJ-02 是加密坐标系。地图 POI 采集来的数据大多是 GCJ-02直接用它在 WGS-84 底图上画点整体偏移几百米到几公里这是画图环节最常翻车的地方。3.1 字段拆解先看列名再看实际值的口径只看列名猜字段最危险同级字段在不同来源里的含义可能完全不同。比如 level 列里可能混着 5A 5A级 国家5A 多种写法score 列可能同时有 4.5分 和 8.5 两种量纲。所以先打印列名再打印随机样本两边对照着看。print(df.columns.tolist()) print(df.sample(10, random_state42).to_string())sample(10)随机抽 10 行完整打印重点看等级、评分、价格这几列的原始写法。看完之后再用unique枚举每一列的实际取值确认口径。for col in [level, score, ticket_price]: if col in df.columns: print(col, 取值样例, df[col].dropna().unique()[:10])这一步不需要写任何清洗逻辑目的只有一个知道每列到底存了什么。如果评分列同时出现 4.5分 和 8.5 两种写法说明来源不同统计前必须统一量纲如果等级列只有少数几个取值说明这是个低基数字段适合做分组统计而非数值计算。3.2 坐标系检查确定数据是WGS-84还是GCJ-02判断坐标系有三种方法按成本排序。第一种抽 10 条带详细地址的记录把经纬度丢进公开在线地图的坐标拾取功能反查地址看返回地址是否吻合第二种看采集来源特征来自在线地图 POI 接口的数据大多是 GCJ-02来自 GPS 记录的多是 WGS-84第三种把全部点和省市边界叠加若整体向同一方向偏移多半是坐标系不匹配。三种方法结合结论比较靠得住。确定是 GCJ-02 之后不要手工改数据用公开的近似反算函数统一转 WGS-84。下面这段是我常用的版本。import math pi math.pi a 6378245.0 ee 0.006693421622965943 def out_of_china(lng, lat): # 全国范围的粗略边界超出则不做偏移处理 return not (73.66 lng 135.05 and 3.86 lat 53.55) def _transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * pi) 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * pi) 40.0 * math.sin(y / 3.0 * pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * pi) 320.0 * math.sin(y * 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 * pi) 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * pi) 40.0 * math.sin(x / 3.0 * pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * pi) 300.0 * math.sin(x / 30.0 * pi)) * 2.0 / 3.0 return ret def gcj02_to_wgs84(lng, lat): if out_of_china(lng, lat): return lng, lat dlat _transform_lat(lng - 105.0, lat - 35.0) dlng _transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi) mglat lat dlat mglng lng dlng return lng * 2 - mglng, lat * 2 - mglat提示这段公式是社区使用多年的近似算法不是精确逆变换转换后点位误差在几十米以内对密度统计和热力图够用。如果用在线坐标转换接口注意服务限制和配额别把 32 万条一次性怼过去。整个表转完建议在文件里加一列coord_sys标注wgs84 或 gcj02方便回溯。转换前最好抽 5 条记录手工核对转换后的坐标应该比转换前更接近真实位置否则说明数据本来就是 WGS-84不需要转。3.3 经纬度合法性筛查把空坐标和非法值挡在前面坐标系确认后还要做一道合法性筛查。0,0 坐标是脏数据重灾区经纬度越界的也要过滤掉。df[lng] pd.to_numeric(df[lng], errorscoerce) df[lat] pd.to_numeric(df[lat], errorscoerce) bad_zero ((df[lng] 0) (df[lat] 0)).sum() bad_range ((df[lng] 73) | (df[lng] 135) | (df[lat] 3) | (df[lat] 54)).sum() print(非法坐标行数, bad_zero bad_range) clean_df df[(df[lng].between(73, 135)) (df[lat].between(3, 54)) (df[lng] ! 0) (df[lat] ! 0)]边界范围使用全国大致的经纬度区间73~135、3~54海外景点可单独存一个表先to_numeric转数值型再用between过滤避免字符串比较导致误滤。errorscoerce会把无法解析的文本变成 NaN后续按缺失处理。经过这道筛查剩下的坐标基本可以放心进地图。4. 清洗与去重把32万条整理成能直接查询的表字段和坐标看明白后进入最耗时的清洗阶段。目标不是把数据洗到完美而是让每列口径一致、每行可定位、整表能直接丢进统计或可视化流程。这里每一步都要保留原始列清洗逻辑写在新增列里方便随时回溯。4.1 名称规范化与去重策略景点名的常见脏数据括号备注如某某景区(暂停营业)、全角半角空格、不同来源对同一景点的叫法差异。先用正则做一层规范化再去讨论去重。import re def normalize_name(s): if pd.isna(s): return s str(s).strip() s re.sub(r[(][^)]*[)], , s) # 去掉括号中的备注 s re.sub(r[·•\s], , s) # 去掉分隔符和空格 return s df[name_key] df[name].map(normalize_name) df[dup_flag] df.duplicated(subset[name_key, province, city], keepFalse) print(按 名称省市 疑似重复, df[dup_flag].mean())keepFalse用于统计疑似重复量不是用来删数据的真正删的时候改成keepfirst。去重维度上我不用纯 name而是name_key province city这个组合的误杀率最低能避免把不同城市的同名景点比如各种人民公园湖滨公园合并。如果两个坐标点直线距离在 500 米内、名称又完全一致才考虑是同一个景点的重复录入。确定去重规则后保留哪一行需要排序决定。常见做法是按评分降序排让信息最全的行排在前面。df_sorted df.sort_values( [name_key, province, city, score], ascending[True, True, True, False] ) df_dedup df_sorted.drop_duplicates( subset[name_key, province, city], keepfirst ) print(去重前后行数, len(df), -, len(df_dedup))这里用drop_duplicates的 keep 机制配合排序实现保留评分最高行的效果。评分缺失的行会排到最后被自动丢弃所以评分列缺失率过高时这个策略会误伤需要先看 2.3 的缺失率报告再决定是否调整。4.2 省市字段补齐与直辖市处理省市区字段缺失时补字段不要脑补优先找已有列推。城市字段有值但省缺失时用一张城市到省份的映射表补齐直辖市常表现为只有市没有区县分析时可以另行标记不强行填。# 维护一张城市-省份映射表示例格式如下 city_to_province {某市: 某省} def fill_province(row): if pd.isna(row.get(province)) and not pd.isna(row.get(city)): return city_to_province.get(str(row[city]).strip(), None) return row.get(province) df[province] df.apply(fill_province, axis1) print(省份缺失率, df[province].isna().mean())映射表可能只覆盖部分城市填不上的保留缺失。不确凿的信息宁可不填这是这类数据处理的基本原则引入第三方在线逆地理接口前先想清楚32 万条数据要发多少次请求接口配额够不够返回结果的稳定性是否可控。除非省份字段是核心分析维度否则缺省值直接保留不做任何猜测性填充。4.3 业务字段口径统一等级、评分、门票价格这类业务字段统计口径比字段名更值得关注。目标是新增标准化列保留原列。def parse_level(s): if pd.isna(s): return None text str(s) for lv in [5A, 4A, 3A, 2A, 1A]: if lv in text: return lv return 未评级 def parse_score(s): if pd.isna(s): return None m re.search(r(\d(?:\.\d)?), str(s)) return float(m.group(1)) if m else None df[level_clean] df[level].map(parse_level) if level in df.columns else 未评级 df[score_clean] df[score].map(parse_score) if score in df.columns else None print(df[[level, level_clean, score, score_clean]].head(20).to_string())parse_level会把5A级景区提取成5A把待评级归类为未评级既保留信息又便于分组统计parse_score只存活数字避免暂无评分这类文本干扰聚合。新增列不覆盖原列保留原始值方便回溯也方便在清洗逻辑出问题时对比排查。5. 常见问题排查处理全国景点数据容易翻车的五个环节数据清洗跑一遍并不算完踩过下面五个坑数据才算真正能交出去。每一条都是我在类似数据集上真实遇到过的现象、原因、解决按顺序写方便你对照。5.1 解压后文件名乱码文件打不开现象解压出来的文件名是一串锟斤拷或替换字符双击打不开命令行也识别不了。原因压缩包内文件名使用 GBK 编码而系统或解压工具按 UTF-8 解码导致名称损坏。解决先用getnames()检查文件名是否乱码若确认乱码解压后写一个批量重命名脚本按文件位置和扩展名修复。import os, py7zr out rD:\data\tourism with py7zr.SevenZipFile(rC:\downloads\32万条全国旅游景点数据.7z, r) as z: names z.getnames() z.extractall(pathout) for i, name in enumerate(names): if in name or 锟 in name: ext os.path.splitext(name)[1] fixed os.path.join(out, ffile_{i}{ext}) os.rename(os.path.join(out, name), fixed) print(重命名, name, -, fixed)getnames()在解压前先保存一份文件列表解压后按列表逐一检查。乱码通常表现为锟斤拷这类替换字符批量重命名按扩展名保留即可。CSV 文件内部的编码是否正常不取决于文件名要靠 2.2 的编码轮询来确认。5.2 read_csv读进来中文全乱现象用 pandas 读 CSV中文列名和内容全变成乱码英文和数字正常。原因文件是 GB18030 或 GBK 编码默认 UTF-8 解码失败。解决写一个自动编码探测函数按固定顺序尝试常见编码。def read_csv_auto(path, **kwargs): for enc in [gb18030, gbk, utf-8-sig]: try: return pd.read_csv(path, encodingenc, **kwargs) except UnicodeDecodeError: continue raise ValueError(无法识别的编码) df read_csv_auto(rD:\data\tourism\attractions.csv) print(df.head())gb18030是 GBK 的超集优先尝试它utf-8-sig能处理带 BOM 的文件sig会自动去掉开头的 BOM 字符避免第一列列名出现奇怪前缀。注意UnicodeDecodeError只在解码失败时抛出文件如果混着多种编码这个函数只能保证读入乱码内容要靠后续抽检发现。5.3 数据点在底图上整体偏移现象把经纬度画到底图上所有点整体向东或向北偏移几百米到几公里形状不变。原因坐标系不匹配。数据是 GCJ-02底图是 WGS-84或反过来。解决抽几条记录在支持坐标拾取的地图上反查地址确认坐标系后用第 3 章的gcj02_to_wgs84统一转换。转换完成后抽 3~5 个点再验证一次。若数据里混着两套坐标偏移方向会不一致这时候按来源分桶分别转换。5.4 去重去不干净或误删有效景点现象同一景区依然有两条记录或去重后某些著名景区凭空消失。原因去重维度选错。只按 name 去重会把跨城市同名景点误删只按坐标去重两个坐标差几十米的不同景点会被合并。解决用name_key province city组合配合坐标距离做二次判断。先看疑似重复样本再决定规则别一上来就删。df.loc[df[dup_flag], [name, province, city]].head(20)打印前 20 条疑似重复记录人工确认它们确实是同一景点还是同名不同景。这个抽检每次都要做偷懒跳过后面多半要返工。5.5 评分与门票字段大量缺失现象score、ticket_price 列缺失率超过 60%直接统计结果失真。原因不同来源站点的页面结构不同部分景区没有挂评分或价格字段采集时只能留空。解决把这类业务字段设为可选维度从核心分析列里剔除在数据字典里保留说明。不要用均值或默认值填充缺失那会让统计结论更难解释必要时用其他来源单独补不混进当前表。missing_rate df[[score, ticket_price]].isna().mean() print(missing_rate) core_cols [name, province, city, lng, lat, level_clean]核心列只保留确定性和可追溯性强的字段业务字段留给具体分析场景按需取用。这样整张表结构稳定后续不管是做 SQL 查询还是画图都不用担心某一列突然出现大量空洞。6. 落地把清洗结果入库SQLite并做全国景点密度统计清洗完成后的下一步是把数据放进可查询的载体。SQLite 是最轻的选择单文件、无服务、跨平台32 万行数据存进去查询依然是毫秒级不需要额外部署数据库。6.1 建库与索引32万条数据查询秒回把清洗后的df_dedup直接写入 SQLite并在查询最常用的字段上建索引。import sqlite3 conn sqlite3.connect(rD:\data\tourism.db) df_dedup.to_sql(attractions, conn, if_existsreplace, indexFalse) conn.execute(CREATE INDEX IF NOT EXISTS idx_province ON attractions(province)) conn.execute(CREATE INDEX IF NOT EXISTS idx_coord ON attractions(lng, lat)) conn.commit() result pd.read_sql( SELECT province, COUNT(*) AS cnt FROM attractions GROUP BY province ORDER BY cnt DESC LIMIT 10, conn ) print(result)to_sql负责建表和数据写入if_existsreplace保证重复执行不会累积脏数据索引建在province和lng, lat上是因为后续统计基本都按省份分组、按坐标过滤。聚合查询返回的是按景点数量排序的省份排行这已经是省级密度分析的基础结果。6.2 导出GeoJSON一张图看清全国景点密度最后一步把清洗结果导出成 GeoJSON可以直接丢进支持该格式的可视化工具配合省市边界做热力叠加。import json features [] for r in df_dedup.itertuples(indexFalse): features.append({ type: Feature, geometry: {type: Point, coordinates: [r.lng, r.lat]}, properties: {name: r.name, province: r.province, level: r.level_clean}, }) geojson {type: FeatureCollection, features: features} with open(rD:\data\attractions.geojson, w, encodingutf-8) as f: json.dump(geojson, f, ensure_asciiFalse) print(导出点数, len(features))GeoJSON 的坐标顺序是[经度, 纬度]很多表格习惯存纬度, 经度这里写反会导致所有点飞到大洋里去我在这上面栽过一次现在已经养成导出前先打印一条记录的习惯。ensure_asciiFalse保证中文属性正常显示否则名称字段会变成\u转义序列。我现在的固定流程是拿到任何新数据包先花半小时做体检和坐标判断再顺着第 2 到第 5 章的顺序清洗最后统一入库。这个流程跑顺之后换一个数据包也能复用同一套管道。第一次处理 32 万条这类数据时我跳过体检直接画图结果深夜排查偏移问题到凌晨两点之后才把先体检再清洗变成铁律。希望帮到你。本文还有配套的精品资源点击获取