PHP项目统计SofaScore综合评分方案
理解需求
SofaScore 综合评分(Rating)是针对球员单场表现生成的评分(1.0~10.0),综合评分一般是多场比赛评分的加权平均,要统计综合评分,核心是:

- 获取球员每场比赛的评分数据
- 设计合理的加权算法
- 存储与聚合计算
数据来源
方式1:SofaScore 官方 API(非公开,需逆向)
SofaScore 有内部 API,典型接口:
GET https://api.sofascore.com/api/v1/player/{playerId}/statistics/overall
GET https://api.sofascore.com/api/v1/player/{playerId}/events/last/0
GET https://api.sofascore.com/api/v1/event/{eventId}/player/{playerId}/statistics
⚠️ 注意:官方未授权公开 API,使用需遵守其服务条款,建议:
- 个人学习/研究使用,控制请求频率
- 加
User-Agent、代理、缓存,避免封 IP - 商业项目建议购买数据服务(如 API-Football、Sportmonks)
方式2:第三方数据源
- API-Football(RapidAPI):有球员评分
- Sportmonks:付费正规
- Football-Data.org:免费,但无球员评分
方式3:自行抓取(爬虫)
用 PHP + Guzzle 抓页面或内部接口,需注意反爬。
核心数据结构设计(MySQL)
-- 球员表
CREATE TABLE players (
id INT PRIMARY KEY AUTO_INCREMENT,
sofa_player_id BIGINT UNIQUE,
name VARCHAR(100),
team_id INT,
position VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 单场评分记录表
CREATE TABLE player_match_ratings (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
player_id INT NOT NULL,
match_id BIGINT NOT NULL,
match_date DATE NOT NULL,
competition_id INT,
season VARCHAR(20),
minutes_played INT DEFAULT 0,
rating DECIMAL(3,1) NOT NULL, -- 1.0 ~ 10.0
goals INT DEFAULT 0,
assists INT DEFAULT 0,
is_starter TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_player_match (player_id, match_id),
KEY idx_player_date (player_id, match_date),
KEY idx_competition (competition_id, season)
);
-- 综合评分缓存/汇总表(可选,提升查询性能)
CREATE TABLE player_aggregate_ratings (
player_id INT PRIMARY KEY,
competition_id INT DEFAULT 0, -- 0 表示全部赛事
season VARCHAR(20) DEFAULT 'all', -- all 表示全部赛季
matches_count INT DEFAULT 0,
total_minutes INT DEFAULT 0,
weighted_sum DECIMAL(10,2) DEFAULT 0,
avg_rating DECIMAL(3,2) DEFAULT 0, -- 简单平均
weighted_rating DECIMAL(3,2) DEFAULT 0, -- 加权平均
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
综合评分算法
简单平均(不推荐,短时间出场拉低/拉高失真)
$$\text{avg} = \frac{\sum r_i}{n}$$
出场时间加权(推荐)
$$\text{weighted} = \frac{\sum (r_i \times \min(m_i, 90))}{\sum \min(m_i, 90)}$$
理由:出场 5 分钟得 7.0 和踢满 90 分钟得 7.0 分量不同。
更多维度加权(进阶)
weight = minutes_factor × competition_factor × recency_factor
minutes_factor:min(minutes, 90) / 90competition_factor:欧冠 1.2 / 联赛 1.0 / 杯赛 0.8recency_factor:exp(-λ × 天数差)做时间衰减,λ ≈ 0.002(约一年半衰期)
PHP 实现示例
<?php
class RatingCalculator
{
/**
* 出场时间加权平均
* @param array $ratings 每项 ['rating'=>7.5,'minutes'=>90,'competition_id'=>1,'match_date'=>'2024-03-01']
*/
public function weightedAverage(array $ratings): float
{
if (empty($ratings)) return 0.0;
$sum = 0.0;
$weightSum = 0.0;
foreach ($ratings as $r) {
$minutes = min((int)$r['minutes'], 90);
if ($minutes <= 0) continue; // 未出场忽略
$w = $minutes
* $this->competitionFactor($r['competition_id'] ?? 0)
* $this->recencyFactor($r['match_date'] ?? date('Y-m-d'));
$sum += $r['rating'] * $w;
$weightSum += $w;
}
return $weightSum > 0 ? round($sum / $weightSum, 2) : 0.0;
}
private function competitionFactor(int $competitionId): float
{
// 按赛事重要性调整,示例映射
$map = [
1 => 1.2, // 欧冠
2 => 1.1, // 欧联
8 => 1.0, // 国内联赛
0 => 0.9, // 友谊赛/其他
];
return $map[$competitionId] ?? 1.0;
}
private function recencyFactor(string $matchDate): float
{
$days = (time() - strtotime($matchDate)) / 86400;
$lambda = 0.002; // 半衰期约 346 天
return exp(-$lambda * max(0, $days));
}
/** 简单平均,供对比 */
public function simpleAverage(array $ratings): float
{
$vals = array_filter(array_column($ratings, 'rating'), fn($v) => $v > 0);
return count($vals) ? round(array_sum($vals) / count($vals), 2) : 0.0;
}
}
数据采集(Guzzle 示例)
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
class SofaFetcher
{
private Client $http;
public function __construct()
{
$this->http = new Client([
'base_uri' => 'https://api.sofascore.com/',
'timeout' => 10,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Accept' => 'application/json',
'Referer' => 'https://www.sofascore.com/',
],
]);
}
/** 获取球员近 N 场评分 */
public function getPlayerRecentRatings(int $playerId, int $page = 0): array
{
$res = $this->http->get("api/v1/player/{$playerId}/events/last/{$page}");
$data = json_decode($res->getBody()->getContents(), true);
$result = [];
foreach ($data['events'] ?? [] as $ev) {
// 该接口返回赛事列表,还需要再查每场评分
// 实际可用 /event/{id}/player/{pid}/statistics
$result[] = [
'match_id' => $ev['id'],
'match_date' => date('Y-m-d', $ev['startTimestamp']),
'competition_id' => $ev['tournament']['uniqueTournament']['id'] ?? 0,
];
}
return $result;
}
/** 获取单场评分 */
public function getMatchRating(int $eventId, int $playerId): ?array
{
try {
$res = $this->http->get("api/v1/event/{$eventId}/player/{$playerId}/statistics");
$data = json_decode($res->getBody()->getContents(), true);
return $data['statistics'] ?? null;
} catch (\Throwable $e) {
return null;
}
}
}
配合限速(sleep(1))、缓存(Redis 存 24h)、失败重试。
完整统计流程
┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ 采集层 │→→│ 清洗/入库 │→→│ 聚合计算 │→→│ 对外接口/展示 │
│ Sofa API │ │ 去重+校验 │ │ 加权评分 │ │ REST/前端 │
└─────────────┘ └──────────────┘ └───────────────┘ └──────────────┘
定时任务(cron / 队列):
- 每日拉取昨日比赛评分 → 入库
- 重算受影响球员的聚合
- 写入
player_aggregate_ratings
聚合 SQL 示例(简单加权):
INSERT INTO player_aggregate_ratings
(player_id, competition_id, season, matches_count, total_minutes, weighted_sum, avg_rating, weighted_rating)
SELECT
player_id,
0,
'all',
COUNT(*),
SUM(minutes_played),
SUM(rating * LEAST(minutes_played, 90)),
ROUND(AVG(rating), 2),
ROUND(SUM(rating * LEAST(minutes_played, 90)) / NULLIF(SUM(LEAST(minutes_played, 90)), 0), 2)
FROM player_match_ratings
GROUP BY player_id
ON DUPLICATE KEY UPDATE
matches_count = VALUES(matches_count),
total_minutes = VALUES(total_minutes),
weighted_sum = VALUES(weighted_sum),
avg_rating = VALUES(avg_rating),
weighted_rating = VALUES(weighted_rating),
updated_at = NOW();
REST 接口示例
// GET /api/players/{id}/rating?competition=0&season=all&method=weighted
$stmt = $pdo->prepare(
'SELECT * FROM player_aggregate_ratings
WHERE player_id = ? AND competition_id = ? AND season = ?'
);
$stmt->execute([$playerId, $competitionId, $season]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'player_id' => $playerId,
'matches' => (int)$row['matches_count'],
'avg_rating' => (float)$row['avg_rating'],
'weighted_rating' => (float)$row['weighted_rating'],
'updated_at' => $row['updated_at'],
]);
实战注意事项
| 问题 | 建议 |
|---|---|
| 反爬/封禁 | 加缓存(Redis 24h)、随机延迟、代理池,分散请求 |
| 评分口径 | SofaScore 会回溯修正评分,需定期重拉历史(如 7 天内比赛) |
| 短时间出场 | 出场 < 15 分钟的比赛建议按系数削减或排除出"综合评分" |
| 位置差异 | 门将/后卫/前锋评分基准不同,可按位置分组统计 |
| 合法合规 | 商业项目请使用 API-Football 等授权渠道 |
| 重复比赛 | 用 UNIQUE(player_id, match_id) 保证幂等 |
可选进阶:综合评分模型
不只依赖 SofaScore 单一评分,可融合多源做综合分:
$$ S = w1 \cdot \text{SofaScore}{\text{加权}} + w_2 \cdot \frac{\text{进球+助攻}}{\text{出场}} + w_3 \cdot \text{球队胜率} + \dots $$
标准化到 0~10 后加权——这就是"自建综合评分",SofaScore 本身只是输入之一。
如果需要,我可以进一步提供:
- Laravel 版的 Service/Command/模型代码
- Redis 缓存 + 队列的完整采集流程
- 爬虫防封的具体策略(UA 池、代理、异常重试)
- 针对某个 具体球员/球队的完整可运行 demo
告诉我你的 PHP 框架(原生/Laravel/Symfony)和目标场景,我可以直接给可跑代码。