
简介这份资源面向旅游规划场景的开发者与学习者提供一套基于PyQt5与Folium的景点路线规划工具。它用PyQt5搭建直观的图形界面借助Folium与QWebEngineView渲染交互式地图直观展示景点分布底层通过NetworkX实现Dijkstra等最短路径算法支持景点搜索、路线规划与时间估算帮助游客高效安排行程。资源包共312个文件以300个xlsx景点数据表为主体另有6个py源码文件、4个pyc编译文件、1个pth模型权重与1个txt说明压缩包约6.67MB涵盖数据、算法与界面模块。目前已有99人学习下载。读者可从中获取完整的景点数据库、路径规划算法实现、地图可视化方案与界面设计思路适合作为旅游类桌面应用的参考范例也便于在此基础上二次开发或用于课程设计实践。1. 把旅游路线画在地图上PyQt5 与 Folium 能拼出什么做旅游规划工具的人大多经历过一个尴尬阶段路线数据在 Excel 里躺得好好的一放到界面上就变成一堆冷冰冰的经纬度数字用户根本看不出「从西湖到灵隐寺再到宋城」到底顺不顺路。我最早做的一个模拟项目就栽在这上面——后台算得再准前端只甩出一张静态截图没人愿意用。后来换成 PyQt5 做桌面壳、Folium 做地图渲染才算把「能算」和「能看」这两件事接上。这个组合解决的核心问题很具体用 PyQt5 搭一个本地可跑的桌面界面把景点、住宿、交通节点这些带经纬度的数据通过 Folium 生成可交互的 HTML 地图再嵌回窗口里显示。适合谁适合手里有 POI 数据、想快速验证路线合理性又不想折腾 Web 前后端全栈的开发者。它不追求高并发也不适合做在线多人协作但用来做单机版规划工具、内部演示或者课程设计够用且落地快。2. 环境搭建与最小可运行骨架别一上来就装全家桶2.1 依赖选型为什么是 PyQt5 Folium 而不是别的先把这个组合的边界说清楚。PyQt5 是 Qt 的 Python 绑定优势在于控件成熟、跨平台、能直接嵌 Web 视图Folium 本质是 Leaflet.js 的 Python 封装生成的是独立 HTML 文件不依赖在线瓦片服务也能换成本地瓦片。两者之间靠 QWebEngineView 桥接——这是 PyQt5 里唯一能稳定渲染 Folium 输出的控件别想着用 QTextBrowser 凑合它不执行 JavaScript地图会白屏。常见做法是PyQt5 负责窗口、按钮、表格、文件对话框Folium 负责把 DataFrame 里的经纬度转成 Marker 和 PolyLine最后把生成的 HTML 塞进 QWebEngineView。选 Folium 而不是直接写 Leaflet是因为它的 Python API 对「批量加点、批量连线」更友好几行代码就能出图选 PyQt5 而不是 Tkinter是因为 QWebEngineView 的渲染质量明显更稳Tkinter 嵌浏览器控件在 Windows 上经常出玄学问题。版本上不用追新PyQt5 用 5.15.x 系列Folium 用 0.14 以上即可Python 3.8 到 3.11 都验证过。注意 QWebEngineView 在部分 Linux 发行版上需要额外装 libxcb 相关库这是系统级依赖pip 装不上。2.2 最小骨架一个窗口加一张地图先跑通「窗口里显示一张地图」这个最小闭环再往上叠功能。下面这段代码是骨架直接抄import sys import folium from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget from PyQt5.QtWebEngineWidgets import QWebEngineView class MapWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(旅游路线预览) self.resize(1000, 700) # 1. 用 Folium 生成地图对象初始中心点设在杭州附近 self.map folium.Map( location[30.25, 120.15], zoom_start12, tilesOpenStreetMap # 换成 CartoDB positron 更清爽 ) # 2. 把地图对象存成 HTML 字符串而不是文件 html self.map.get_root().render() # 3. 用 QWebEngineView 加载 HTML self.view QWebEngineView() self.view.setHtml(html, QUrl(about:blank)) layout QVBoxLayout() layout.addWidget(self.view) container QWidget() container.setLayout(layout) self.setCentralWidget(container) if __name__ __main__: app QApplication(sys.argv) window MapWindow() window.show() sys.exit(app.exec_())逻辑说明folium.Map的location参数是[纬度, 经度]别写反这是血泪经验写反了地图会跑到南极附近。get_root().render()返回完整 HTML 字符串比先存文件再加载更干净避免临时文件残留。setHtml的第二个参数给一个空 QUrl 是必须的否则某些 PyQt5 版本会报安全策略错误。参数怎么改zoom_start控制初始缩放级别城市级规划用 11 到 13跨省路线用 6 到 8。tiles参数决定底图风格OpenStreetMap 信息全但颜色杂CartoDB positron 灰白底更适合叠加彩色路线。如果在内网环境跑可以把 tiles 换成本地瓦片路径但需要自己搭瓦片目录这个后面章节再说。跑通这一步你会看到一个带缩放和拖拽的交互地图嵌在窗口里。接下来才是真正的活把景点数据变成地图上的点和线。3. 把景点数据变成地图图层Marker、PolyLine 与弹窗的实操3.1 数据结构设计先定字段再写代码旅游规划的数据源通常是一张表字段至少要有名称、纬度、经度、类型景点/餐饮/住宿、建议停留时长、备注。我一般用 pandas 的 DataFrame 来管因为后面排序、筛选、分组都方便。下面是一个模拟数据集你可以直接替换成自己的 CSVimport pandas as pd data { name: [断桥残雪, 平湖秋月, 灵隐寺, 宋城, 河坊街], lat: [30.2590, 30.2530, 30.2410, 30.1920, 30.2420], lon: [120.1480, 120.1420, 120.1010, 120.0930, 120.1690], type: [景点, 景点, 景点, 景点, 街区], stay: [0.5, 0.5, 2.0, 3.0, 1.5], note: [白堤起点, 孤山东侧, 飞来峰旁, 需提前订票, 晚上更热闹] } df pd.DataFrame(data)字段设计上stay用小时为单位方便后面算总时长type用来决定 Marker 颜色这是视觉区分的关键。别小看颜色用户扫一眼地图就能分清哪些是玩的、哪些是住的比看列表快得多。3.2 批量加点与连线循环里别踩闭包坑有了 DataFrame接下来遍历它往地图上加 Marker 和 Popup。这里有个经典翻车点在循环里用 lambda 绑定弹窗内容如果不做参数固化所有弹窗都会显示最后一条数据。正确写法是用默认参数固化import folium # 按类型分配颜色 color_map {景点: blue, 餐饮: orange, 住宿: green, 街区: purple} for idx, row in df.iterrows(): folium.Marker( location[row[lat], row[lon]], popupfolium.Popup( fb{row[name]}/bbr类型{row[type]}br建议停留{row[stay]}小时br{row[note]}, max_width250 ), tooltiprow[name], # 鼠标悬停显示名称 iconfolium.Icon(colorcolor_map.get(row[type], gray), iconinfo-sign) ).add_to(self.map) # 按顺序连线模拟游览路线 points df[[lat, lon]].values.tolist() folium.PolyLine( locationspoints, colorred, weight3, opacity0.7, dash_array5, 10 # 虚线表示建议路线而非实际道路 ).add_to(self.map)逻辑说明folium.Popup里可以直接写 HTMLmax_width控制弹窗最大宽度不设的话长文本会撑得很宽。tooltip是悬停提示和点击弹窗是两回事两个都设体验更好。PolyLine的dash_array参数让线变成虚线视觉上区分「规划路线」和「实际道路」这个细节很多教程不提但用户反馈里很吃这一套。参数怎么改weight控制线宽屏幕显示用 3 到 5导出打印用 6 以上。opacity别设 1.0半透明能看清底图路网。如果景点顺序需要优化连线前先对 DataFrame 排序排序逻辑可以是「按建议停留时长降序」或者「按区域聚类」这部分属于路线算法不在这个资源范围内但接口留好了。3.3 把生成的地图嵌回 PyQt5 窗口上一章的骨架里已经用了setHtml但实际项目中地图会随用户操作重新生成比如切换日期、增删景点。这时候不能反复创建 QWebEngineView正确做法是复用同一个 view只更新 HTMLdef refresh_map(self, df): 根据最新 DataFrame 重新生成地图并刷新视图 self.map folium.Map(location[30.25, 120.15], zoom_start12, tilesCartoDB positron) # ... 这里放上面的加点和连线逻辑 ... html self.map.get_root().render() self.view.setHtml(html, QUrl(about:blank))注意每次刷新都重建 folium.Map 对象而不是在旧对象上删删改改因为 Folium 没有提供干净的「清空图层」API重建最省心。如果数据量大重建会有短暂闪烁可以在刷新前显示一个加载提示这个用 QLabel 叠加就行不复杂。4. 避坑与排查地图不显示、弹窗错乱、打包失败4.1 地图白屏或只显示灰色网格现象窗口打开了但 QWebEngineView 区域一片灰没有地图瓦片。原因最常见的是 QWebEngineView 没有正确初始化或者setHtml时 QUrl 传了 None。另一个可能是网络问题——Folium 默认从 OpenStreetMap 拉瓦片内网环境访问不到。解决先确认QUrl(about:blank)传了再检查网络把 tiles 换成CartoDB positron试试它的 CDN 有时更稳如果完全离线需要提前下载瓦片到本地用folium.TileLayer(tiles./tiles/{z}/{x}/{y}.png, attrlocal)加载注意瓦片目录结构必须符合{z}/{x}/{y}格式。4.2 弹窗内容全部一样现象点击每个 Marker弹窗都显示最后一条数据。原因循环里 lambda 或闭包捕获了变量引用而不是当时的值。解决用默认参数固化或者把 popup 内容先拼成字符串再传入。上面 3.2 的写法已经规避了这个问题但如果你用lambda: print(row[name])这种形式就会踩坑。记住一个原则循环里创建回调一定要把外部变量通过默认参数传进去。4.3 打包成 exe 后地图不加载现象开发环境跑得好好的PyInstaller 打包后地图区域空白。原因QWebEngineView 依赖一堆资源文件QtWebEngineProcess.exe、translations、resourcesPyInstaller 默认不会全打进去。解决用--add-data把 PyQt5 的 Qt5 目录下resources和translations加进去同时确保QtWebEngineProcess.exe在打包目录里。更省事的办法是用pyinstaller --collect-all PyQt5但体积会大不少。这个坑我踩过两次后来固定用 spec 文件写清楚 datas 和 binaries不再依赖命令行参数。4.4 地图缩放后 Marker 偏移现象地图放大后Marker 的位置和实际经纬度对不上偏了几十米。原因Folium 默认用 WGS84 坐标系而国内很多地图数据是 GCJ02 或 BD09 加密坐标直接混用就会偏。解决统一坐标系。如果数据源是 GCJ02要么在加 Marker 前做一次 GCJ02 转 WGS84 的偏移校正要么把底图换成支持 GCJ02 的瓦片服务。常见做法是写一个转换函数对每个点的经纬度做一次非线性偏移具体公式网上有公开实现这里不展开。注意转换只做一次别在每次刷新时重复转否则会累积误差。4.5 大量 Marker 导致界面卡顿现象景点超过 200 个后拖动地图明显掉帧。原因每个 Marker 都是一个独立 DOM 元素Folium 默认全部渲染。解决改用folium.plugins.MarkerCluster做聚合或者对非重点景点用CircleMarker替代Marker后者更轻量。如果还是卡考虑分页加载——只渲染当前视野范围内的点但这需要前后端配合超出这个资源范围了。我的经验是单机工具控制在 300 个点以内体验都还能接受。5. 进阶技巧路线顺序优化与地图导出5.1 用贪心算法做初步路线排序景点连线顺序如果按数据表原始顺序来很可能绕路。一个够用的改进是贪心最近邻从起点出发每次选最近的未访问点。代码不长但效果立竿见影import numpy as np def greedy_route(df, start_idx0): 贪心最近邻排序返回按访问顺序排列的索引列表 unvisited list(df.index) route [unvisited.pop(start_idx)] while unvisited: last route[-1] # 计算当前点到所有未访问点的距离 dists [ np.hypot(df.loc[last, lat] - df.loc[i, lat], df.loc[last, lon] - df.loc[i, lon]) for i in unvisited ] nearest unvisited[int(np.argmin(dists))] route.append(nearest) unvisited.remove(nearest) return route order greedy_route(df) df_sorted df.loc[order].reset_index(dropTrue)逻辑说明np.hypot算的是平面距离在经纬度上不精确但用于排序足够。start_idx可以指定起点比如从酒店出发。这个算法不保证全局最优但比原始顺序强很多而且 O(n²) 的复杂度在几百个点以内毫无压力。参数怎么改如果希望终点回到起点在循环结束后把route[0]追加到末尾即可。如果某些点有开放时间限制需要把时间窗作为约束加进去那就得换更复杂的算法这个资源不覆盖。5.2 导出为独立 HTML 和图片Folium 的save方法可以直接导出 HTML用户双击就能在浏览器里看不依赖你的程序self.map.save(route_preview.html)如果要做汇报材料还需要静态图片。Folium 本身不导出图片常见做法是用selenium打开 HTML 再截图或者用folium.Map._to_png()需要额外装selenium和pillow。我一般用 selenium 方案因为可控性更强from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headless) options.add_argument(--window-size1200,800) driver webdriver.Chrome(optionsoptions) driver.get(file:///绝对路径/route_preview.html) driver.save_screenshot(route_preview.png) driver.quit()注意headless 模式下地图瓦片加载需要时间直接截图可能截到空白稳妥做法是driver.implicitly_wait(5)或者显式等待某个元素出现。这个坑很隐蔽我第一次导出时截了十张灰图。5.3 一个我固定会走的验证流程每次改完地图逻辑我不会直接看界面而是先跑一遍「数据校验 → 生成 HTML → 浏览器打开 → 再嵌回窗口」这个链路。因为 QWebEngineView 的报错信息经常被吞掉浏览器里能看到 console 错误定位快得多。从那以后我每次调整 Folium 图层参数都强制走一遍浏览器预览确认没问题再嵌回 PyQt5。这个习惯帮我省了大量「界面白屏但不知道哪错了」的时间。希望帮到你。本文还有配套的精品资源点击获取