Python景区民宿推荐系统:地理围栏+价格敏感+文本相似度三重加权
简介这是一份面向Python中初级开发者与计算机专业学生的景区民宿推荐系统实战项目资料聚焦旅游科技场景下的个性化推荐落地解决用户住宿匹配、文旅数据决策支持等实际问题。资源为1个126KB的docx文档完整覆盖项目背景、多模型架构内容推荐协同过滤混合策略、FastAPI后端接口设计、pandas特征工程、scikit-learn算法实现、GUI界面逻辑及数据库ER图与建表脚本目录结构清晰含12处代码示例与模块化说明如景区周边距离筛选、标签向量构建、用户行为矩阵生成等关键环节。已有57人学习下载适合1–3年经验的软件或算法工程师在智慧文旅平台开发中复用模型思路也适合作为高校推荐系统课程的教学案例帮助读者从零搭建可运行的轻量化推荐服务深入理解地理特征融合、冷启动应对与实时排序机制。1. 为什么游客搜“西湖边民宿”却总刷出千篇一律的连锁酒店——一个能跑通、能改、能上线的Python景区民宿推荐系统实录你有没有试过在旅游平台搜“鼓浪屿民宿”结果首页全是带“网红打卡”标签、价格虚高、真实评价寥寥的房源不是算法不行而是大多数推荐逻辑根本没碰过“景区周边”这个特殊场景步行5分钟和15分钟对游客体验是天壤之别旺季平日价差可能翻3倍老城区小巷里的家庭旅馆评分不高但厨房能做饭、房东会画手绘地图——这些信息既不在结构化数据库里也不在用户显式评分中。本项目就是为解决这个断层而生它不依赖第三方API从零构建本地化数据闭环——爬取真实房源含位置坐标、房型图、住客实拍、用地理围栏文本相似度价格敏感度三重加权生成推荐最后用PyQt5搭出可直接双击运行的GUI界面数据库用SQLite轻量嵌入所有代码、建表SQL、测试数据、UI资源全打包连Windows下Python环境怎么配都写进README。适合想快速验证推荐逻辑的学生、接小型文旅项目的技术 freelancer或是想把“推荐系统课设”真正跑起来的开发者——不是教你怎么调sklearn而是告诉你当用户点开“大理古城南门”时系统如何在0.8秒内筛出3公里内、带露台、近3个月有早餐好评、且价格低于同区域均值15%的12家民宿。2. 从零搭建数据底座SQLite建库、爬虫存档与地理围栏预处理推荐系统的根基不是算法是数据质量。景区民宿数据有三大硬伤地址描述模糊如“洱海旁”“古城内”、坐标缺失或不准、价格动态浮动。本项目采用“人工校验程序兜底”双轨策略先建库再爬取再清洗避免后期返工。2.1 SQLite建库为什么不用MySQL或PostgreSQL轻量级项目首选SQLite——单文件、免服务、Python原生支持特别适合本项目部署场景景区管理方U盘拷贝即用、学生交作业免装数据库服务、甚至树莓派都能跑。关键字段设计直指痛点-- 民宿主表核心字段已加注释 CREATE TABLE hosts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- 民宿名称去广告词如“【官方】洱海景·海景房”→“洱海景民宿” address TEXT NOT NULL, -- 标准化地址经高德API逆地理编码后存例“大理市大理镇博爱路123号” lat REAL CHECK(lat BETWEEN 20 AND 50), -- 纬度中国范围校验 lng REAL CHECK(lng BETWEEN 73 AND 135),-- 经度中国范围校验 price_avg REAL DEFAULT 0, -- 近30天均价单位元/晚用于价格敏感度计算 rating REAL DEFAULT 0 CHECK(rating BETWEEN 0 AND 5), review_count INTEGER DEFAULT 0, has_breakfast BOOLEAN DEFAULT 0, -- 是否提供早餐布尔值比文本字段更易聚合 room_types TEXT, -- JSON字符串存房型列表例[大床房,亲子房,loft] update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 游记评论表支撑文本相似度计算 CREATE TABLE reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, host_id INTEGER NOT NULL, content TEXT NOT NULL, -- 原始评论文本保留标点用于jieba分词 sentiment_score REAL DEFAULT 0, -- 情感得分-1~1用SnowNLP训练后存 FOREIGN KEY (host_id) REFERENCES hosts(id) );提示room_types用TEXT存JSON而非新建关联表是权衡结果——本项目房型变更极少年均5次且查询时90%场景只需判断“是否含亲子房”JSON字段配合SQLite的json_extract()函数足够高效省去JOIN开销。2.2 爬虫模块绕过反爬的关键三步法非代理、不封IP景区民宿平台反爬极严但本项目不走代理池或云打码——成本高、不稳定。我们用“请求头指纹模拟动态延迟关键字段校验”三步落地# crawler.py 核心片段 import time import random from selenium import webdriver from selenium.webdriver.chrome.options import Options def get_driver(): options Options() options.add_argument(--headless) # 无头模式减少资源占用 options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) # 关键模拟真实用户行为指纹 options.add_argument(--user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) # 绕过webdriver检测 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }) }) return driver def crawl_host_list(city_name, radius_km5): driver get_driver() try: # 第一步搜索页获取基础URL例https://xxx.com/search?city大理keyword民宿 driver.get(fhttps://xxx.com/search?city{city_name}keyword民宿) time.sleep(random.uniform(2, 4)) # 随机延迟避开固定节奏 # 第二步提取所有房源卡片链接用XPath精确定位避免class名变动 links driver.find_elements(xpath, //div[classhotel-item]//a[href]) urls [link.get_attribute(href) for link in links[:20]] # 只取前20条保证质量 # 第三步逐个解析详情页关键字段校验防空数据入库 for url in urls: driver.get(url) time.sleep(random.uniform(1.5, 3)) # 地址必须含“市/区/路/街”三级否则跳过过滤“洱海边”等模糊描述 raw_addr driver.find_element(xpath, //span[contains(text(),地址)]/following-sibling::span).text if not re.search(r.*?(市|区|县).*?(路|街|巷|大道).*?, raw_addr): continue # 坐标必须存在且数值合理防0,0假坐标 lat_lng driver.find_element(xpath, //meta[namelocation]).get_attribute(content) if not lat_lng or 0,0 in lat_lng: continue # 存入数据库此处省略具体insert语句见完整代码包 save_to_db(name, raw_addr, lat, lng, price, rating) finally: driver.quit()参数说明radius_km5地理围栏半径针对景区设定西湖核心区设3km张家界武陵源设8kmrandom.uniform(1.5, 3)延迟区间非固定值模拟人类操作波动re.search(...)地址正则校验确保结构化入库后续地理围栏才可靠。2.3 地理围栏预处理用Haversine公式实现毫秒级距离计算推荐必须响应快。SQLite不支持地理函数我们预计算“景区中心点到所有民宿”的直线距离误差0.5km可接受存入distance_to_scenic字段# utils/geoutil.py from math import radians, sin, cos, sqrt, asin def haversine_distance(lat1, lng1, lat2, lng2): 计算两点间球面距离单位米 参数lat1/lng1 - 景区中心坐标lat2/lng2 - 民宿坐标 R 6371000 # 地球平均半径米 lat1, lng1, lat2, lng2 map(radians, [lat1, lng1, lat2, lng2]) dlat lat2 - lat1 dlng lng2 - lng1 a sin(dlat/2)**2 cos(lat1) * cos(lat2) * sin(dlng/2)**2 c 2 * asin(sqrt(a)) return R * c # 批量预计算示例在数据入库后执行一次 scenic_center (25.0434, 102.7129) # 大理古城中心坐标 for host in all_hosts: dist haversine_distance(scenic_center[0], scenic_center[1], host.lat, host.lng) update_db(host.id, distance_to_scenicdist)为什么不用PostGIS本项目目标设备可能是景区前台老旧电脑PostGIS需额外安装、配置复杂。Haversine预计算后推荐查询变成纯WHERE条件WHERE distance_to_scenic 3000SQLite执行速度稳定在20ms内。3. 推荐引擎核心三重加权排序与文本相似度实战推荐不是“找最近的”而是“找最匹配的”。本项目摒弃协同过滤冷启动严重和纯内容推荐忽略价格敏感采用三重加权地理权重40% 价格权重30% 文本相似度权重30%。重点讲清后两者如何落地。3.1 价格权重用Z-score标准化解决跨城市价格不可比问题杭州西湖边民宿均价800元丽江古城才300元直接比绝对值会误杀优质低价房。我们用Z-score将价格映射到[-1,1]区间# recommender/price_weight.py import numpy as np import sqlite3 def calculate_price_weight(hosts, target_city): 计算单个城市内民宿价格权重 返回{host_id: weight} 字典weight ∈ [-1, 1] conn sqlite3.connect(hosts.db) cursor conn.cursor() # 获取目标城市所有民宿价格排除异常值价格3倍标准差 cursor.execute( SELECT price_avg FROM hosts WHERE city ? AND price_avg 0 , (target_city,)) prices [row[0] for row in cursor.fetchall()] # Z-score标准化z (x - μ) / σ mean_price np.mean(prices) std_price np.std(prices) weights {} for host in hosts: if host.price_avg 0: weights[host.id] 0 else: z_score (host.price_avg - mean_price) / std_price # 映射到[-1,1]z越小价格越低权重越高 weight max(-1, min(1, -z_score / 3)) # 除以3压缩范围避免极端值 weights[host.id] round(weight, 3) conn.close() return weights # 示例大理古城民宿价格权重分布 # 价格200元远低于均值350→ weight 0.82 # 价格600元高于均值→ weight -0.41参数说明z_score / 3分母3是经验值确保95%数据落在[-1,1]内max(-1, min(1, ...))强制截断防止离群值破坏权重平衡权重为负值表示“价格偏高”在最终排序中会被降权。3.2 文本相似度用TF-IDF余弦相似度匹配游客偏好关键词游客搜索“带厨房民宿”“宠物友好”“安静”时不能只靠标签匹配很多民宿不填标签。我们用评论文本挖掘隐含属性# recommender/text_similarity.py from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import jieba def build_tfidf_matrix(reviews): 构建评论TF-IDF矩阵中文分词停用词过滤 # 加载停用词自定义文件stopwords.txt含“非常”“真的”“推荐”等无意义词 with open(data/stopwords.txt, r, encodingutf-8) as f: stopwords set([line.strip() for line in f]) # 中文分词并过滤停用词 def chinese_tokenizer(text): words jieba.lcut(text) return [w for w in words if w not in stopwords and len(w) 1] vectorizer TfidfVectorizer( tokenizerchinese_tokenizer, max_features5000, # 限制特征数加快计算 ngram_range(1, 2) # 支持单字词二字词如“厨房”“宠物友好” ) tfidf_matrix vectorizer.fit_transform(reviews) return tfidf_matrix, vectorizer def calculate_text_similarity(query_keywords, tfidf_matrix, vectorizer): 计算查询词与所有评论的相似度均值 query_keywords: str, 如厨房 宠物 古城 # 将查询词转为TF-IDF向量 query_vec vectorizer.transform([query_keywords]) # 计算余弦相似度矩阵每行是1个评论vs查询 similarities cosine_similarity(query_vec, tfidf_matrix).flatten() # 按host_id聚合取该民宿所有评论相似度的均值 # 假设reviews列表按host_id顺序排列实际需关联host_id host_similarities {} for i, sim in enumerate(similarities): host_id get_host_id_from_review_index(i) # 实际需从reviews表join获取 if host_id not in host_similarities: host_similarities[host_id] [] host_similarities[host_id].append(sim) # 返回每个host_id的平均相似度归一化到[0,1] result {} for host_id, sims in host_similarities.items(): avg_sim np.mean(sims) result[host_id] round(avg_sim, 3) # 保留3位小数 return result # 示例搜索“安静 古城” → 某民宿10条评论平均相似度0.62权重0.31避坑中文TF-IDF的三个血泪经验分词颗粒度陷阱jieba默认分词会把“宠物友好”切成“宠物/友好”丢失语义。解决方案在jieba.load_userdict()中预加载领域词典如data/user_dict.txt含“宠物友好”“厨房设施”“古城夜景”稀疏矩阵内存爆炸5000个民宿×5000特征2500万元素CSR格式内存仅需20MB但.toarray()会暴涨到200MB。务必用cosine_similarity()直接计算勿转稠密矩阵查询词权重失真游客搜“安静”时应比“民宿”权重高。解决方案对查询词做TF-IDF时手动提升关键词权重——vectorizer.transform([f安静:2 宠物:1.5])需修改sklearn源码或用TfidfTransformer单独处理。3.3 三重加权融合线性组合与边界保护最终得分 地理权重 × 0.4 价格权重 × 0.3 文本相似度 × 0.3但需防止单一维度失控# recommender/final_score.py def calculate_final_score(hosts, geo_weights, price_weights, text_weights, geo_threshold3000, price_flex0.3): 计算最终推荐得分 geo_threshold: 地理围栏阈值米超限直接得0分 price_flex: 价格容忍度±30%超限则价格权重置0 scores {} for host in hosts: # 地理硬过滤 if host.distance_to_scenic geo_threshold: scores[host.id] 0 continue # 价格柔性过滤若价格超出目标区间±30%价格权重归零 target_price host.price_avg * (1 price_flex) # 允许上浮30% if host.price_avg target_price: final_price_weight 0 else: final_price_weight price_weights.get(host.id, 0) # 融合计算三者均为[-1,1]最终得分∈[-1,1] score ( geo_weights.get(host.id, 0) * 0.4 final_price_weight * 0.3 text_weights.get(host.id, 0) * 0.3 ) scores[host.id] round(score, 3) return scores # 示例输出 # host_id123: score0.72 (地理0.9×0.4 价格0.6×0.3 文本0.8×0.3) # host_id456: score0.00 (地理距离3200m 3000m阈值)参数说明geo_threshold3000景区核心圈层大理古城设3000m九寨沟设5000mprice_flex0.3允许价格上浮30%但绝不接受降价游客搜“高端民宿”时低价房不应混入得分归一化到[-1,1]便于前端用颜色梯度可视化绿色0.5黄色0~0.5红色0。4. GUI设计PyQt5实现零学习成本的交互界面与数据热更新GUI不是装饰是降低使用门槛的核心。本项目GUI设计原则所有操作双击即用、所有数据实时可见、所有参数可一键重置。不用QML纯PyQt5 Widgets兼容Win7。4.1 主界面布局三层结构解决信息过载# gui/main_window.py from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QStatusBar) from PyQt5.QtCore import Qt class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(景区民宿推荐系统 v1.0) self.setGeometry(100, 100, 1200, 800) # 固定尺寸避免缩放错乱 # 顶层搜索控制区3个输入1个按钮 top_widget QWidget() top_layout QHBoxLayout() self.city_combo QComboBox() self.city_combo.addItems([大理古城, 西湖景区, 鼓浪屿, 张家界]) self.city_combo.setCurrentText(大理古城) self.keyword_input QLineEdit() self.keyword_input.setPlaceholderText(请输入偏好如安静、带厨房、宠物友好) self.radius_input QLineEdit(3) # 默认3km self.radius_input.setFixedWidth(60) self.radius_input.setValidator(QDoubleValidator(0.5, 10, 1)) # 限制0.5~10km search_btn QPushButton( 开始推荐) search_btn.clicked.connect(self.on_search_click) top_layout.addWidget(QLabel(景区)) top_layout.addWidget(self.city_combo) top_layout.addWidget(QLabel(偏好)) top_layout.addWidget(self.keyword_input) top_layout.addWidget(QLabel(半径(km))) top_layout.addWidget(self.radius_input) top_layout.addWidget(search_btn) top_layout.addStretch() top_widget.setLayout(top_layout) # 中层结果表格可排序、可导出 self.result_table QTableWidget() self.result_table.setColumnCount(6) self.result_table.setHorizontalHeaderLabels([ 排名, 民宿名称, 距离(米), 价格(元/晚), 评分, 推荐理由 ]) self.result_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) # 底层状态栏与热更新按钮 self.statusBar QStatusBar() self.statusBar.showMessage(就绪。选择景区后点击搜索。) # 主布局 central_widget QWidget() main_layout QVBoxLayout() main_layout.addWidget(top_widget) main_layout.addWidget(self.result_table) main_layout.addWidget(self.statusBar) central_widget.setLayout(main_layout) self.setCentralWidget(central_widget)为什么用QTableWidget不用QListView游客需要横向对比距离、价格、评分三列同时可见。QTableWidget支持列宽拖拽、点击表头排序self.result_table.setSortingEnabled(True)且导出CSV只需遍历item(row, col).text()代码量10行。4.2 数据热更新SQLite监听自动刷新告别重启用户修改数据库后GUI应立即响应。SQLite本身不支持监听我们用文件时间戳轮询# gui/monitor.py import os import time from threading import Thread class DBMonitor(Thread): def __init__(self, db_path, callback): super().__init__() self.db_path db_path self.callback callback self.last_mtime os.path.getmtime(db_path) self.running True def run(self): while self.running: try: current_mtime os.path.getmtime(self.db_path) if current_mtime ! self.last_mtime: self.last_mtime current_mtime self.callback() # 触发GUI刷新 except OSError: pass # 文件被其他进程占用时忽略 time.sleep(2) # 每2秒检查一次平衡性能与实时性 def stop(self): self.running False # 在MainWindow中启用 self.db_monitor DBMonitor(hosts.db, self.refresh_table) self.db_monitor.start() def refresh_table(self): 重新加载表格数据 self.result_table.setRowCount(0) # 清空 results self.recommender.run_recommendation( self.city_combo.currentText(), self.keyword_input.text(), float(self.radius_input.text()) ) for i, r in enumerate(results): self.result_table.insertRow(i) self.result_table.setItem(i, 0, QTableWidgetItem(str(i1))) self.result_table.setItem(i, 1, QTableWidgetItem(r[name])) self.result_table.setItem(i, 2, QTableWidgetItem(str(int(r[distance])))) self.result_table.setItem(i, 3, QTableWidgetItem(str(r[price]))) self.result_table.setItem(i, 4, QTableWidgetItem(str(r[rating]))) self.result_table.setItem(i, 5, QTableWidgetItem(r[reason]))参数说明time.sleep(2)2秒间隔是实测平衡点——短于1秒CPU占用15%长于5秒用户感知延迟OSError捕获Windows下SQLite写入时文件被锁轮询会报错需静默处理self.result_table.setRowCount(0)比clear()更安全避免列标题丢失。4.3 导出功能一键生成Excel报告适配景区管理需求游客只需看前3条但景区运营方需要完整数据报表。导出按钮生成带样式的Excel# gui/exporter.py import pandas as pd from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment def export_to_excel(results, filename): 导出推荐结果到Excel含样式 df pd.DataFrame(results) df df[[rank, name, distance, price, rating, reason]] df.columns [排名, 民宿名称, 距离(米), 价格(元/晚), 评分, 推荐理由] # 创建Excel文件 wb Workbook() ws wb.active ws.title 推荐结果 # 写入表头加粗背景色 header_font Font(boldTrue) header_fill PatternFill(start_colorD3D3D3, end_colorD3D3D3, fill_typesolid) for col_num, column_title in enumerate(df.columns, 1): cell ws.cell(row1, columncol_num, valuecolumn_title) cell.font header_font cell.fill header_fill cell.alignment Alignment(horizontalcenter) # 写入数据 for row_num, row_data in enumerate(df.values, 2): for col_num, cell_value in enumerate(row_data, 1): ws.cell(rowrow_num, columncol_num, valuecell_value) # 自动列宽 for column in ws.columns: max_length 0 column_letter column[0].column_letter for cell in column: try: if len(str(cell.value)) max_length: max_length len(str(cell.value)) except: pass adjusted_width min(max_length 2, 50) # 限制最大宽度 ws.column_dimensions[column_letter].width adjusted_width wb.save(filename) # GUI中调用 export_btn QPushButton( 导出Excel) export_btn.clicked.connect(lambda: export_to_excel(self.current_results, recommendation.xlsx))为什么用openpyxl不用xlwtxlwt不支持Excel 2007格式.xlsx且无法设置列宽。openpyxl虽慢于pandas.to_excel()但能精确控制样式——景区汇报PPT里常需截图Excel带灰底表头的专业感很重要。5. 避坑指南开发与部署中踩过的5个真实坑及解决方案本项目从学生课设起步历经3个景区实地部署以下坑位均来自真实翻车现场按发生频率排序5.1 现象GUI启动时报错“QApplication was not created in the main thread”原因PyQt5要求QApplication必须在主线程创建但爬虫用了多线程threading.Thread且未正确传递事件循环。常见于在__init__中直接启动爬虫线程。解决所有GUI操作严格限定在主线程。爬虫任务用QThread重写并通过信号槽通信# 正确做法用QThread替代threading.Thread class CrawlWorker(QThread): finished pyqtSignal(list) # 发送结果信号 def run(self): results crawl_host_list(大理古城) self.finished.emit(results) # 在子线程中emit信号 # 在MainWindow中 self.crawler CrawlWorker() self.crawler.finished.connect(self.on_crawl_finished) # 槽函数在主线程执行 self.crawler.start()5.2 现象SQLite数据库被锁定GUI反复弹窗“database is locked”原因爬虫写入和GUI读取同时访问同一数据库文件且未设置连接超时。Windows下尤其严重。解决所有数据库连接添加timeout20.0参数单位秒读操作用sqlite3.connect(..., check_same_threadFalse)并确保每次查询后conn.close()写操作用事务包裹减少锁持有时间conn.execute(BEGIN TRANSACTION) conn.executemany(INSERT INTO hosts VALUES (?, ?, ?, ?, ?), data) conn.execute(COMMIT) # 或 conn.rollback() on error5.3 现象中文路径下程序崩溃报错“UnicodeEncodeError: gbk codec cant encode character”原因Windows默认GBK编码但Python 3.7用UTF-8当用户将程序放在“C:\用户\张三\下载\”路径时os.listdir()返回GBK字符串引发错误。解决全局设置Python默认编码在main.py开头import sys import locale # 强制UTF-8编码 if sys.platform win32: try: locale.setlocale(locale.LC_ALL, Chinese) except: pass sys.stdout.reconfigure(encodingutf-8) sys.stderr.reconfigure(encodingutf-8)5.4 现象PyQt5界面在高DPI屏幕如Surface Pro上文字模糊原因Windows未启用DPI感知Qt自动缩放导致字体渲染失真。解决在main.py最顶部添加import ctypes ctypes.windll.shcore.SetProcessDpiAwareness(1) # 启用DPI感知并确保pyinstaller打包时加参数--add-data path/to/qt/platforms;platforms避免Qt平台插件丢失。5.5 现象打包成exe后jieba分词报错“Dictionary file not found”原因jieba默认词典路径为jieba/dict.txtPyInstaller打包后路径变为_MEIxxxx/jieba/dict.txt但jieba仍读取原始路径。解决打包时用--add-data jieba/dict.txt;jieba包含词典运行时动态设置词典路径import jieba import sys import os if getattr(sys, frozen, False): # PyInstaller打包后 base_path sys._MEIPASS else: base_path os.path.dirname(os.path.abspath(__file__)) jieba.set_dictionary(os.path.join(base_path, jieba, dict.txt))6. 进阶技巧用Flask轻量封装让推荐系统变身为景区微信小程序后端GUI适合单机演示但景区真正需要的是游客扫二维码微信里直接查“我附近有什么民宿”。本项目预留了Flask接口层30行代码即可变身Web服务——不需Docker、不需Nginxpython app.py即启动。6.1 最小可行APIGET接口返回JSON推荐结果# api/app.py from flask import Flask, request, jsonify from recommender.core import run_recommendation import sqlite3 app Flask(__name__) app.route(/recommend, methods[GET]) def recommend_api(): GET /recommend?city大理古城keyword安静radius3 返回JSON格式推荐列表 city request.args.get(city, 大理古城) keyword request.args.get(keyword, ) radius float(request.args.get(radius, 3)) try: results run_recommendation(city, keyword, radius) return jsonify({ status: success, count: len(results), data: results }) except Exception as e: return jsonify({ status: error, message: str(e) }), 400 # 启动服务生产环境请用gunicorn此处仅开发用 if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 关闭debug避免敏感信息泄露关键配置host0.0.0.0允许局域网内其他设备访问景区WiFi下手机可直连debugFalse生产环境必须关闭否则堆栈信息暴露数据库路径返回JSON含count字段小程序前端可据此显示“找到12家民宿”。6.2 微信小程序对接前端只需3行wx.request小程序pages/index/index.js中// 调用推荐API假设服务器IP为192.168.1.100 wx.request({ url: http://192.168.1.100:5000/recommend, data: { city: 大理古城, keyword: 带厨房, radius: 3 }, success: (res) { this.setData({ hosts: res.data.data }) // 直接绑定到页面 } })为什么不用HTTPS景区内网无需SSL证书。若需外网访问用nginx反向代理Lets Encrypt免费证书配置仅需5行详见deploy/nginx.conf。6.3 数据看板用SQLite触发器自动统计告别定时脚本景区管理方需要“今日推荐次数”“各景区热度TOP5”。我们用SQLite触发器实时统计无需额外进程-- 创建统计表 CREATE TABLE stats ( id INTEGER PRIMARY KEY, date TEXT DEFAULT (date(now)), city TEXT, keyword TEXT, count INTEGER DEFAULT 1 ); -- 创建触发器每次INSERT推荐结果时自动更新统计 CREATE TRIGGER update_stats AFTER INSERT ON hosts BEGIN INSERT INTO stats(city, keyword, count) VALUES (NEW.city, NEW.keyword, 1) ON CONFLICT(date, city, keyword) DO UPDATE SET count count 1; END; -- 查询今日大理古城“安静”关键词热度 SELECT SUM(count) FROM stats WHERE date date(now) AND city 大理古城 AND keyword 安静;参数说明ON CONFLICT语法需SQLite 3.24本项目要求最低版本3.25统计表按(date, city, keyword)联合唯一避免重复计数查询时用SUM(count)而非COUNT(*)因单本文还有配套的精品资源点击获取

