ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python和ortools自动生成旅行行程:从西班牙之旅到通用排程器

用Python和ortools自动生成旅行行程:从西班牙之旅到通用排程器 上个月我把两周的西班牙行程交给了一段Python脚本去排结果它给了我一份按小时拆分的逐日计划哪天去马德里、哪天去巴塞罗那、每天上午逛哪个馆、下午几点去哪个广场连跨城高铁该订哪一班都标了出来。排完之后我再手动改了两个小时就把行前最麻烦的事彻底解决了。这个项目最初只是为了一趟家庭旅行临时写的但做下来发现“自动生成逐日行程”这件事比我想象中有价值得多。很多人不缺景点清单缺的是把清单塞进真实日历的能力。如果你也遇到过“收藏了一堆推荐却不知道怎么排日期”“排完了发现两个景点之间根本来不及”这类问题这篇内容大概率能帮你省下几个晚上。1. 为什么要自己写行程规划工具1.1 手动排行程的三座大山做旅行计划的人应该都体会过真正痛苦的从来不是“找景点”而是“排顺序”。第一座大山是开放时间。西班牙南部的很多景点有严格的中午闭馆传统像塞维利亚王宫和阿尔罕布拉宫下午时间段经常约不到上午和下午的游览逻辑完全不同。你手动排的时候要求自己在脑子里维护一个“每个景点哪天开、几点开、几点关”的表这本身就是一件特别反人类的事情。第二座大山是地理位置与交通时长的耦合。马德里市中心的普拉多博物馆和索菲亚王后艺术中心离得不远但如果把伯纳乌球场排在同一天的下午就得算清楚地铁和步行时间。去托莱多的一日游往返要留出大半天很多人排的时候压根没把这段“无效移动时间”算进去结果行程变成跑酷。第三座大山是体力分配。你看地图觉得A到B只要两公里走起来加上找路、排队、上坡一个下午可能只能完成两个点。手动排表时一旦忽略体力衰减第二天基本就是酒店躺平。这三座大山叠加在一起导致传统的“Excel排行程”方案本质上是一个不断返工的过程每调整一个景点后面所有安排都要跟着动牵一发动全身。1.2 现成旅行App为什么救不了你我也试过各种现成的旅行规划工具。Google Maps的“想去的地方”只能做收藏夹不能告诉你哪天去最顺旅游App的推荐路线大多面向“首次打卡型游客”路线固化、不含个性化偏好真正能做自动优化的工具又普遍需要付费订阅而且数据源以欧美用户为主对西班牙南部的一些中小城市覆盖得很差。还有一个更麻烦的问题现成工具普遍不让你批量导出。我想要的是每个景点一个日历事件、每个时间段可手动微调、离线也能打开的文件。把这些要求加在一起市面上几乎没有能一步到位的东西。所以我才决定自己写一个。你要知道当一件事的约束条件特别多而且相互冲突时手工方案和人脑的短期记忆就顶不住了。这类问题恰恰是代码擅长的。旅行规划本质上是一个带时间窗约束的路径优化问题这正是计算机算法的主场。2. 整体方案设计与技术栈选型2.1 核心思路把旅行抽象成排课表我最终选定的思路很简单把“规划行程”抽象成“给一个学生排一周课表”。学生是你自己课程是各个景点教室是城市交通网上课时间则是景点的开放时间。你要保证课程不冲突、顺序不绕路、每天总课时别太满同时尽量把你想上的课都排进去。这个抽象听起来简单但它解决了手动排表的根本矛盾人脑一次只能处理五六个变量而一次完整的西班牙两周行程涉及至少50个候选景点、5个城市、每天的用餐时间、交通切换和预订时段变量数量远超工作记忆的极限。所以我给自己定的设计目标是脚本负责生成一份“合理的初稿”我负责在初稿基础上做少量人工调整。不是追求完全自动化而是追求“自动化完成80%的脏活累活”。2.2 技术栈与选型理由整个项目我用Python完成选型逻辑非常直白。Python生态最全处理JSON、调用API、生成文件都很顺手写这种重度数据处理的工具效率最高。Google Places API用来拉取景点POI(兴趣点)的基础数据包括名称、坐标、评分、用户评论数、开放时间。在西班牙主要城市这个数据源的覆盖度可以接受。OSMnx用于获取城市路网并计算两点之间的步行/驾车距离矩阵比调用地图Web API更稳定且不消耗配额。ortoolsGoogle开源的运筹优化库用来处理行程排序这类组合优化问题比自己写贪心搜索更可靠。folium生成可视化地图方便在地图上检查路线是否合理。ics生成标准日历文件直接导入手机日历这是最终的呈现形式。为什么不直接用现成的行程推荐API因为市面上的旅行规划API要么返回的是“顺序无关的景点列表”要么只支持单日路线规划。我要的是跨多天、跨城市、可自定义约束的行程这属于定制逻辑只能自己写。2.3 数据结构设计一个景点到底需要哪些字段项目的第一步是定义景点数据结构。我把一个景点抽象成下面这个Python字典{ name: 圣家堂, city: 巴塞罗那, lat: 41.4036, lng: 2.1744, rating: 4.8, price_level: 2, duration_min: 120, # 预计停留分钟数 best_visit: morning, # 最佳访问时段: morning/afternoon/evening closed_days: [2024-12-25], # 特殊闭馆日 time_windows: [ {open: 09:00, close: 18:00, days: [0, 1, 2, 3, 4, 5, 6]} ], must_see: True # 是否必去 }这里最关键的字段是duration_min。它表示“你大概会在里面待多久”这个值决定了所有时间窗口能否排得开。我一开始把这个时间设成固定的“平均2小时”后来发现不同景点之间的差异极大普拉多博物馆如果走马观花一个半小时可以出来但艺术爱好者在里面待四个小时也不奇怪。我在代码里给每个景点维护了一个“快速游览”和“深度游览”两个时长默认取中间值。如果你知道自己属于哪种游客可以在配置文件里改这个参数。这算是数据驱动行程规划里最容易被忽略的一环不只景点的位置重要你在每个景点停留多久更重要。3. 核心实现细节与关键逻辑3.1 跨城市行程的日粒度拆分西班牙旅行最常见的玩法是马德里进、巴塞罗那出中间穿插安达卢西亚地区的塞维利亚、格拉纳达、科尔多瓦。我的脚本需要先决定“哪天在哪个城市”。这个决策我用了非常简单但有效的规则计算每个城市你需要拜访的景点数量、每个景点的建议游览时长、该城市内部交通成本然后用ortools做一个城市维度的排序优化。目标是让跨城移动次数最少同时让每个城市内安排的景点总量均衡。举例来说我最初的设计里包含马德里、托莱多、塞维利亚、格拉纳达、巴塞罗那五个城市节点。脚本计算后发现托莱多虽然离马德里很近但完整游览需要一整天所以它被自动安排成“马德里中间的当日往返”而不是单独作为住宿城市。这种决策如果你手动排也能得出同样结论但脚本的意义在于它会把这个约束在所有方案中反复校验不会出现“前一天在塞维利亚深夜回酒店、第二天一早赶去格拉纳达”这种自杀式安排。3.2 单日行程生成的贪心调度城市内一天的行程我采用了两阶段方法先初始化再优化。初始化阶段用贪心策略从酒店位置出发选择“当前时间能去的最值得去且时间允许的景点”。具体评分函数是score rating_weight * rating must_see_bonus time_fit_penalty - travel_time_penaltytime_fit_penalty用来惩罚“即使现在赶过去也快关门”的景点travel_time_penalty用来惩罚当前地点相距过远的景点。这两个惩罚项非常关键它们让脚本天然倾向于“顺路安排”而不是“哪里分高去哪里”。接下来是优化阶段。我用ortools的ConstraintProgramming模型把一天拆成15分钟粒度的时间片每个景点分配一个“游览区间”约束条件包括景点开放时间窗口景点间交通时间午晚餐时间每天游览总时长上限相邻景点位置的最大通勤时间这个模型不强求塞满所有景点而是允许“留白”。松弛一开始我还不放心觉得少排一个景点浪费了实际用下来发现留白是给人喘气的也是给意外留余地的。3.3 交通时间矩阵的计算技巧交通时间的计算是整个项目里最容易出错也最影响体验的部分。我用OSMnx获取城市步行路网计算景点两两之间的步行时间。对于跨城市的转移我直接写死了一个速度参数高铁按均速250km/h加上进站出站各45分钟来估算城市内打车按均速25km/h估算步行按4.5km/h估算。有个细节值得说很多行程规划工具会把“打车5分钟”和“步行15分钟”混在一起比较但实际在西班牙的窄巷老城里步行往往比打车更快更省心。所以我给交通模式设置了一个优先级距离1.5公里以内默认步行超过1.5公里且城市支持打车才使用机动车。这样生成的路线更符合真实旅行体验。3.4 代码实现从数据到日历文件的完整链路整个项目的入口函数长这样def build_itinerary(cities, stay_days, must_see, output_dir): pois fetch_all_pois(cities) # 第1步: 获取景点数据 travel_matrix build_travel_matrix(pois) # 第2步: 计算交通时间矩阵 daily_plan optimize_city_sequence(pois, stay_days) # 第3步: 城市维度排序 day_details optimize_daily_schedule(daily_plan, travel_matrix) # 第4步: 逐日排程 export_calendar(day_details, output_dir) # 第5步: 导出 .ics 日历 export_geojson(day_details, output_dir) # 第6步: 导出 GeoJSON 地图 export_markdown(day_details, output_dir) # 第7步: 导出可读的 Markdown 行程单核心的optimize_daily_schedule代码片段如下from ortools.sat.python import cp_model def optimize_daily_schedule(pois, time_matrix, day_start09:00, day_end20:00): model cp_model.CpModel() # 每个景点一个 optional 区间变量 intervals [] for i, poi in enumerate(pois): start model.NewIntVar(day_start_min, day_end_min, fstart_{i}) end model.NewIntVar(day_start_min, day_end_min, fend_{i}) duration poi[duration_min] interval model.NewOptionalIntervalVar( start, duration, end, model.NewBoolVar(factive_{i}), finterval_{i} ) intervals.append((poi[name], start, end, interval)) # 添加开放时间约束、交通时间约束 for i in range(len(pois)): for j in range(i 1, len(pois)): if i j: continue # 这里只是示意实际用 AddNoOverlap 加 distance 约束 pass # 求解 solver cp_model.CpSolver() status solver.Solve(model) return extract_schedule(solver, intervals)实际代码比这个复杂不少但核心思想就是上面这些每个景点是一个可选区间所有区间之间不能重叠同时还要满足两个景点之间的交通衔接。这里需要说明一下上面的代码是一个简化的示意版本重点在于理解“可选区间变量时间窗约束”的组合思路。3.5 输出形式日历、地图、文字三管齐下行程排完之后我拿到了三种格式的输出分别应对不同的使用场景ICS日历文件导入手机日历后每天几点到哪里一目了然。这是我在旅行中真正使用的版本。GeoJSON地图用folium生成一个带标记点的交互式网页地图方便在酒店大堂打开看今天的地理路线是否合理。Markdown行程单一份纯文本的可打印版本包含每个景点的地址、预计到达时间、预计离开时间、备注发给同行的人确认最方便。这三种输出各有用途。日历负责“提醒”地图负责“导航”Markdown负责“沟通”。如果只输出一种格式工具的使用价值会大打折扣而我一开始全都做了后来的旅行中三种格式都用上了。4. 调试过程中的常见坑与实用经验4.1 数据质量问题API返回的开放时间真的不能直接信这个项目里我踩的最大坑是Google Places API返回的开放时间可信度有限。比如我去巴塞罗那的毕加索博物馆时API返回的开放时间是正常的“上午10点到晚上7点”但我后来查到它的周一闭馆规则只在某些季节生效淡季甚至周末都不开。更麻烦的是有些景点的开放时间在节假日会突然改变API并不会实时反映这种变化。我采用的折中方案是脚本生成的行程会标注“需要二次确认开放时间”的景点旅行前再逐个人工核对。这不是偷懒而是因为数据源本身的更新滞后问题再聪明的算法也无法靠脏数据生成完全准确的行程。4.2 时间容忍度的设置如果你把行程排得太精确比如“12:47离开圣家堂12:52到达米拉之家”一旦某个环节延误整天的计划就全乱了。我在时间模型里加了一个“缓冲时间”参数默认每个景点之间的交通时间额外增加20%作为缓冲午餐时间固定给1小时而不是30分钟。这看起来浪费了时间但从实际体验看这个20%的缓冲几乎每次都会被排队、买水、拍照消耗掉最后生成的行程恰恰是刚好能完成的状态。还有一个隐藏参数是“每天景点数量上限”。我最初把每天上限设为5个结果模拟出来的行程从早上8点到晚上11点全是景点。后来我强制设置每天最多4个核心景点且必须包含一个“灵活替补景点”——如果提前逛完了可以“顺路加塞”参观如果延误了也可以毫无压力地放弃。这个设计极大提升了行程的容错率。4.3 城市顺序优化里最容易犯的错跨城行程的排序一开始我用的是简单的“距离最近优先”结果排出来的路线是马德里-托莱多-塞维利亚-格拉纳达-巴塞罗那看起来合理但忽略了高铁班次和“周五晚到格拉纳达”这种时间槽是不是合理的问题。后来我在模型里加入了“城市驻留最低时长”和“到达时间不晚于晚上8点”两个约束整个路线才开始合理起来。比如从塞维利亚去格拉纳达的高铁如果下午班次较少模型会自动把格拉纳达安排在塞维利亚之后且留足两天避免“到了地方只能吃夜宵”的尴尬。4.4 常见问题排查速查表症状可能原因解决办法行程里出现了“先往东走再往西折返”的路线评分函数里 travel_time_penalty 权重过低增大通勤惩罚项权重或把通勤改为硬约束某天景点数量太多根本不现实每日游览时长上限没设或设过大强制设置单日时长上限10小时以内景点开放时间明显错误API数据过时或当地节假日临时调整行程标注“需人工确认”出发前统一核验跨城高铁时间被忽略城市间转移的交通时间未计入当天把“跨城日”单独建模只安排到达后顺路景点两次请求API返回同一个景点有两份不同记录POI数据存在重复用名称地址双重去重输出地块没有步行路径OSMnx路网数据缺少某些街道切换到驾车模式或增加步行绕行系数这些坑在写第一版时几乎都踩了一遍每次返工都让我更确信一件事行程规划的难点不是算法理论而是把真实世界的约束准确地翻译成代码里的变量和条件。5. 这套工具还能怎么扩展写完这个西班牙行程工具之后我发现它不只是“旅行专用”。拆开来看它其实是一个“约束满足型排程器”换几个数据源就能适配更多场景。比如把“景点”换成“拜访客户”把“开放时间”换成“客户空闲时段”把“城市间交通”换成“航班/高铁时刻表”它就能变成一个差旅规划器把“景点”换成“餐厅候选”加上“营业时间和桌位预订”约束它又能变成美食探店计划器。我现在已经在规划把工具重构成一个配置文件驱动的通用版本。通过一个YAML文件定义“日历事件类型”和“约束规则”脚本就能自动适配不同场景。核心的ortools模型基本不用动变的只是输入数据的格式和输出模板的字段。对于旅行场景还有一个很实用的扩展方向把“预订环节”集成进来。目前脚本只负责生成建议后续可以接入景点门票预订页面的URL甚至根据开放日期的票务情况反向优化行程顺序。比如阿尔罕布拉宫的票经常需要提前两周抢那行程生成时就该把“可约票的日期”作为硬约束而不是事后让别人迁就你的行程。6. 写在最后的实际操作体会回头复盘这个项目最有成就感的瞬间不是脚本跑通的那一刻而是旅行中真的按照脚本生成的时间表走完一整天、却没有觉得被时间追赶的时候。那份行程单并没有把每天塞满它反而留出了大量看似“浪费”的缓冲时间而这些缓冲最后都被值得停留的地方填满了。我个人经验是行程规划工具的价值不是“替你决定去哪里”而是“替你处理那些单调、重复、容易出错的约束计算”把决策精力留给你真正想做的选择。如果你也经常为这种约束型问题头疼不妨试着把这套“抽象成数据 交给优化器求解”的思路用在手头的事情上很多时候只需要一次小小的拆解效果就会完全不一样。
返回列表