从河北焖子到安徽板面:用Python爬虫与GIS技术分析地方美食分布
最近在和朋友聊起地方特色美食时发现一个挺有意思的现象很多朋友在河北尝过地道的“焖子”后都会好奇地问那在安徽能不能吃到同样正宗的“板面”呢这背后其实不只是“吃”的问题更涉及到地方美食的传播、供应链、口味适应以及我们如何利用技术去发现和体验这些美食。作为一名开发者我习惯性地会从数据和技术的角度去拆解这类问题。本文将从一个技术实践者的视角带大家一步步分析“地方美食的可及性”问题并尝试用数据爬取、地理信息处理和简单的推荐逻辑构建一个探索性的分析模型。无论你是对美食数据感兴趣还是想学习如何将生活问题转化为技术项目这篇文章都能给你带来一些实用的思路和可运行的代码。1. 背景与核心概念从美食到数据在开始技术部分之前我们先明确几个核心概念。这有助于我们理解后续要处理的数据和技术目标。“河北焖子”与“安徽板面”这里我们将其抽象为两类具有强烈地域属性的商品或服务POI, Point of Interest。焖子主要流行于河北部分地区是当地饮食文化的代表安徽板面尤其是太和板面则是安徽的特色小吃。它们的“可及性”问题本质上是一个空间查询与匹配问题。核心问题拆解数据获取我们如何知道哪些地方有卖“焖子”或“板面”需要从互联网如生活服务类App、地图、点评网站获取店铺信息。地理位置处理获取的店铺都有经纬度坐标如何判断一个地点如“安徽”是否有足够多的目标店铺分析与可视化如何将分析结果清晰呈现比如在安徽地图上标注出板面店的位置。技术映射数据获取- 网络爬虫技术如requests,BeautifulSoup,Selenium。地理位置处理- 地理信息系统GIS基础坐标处理空间过滤。数据可视化- 地图库如folium,pyecharts。项目整合- 使用Python作为主要语言构建一个端到端的分析脚本。本文的目标不是做一个商业级的推荐系统而是通过这个有趣的问题串联起数据采集、清洗、分析和可视化的完整流程提供一个可复现的技术实践案例。2. 环境准备与版本说明我们将使用 Python 作为主要工具。请确保你的开发环境已就绪。操作系统Windows 10/11, macOS 或 Linux 均可。本文示例在 Windows 11 下开发。Python 版本3.8 及以上。推荐使用 3.9 或 3.10以获得更好的库兼容性。开发工具你可以使用任何喜欢的 IDE如 PyCharm、VS Code 或 Jupyter Notebook。本文代码将以脚本形式呈现。核心依赖库 我们需要安装以下 Python 库。建议使用pip在虚拟环境中安装。# 数据请求与解析 pip install requests beautifulsoup4 lxml # 模拟浏览器操作针对动态加载的网站 pip install selenium webdriver-manager # 数据处理 pip install pandas numpy # 地理编码与地图可视化 pip install geopy folium # 进度显示可选但推荐 pip install tqdm版本参考写作时测试环境requests2.28.2beautifulsoup44.12.2selenium4.15.0pandas2.1.4folium0.15.1geopy2.4.0重要提示网络爬虫应遵守目标网站的robots.txt协议并控制请求频率避免对对方服务器造成压力。本文示例仅用于技术学习请勿用于大规模商业数据抓取。3. 核心思路与技术选型拆解我们的项目流程可以划分为四个主要阶段每个阶段都有对应的技术选型和考量。3.1 阶段一数据来源与爬取策略数据是分析的基础。对于“美食店铺”数据有几个潜在来源公开地图API如高德、百度地图提供丰富的POI数据包括名称、地址、坐标、评分等。通常有免费额度但需申请密钥AK且有调用次数限制。生活服务/点评网站如大众点评数据更垂直包含用户评价、人均消费等。但反爬机制严格需要更复杂的技术如Selenium模拟、Cookie池。本地信息网站或公众号数据可能不全面格式不规范清洗成本高。技术选型入门/学习优先选择提供友好API的平台如高德地图开放平台。它流程规范适合学习HTTP请求和JSON数据处理。进阶/挑战可以尝试用Selenium模拟浏览器操作抓取点评类网站。这涉及到动态页面渲染、验证码处理等更深的内容。本文策略为了聚焦核心流程并保证可复现性我们将以高德地图Web服务API作为主要数据源进行演示。你需要自行前往高德开放平台注册并获取一个Web服务类型的Key。3.2 阶段二地理编码与区域过滤我们拿到店铺的坐标后需要解决两个问题地理编码Geocoding如何将“安徽省”这样一个文本地名转换成可以用于空间查询的地理范围如矩形区域或多边形这需要用到地理编码服务将地址转换为坐标。高德API也提供此功能。空间过滤如何判断一个坐标点店铺是否落在“安徽省”的范围内简单的方法是使用“矩形边界框”Bounding Box。我们可以获取安徽省的大致经纬度范围最小经度、最小纬度、最大经度、最大纬度然后检查店铺坐标是否在此框内。更精确的方法需要地理围栏Geo-fencing或多边形包含判断但矩形框对于省级范围的分析通常足够。3.3 阶段三数据清洗与存储爬取到的原始数据往往是杂乱无章的需要清洗去重同一家店可能被多次抓取。字段提取从API返回的复杂JSON中提取我们关心的字段店名、地址、坐标、电话等。异常值处理剔除坐标明显错误如落在海洋里的数据。存储对于中小规模数据保存为CSV或JSON文件最为方便。使用pandas的DataFrame进行中间处理是标准做法。3.4 阶段四可视化与分析结论最后我们需要将结果直观展示地图可视化使用folium库可以轻松生成交互式HTML地图。将河北的焖子店和安徽的板面店分别用不同颜色的标记点标注在地图上。简单统计计算每个省份目标店铺的数量、密度需结合面积并输出基本的统计信息。结论基于可视化结果和统计数据回答“在安徽能否方便地吃到板面”这个问题。答案可能是一个分布图板面店在安徽尤其是太和县周边高度聚集而在其他省份则零星分布。4. 完整实战案例从API到地图接下来我们按照上述流程实现一个完整的分析脚本。我们将分别抓取“焖子”和“板面”相关的POI并在地图上展示。4.1 第一步获取并配置高德地图API Key访问 高德开放平台 。注册并登录。进入「应用管理」-「我的应用」点击「创建新应用」。应用类型选择「Web服务」。创建成功后在该应用下点击「添加Key」。Key名称自定服务平台选择「Web服务」。提交后你将获得一个以“你的Key”形式的字符串。请妥善保管。在代码中我们将其存储为变量。# config.py (或直接在脚本开头定义) AMAP_API_KEY 你的高德地图Web服务API Key # 请替换为你的真实Key4.2 第二步编写核心数据抓取函数高德地图的POI搜索API文档非常详细。我们编写一个通用的抓取函数。# poi_crawler.py import requests import pandas as pd import time from tqdm import tqdm # 用于显示进度条 import math def fetch_poi_by_polygon(keywords, city, api_key, page_size20, max_pages50): 根据城市名称抓取指定关键词的POI数据。 由于API单次返回最多100条且受总条数限制此函数通过分页抓取。 Args: keywords (str): 搜索关键词如“焖子”、“板面”。 city (str): 城市名称如“石家庄市”、“合肥市”。支持省、市、区。 api_key (str): 高德地图API Key。 page_size (int): 每页条数最大25免费版实际测试20较稳。 max_pages (int): 最大抓取页数防止无限循环。 Returns: pandas.DataFrame: 包含POI信息的DataFrame。 url https://restapi.amap.com/v3/place/text # 先获取指定城市的地理编码用于后续的矩形区域搜索更准确 # 这里为了简化我们直接使用城市名进行文本搜索。更精确的做法是先获取城市边界。 all_pois [] for page in tqdm(range(1, max_pages 1), descf抓取 {keywords} in {city}): params { key: api_key, keywords: keywords, city: city, # 限定城市 offset: page_size, # 每页记录数 page: page, # 页码 extensions: all, # 返回详细信息 output: json } try: response requests.get(url, paramsparams, timeout10) result response.json() if result[status] 1 and result[info] OK: pois result[pois] if not pois: # 如果当前页没有数据说明已抓取完毕 break for poi in pois: # 提取我们需要的信息 poi_info { id: poi.get(id), name: poi.get(name), address: poi.get(address), location: poi.get(location), # 格式经度,纬度 pname: poi.get(pname), # 省份 cityname: poi.get(cityname), # 城市 adname: poi.get(adname), # 区县 tel: poi.get(tel), type: poi.get(type), keywords: keywords } all_pois.append(poi_info) # 礼貌性延迟避免请求过快 time.sleep(0.1) else: print(f请求失败: {result.get(info)}) break except Exception as e: print(f第{page}页抓取出错: {e}) break # 转换为DataFrame df pd.DataFrame(all_pois) if not df.empty: # 拆分location字段为单独的经度(longitude)和纬度(latitude) df[[longitude, latitude]] df[location].str.split(,, expandTrue).astype(float) df.drop(columns[location], inplaceTrue) return df4.3 第三步抓取“焖子”和“板面”数据我们选择几个代表性城市进行抓取。注意API对免费用户有每日调用次数限制请酌情选择城市范围。# main.py from poi_crawler import fetch_poi_by_polygon from config import AMAP_API_KEY import pandas as pd # 定义搜索任务 search_tasks [ {keywords: 焖子, city: 石家庄市}, {keywords: 焖子, city: 保定市}, {keywords: 焖子, city: 唐山市}, {keywords: 板面, city: 合肥市}, {keywords: 板面, city: 阜阳市}, # 太和板面属于阜阳市 {keywords: 板面, city: 北京市}, # 作为对比看看外地有多少 {keywords: 板面, city: 上海市}, ] all_data_frames [] for task in search_tasks: print(f\n开始抓取: {task[keywords]} - {task[city]}) df fetch_poi_by_polygon( keywordstask[keywords], citytask[city], api_keyAMAP_API_KEY, page_size20, max_pages10 # 每个城市最多抓10页约200条 ) if not df.empty: all_data_frames.append(df) else: print(f 未在{task[city]}找到关于{task[keywords]}的POI。) # 合并所有数据 if all_data_frames: combined_df pd.concat(all_data_frames, ignore_indexTrue) print(f\n总共抓取到 {len(combined_df)} 条POI数据。) # 简单查看数据 print(combined_df[[name, pname, cityname, address, keywords]].head()) # 保存到CSV文件 combined_df.to_csv(poi_data.csv, indexFalse, encodingutf-8-sig) print(数据已保存到 poi_data.csv) else: print(未抓取到任何数据。)运行结果预览 执行上述脚本后你会看到控制台输出抓取进度并最终生成一个poi_data.csv文件。用Excel或文本编辑器打开可以看到类似下面的数据namepnamecitynameaddresskeywordslongitudelatitude老王焖子河北省石家庄市中山路XX号焖子114.51234538.043210太和板面阜阳一店安徽省阜阳市颍州路XX号板面115.81432132.8897654.4 第四步数据分析与可视化现在我们有了数据可以开始分析了。首先加载数据然后进行可视化。# analysis_and_viz.py import pandas as pd import folium from folium import plugins import webbrowser # 1. 加载数据 df pd.read_csv(poi_data.csv, encodingutf-8-sig) print(f数据概览) print(df[keywords].value_counts()) print(f\n省份分布) print(df[pname].value_counts()) # 2. 创建基础地图以中国中部为中心 map_center [34.0, 113.0] # 大致中国中部坐标 m folium.Map(locationmap_center, zoom_start5, control_scaleTrue) # 3. 为不同关键词定义不同的图标颜色 color_dict { 焖子: red, 板面: blue } # 4. 将数据点添加到地图 for idx, row in df.iterrows(): if pd.notna(row[latitude]) and pd.notna(row[longitude]): # 创建弹出信息 popup_text f b店名/b: {row[name]}br b地址/b: {row[address]}br b类型/b: {row[keywords]}br b区域/b: {row[pname]}-{row[cityname]} # 添加标记 folium.CircleMarker( location[row[latitude], row[longitude]], radius5, # 点的大小 popuppopup_text, colorcolor_dict.get(row[keywords], gray), fillTrue, fill_colorcolor_dict.get(row[keywords], gray), fill_opacity0.7 ).add_to(m) # 5. 添加图例需要自定义HTML legend_html div styleposition: fixed; bottom: 50px; left: 50px; width: 150px; height: 90px; border:2px solid grey; z-index:9999; font-size:14px; background-color:white; padding: 10px; b美食分布图/bbr i stylebackground:red; width:15px; height:15px; display:inline-block; margin-right:5px;/i 焖子br i stylebackground:blue; width:15px; height:15px; display:inline-block; margin-right:5px;/i 板面 /div m.get_root().html.add_child(folium.Element(legend_html)) # 6. 保存地图并自动打开 map_file china_food_map.html m.save(map_file) print(f地图已生成: {map_file}) # 自动在默认浏览器中打开 webbrowser.open(file:// os.path.realpath(map_file))运行与结果 执行此脚本后它会生成一个名为china_food_map.html的交互式地图文件并自动在浏览器中打开。你可以看到红色点代表“焖子”店主要集中在河北省的几个城市。蓝色点代表“板面”店在安徽省尤其是阜阳市密集分布在北京、上海也有零星分布。4.5 第五步深入分析 - 计算分布密度仅仅看地图还不够直观。我们可以进行简单的量化分析。# density_analysis.py import pandas as pd df pd.read_csv(poi_data.csv, encodingutf-8-sig) # 按省份和关键词统计数量 province_keyword_stats df.groupby([pname, keywords]).size().unstack(fill_value0) print( 各省份美食数量统计 ) print(province_keyword_stats) # 计算“板面”在安徽省的集中度 anhui_banmian province_keyword_stats.loc[安徽省, 板面] if 安徽省 in province_keyword_stats.index and 板面 in province_keyword_stats.columns else 0 total_banmian df[df[keywords] 板面].shape[0] if total_banmian 0: concentration_ratio anhui_banmian / total_banmian print(f\n 板面分布分析 ) print(f板面店总数量: {total_banmian}) print(f安徽省板面店数量: {anhui_banmian}) print(f板面在安徽省的集中度: {concentration_ratio:.2%}) if concentration_ratio 0.5: print(结论板面具有显著的地域集中性在安徽更容易吃到正宗的。) else: print(结论板面分布较为分散在安徽以外也可能找到。) else: print(未抓取到板面数据。) # 同样可以分析“焖子”在河北的集中度 # ... (代码类似略)通过这个简单的统计我们可以得到一个量化的结论例如“本次抓取的数据显示超过80%的板面店位于安徽省内”从而科学地回答最初的问题。5. 常见问题与排查思路在实践过程中你可能会遇到以下问题问题现象可能原因解决思路运行爬虫脚本无数据返回或返回“INVALID_USER_KEY”1. API Key 错误或未启用。2. 未正确配置“Web服务”类型的Key。3. 当日调用次数已超限。1. 检查AMAP_API_KEY变量是否填写正确。2. 登录高德开放平台确认Key的“服务平台”是否为“Web服务”。3. 在控制台查看调用次数统计或次日再试。抓取数据量远少于预期1. 搜索关键词不准确或城市范围太小。2. API分页逻辑问题max_pages设置太小。3. 网络请求被限制或中断。1. 尝试更宽泛的关键词如“河北焖子”或更大的城市范围如“河北省”。注意省级搜索可能返回不精确。2. 适当增加max_pages参数并添加异常捕获和重试机制。3. 增加time.sleep()延迟模拟真人操作。folium生成的地图为空白或无法显示1. 经纬度数据列有误导致坐标点落在境外如非洲。2. 地图初始缩放级别或中心点设置不当。3. 浏览器禁止加载本地HTML文件。1. 检查df[[longitude, latitude]]的值是否在合理范围内中国大致经度73-135纬度3-53。2. 调整Map的location和zoom_start参数。3. 尝试使用其他浏览器打开或通过Python的HTTP服务器访问python -m http.server。错误ImportError: No module named ‘xxx’所需的Python库没有安装。使用pip install xxx命令安装缺失的库。确保在正确的Python环境下安装。数据中有大量重复项1. 不同搜索任务间有重叠区域。2. API本身返回了重复数据。1. 在合并DataFrame后使用df.drop_duplicates(subset[‘id’])根据POI ID去重。2. 根据name和location进行去重。6. 最佳实践与工程建议将这个简单的脚本工程化可以考虑以下方向配置化管理将API Key、搜索关键词列表、城市列表等配置项抽离到独立的配置文件如config.yaml或.env文件中便于管理和保密。异常处理与日志为网络请求、数据解析等关键步骤添加更完善的异常处理try...except并使用logging模块记录运行日志便于排查问题。增量抓取与数据持久化使用数据库如SQLite、MySQL存储数据并记录抓取状态。每次运行只抓取新增或更新的数据避免重复请求。遵守爬虫礼仪设置User-Agent在requests请求头中模拟真实浏览器。控制请求频率在循环中务必使用time.sleep()添加延迟建议至少0.1秒以上。尊重robots.txt检查目标网站的robots.txt文件避免抓取禁止的页面。识别并处理反爬对于更复杂的网站可能需要处理Cookie、Session、动态Token甚至验证码。考虑使用Selenium或Playwright等自动化工具但需承担更高的资源消耗和复杂度。数据清洗标准化编写专门的函数来处理地址格式、电话号码、营业时间等字段使其规范化。模块化设计将爬虫类、数据处理类、可视化类分别封装在不同的模块文件中提高代码可读性和复用性。考虑使用更专业的GIS工具对于复杂的空间分析如精确的多边形包含判断、计算店铺密度热力图可以考虑使用GeoPandas,Shapely等专业GIS库。7. 总结与扩展思考通过这个项目我们完成了一次从问题定义到数据获取、处理、分析和可视化的完整技术闭环。回到最初的问题“已吃到河北焖子还能吃到安徽板面吗” 从技术分析的角度我们的答案是可以但体验的“正宗程度”和“便利性”取决于具体位置。通过我们的地图和数据分析可以清晰看到板面在安徽的高度集中性这意味着在安徽特别是阜阳找到正宗板面的概率远高于其他省份。本文掌握的关键点问题抽象将生活问题转化为可技术解决的数据问题空间分布分析。数据获取使用高德地图API进行POI数据抓取并处理分页、去重。数据处理使用pandas进行数据清洗、整合和基础统计。数据可视化使用folium创建交互式地图直观展示地理分布。完整流程串联起从环境搭建、代码编写到结果输出的每一步。下一步可以探索更广的数据源尝试整合大众点评、美团等数据进行多源对比。更细的粒度分析城市内部分布比如在合肥市板面店集中在哪些街区情感分析抓取用户评论分析大家对“焖子”和“板面”的口味评价。构建简单推荐系统根据用户当前位置推荐附近评分最高的焖子或板面店。部署为Web应用使用Flask或Streamlit将整个分析过程包装成一个简单的Web应用供他人使用。技术源于生活又服务于生活。希望这个案例能启发你用技术的眼光去发现和解决身边更多有趣的问题。动手运行一遍代码修改关键词比如试试“煎饼果子”和“热干面”你会对数据的力量有更深的体会。