ARTICLE DETAIL

资讯详情

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

从零搭建Steam挂刀行情追踪站:Python爬虫+Flask实战复盘

从零搭建Steam挂刀行情追踪站:Python爬虫+Flask实战复盘 1. 从零搭建一个Steam挂刀行情追踪站我的完整实战复盘做Steam饰品交易的人都有一个共同的痛点价格波动太快手动盯盘根本盯不过来。尤其是做挂刀用饰品换余额再买游戏的玩家往往需要在几十个饰品之间来回比价算上Steam市场手续费、第三方平台售价、汇率差脑子根本算不过来。我最早也是手动开Excel一行行填后来实在受不了花了两周时间搭了一套24小时自动追踪饰品价格的行情站跑了大半年效果比我预期好得多。这套系统核心做的事情就三件定时抓取Steam社区市场和各第三方交易平台的饰品价格按挂刀比例算出实际收益率把结果存进数据库并通过一个简单的Web页面展示出来。说白了就是让你打开一个页面就能看到“现在哪个饰品挂刀最划算”。适合有一定编程基础、想自己做工具提升交易效率的玩家也适合对数据采集和Web开发感兴趣、想拿一个真实项目练手的人。下面我把整个搭建过程拆开讲包括我踩过的坑和后来优化出来的方案。2. 整体架构设计与技术选型思路2.1 为什么选择自建而不是用现成工具市面上确实有一些挂刀行情网站但用下来有几个问题让我最终决定自己搞。第一是更新频率不够很多站点半小时甚至一小时才刷新一次等你看到“好价”的时候早就被人扫光了。第二是覆盖的平台有限有些只盯Steam市场和某一个第三方平台挂刀比例算得不够准。第三是我想要自定义筛选条件比如只看某个价格区间、只看某类饰品、只保留收益率超过某个阈值的现成工具很难满足。自建的好处是所有环节都可控。抓取频率我自己定想5分钟一轮就5分钟一轮数据存自己手里想怎么分析就怎么分析页面想加什么筛选就加什么筛选。代价就是需要自己维护尤其是Steam的接口时不时会变得盯着点。2.2 技术栈选型与理由我的选型原则是“够用、好维护、部署便宜”。最终方案如下模块技术选择选型理由数据采集Python requests生态成熟处理JSON和HTML都方便写起来快定时调度APScheduler轻量不用额外搭Celery那样重的队列数据存储SQLite后期转PostgreSQL初期数据量小SQLite零配置数据涨了再迁移后端服务Flask轻量几个接口够用了没必要上Django前端展示原生HTML 轻量JS表格库页面简单不需要React那套构建链部署一台低配云服务器 systemd成本低进程守护用systemd就够了这里重点说几个取舍。调度为什么不用crontab因为crontab跑独立进程抓取任务之间的状态不好共享比如我想让两次抓取之间间隔动态调整crontab就做不到。APScheduler跑在主进程里可以方便地做任务编排和异常重试。数据库为什么先SQLite因为初期就几万条记录SQLite完全扛得住而且备份就是复制一个文件特别省心。等数据量到百万级、并发查询变多的时候再迁PostgreSQL迁移成本也不高。2.3 数据流整体设计整个系统的数据流是这样的调度器每隔固定时间触发采集任务采集模块分别去Steam社区市场和第三方平台拉取饰品价格拿到原始数据后做清洗和归一化统一货币单位、统一饰品标识然后计算挂刀比例和收益率写入数据库。Web服务从数据库读取最新数据按收益率排序后返回给前端展示。这里有个关键设计采集和计算分离。采集模块只负责拿到原始价格计算模块单独跑这样如果计算逻辑要调整比如手续费规则变了不用重新抓数据直接拿历史原始数据重算就行。我一开始把两者耦合在一起后来改手续费规则的时候被迫重抓了一遍全量数据浪费了好几个小时这个教训很深刻。3. 核心细节解析与实操要点3.1 Steam市场价格抓取的关键细节Steam社区市场的价格接口是整套系统里最核心也最麻烦的部分。常用的入口是社区市场的搜索接口可以按饰品名称搜索并返回价格列表。这个接口返回的是JSON但有几个坑必须注意。第一个坑是货币和地区。接口返回的价格跟你请求时带的货币参数有关如果不指定默认可能返回你账号所在地区的货币。做挂刀计算必须统一货币我一般统一用人民币或者美元请求时显式带上货币参数避免混算。第二个坑是价格字段的格式。返回的价格往往是带货币符号的字符串比如“¥ 12.34”或者“$1,234.56”需要写解析函数把符号、千分位逗号都去掉再转成浮点数。这个解析函数一定要写健壮因为不同货币格式不一样我见过有人因为没处理千分位逗号把1234.56解析成了1.23456结果算出来的收益率离谱到天上去了。第三个坑是请求频率。Steam对社区市场的请求有频率限制请求太密会被临时限制访问。我的做法是每轮抓取控制在合理数量内请求之间加随机延时比如0.5到1.5秒之间随机并且做好失败重试。重试不要无脑立刻重试用指数退避第一次等2秒第二次等4秒第三次等8秒避免触发更严格的限制。注意抓取时一定要设置合理的User-Agent和超时时间。超时我一般设10秒太短容易误判失败太长会拖慢整轮抓取。User-Agent用常见的浏览器标识不要用默认的python-requests标识。3.2 第三方平台价格获取与归一化第三方交易平台的价格获取方式各不相同有的有公开接口有的只能解析页面。这里我不具体点名平台讲通用思路。有公开接口的优先用接口稳定且解析简单只能解析页面的用BeautifulSoup或者lxml提取价格节点但要做好页面结构变化的心理准备最好把选择器配置化页面改版时改配置就行不用动代码。归一化的核心是饰品标识对齐。同一个饰品在Steam和第三方平台的名称可能略有差异比如有没有磨损前缀、有没有特殊符号。我的做法是维护一张映射表把各平台的饰品名称映射到一个统一的内部ID。这张表初期手动维护几十条后面可以写个模糊匹配辅助生成但最终还是要人工确认因为匹配错了会导致价格对不上算出来的收益率完全是错的。3.3 挂刀比例与收益率的计算逻辑这是整套系统的灵魂必须算准。挂刀的本质是你在第三方平台用现金买入饰品然后在Steam市场卖出换成Steam余额再用余额买游戏。所以核心指标是“用多少钱的现金换到了多少Steam余额”。计算步骤是这样的假设某饰品在第三方平台售价为P_cash现金价在Steam市场的当前最低售价为P_steam挂单价Steam市场卖出时平台要抽成通常是15%左右具体比例以实际为准那么你实际到手的Steam余额是 P_steam × (1 - 手续费率)。挂刀比例就是 P_cash / (P_steam × (1 - 手续费率))这个值越小越划算意思是花更少的现金换到更多的余额。举个例子某饰品第三方卖100元Steam市场卖150元手续费15%那么到手余额是150 × 0.85 127.5元挂刀比例是100 / 127.5 ≈ 0.784相当于用7.84折换余额。如果另一个饰品算出来是0.72那显然第二个更划算。提示手续费率一定要用实际值不同饰品类型、不同地区可能有差异。我建议把手续费率做成配置项方便调整。另外Steam市场的“最低售价”要用当前在售的最低挂单价而不是最近成交价因为挂刀看的是你现在能不能以这个价卖出去。3.4 数据存储表结构设计表结构设计得好不好直接决定后面查询和扩展顺不顺手。我最终用了三张核心表items表存饰品的基础信息包括内部ID、名称、所属游戏、类型等。prices表存价格快照字段包括饰品ID、平台标识、价格、货币、抓取时间。这张表是增长最快的每轮抓取每个饰品每个平台都插一条。metrics表存计算后的指标包括饰品ID、挂刀比例、收益率、计算时间。这张表可以从prices表重算生成。prices表一定要在(饰品ID, 平台, 抓取时间)上建索引否则数据量大了之后查询最新价格会非常慢。我一开始没建索引数据到几十万条的时候查询要好几秒加上索引后降到毫秒级。另外可以考虑做数据归档比如只保留最近30天的明细更早的按天聚合后删掉明细控制表的大小。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把基础环境搭起来。我用的Python 3.10太老的版本有些库不兼容。创建虚拟环境是个好习惯避免污染系统环境。python3 -m venv venv source venv/bin/activate pip install requests apscheduler flask beautifulsoup4 lxml如果你打算用PostgreSQL再加一个psycopg2-binary。初期用SQLite的话Python标准库自带sqlite3不用额外装。4.2 采集模块的实现采集模块我拆成了两个函数一个抓Steam一个抓第三方结构类似。以Steam为例核心逻辑是构造请求、发请求、解析响应、提取价格。import requests import time import random from decimal import Decimal def parse_price(price_str): # 去掉货币符号和千分位逗号转成Decimal避免浮点误差 cleaned price_str.replace(,, ) cleaned .join(c for c in cleaned if c.isdigit() or c .) return Decimal(cleaned) def fetch_steam_price(item_name, currencyCNY): url https://steamcommunity.com/market/search/render/ params { query: item_name, currency: currency, count: 10, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... } for attempt in range(3): try: resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() results data.get(results, []) if results: price_str results[0].get(sell_price_text, ) return parse_price(price_str) except Exception as e: wait 2 ** attempt time.sleep(wait) return None这里用Decimal而不是float是因为价格计算涉及乘除和比较浮点误差累积起来可能导致排序出错。Decimal虽然慢一点但精度有保障值得。请求之间加随机延时time.sleep(random.uniform(0.5, 1.5))这个延时不是随便加的是为了模拟人类操作节奏降低被限制的概率。实测下来加了随机延时之后连续跑几天都没被限制过。4.3 调度模块的配置用APScheduler配置定时任务我设的是每5分钟跑一轮。为什么是5分钟因为挂刀的好价通常几分钟内就会被扫掉抓太慢没意义抓太快又容易触发限制5分钟是个平衡点。from apscheduler.schedulers.blocking import BlockingScheduler scheduler BlockingScheduler() scheduler.add_job( run_full_cycle, interval, minutes5, max_instances1, # 防止上一轮没跑完下一轮又启动 coalesceTrue, # 错过的任务合并执行不堆积 ) scheduler.start()max_instances1这个参数很关键。如果某一轮抓取因为网络慢跑了6分钟没有这个限制的话下一轮会同时启动两个进程同时抓取既浪费资源又容易触发限制。coalesceTrue是防止服务器重启后积压的任务一次性全跑。4.4 计算模块与Web展示计算模块从prices表读取每个饰品在各平台的最新价格按前面讲的公式算出挂刀比例写入metrics表。这里要注意处理“某个平台没有价格”的情况缺数据的饰品直接跳过不要用0填充否则会算出错误的收益率。Web展示用Flask写两个接口就够了一个返回饰品列表和指标一个返回某个饰品的历史价格曲线。前端用一个表格展示按挂刀比例升序排列比例最低的排最前面。表格里我加了几个筛选价格区间、收益率阈值、平台来源。这样打开页面一眼就能看到当前最划算的几个饰品。from flask import Flask, jsonify app Flask(__name__) app.route(/api/metrics) def get_metrics(): # 从数据库读取最新指标按挂刀比例升序 rows query_latest_metrics() return jsonify(rows)4.5 部署与进程守护部署到云服务器后用systemd守护进程保证程序崩溃后自动重启。写一个service文件[Unit] DescriptionSteam Market Tracker Afternetwork.target [Service] Useryouruser WorkingDirectory/path/to/project ExecStart/path/to/venv/bin/python main.py Restartalways RestartSec10 [Install] WantedBymulti-user.targetRestartalways配合RestartSec10程序挂了10秒后自动拉起基本不用人工干预。日志用journalctl查看方便排查问题。5. 常见问题与排查技巧实录5.1 抓取失败与连接问题跑这套系统最常见的问题就是抓取失败。表现是日志里出现连接超时、返回状态码异常、或者返回内容不是预期的JSON。我整理了一个排查顺序现象可能原因排查方法解决方式连接超时网络波动或目标限流手动curl测试连通性增加重试和退避检查服务器网络返回非JSON被重定向到验证页打印响应前200字符检查请求头降低频率价格为空饰品名称不匹配核对搜索关键词修正映射表数据量骤降接口结构变化对比历史响应结构更新解析逻辑有一次我遇到连续几轮抓取全部失败日志显示连接被拒绝。排查下来是服务器出口IP被临时限制了原因是那段时间我把抓取频率调得太高。后来把频率降回5分钟一轮加了随机延时再没出现过。所以频率这个东西宁可慢一点稳定比快更重要。5.2 价格数据异常的处理数据异常比抓取失败更隐蔽因为程序不报错但算出来的结果是错的。常见的异常有价格突然变成0、价格突然翻了几十倍、同一饰品两个平台价格差异巨大。我的做法是在写入数据库前加一层校验价格必须大于0且与上一轮价格相比波动不超过某个阈值比如50%超过阈值的标记为可疑不参与计算同时记一条告警日志。这样即使某个平台返回了脏数据也不会污染整个行情列表。提示校验阈值不要设得太死有些饰品确实会因为市场供需突然大涨大跌。我的经验是设50%作为告警线人工确认后再决定是否放行而不是直接丢弃。5.3 数据库性能优化数据量涨起来之后查询会变慢。除了前面说的建索引还有几个优化点。一是查询最新价格时用子查询或者窗口函数避免全表扫描。二是定期做VACUUMSQLite或者ANALYZEPostgreSQL保持查询计划最优。三是把历史明细归档只保留最近的数据在热表里。我实测下来prices表在500万条以内加了正确索引后SQLite查询最新价格仍在100毫秒以内完全够用。超过这个量级再考虑迁移。5.4 我踩过的几个坑第一个坑是时区问题。抓取时间我一开始用的本地时间后来发现服务器时区和本地不一致导致历史曲线对不上。统一用UTC存储展示时再转本地时区这个问题就解决了。第二个坑是饰品名称里的特殊字符。有些饰品名称带引号、斜杠、特殊符号直接拼进URL会出错。一定要用URL编码处理requests的params参数会自动处理但如果你手动拼URL就要注意。第三个坑是手续费率写死。我一开始把15%写死在代码里后来发现不同情况下费率有差异改的时候要改代码重新部署。后来改成配置文件读取改个数字重启就行方便多了。第四个坑是没有做去重。同一轮抓取如果因为重试导致同一个饰品插入了两条价格计算时会取到重复数据。后来在插入时加了唯一约束同一饰品同一平台同一轮次只保留一条。6. 系统优化与功能扩展方向6.1 提升抓取效率的几个手段当饰品数量涨到几百上千个的时候串行抓取会非常慢。我后来改成了并发抓取用线程池控制并发数比如10个线程配合每个线程独立的随机延时。这样一轮抓取时间从十几分钟降到两三分钟。但并发数不能太高太高一样会触发限制10个左右是个比较安全的范围。另一个手段是增量抓取。不是每轮都抓全部饰品而是根据历史数据判断哪些饰品值得关注优先抓这些。比如最近有价格波动的、或者挂刀比例一直不错的提高它们的抓取优先级。冷门饰品降低频率比如一小时抓一次。6.2 通知提醒功能的实现光有页面还不够好价出现的时候你得及时知道。我加了一个通知模块当某个饰品的挂刀比例低于设定阈值时通过邮件或者即时通讯工具推送提醒。实现很简单在计算模块算完指标后判断一下满足条件就调通知接口。阈值不要设得太低否则一天到晚响个不停反而麻木了。我的经验是设在历史平均挂刀比例往下浮动一定幅度这样只有真正的好价才会触发。6.3 历史数据分析的玩法数据攒久了之后可以做很多有意思的分析。比如看某个饰品的挂刀比例在一周内、一个月内的走势找出它的规律。有些饰品在特定时间段比如周末、促销季挂刀比例会明显变好提前布局就能吃到。还可以做饰品之间的相关性分析找出哪些饰品的价格联动分散风险。这些分析我用pandas做从数据库拉数据出来跑结果生成图表。虽然不在Web页面上实时展示但作为决策参考很有价值。7. 一些实操心得与建议这套系统我跑了大半年最大的体会是稳定比功能多更重要。一开始我总想加各种功能结果系统越来越复杂出问题的概率也高了。后来做减法把核心的抓取、计算、展示做扎实反而用得更顺。另外就是数据质量是生命线。行情站的价值全在数据准不准一个错误的价格可能让你做出错误的交易决策。所以校验、告警、人工复核这些环节不能省。宁可少抓几个饰品也要保证抓到的数据是对的。最后说个实际的这套系统搭起来之后我挂刀的效率提升非常明显以前手动比价半小时的事现在打开页面几秒钟就有答案。如果你也经常做挂刀花点时间搭一套自己的行情站长期看绝对值得。后续如果数据量继续涨我打算把SQLite换成PostgreSQL再把前端做得更顺手一些这些都是后话了。
返回列表