相关新闻

ModelArts入门实操指南:从数据准备到模型部署的全流程解析

ModelArts入门实操指南:从数据准备到模型部署的全流程解析

1. 先从"为什么学ModelArts"说起:平台定位与我的学习路线图我是在一次内部技术分享会上第一次接触华为云ModelArts的。当时组里一个图像识别项目要在三周内出可演示的Demo,传统做法是本地训练完再手动打镜像、写部署脚本,光环境迁移…

2026/10/12 9:42:25 阅读更多 →
porn-image-detect实战指南:语义边界、轻量化部署与多模态证据链

porn-image-detect实战指南:语义边界、轻量化部署与多模态证据链

简介:这是一份轻量级色情图片检测工具,面向Python初学者与图像处理入门开发者,提供基于色彩空间转换的简易实现方案,适用于内容安全审核、教学演示或本地化图像过滤等低复杂度场景。资源包共7个文件,含2个核心Python脚…

2026/10/12 9:41:23 阅读更多 →
从感知机到SVM:三次升级解锁分类算法核心

从感知机到SVM:三次升级解锁分类算法核心

1. 为什么把感知机和SVM放在一起讲一谈到基础分类算法,感知机和支持向量机(SVM)这两兄弟总是被放在一起讲。很多人学完一轮课程,感知机会用,SVM也会调包,但问一句“SVM到底比感知机强在哪”,十个…

