追踪Costco油价:全球燃料危机下的600家加油站数据

总结:DR:我在霍尔木兹海峡燃料危机期间追踪了600个Costco仓库的燃油价格,并绘制了它们的变化。
今年四月,我几乎正好住在两个Costco仓库之间,多年来每次需要加油时我都会在脑海里计算:那周哪一家更便宜。
遗憾的是,Costco没有在任何地方公布油价。官方网站把它们放在各个仓库页面上,我最近的两个仓库很少定价一样。Costco的燃油以亏损著称,通常比周边加油站低15到20美分。
这段间隔通常足以让绕道变得值得,但还不足以让我在两家Costco之间做出明显选择。
作为坦帕的通勤者,这个城市广阔且几乎没有公共交通,随着那年春天油价上涨,我变得更加在意自己加油的地方。我开始对Costco的价格产生好奇:他们的涨幅是否和其他加油站一样快?
如果价格持续上涨,Costco是否真的能继续以亏损方式继续运营加油?
历史上没有好方法去了解这一趋势。所以,我自然地造了一个。
大口喝气数据
不幸的是,这可能是项目中最简单的部分。真希望我有个很酷的反转方法可以聊聊华夫屋的故事但Costco在收集这些数据时几乎没有给我任何阻力。
我开始在Costco网站上挖掘时发现的第一个东西是AjaxGetGasPricesService,一个端点,接收格式为ID1_ID2_ID3_ID4.下划线作为数组分隔符是个有趣的选择!
这很有用,但不是我想要的;我需要每个仓库的位置数据,这样我才能绘制地图,判断哪个仓库开车距离更短。
进一步挖掘后,我找到了我需要的正是这些:AjaxWarehouseBrowseLookupView.顾名思义,该函数通过输入经纬度来查找仓库。更好的是参数populateWarehouseDetails回答里塞满了你能想到的所有Costco信息:地址、营业时间、服务、美食广场的可用性(!!),最重要的是油价。
GET /AjaxWarehouseBrowseLookupView
?latitude=27.95
&longitude=-82.45
&hasGas=true
&populateWarehouseDetails=true
&countryCode=US
唯一的问题是populateWarehouseDetails是响应体巨大.虽然我很想一次性获取美国所有的Costco,但他们的API限制了每次通话最多50个仓库,并且按你提供的纬度/经度距离排序。
为了获得全国范围的报道,我需要扫视全地图。
价格席卷全国!
方法很简单:在全国范围内布置坐标网格,扫描每个点,并删除返回的仓库ID。Costco有约600家门店,每个响应50个,美国大陆上3度网格,加上阿拉斯加和夏威夷的几个精心挑选点,重叠度足够让每个仓库都能跟上。
def grid_points(step: int = 3) -> list[tuple[float, float]]:
points = []
for lat in range(25, 50, step):
for lng in range(-125, -65, step):
points.append((float(lat), float(lng)))
points.extend([
(61.2, -149.9), # Anchorage
(64.8, -147.7), # Fairbanks
(21.3, -157.8), # Honolulu
(20.9, -156.5), # Kahului
])
return points
我限制了一次能运行的请求数量,这样就不会一次性砸到他们的服务器,并且对罕见失败的请求增加了自动重试。
async def fetch_all_costcos() -> list[CostcoStation]:
points = grid_points()
seen: dict[int, CostcoStation] = {}
semaphore = asyncio.Semaphore(CONCURRENCY)
async with httpx.AsyncClient(headers=HEADERS, timeout=30) as client:
tasks = [fetch_point(client, lat, lng, semaphore, ts)
for lat, lng in points]
results = await asyncio.gather(*tasks)
for result in results:
for station in result:
seen.setdefault(station.id, station)
return list(seen.values())
整个扫描不到60秒,一次就给我找到了美国所有Costco加油站的当前价格、地址和坐标!
把数据存放在某处
扫描功能正常后,我需要一个地方放数据。既然整个目的是观察价格随时间变化,这本质上是一个时间序列问题:同样的~600个仓库,反复抽样,永远如此,所有过去的读数都被记录下来,以便我回头查看每个仓库的变化。
幸运的是,TimescaleDB是这方面的完美选择。它是基于Postgres的扩展,所以我用的是我已经熟悉的查询语言和工具,但叠加了时间序列功能。
而且它免费且快速上线(感谢TigerData!),这大概就是我对这种项目基础设施的全部要求。
一旦我找到了放数据的地方,我就需要一个地方展示它们。没什么花哨的:一个快速的Next.js应用,一些Tailwind,让它看起来不像电子表格,一个稍微让人沮丧的下午和Cloudflare斗争,努力让Workers部署正常,你就有一个实时、可搜索的Costco油价数据库。
但做一个不能和别人分享的东西,真的没意思。一旦我有了让我自豪的东西,我就为我追踪的每个站点建立了一个页面,让搜索引擎索引所有站点,这样下次有人站在Costco停车场里不知道该开车去哪个仓库时,他们可以直接查,而不是像我那样逆向工程API了。
所以......油价变便宜了吗?
我开始这个项目是在霍尔木兹海峡恐慌后不久,石油市场陷入紧张状态。这里是全球油轮的狭窄瓶颈,甚至关闭的威胁足以引发天然气价格飞涨。
当然,我想要数据属于我自己,但脑海中总有个声音想为我自己掏钱寻找答案。
那么,我到底学到了什么?即使是强大的Costco也无法免疫于霍尔木兹海峡的变幻莫测。一家把天然气当作四舍五入误差的公司,仍然必须跟随市场走势......迟早会的。
我像理智的人一样对这个发现做出了反应——经过几个月完善系统,节省油量几分钱后,我把CRV换成了特斯拉Model Y,现在每个月的车贷都比我在两个仓库之间追逐15美分的空档还要多。
就在我终于有能力回答“我应该开车去哪家Costco”的那一刻,我让这个问题对我个人来说永远变得无关紧要。
如果车牌看起来很眼熟(或者不熟悉),你应该去看看我的其他帖子说我怎么拿到佛罗里达州最漂亮的两个字母组合之一。