尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Steam挂刀行情追踪站搭建实战:Python爬虫+SQLite+Flask全流程

Steam挂刀行情追踪站搭建实战:Python爬虫+SQLite+Flask全流程 1. 从零搭建一个Steam挂刀行情追踪站我的完整实战复盘做Steam饰品交易的人都有一个共同的痛点价格波动太快手动盯盘根本盯不过来。尤其是做“挂刀”这一块——也就是利用Steam社区市场与第三方交易平台之间的价差来套利——你需要在正确的时间点买入正确的饰品差几分钟可能利润就没了。我从去年开始自己动手搭了一套24小时自动追踪饰品价格的行情站跑了大半年中间踩了不少坑也积累了一些实打实的经验。这篇文章就把整个搭建过程、技术选型、核心代码逻辑、以及那些只有真正跑过的人才会遇到的问题完整地分享出来。先说清楚这套东西是干什么的。简单讲它会定时抓取Steam社区市场上指定饰品的价格数据同时对比第三方交易平台的价格计算出一个“折扣率”或者“利润空间”然后通过一个Web页面展示出来。你打开页面就能看到哪些饰品当前处于适合入手的价位不用自己一个个去搜。适合谁看有一定编程基础、想自己搭一套工具来辅助饰品交易的玩家或者对数据采集和可视化感兴趣的技术爱好者。完全不懂代码的朋友也不用急我会尽量把每个环节讲透你照着抄作业也能跑起来。我搭建这套系统的核心动机其实很朴素一开始我也是手动刷社区市场每天花一两个小时看价格效率极低不说还经常错过好的入手时机。后来我想这个事情本质上就是一个数据采集加计算展示的活儿为什么不自动化呢于是就有了这个项目。下面我从整体设计思路开始一步步拆解。2. 整体架构设计与技术选型思路2.1 为什么选择“定时爬取本地计算Web展示”这套方案在动手之前我先想清楚了这个系统的数据流数据源在Steam社区市场和第三方平台我需要定期去拉取价格存到本地做计算然后展示。这个流程决定了架构的基本形态。最核心的决策是不做实时推送做定时轮询。原因很简单Steam社区市场的价格接口本身就不是实时流式的它是按请求返回当前快照。你就算每秒请求一次拿到的数据粒度也就那样反而容易触发限流。我实测下来对于大多数饰品来说每5到10分钟采集一次完全够用热门饰品可以缩短到3分钟冷门饰品甚至可以放宽到15分钟。这个粒度既能捕捉到价格变化趋势又不会给服务器和自己惹麻烦。另一个决策是数据存储用SQLite而不是MySQL或PostgreSQL。这套系统是个人用的数据量并不大——假设你追踪500个饰品每10分钟采集一次一天也就72000条记录一个月两百多万条。SQLite处理这个量级绰绰有余而且零配置、单文件、备份方便。我用了一个prices.db文件直接扔在项目目录里想迁移就复制走省心。Web展示层我选了Flask 原生HTML/JS没有上React或Vue。不是说前端框架不好而是这个页面的交互复杂度很低——就是一个表格加一些筛选和排序功能用原生JS完全hold住引入构建工具反而增加维护成本。Flask的好处是轻量一个app.py就能跑起来配合Jinja2模板渲染开发速度快。提示如果你打算追踪的饰品数量超过2000个或者需要多用户同时访问建议把SQLite换成PostgreSQLFlask换成FastAPI整体架构不用变只是替换组件。2.2 数据源分析与采集策略的取舍数据源这块是整个项目最关键的环节。Steam社区市场的价格数据可以通过其公开的market/search/render接口获取返回的是JSON格式包含饰品的当前最低售价、最近成交价、挂单数量等信息。这个接口不需要登录就能访问但有几个坑要注意。第一个坑是请求频率限制。Steam对未登录请求的限流比较严格我实测大概每分钟超过20次请求就会开始返回空数据或者429状态码。所以采集器必须做速率控制我设置的是每次请求间隔3到5秒加上随机抖动模拟人类操作节奏。第二个坑是分页与搜索参数。这个接口支持通过query参数搜索饰品名称通过start和count控制分页。但它的搜索结果排序并不完全稳定有时候同一个饰品在不同时间搜索出来的位置会变。我的做法是维护一个饰品清单表每个饰品记录它的market_hash_name采集时直接用精确名称去查而不是依赖模糊搜索。第三个坑是货币与地区。Steam社区市场的价格会根据请求的货币和地区不同而不同。我建议统一用人民币或者美元来采集避免汇率换算带来的误差。在请求头里设置好Accept-Language和Cookie中的货币偏好保证每次拿到的价格单位一致。第三方平台的价格数据我用的是几个主流的饰品交易平台。它们的页面结构各不相同有的提供公开API有的只能通过解析HTML来获取。对于提供API的平台优先走API稳定且数据结构清晰对于只能爬HTML的用BeautifulSoup或者lxml解析但要注意页面结构可能随时改版需要定期检查。2.3 技术栈清单与依赖说明整个项目的技术栈如下组件选型用途语言Python 3.10主开发语言采集requests BeautifulSoupHTTP请求与HTML解析存储SQLite 3价格数据持久化调度APScheduler定时任务管理Web框架Flask提供Web页面和API前端原生HTML/CSS/JS页面展示与交互部署一台常开的云服务器7x24小时运行依赖安装很简单pip install requests beautifulsoup4 flask apscheduler lxml这里重点说一下APScheduler。它比用while True time.sleep优雅得多支持cron表达式、间隔触发、以及任务持久化。我配置了两个任务一个每5分钟采集一次热门饰品一个每15分钟采集一次全量饰品。两个任务错开执行避免同时发起大量请求。注意如果你把采集任务放在Flask的主进程里跑开发模式下Flask的重载机制会导致任务重复启动。解决办法是用if __name__ __main__保护或者把采集器单独跑一个进程Flask只负责读数据库展示。3. 核心模块拆解与关键实现细节3.1 饰品清单的建立与维护在开始采集之前你需要先有一份饰品清单。这份清单决定了你要追踪哪些东西。我的做法是分两步走先通过Steam社区市场的搜索接口批量拉取热门饰品然后人工筛选出适合挂刀的品类。具体操作上我写了一个build_watchlist.py脚本它会遍历几个热门游戏比如CS2、DOTA2、PUBG的饰品分类拉取前N页的饰品列表提取market_hash_name、game、icon_url等字段存入watchlist表。这个脚本不需要频繁跑大概每周更新一次就行因为热门饰品的构成变化不会太快。import requests import sqlite3 import time import random def fetch_market_items(game, start0, count100): url https://steamcommunity.com/market/search/render/ params { appid: game, norender: 1, start: start, count: count, search_descriptions: 0, sort_column: popular, sort_dir: desc } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9, } resp requests.get(url, paramsparams, headersheaders, timeout15) if resp.status_code 200: return resp.json() return None def build_watchlist(): conn sqlite3.connect(prices.db) c conn.cursor() c.execute(CREATE TABLE IF NOT EXISTS watchlist ( market_hash_name TEXT PRIMARY KEY, game TEXT, icon_url TEXT, added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )) games {730: CS2, 570: DOTA2, 578080: PUBG} for appid, game_name in games.items(): for start in range(0, 500, 100): data fetch_market_items(appid, startstart, count100) if not data or results not in data: break for item in data[results]: name item.get(hash_name, ) icon item.get(asset_description, {}).get(icon_url, ) if name: c.execute(INSERT OR IGNORE INTO watchlist (market_hash_name, game, icon_url) VALUES (?, ?, ?), (name, game_name, icon)) conn.commit() time.sleep(random.uniform(3, 5)) conn.close()这段代码的核心逻辑就是分页拉取、去重入库、控制请求间隔。INSERT OR IGNORE保证重复的饰品不会报错time.sleep加上随机抖动是为了降低被限流的概率。实操心得Steam的搜索接口返回的hash_name有时候会包含特殊字符比如★、|、(等这些在后续请求价格时需要做URL编码处理。我建议在入库时就统一处理好编码避免后续每次请求都要转一次。3.2 价格采集器的实现与反限流策略价格采集器是整个系统的心脏。它的任务是读取watchlist中的饰品逐个请求Steam社区市场的价格接口解析出当前最低售价和挂单数量存入价格历史表。Steam的价格接口是market/priceoverview参数包括appid、market_hash_name、currency。返回的JSON里lowest_price是当前最低售价volume是24小时成交量median_price是中位价。我主要用lowest_price来做挂刀计算。import requests import sqlite3 import time import random import re def parse_price(price_str): if not price_str: return None # 处理 ¥ 12.34 或 $12.34 等格式 match re.search(r[\d.,], price_str.replace(,, )) if match: return float(match.group()) return None def fetch_price(appid, market_hash_name, currency23): # currency23 表示人民币 url https://steamcommunity.com/market/priceoverview/ params { appid: appid, market_hash_name: market_hash_name, currency: currency } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9, Referer: https://steamcommunity.com/market/ } try: resp requests.get(url, paramsparams, headersheaders, timeout15) if resp.status_code 200: data resp.json() if data.get(success): return { lowest_price: parse_price(data.get(lowest_price)), median_price: parse_price(data.get(median_price)), volume: int(data.get(volume, 0).replace(,, )) } except Exception as e: print(fError fetching {market_hash_name}: {e}) return None def collect_prices(batch_size50): conn sqlite3.connect(prices.db) c conn.cursor() c.execute(SELECT market_hash_name, game FROM watchlist) items c.fetchall() appid_map {CS2: 730, DOTA2: 570, PUBG: 578080} for i, (name, game) in enumerate(items): appid appid_map.get(game, 730) price_data fetch_price(appid, name) if price_data and price_data[lowest_price]: c.execute(INSERT INTO price_history (market_hash_name, lowest_price, median_price, volume, collected_at) VALUES (?, ?, ?, ?, datetime(now)), (name, price_data[lowest_price], price_data[median_price], price_data[volume])) conn.commit() # 每采集一批休息一下 if (i 1) % batch_size 0: time.sleep(random.uniform(30, 60)) else: time.sleep(random.uniform(3, 6)) conn.close()这段代码有几个关键点。第一currency23对应人民币如果你想用美元就改成1。第二parse_price函数要处理不同货币符号和千分位分隔符我见过¥1,234.56这种格式直接float()会报错。第三每采集50个饰品就休息30到60秒这是为了防止连续请求触发限流。第四所有请求都带了Referer头模拟从市场页面发起的请求降低被识别为爬虫的概率。注意如果你在请求时遇到server failed to connected to steam这类错误大概率是网络波动或者Steam服务端临时不可用。我的处理方式是重试3次每次间隔递增5秒、15秒、30秒如果还是失败就跳过这个饰品记录到日志里下一轮再补采。3.3 挂刀利润计算逻辑与阈值设定采集到价格之后下一步是计算挂刀利润。所谓挂刀本质上是比较Steam社区市场的价格和第三方平台的价格找出那些在第三方平台买入、在Steam市场卖出后能获得正收益的饰品。计算逻辑并不复杂但有几个细节容易忽略。假设第三方平台的价格是P_extSteam市场的最低售价是P_steamSteam市场的手续费是15%实际是13%给开发者2%给Steam但卖家实际到手是P_steam * 0.85那么利润空间就是profit P_steam * 0.85 - P_ext profit_rate profit / P_ext * 100%但这里有个问题Steam市场的价格是买家支付的价格卖家到手要扣掉手续费。而第三方平台的价格通常是卖家挂单的价格买家支付时可能还有额外费用。所以实际计算时我用的公式是net_receive P_steam * 0.85 cost P_ext * (1 platform_fee) profit_rate (net_receive - cost) / cost * 100%其中platform_fee是第三方平台的交易手续费不同平台不一样我一般按2%到5%估算。def calculate_profit(steam_price, external_price, platform_fee0.03): if not steam_price or not external_price: return None net_receive steam_price * 0.85 cost external_price * (1 platform_fee) profit net_receive - cost profit_rate profit / cost * 100 return { net_receive: round(net_receive, 2), cost: round(cost, 2), profit: round(profit, 2), profit_rate: round(profit_rate, 2) }阈值设定这块我个人的经验是利润率低于5%的基本不用看因为还要考虑时间成本和价格波动风险。10%以上算是不错的入手时机15%以上就值得果断出手了。当然这个阈值因人而异你可以根据自己的风险偏好调整。实操心得不要只看利润率还要看成交量。一个饰品如果24小时成交量只有个位数就算利润率20%也不建议碰因为流动性太差买进去可能卖不掉。我一般会过滤掉volume 10的饰品。3.4 Web展示页面的搭建与交互设计Web页面这块我的设计原则是打开就能看不用点太多。首页直接展示一个表格列出当前所有追踪饰品的最新价格、利润率、成交量支持按利润率排序、按游戏筛选、按名称搜索。Flask的路由很简单from flask import Flask, render_template, jsonify, request import sqlite3 app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/prices) def api_prices(): game request.args.get(game, ) min_rate request.args.get(min_rate, 0, typefloat) conn sqlite3.connect(prices.db) c conn.cursor() query SELECT w.market_hash_name, w.game, w.icon_url, p.lowest_price, p.volume, p.collected_at FROM watchlist w JOIN ( SELECT market_hash_name, lowest_price, volume, collected_at, ROW_NUMBER() OVER (PARTITION BY market_hash_name ORDER BY collected_at DESC) as rn FROM price_history ) p ON w.market_hash_name p.market_hash_name AND p.rn 1 WHERE 11 params [] if game: query AND w.game ? params.append(game) query ORDER BY p.lowest_price DESC c.execute(query, params) rows c.fetchall() conn.close() result [] for row in rows: result.append({ name: row[0], game: row[1], icon: row[2], price: row[3], volume: row[4], updated: row[5] }) return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000)前端页面用了一个简单的表格配合fetch请求API支持排序和筛选。图标直接从Steam的CDN加载icon_url拼上https://community.cloudflare.steamstatic.com/economy/image/前缀就能显示。提示如果你想让页面自动刷新可以用setInterval每60秒重新请求一次API。但注意不要设置得太频繁否则服务器压力大而且数据本身也不是每秒都在变。4. 部署上线与7x24小时稳定运行方案4.1 服务器环境准备与依赖安装这套系统需要一台常开的机器来跑。我用的是最基础的云服务器1核2G的配置跑这个绰绰有余。操作系统选的Ubuntu 22.04因为包管理方便社区资料也多。环境准备步骤# 更新系统 sudo apt update sudo apt upgrade -y # 安装Python和pip sudo apt install python3 python3-pip python3-venv -y # 创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装依赖 pip install requests beautifulsoup4 flask apscheduler lxml数据库文件放在项目目录下不需要额外安装数据库服务。整个项目目录结构大概是这样的steam-tracker/ ├── venv/ ├── prices.db ├── collector.py ├── app.py ├── build_watchlist.py ├── templates/ │ └── index.html └── static/ └── style.css4.2 用systemd守护进程保证服务不中断直接python app.py跑有个问题SSH一断开进程就没了。解决办法是用systemd把它注册成系统服务开机自启崩溃自动重启。创建服务文件/etc/systemd/system/steam-tracker.service[Unit] DescriptionSteam Tracker Service Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/home/ubuntu/steam-tracker ExecStart/home/ubuntu/steam-tracker/venv/bin/python /home/ubuntu/steam-tracker/app.py Restartalways RestartSec10 [Install] WantedBymulti-user.target采集器也单独注册一个服务[Unit] DescriptionSteam Price Collector Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/home/ubuntu/steam-tracker ExecStart/home/ubuntu/steam-tracker/venv/bin/python /home/ubuntu/steam-tracker/collector.py Restartalways RestartSec30 [Install] WantedBymulti-user.target然后启用并启动sudo systemctl daemon-reload sudo systemctl enable steam-tracker sudo systemctl start steam-tracker sudo systemctl enable steam-collector sudo systemctl start steam-collector这样即使服务器重启两个服务也会自动拉起来。Restartalways保证进程意外退出后会自动重启RestartSec控制重启间隔避免频繁重启导致资源浪费。4.3 日志监控与异常告警的简易实现跑7x24小时的服务没有日志和告警是不行的。我的做法很简单采集器和Web服务都把日志写到文件里然后用一个定时脚本检查日志中的错误关键字发现异常就发通知。日志配置import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(collector.log, encodingutf-8), logging.StreamHandler() ] ) logger logging.getLogger(__name__)然后在采集过程中把关键信息都记下来logger.info(fCollected {name}: price{price_data[lowest_price]}, volume{price_data[volume]}) logger.warning(fFailed to fetch {name}, retrying...) logger.error(fSteam API returned non-200 status: {resp.status_code})告警这块我用了一个简单的脚本每10分钟检查一次日志文件如果最近10分钟内出现了ERROR级别的日志超过5次就通过邮件或者Webhook发通知。这个脚本也挂在crontab里*/10 * * * * /home/ubuntu/steam-tracker/venv/bin/python /home/ubuntu/steam-tracker/check_alerts.py实操心得日志文件一定要做轮转否则跑几个月就变成几个G了。可以用Python的RotatingFileHandler设置单文件最大10MB保留5个备份。这样既不会丢日志也不会把磁盘撑爆。5. 踩坑实录与常见问题排查手册5.1 采集失败的高频原因与应对方法跑了大半年采集失败的原因基本就那么几类。我整理了一个速查表遇到问题直接对照排查现象可能原因解决方法返回空数据请求频率过高被限流增加请求间隔加入随机抖动返回429状态码短时间内请求过多暂停采集30分钟降低并发返回403状态码请求头不完整或被识别补全User-Agent、Referer、Accept-Language价格解析为None货币格式变化检查parse_price正则适配新格式数据库写入失败磁盘满或数据库锁检查磁盘空间SQLite加WAL模式服务频繁重启内存泄漏或未捕获异常检查日志加try-except兜底其中最常见的是限流问题。我的经验是未登录状态下Steam对单个IP的请求限制大概在每分钟15到20次左右。超过这个频率轻则返回空数据重则封禁IP一段时间。所以采集器的速率控制一定要做好宁可慢一点也不要贪快。另一个坑是SteamWebHelper无响应的问题。这个其实不是我们采集器的问题而是Steam客户端本身的问题。如果你在本地开发时开着Steam客户端有时候它会占用一些网络资源导致请求变慢。解决办法很简单开发时关掉Steam客户端或者用一台独立的服务器来跑采集。5.2 价格数据异常波动的识别与过滤价格数据偶尔会出现异常值比如某个饰品突然显示价格为0.01元或者突然变成99999元。这些异常值如果不处理会严重影响利润计算的准确性。我的做法是加一层异常值过滤。具体规则价格低于0.1元的直接丢弃大概率是解析错误价格比上一次采集结果波动超过50%的标记为可疑暂不参与计算成交量突然从几百变成0的也标记为可疑def is_price_valid(new_price, last_price): if new_price is None or new_price 0.1: return False if last_price and last_price 0: change_rate abs(new_price - last_price) / last_price if change_rate 0.5: return False return True这个过滤逻辑帮我避免了好几次误判。有一次某个饰品的价格因为接口返回格式变化被解析成了0.01如果没有过滤系统会显示利润率几千个百分点完全没法看。5.3 数据库性能优化与历史数据清理SQLite在数据量大了之后查询会变慢。我做了两件事来优化第一加索引。在price_history表的market_hash_name和collected_at字段上建联合索引CREATE INDEX idx_price_history_name_time ON price_history (market_hash_name, collected_at DESC);这个索引让“查询每个饰品最新价格”的操作从全表扫描变成了索引查找速度快了十几倍。第二定期清理历史数据。我只保留最近30天的价格记录更早的数据归档到另一个表或者直接删除。清理脚本每周跑一次DELETE FROM price_history WHERE collected_at datetime(now, -30 days);清理完之后执行VACUUM回收空间VACUUM;注意VACUUM操作会锁库建议在采集任务间隙执行避免和写入操作冲突。我一般设置在凌晨4点跑清理脚本那个时间段采集频率最低。5.4 第三方平台页面改版的应对策略第三方平台的页面结构不是一成不变的它们时不时会改版。改版之后原来的HTML解析规则就失效了价格采集会返回None。我的应对策略是把解析规则做成可配置的。每个平台对应一个解析配置存在JSON文件里改版时只需要更新配置不用改代码。{ platform_a: { price_selector: .item-price .price-value, name_selector: .item-name, encoding: utf-8 }, platform_b: { price_selector: span.current-price, name_selector: h1.item-title, encoding: utf-8 } }同时我会定期检查采集结果如果某个平台连续多次返回None就触发告警提醒我去检查页面是否改版了。这个检查逻辑很简单统计每个平台最近100次采集的成功率低于80%就发通知。5.5 关于Steam Economy Enhancer等辅助工具的使用体会在搭建这套系统之前我也用过一些现成的辅助工具比如Steam Economy Enhancer这类浏览器插件。它们的功能确实方便能在Steam市场页面上直接显示一些额外信息比如批量出售、价格历史等。但它们的局限性也很明显只能在浏览器里用不能自动化不能7x24小时跑。我的建议是如果你只是偶尔看看价格用这类插件就够了。但如果你想做系统化的挂刀交易还是得自己搭一套自动化系统。插件可以作为辅助比如在手动确认交易时用来快速查看信息但核心的价格追踪和计算还是交给自己的系统来做更靠谱。另外关于Goldberg Steam Emulator这类工具我了解不多也不建议在正式的交易环境中使用。做饰品交易最重要的是稳定和安全用正规的接口和工具避免不必要的风险。6. 一些实战中总结的零碎经验最后分享几个我在实际运行中总结的小技巧都是些不起眼但很实用的东西。关于采集时间的选择。Steam社区市场的价格在一天之内是有波动的。我观察下来北京时间凌晨3点到6点这段时间在线人数少价格往往偏低是挂刀入手的好时机。而晚上8点到11点是交易高峰期价格偏高适合出货。你可以根据这个规律调整采集频率在低价时段加密采集。关于饰品的选取。不是所有饰品都适合挂刀。我一般只关注那些成交量稳定在50以上、价格在10到500元之间的饰品。价格太低的利润空间小价格太高的流动性差。成交量太低的容易砸手里成交量太高的竞争激烈利润空间反而小。关于利润率的心理预期。刚开始做的时候我看到10%的利润率就激动得不行结果买进去发现价格半天不涨资金压了好几天。后来我调整了策略5%到8%的利润率快进快出资金周转快总体收益反而更高。追求高利润率往往意味着更长的等待时间和更高的风险。关于系统的扩展方向。这套系统目前只做了价格追踪和展示后续还可以加很多功能。比如自动下单需要对接平台的交易API、价格预警当利润率超过阈值时发通知、历史价格图表用ECharts画走势图、多平台比价同时对比三四个平台的价格。我目前正在做价格预警这块实现思路是加一个定时任务每5分钟检查一次最新价格如果发现符合条件的饰品就发通知。关于代码的维护。这套系统我跑了大半年中间改过好几次。我的经验是把配置和代码分开。采集频率、利润率阈值、平台手续费率这些参数全部放在一个config.py或者config.json里改参数不用动代码。另外每个模块只做一件事采集器只管采集计算器只管计算Web只管展示这样出问题的时候容易定位。这套系统到现在还在跑每天帮我省下至少一个小时的盯盘时间而且捕捉到的机会比我手动看的时候多得多。如果你也在做Steam饰品交易强烈建议自己搭一套投入的时间很快就能回本。
返回列表