资讯动态

Python威士忌数据工具库scotpy:从API封装到数据分析实战

发布时间:2026/8/22 0:26:53 来源:尧图企业网站定制
1. 项目概述一个为苏格兰威士忌爱好者打造的Python工具库如果你和我一样既是个Python开发者又是个苏格兰威士忌的深度爱好者那你肯定也遇到过类似的困扰想写个小程序分析一下自己收藏的酒款或者想从某个威士忌数据库里批量抓点信息结果发现要么没有现成的API要么接口设计得极其反人类数据格式五花八门光是处理各种缩写、酒厂别名、年份表示法就够喝一壶的了。每次都得从零开始写爬虫、解析HTML、清洗数据重复造轮子不说还特别容易出错。直到我发现了ezorita/scotpy这个项目。从名字就能看出来scotpy是 “Scotch” 和 “Python” 的结合体。它本质上是一个专门为苏格兰威士忌数据设计的Python客户端库。简单来说它把访问和处理威士忌数据这个复杂、琐碎的过程封装成了一组简单、直观的Python函数和类。你不用再关心底层的网络请求、HTML解析或是数据清洗只需要像调用本地函数一样就能获取到结构清晰、格式统一的威士忌信息。这个项目解决的痛点非常明确标准化和简化对苏格兰威士忌相关数据的程序化访问。无论是想构建一个个人威士忌收藏管理应用还是做一个酒款比价工具甚至是进行一些风味数据分析scotpy都试图为你提供一个可靠的基础设施。它瞄准的用户群体非常垂直就是那些有一定编程基础至少熟悉Python并且对威士忌数据有自动化处理需求的爱好者、收藏家、酒吧经营者甚至是行业内的数据分析师。2. 核心设计思路与架构解析2.1 为什么需要一个专门的威士忌库在深入代码之前我们先聊聊为什么不能直接用requests加BeautifulSoup来解决问题。威士忌数据尤其是来自爱好者社区或商业网站的数据有几个显著特点数据源分散且非结构化权威数据可能来自官方装瓶商OB的PDF手册爱好者品饮笔记在论坛和博客价格信息在拍卖行和电商平台。每个来源的HTML结构、数据呈现方式都完全不同。领域知识门槛高数据中充斥着大量行业术语和缩写。比如“CS”可能指“Cask Strength”桶强也可能指“Chill Filtered”冷凝过滤酒厂名称可能有官方名、常用名、已关闭酒厂的旧名等多种变体。数据一致性差同一款酒在不同平台上其年份、酒精度、容量等信息格式可能千差万别。有的用“43% vol”有的用“43% ABV”有的甚至只写“43%”。scotpy的设计哲学就是要在应用层面对这些混乱进行抽象和统一。它不是一个试图爬取所有网站的大杂烩而更可能是一个针对某个或某几个结构化程度相对较好、数据质量较高的威士忌数据源例如某个知名的威士忌数据库公开API或一个维护良好的社区数据文件的封装客户端。2.2 推测的核心架构与模块划分虽然无法看到ezorita/scotpy的具体源码但根据其项目定位和常见类库的设计模式我们可以合理推断其内部架构会包含以下几个核心模块客户端模块这是库的入口和大脑。它负责管理配置如API密钥、请求超时、重试策略封装对底层数据源的所有HTTP请求。通常会提供一个主类比如ScotchClient用户通过实例化这个类来开始使用。数据模型模块这是库的核心价值所在。它会定义一系列Python类来映射威士忌领域的实体。例如Distillery: 包含酒厂名称、地区高地、艾雷岛等、状态运营中、关闭、成立年份等。Bottling: 包含装瓶信息如酒款名称、酒厂引用、年份/酒龄、酒精度、容量、装瓶类型单桶、小批次等、官方品鉴笔记。Price: 包含价格、货币、时间戳、数据来源等。 这些类不仅存储数据还会实现数据验证、格式化输出如JSON、字典等方法确保内部数据的一致性。查询与过滤模块提供灵活的数据检索方式。可能包括根据酒厂名、地区进行筛选。根据酒龄范围、酒精度范围进行过滤。根据关键词如“雪莉桶”、“泥煤味”进行搜索。支持分页获取大量数据。工具与工具模块包含一些实用的辅助功能例如将酒精度从“% vol”转换为“Proof”的函数。计算近似桶陈年份的估算工具如果已知蒸馏和装瓶年份。处理常见缩写和别名的映射字典。错误处理模块定义库自定义的异常类型如ScotpyError,NotFoundError,RateLimitError等让用户能够更精确地捕获和处理调用API时可能发生的问题。这样的架构设计使得scotpy的使用者能够完全站在“威士忌数据”这个业务层面进行思考而无需关心数据是如何获取和清洗的实现了高度的关注点分离。3. 核心功能拆解与实操要点3.1 安装与环境配置首先你需要安装这个库。通常这类库会发布在PyPI上因此安装非常简单。打开你的终端或命令行使用pip进行安装pip install scotpy如果开发者还在积极维护可能还会提供安装特定版本或从GitHub直接安装的方式# 安装特定版本 pip install scotpy0.1.0 # 从GitHub仓库主分支安装适用于尝鲜或贡献代码 pip install githttps://github.com/ezorita/scotpy.git安装完成后在Python中导入它。通常主客户端类会被直接暴露在库的顶层import scotpy # 或者从scotpy中导入客户端 from scotpy import ScotchClient接下来是配置。绝大多数此类库都需要某种形式的认证尤其是当它背后连接的是一个有访问限制的API时。常见的配置方式是使用API密钥。重要提示永远不要将你的API密钥硬编码在脚本中并上传到GitHub等公开仓库。这会导致密钥泄露可能产生未经授权的使用和费用。务必使用环境变量或配置文件来管理密钥。推荐的做法是使用环境变量在命令行中临时设置仅对当前会话有效export SCOTPY_API_KEYyour_actual_api_key_here或者更持久的方法是在你的项目根目录创建一个.env文件# .env 文件内容 SCOTPY_API_KEYyour_actual_api_key_here然后在Python代码中使用python-dotenv库来加载from dotenv import load_dotenv import os load_dotenv() # 加载 .env 文件中的环境变量 api_key os.getenv(SCOTPY_API_KEY)有了密钥就可以初始化客户端了client ScotchClient(api_keyapi_key) # 或者如果库支持从环境变量自动读取 client ScotchClient() # 它会自动查找名为 SCOTPY_API_KEY 的环境变量有些客户端还允许你配置其他参数比如请求超时时间、重试次数、自定义用户代理等这些在访问不稳定或限制严格的API时很有用。client ScotchClient( api_keyapi_key, timeout30, # 请求超时设为30秒 max_retries3, # 失败后重试最多3次 user_agentMyWhiskyApp/1.0 # 自定义用户代理方便对方识别 )3.2 基础数据检索获取酒厂与酒款信息库的核心功能自然是获取数据。我们假设scotpy提供了类似以下的基础检索方法。获取所有酒厂列表这通常是一个简单的GET请求返回一个酒厂对象的列表。# 获取所有酒厂 distilleries client.get_distilleries() for distillery in distilleries: print(f{distillery.name} - {distillery.region} - Active: {distillery.is_active})返回的distilleries很可能是一个由Distillery对象组成的列表。每个对象都有定义好的属性如name,region,is_active等你可以像访问普通Python对象属性一样访问它们。根据ID或名称获取特定酒厂如果你知道目标酒厂的具体标识符可以直接获取其详细信息。# 通过ID获取假设ID为lagavulin lagavulin client.get_distillery(lagavulin) print(f酒厂: {lagavulin.name}) print(f地区: {lagavulin.region}) print(f介绍: {lagavulin.description[:100]}...) # 只打印前100个字符 # 通过名称搜索可能返回列表 results client.search_distilleries(Ardbeg) for result in results: print(result.name, result.id)获取某个酒厂旗下的装瓶酒款这是更常见的需求。一个酒厂会生产数十甚至上百款不同的装瓶。# 获取拉加维林酒厂的所有装瓶记录 bottlings client.get_bottlings_by_distillery(lagavulin) print(f找到了 {len(bottlings)} 款拉加维林装瓶。) for bottling in bottlings[:5]: # 只打印前5款 print(f - {bottling.name} ({bottling.age}年, {bottling.strength}% vol))这里返回的bottlings是Bottling对象的列表。注意对于数据量大的酒厂这个列表可能很长。一个设计良好的库应该在这里支持分页。# 假设库支持分页参数 bottlings_page1 client.get_bottlings_by_distillery(lagavulin, page1, per_page50) bottlings_page2 client.get_bottlings_by_distillery(lagavulin, page2, per_page50)直接搜索或获取特定酒款你也可以直接搜索酒款名。# 搜索所有名称中包含“16年”的酒款 results client.search_bottlings(16年) for bottling in results: print(f{bottling.distillery_name} {bottling.name}) # 通过唯一ID获取某款酒的详细信息假设从搜索结果或列表中获得了ID bottling_id lagavulin-16-43vol-700ml detailed_bottling client.get_bottling(bottling_id) print(detailed_bottling.tasting_notes) # 打印官方品鉴笔记3.3 高级查询与过滤精准定位目标数据基础检索只能满足简单需求。一个强大的库必须提供灵活的过滤和查询能力。这通常通过向查询方法传递一系列过滤参数来实现。按地区过滤酒厂苏格兰威士忌的一大特色就是产区风味差异。你可能只想看艾雷岛的酒厂。# 获取所有艾雷岛Islay的酒厂 islay_distilleries client.get_distilleries(regionIslay)按酒龄和酒精度过滤酒款寻找高年份或高酒精度的酒款。# 寻找酒龄在18年以上酒精度高于50%的酒款 strong_old_whiskies client.get_bottlings(min_age18, min_strength50.0)组合过滤条件这才是查询的威力所在。# 寻找艾雷岛产区酒龄在10-15年之间酒精度在46%-55%之间并且是桶强Cask Strength的非冷凝过滤Non Chill Filtered酒款 my_preference client.get_bottlings( regionIslay, min_age10, max_age15, min_strength46.0, max_strength55.0, is_cask_strengthTrue, is_non_chill_filteredTrue )关键词全文搜索在酒款描述、品鉴笔记中搜索特定风味关键词。# 搜索带有“雪莉”、“干果”风味的酒款 sherry_bombs client.search_bottlings(sherry dried fruit, search_intasting_notes)为了实现这些过滤库的内部需要将你的Python参数转换为底层API能够理解的查询字符串或请求体。同时它必须处理参数验证确保你传递的min_age是数字region是有效的产区名等。3.4 数据处理与对象关系映射scotpy返回的不是原始的JSON字典而是Python对象。这带来了巨大的便利。访问对象属性数据被标准化了。bottling client.get_bottling(some-id) # 直接访问属性无需记忆JSON键名 print(bottling.name) print(bottling.distillery.name) # 可能是一个嵌套的Distillery对象 print(bottling.strength) print(bottling.volume_ml)对象的方法这些类可能还包含一些业务逻辑方法。# 假设Bottling类有一个方法可以估算当前市场价格如果库整合了价格数据 estimated_price bottling.estimate_current_price(currencyGBP) print(f预估价格: £{estimated_price:.2f}) # 或者一个格式化描述的方法 print(bottling.get_formatted_description()) # 输出可能像“拉加维林16年43% vol700ml发布于2000年代初期”转换为常用格式方便数据交换或存储。# 转换为字典方便存入数据库或转为JSON bottling_dict bottling.to_dict() import json bottling_json json.dumps(bottling_dict, ensure_asciiFalse, indent2) # 转换为Pandas DataFrame如果安装了pandas import pandas as pd bottlings_list client.get_bottlings_by_distillery(highland_park, limit100) df pd.DataFrame([b.to_dict() for b in bottlings_list]) print(df[[name, age, strength]].head())这种面向对象的设计让代码更清晰、更符合直觉也减少了出错的可能。4. 实战应用场景与代码示例理解了核心功能后我们来看几个具体的应用场景把scotpy用起来。4.1 场景一构建个人威士忌收藏数据库假设你是一个收藏家想用Python和SQLite或任何其他数据库管理自己的藏酒。import sqlite3 from scotpy import ScotchClient client ScotchClient(api_keyyour_key) # 1. 创建数据库和表 conn sqlite3.connect(my_whisky_collection.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS collection ( id TEXT PRIMARY KEY, name TEXT NOT NULL, distillery_id TEXT, age INTEGER, strength REAL, volume_ml INTEGER, purchase_date TEXT, purchase_price REAL, notes TEXT ) ) # 2. 假设你想把艾雷岛所有酒龄12年的酒款信息先预存到数据库作为参考 print(正在获取艾雷岛核心酒款信息...) islay_bottlings client.get_bottlings(regionIslay, min_age12) for bottling in islay_bottlings: # 检查是否已存在 cursor.execute(SELECT id FROM collection WHERE id ?, (bottling.id,)) if cursor.fetchone() is None: cursor.execute( INSERT INTO collection (id, name, distillery_id, age, strength, volume_ml) VALUES (?, ?, ?, ?, ?, ?) , ( bottling.id, bottling.name, bottling.distillery.id if bottling.distillery else None, bottling.age, bottling.strength, bottling.volume_ml )) print(f已添加: {bottling.name}) conn.commit() print(参考数据库更新完成。) # 3. 现在当你购买了一款新酒可以手动或通过扫描条码添加购买信息 new_purchase_id ardbeg-10-46vol-700ml # 假设这是你刚买的阿贝10年的ID purchase_date 2023-10-27 purchase_price 65.99 # 英镑 personal_notes 经典的泥煤烟熏带有柠檬和奶油香草味。 cursor.execute( UPDATE collection SET purchase_date ?, purchase_price ?, notes ? WHERE id ? , (purchase_date, purchase_price, personal_notes, new_purchase_id)) conn.commit() conn.close()这个简单的脚本建立了一个本地数据库将线上数据与你的个人收藏信息结合了起来。4.2 场景二威士忌风味分析与可视化我们可以利用获取的数据进行一些简单的分析比如看看不同产区的平均酒精度有什么差异。import matplotlib.pyplot as plt from scotpy import ScotchClient client ScotchClient() regions_of_interest [Highlands, Islay, Speyside, Lowlands, Campbeltown] region_data {} for region in regions_of_interest: print(f正在分析{region}产区...) # 获取该产区一定数量的酒款样本避免请求过多 bottlings client.get_bottlings(regionregion, limit100) if bottlings: strengths [b.strength for b in bottlings if b.strength is not None] avg_strength sum(strengths) / len(strengths) if strengths else 0 region_data[region] { avg_strength: avg_strength, sample_size: len(strengths), min: min(strengths) if strengths else 0, max: max(strengths) if strengths else 0 } print(f {region}: 平均酒精度 {avg_strength:.1f}% (样本数: {len(strengths)})) else: print(f {region}: 未找到数据) # 数据可视化 regions list(region_data.keys()) avg_strengths [region_data[r][avg_strength] for r in regions] sample_sizes [region_data[r][sample_size] for r in regions] fig, ax1 plt.subplots(figsize(10, 6)) bars ax1.bar(regions, avg_strengths, colorskyblue, edgecolorblack) ax1.set_xlabel(产区) ax1.set_ylabel(平均酒精度 (% vol), colorblack) ax1.tick_params(axisy, labelcolorblack) ax1.set_title(苏格兰主要威士忌产区平均酒精度分析 (样本数据)) # 在柱子上方标注样本数 for bar, sample in zip(bars, sample_sizes): height bar.get_height() ax1.text(bar.get_x() bar.get_width()/2., height 0.2, fn{sample}, hacenter, vabottom, fontsize9) plt.xticks(rotation45) plt.tight_layout() plt.savefig(whisky_region_strength.png, dpi300) plt.show()这段代码会生成一张柱状图直观展示不同产区的平均酒精度差异。当然这是一个非常简单的分析你可以进一步分析酒龄分布、价格趋势甚至用NLP技术分析品鉴笔记中的风味词频。4.3 场景三价格监控与提醒工具对于想“捡漏”的爱好者可以写一个定时脚本监控特定酒款的价格变化。import time import smtplib from email.mime.text import MIMEText from datetime import datetime from scotpy import ScotchClient client ScotchClient() # 你心仪的酒款ID列表 watch_list [springbank-15-46vol-700ml, macallan-18-sherry-oak-43vol-700ml] # 存储上次检查的价格 price_history {} def check_prices(): current_prices {} for bottling_id in watch_list: try: bottling client.get_bottling(bottling_id) # 假设Bottling对象有一个current_price属性或者需要通过其他方法获取 # 这里我们假设有一个get_price()方法返回最新价格和货币 price_info bottling.get_price() # 返回类似 {price: 120.50, currency: GBP, source: AuctionSiteX} current_prices[bottling_id] price_info old_price_info price_history.get(bottling_id) if old_price_info: price_change ((price_info[price] - old_price_info[price]) / old_price_info[price]) * 100 if abs(price_change) 5: # 价格变动超过5%时触发提醒 send_alert(bottling.name, old_price_info, price_info, price_change) except Exception as e: print(f获取 {bottling_id} 价格时出错: {e}) # 更新历史记录 price_history.update(current_prices) print(f{datetime.now()}: 价格检查完成。当前监控 {len(current_prices)} 款酒。) def send_alert(bottling_name, old_info, new_info, change_pct): # 这里是一个简单的邮件提醒示例需要配置SMTP subject f威士忌价格提醒: {bottling_name} body f 酒款: {bottling_name} 价格变动: 之前: {old_info[price]} {old_info[currency]} ({old_info.get(date, N/A)}) 现在: {new_info[price]} {new_info[currency]} ({datetime.now().strftime(%Y-%m-%d %H:%M)}) 变动: {change_pct:.1f}% 数据源: {new_info[source]} # ... 这里省略具体的邮件发送代码 (使用smtplib) print(f【提醒】{subject}\n{body}) # 主循环每6小时检查一次 while True: check_prices() time.sleep(6 * 60 * 60) # 休眠6小时这个脚本框架展示了如何将scotpy用于自动化监控。你可以将其部署在服务器上实现真正的价格追踪机器人。5. 常见问题、避坑指南与进阶技巧在实际使用中你肯定会遇到各种问题。下面分享一些我总结的经验和可能遇到的坑。5.1 网络请求与错误处理问题1请求超时或速率限制任何依赖外部API的库都会面临网络问题。scotpy的内部请求逻辑必须健壮。from scotpy import ScotchClient, RateLimitError, RequestTimeoutError client ScotchClient(timeout30, max_retries3) try: data client.get_bottlings(regionSpeyside, limit500) except RateLimitError as e: print(f触发速率限制建议{e.retry_after_seconds}秒后重试。) # 可以实现一个指数退避的重试逻辑 time.sleep(e.retry_after_seconds) data client.get_bottlings(regionSpeyside, limit500) except RequestTimeoutError: print(请求超时请检查网络或增加超时时间。) except Exception as e: print(f发生未知错误: {type(e).__name__}: {e})实操心得务必为生产环境代码添加完善的错误处理和日志记录。对于批量获取数据在循环中每次请求之间加入短暂延迟如time.sleep(1)是对数据源友好的做法也能避免因请求过快被屏蔽。5.2 数据不一致与清洗问题2返回数据缺失或格式异常即使API返回了数据也可能存在字段为None、字符串格式不一致如“43%” vs “43.0% vol”等问题。bottlings client.get_bottlings_by_distillery(some_distillery) cleaned_data [] for b in bottlings: # 处理缺失的酒精度 strength b.strength if strength is None: strength 0.0 print(f警告: 酒款 {b.name} 酒精度缺失已设为0.0) elif isinstance(strength, str): # 尝试从字符串中提取数字例如 43% vol - 43.0 try: import re match re.search(r(\d(\.\d)?), strength) if match: strength float(match.group(1)) else: strength 0.0 except: strength 0.0 # 统一容量单位假设库返回的是ml但确保是整数 volume int(b.volume_ml) if b.volume_ml else 700 # 默认700ml cleaned_data.append({ id: b.id, name: b.name, strength: strength, volume_ml: volume, age: b.age if b.age else NAS # NAS表示未标明年份 })避坑技巧在将数据用于分析或存储前增加一个数据清洗和验证的步骤。编写一个通用的数据清洗函数处理常见的异常情况。5.3 性能优化与缓存策略问题3大量数据查询速度慢如果你需要分析成百上千款酒的数据频繁调用API会非常慢并且可能触发速率限制。解决方案实现本地缓存。对于不常变化的数据如酒厂信息可以缓存到本地文件或数据库。import pickle import os from datetime import datetime, timedelta CACHE_FILE scotpy_cache.pkl CACHE_EXPIRE_HOURS 24 # 缓存24小时 def get_distilleries_with_cache(client, force_refreshFalse): 带缓存功能的获取酒厂列表 cache_data {} # 1. 尝试加载缓存 if os.path.exists(CACHE_FILE) and not force_refresh: try: with open(CACHE_FILE, rb) as f: cache_data pickle.load(f) cache_time cache_data.get(timestamp) # 检查缓存是否过期 if cache_time and datetime.now() - cache_time timedelta(hoursCACHE_EXPIRE_HOURS): print(从缓存加载酒厂数据。) return cache_data[distilleries] except Exception as e: print(f加载缓存失败: {e}) # 2. 缓存无效或强制刷新从API获取 print(从API获取最新酒厂数据...) distilleries client.get_distilleries() # 3. 保存到缓存 cache_data { timestamp: datetime.now(), distilleries: distilleries } try: with open(CACHE_FILE, wb) as f: pickle.dump(cache_data, f) print(酒厂数据已缓存。) except Exception as e: print(f保存缓存失败: {e}) return distilleries # 使用方式 client ScotchClient() distilleries get_distilleries_with_cache(client) # 第一次调用会访问API并缓存 # ... 一段时间后再次调用 distilleries_cached get_distilleries_with_cache(client) # 第二次调用直接读缓存速度极快这个简单的缓存机制能极大提升重复查询的效率并减少对API的调用压力。5.4 扩展库的功能包装与适配问题4scotpy的功能不满足我的特定需求比如它可能没有提供你需要的某个数据源的价格信息。解决方案包装与扩展。你可以创建一个自己的类继承或组合ScotchClient然后添加额外的方法。class EnhancedScotchClient: 增强版的Scotch客户端整合多个数据源 def __init__(self, scotpy_api_key, other_source_api_key): self.scotpy_client ScotchClient(api_keyscotpy_api_key) self.other_source_key other_source_api_key def get_bottling_with_prices(self, bottling_id): 获取酒款详情并整合来自其他数据源的价格信息 # 1. 从scotpy获取基础信息 base_info self.scotpy_client.get_bottling(bottling_id) # 2. 从其他价格数据源获取价格假设有一个函数 prices_from_other self._fetch_prices_from_other_source(bottling_id) # 3. 合并信息 enhanced_info { base: base_info.to_dict(), prices: prices_from_other, price_stats: self._calculate_price_stats(prices_from_other) } return enhanced_info def _fetch_prices_from_other_source(self, bottling_id): # 这里实现调用另一个价格API的逻辑 # 使用 requests 库带上 self.other_source_key # ... return [] # 返回价格列表 def _calculate_price_stats(self, price_list): if not price_list: return None avg_price sum(p[price] for p in price_list) / len(price_list) min_price min(p[price] for p in price_list) max_price max(p[price] for p in price_list) return {avg: avg_price, min: min_price, max: max_price} # 使用增强客户端 enhanced_client EnhancedScotchClient(scotpy_api_keyyour_key, other_source_api_keyother_key) full_data enhanced_client.get_bottling_with_prices(lagavulin-16) print(f平均市场价: £{full_data[price_stats][avg]:.2f})通过这种方式你可以在不修改原始库的基础上灵活地扩展其功能适配更复杂的业务需求。5.5 贡献与社区如果你在使用中发现bug或者有很好的功能建议并且ezorita/scotpy是一个开源项目那么最好的方式是参与到社区中。查看GitHub仓库首先去项目的GitHub页面https://github.com/ezorita/scotpy阅读README.md和CONTRIBUTING.md文件。搜索现有问题在Issues页面搜索看看是否已经有人提出了类似的问题或建议。提交Issue如果没有可以新建一个Issue。清晰地描述问题使用场景、期望行为、实际行为、错误信息或功能建议解决什么痛点、大致实现思路。提交Pull Request如果你有能力修复bug或实现新功能可以Fork仓库在自己的分支上修改然后提交PR。确保代码风格与原有项目一致并添加相应的测试。参与开源项目不仅能解决你遇到的问题还能让工具变得更好惠及整个威士忌爱好者技术社区。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价