2026/10/12 9:41:23 阅读更多 →

最新新闻

收藏 | 2026年颠覆工作方式的AI编程(小白程序员必备)!TaoToken 统一 Key 接入 Claude Code 与 Codex 实战

收藏 | 2026年颠覆工作方式的AI编程(小白程序员必备)!TaoToken 统一 Key 接入 Claude Code 与 Codex 实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 10:23:11 阅读更多 →
Elsevier返修投稿踩坑分享:LaTeX编译tex转PDF的隐藏坑与TaoToken排查思路

Elsevier返修投稿踩坑分享:LaTeX编译tex转PDF的隐藏坑与TaoToken排查思路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 10:23:10 阅读更多 →
Postgresql报错字段找不到?用TaoToken统一通道排查psycopg2.errors.UndefinedColumn的完整思路

Postgresql报错字段找不到?用TaoToken统一通道排查psycopg2.errors.UndefinedColumn的完整思路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 10:23:10 阅读更多 →
驱动开发入门:从最小骨架到中断、并发与内存实战

驱动开发入门:从最小骨架到中断、并发与内存实战

《驱动之路》这套笔记,我从大半年前就开始准备了。真正动笔时,摆在面前的头一个问题并不是技术选型,而是“怎么开篇”。技术内容可以慢慢排,但作为整套内容的门面,这篇开篇得同时干两件事:一是自序&#xf…

2026/10/12 10:23:10 阅读更多 →
容器化 Claude Code 运行环境:Docker 镜像构建与 CI 环境一致性解决方案(TaoToken 统一 Key 接入版)

容器化 Claude Code 运行环境:Docker 镜像构建与 CI 环境一致性解决方案(TaoToken 统一 Key 接入版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 10:23:10 阅读更多 →
完美立方问题详解:循环边界、枚举优化与浮点比较避坑指南

完美立方问题详解:循环边界、枚举优化与浮点比较避坑指南

前阵子刷某高校《计算机概论和程序设计基础》这门MOOC课程,循环练习那一节配了一道非常经典的题——完美立方。第一眼看过去真不难:枚举四个整数,判断立方等式成立就行。可真正动手才发现,题面里那些不起眼的限制条件,…

2026/10/12 10:22:09 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →