使用Python机器学习预测足球比赛结果:第一篇 数据采集 (下)

利物浦7比0狂胜曼联,这个锅不能再让C罗背了吧。预测足球比分有什么好方法吗?

微信搜索关注《Python学研大本营》,加入读者群,分享更多精彩

探索足球结果和赔率的 Python 项目。

那么,让我们按照我所遵循的步骤进行:

Old Matches Scraper

在比赛页面中,到达指定日期

yearNow, monthNow, dayNow = self._getDate(day)
urlDay = self.originLink + "/en/matches/{year}-{month}-{day}".format(year=yearNow, month=monthNow, day=dayNow)
print(urlDay)
html = urlopen(urlDay)
bs = BeautifulSoup(html.read(), 'html.parser')

def _getDate(self, date):
  """
    Helper function used to format url in the desired date in getMatches()
    :param date: datetime.date object
    :return: The formatted year, month and day of the date object
    """
    year = str(date.year)
    month = str(date.month) if date.month >= 10 else '0' + str(date.month)
    day = str(date.day) if date.day >= 10 else '0' + str(date.day)
    return year, month, day

这个过程和下面的所有过程都是在用户定义的迭代宇宙中每天进行的。函数 getMatches() 有一个开始日期和一个结束日期,它设置了抓取器将执行的边界。

2.获取每张冠军表

championshipTables = bs.find_all('div', {'class':'table_wrapper'})
errorList = []
for i in range(len(championshipTables)):
  try:
    championshipTables[i].find('a', {'href':re.compile('^/en/comps/')}).get_text()
  except AttributeError:
    errorList.append(i)
  for error in errorList:
    del championshipTables[error]
  desiredTables = [ch for ch in championshipTables if ch.find('a', {'href':re.compile('^/en/comps/')}).get_text() in leagues]

按照第一步的例子,联赛变量可以由用户输入,所以他选择他想要报废的联赛。我们还可以在代码中看到一个 try-except 子句,它处理结构错误,例如网站中可能出现的假表。

3.从每个冠军表中,从比赛行中获取信息

for table in desiredTables:
  time.sleep(4)
  matchesLinks = []
  homeTeams = table.find_all('td', {'data-stat':'home_team'})
  for team in homeTeams:
    self.homeTeams.append(team.get_text())
    self.dates.append(day)
    awayTeams = table.find_all('td', {'data-stat':'away_team'})
  for team in awayTeams:
    self.awayTeams.append(team.get_text())
    scores = table.find_all('td', {'data-stat':'score'})
  for score in scores:
    scoreHome, scoreAway = self._getScore(score.get_text())
    self.scoresHome.append(scoreHome)
    self.scoresAway.append(scoreAway)
    matchesLinks.append(score.find('a', {'href':re.compile('^/')})['href'])

  if table.find_all('td', {'data-stat':'home_xg'}):
    homeXG = table.find_all('td', {'data-stat':'home_xg'})
    awayXG = table.find_all('td', {'data-stat':'away_xg'})
    for xg in homeXG:
      self.homeXG.append(xg.get_text())
    for xg in awayXG:
      self.awayXG.append(xg.get_text())
  else:
    for team in homeTeams:
      self.homeXG.append(np.nan)
      self.awayXG.append(np.nan)

在这里,除了在我们的列表中添加我们最开始想要的信息外,我突出显示了睡眠时间,用于控制我们在一定时间内发出的请求数量,避免我们的IP被禁止。另外值得注意的是每个比赛报告链接的存储,它包含在分数变量中。通过从分数变量而不是“匹配报告”中捕获链接,我们可以避免在内存中存储延迟或取消的匹配链接。这引导我们进入下一步:

4.获取每场比赛报告并检索信息

for link in matchesLinks:
  dfMatchStats.loc[len(dfMatchStats)] = self._getMatchStats(link)
  
def _getMatchStats(self, url):
  """
    Helper function to extract the match stats for each match in getMatches()
    :param url: The match report url - is extracted in getMatches()
    :return: List with match stats
  """

  stats={"Fouls":[np.nan, np.nan], "Corners":[np.nan, np.nan], "Crosses":[np.nan, np.nan], "Touches":[np.nan, np.nan],
        "Tackles":[np.nan, np.nan], "Interceptions":[np.nan, np.nan],"Aerials Won":[np.nan, np.nan],
        "Clearances":[np.nan, np.nan], "Offsides":[np.nan, np.nan], "Goal Kicks":[np.nan, np.nan], "Throw Ins":[np.nan, np.nan],
        "Long Balls":[np.nan, np.nan]}

  matchStatsList = []
  htmlMatch = urlopen(self.originLink + url)
  bsMatch = BeautifulSoup(htmlMatch.read(), 'html.parser')
  homeLineup = bsMatch.find('div', {'class':'lineup', 'id':'a'})
  if not homeLineup:
    homePlayers = []
    awayPlayers = []
    for i in range(0,11):
      homePlayers.append(np.nan)
      awayPlayers.append(np.nan)
    yellowCardsHome = np.nan
    redCardsHome = np.nan
    yellowCardsAway = np.nan
    redCardsAway = np.nan
    matchStatsList.extend([yellowCardsHome, redCardsHome, yellowCardsAway, redCardsAway])
    for key, value in stats.items():
      matchStatsList.extend(value)
    return homePlayers + awayPlayers + matchStatsList
  homePlayers = homeLineup.find_all('a', {'href':re.compile('^/en/players')})[0:11]
  homePlayers = [player.get_text() for player in homePlayers]
  awayLineup = bsMatch.find('div', {'class':'lineup', 'id':'b'})
  awayPlayers = awayLineup.find_all('a', {'href':re.compile('^/en/players')})[0:11]
  awayPlayers = [player.get_text() for player in awayPlayers]
  matchCards = bsMatch.find_all('div', {'class':'cards'})
  yellowCardsHome = len(matchCards[0].find_all('span', {'class':'yellow_card'})) + len(matchCards[0].find_all('span', {'class':'yellow_red_card'}))
  redCardsHome = len(matchCards[0].find_all('span', {'class':'red_card'})) + len(matchCards[0].find_all('span', {'class':'yellow_red_card'}))
  yellowCardsAway = len(matchCards[1].find_all('span', {'class':'yellow_card'})) + len(matchCards[1].find_all('span', {'class':'yellow_red_card'}))
  redCardsAway = len(matchCards[1].find_all('span', {'class':'red_card'})) + len(matchCards[1].find_all('span', {'class':'yellow_red_card'}))
  matchStatsList.extend([yellowCardsHome, redCardsHome, yellowCardsAway, redCardsAway])
  
  extraStatsPanel = bsMatch.find("div", {"id":"team_stats_extra"})
  for statColumn in extraStatsPanel.find_all("div", recursive=False):
    column = statColumn.find_all("div")
    columnValues = [value.get_text() for value in column]
    for index, value in enumerate(columnValues):
      if not value.isdigit() and value in stats:
        stats[value] = [int(columnValues[index-1]), int(columnValues[index+1])]
  for key, value in stats.items():
    matchStatsList.extend(value)
    
  return homePlayers + awayPlayers + matchStatsList

正如您所看到的,这个过程有点棘手,所以让我们做一个简单的解释。黄色和红色卡片是通过将黄色或红色类别的卡片对象的数量相加而得出的。其他统计数据来自:

  • 检查预期统计数据字典中的统计数据

  • 如果为真,则使用链接到该统计的值更新字典,这些值是与统计名称相关的上一个和下一个值 热切的读者可能已经意识到第 2 步——获取每个冠军表——不是强制性的,但它使我们能够灵活地只过滤我们想要的联赛的比赛,这就是我采用的方法。

作为一个额外的步骤,我意识到需要创建一个检查点触发器,因为爬虫可能会面临无法预料的错误,或者 fbref 可能会不允许您的 IP 发出新请求,而这种情况将意味着大量时间的浪费。然后,每个月的每个第一天,我们都会保存到目前为止的爬虫工作,以防万一发生意外错误,我们有一个安全检查点可以检索。

仅此而已。在下面代码的底部,您可以看到日期更新 iteraroe 和格式化最终数据框所需的操作。

if day.day == 1:
  # if the process crashes, we have a checkpoint every month starter
  dfCheckpoint = dfMatchStats.copy()
  dfCheckpoint["homeTeam"] = self.homeTeams
  dfCheckpoint["awayTeam"] = self.awayTeams
  dfCheckpoint["scoreHome"] = self.scoresHome
  dfCheckpoint["scoreAway"] = self.scoresAway]
  dfCheckpoint["homeXG"] = self.homeXG
  dfCheckpoint["awayXG"] = self.awayXG
  dfCheckpoint["date"] = self.dates
  dfCheckpoint.to_pickle(os.path.join(self.dataFolder, 'checkPoint.pkl'))
  
day = day + timedelta(days=1)
dfMatchStats["homeTeam"] = self.homeTeams
dfMatchStats["awayTeam"] = self.awayTeams
dfMatchStats["scoreHome"] = self.scoresHome
dfMatchStats["scoreAway"] = self.scoresAway
dfMatchStats["homeXG"] = self.homeXG
dfMatchStats["awayXG"] = self.awayXG
dfMatchStats["date"] = self.dates

return dfMatchStats

数据框预览

整个过程允许我们抓取一些数据来建立模型来预测足球比赛,但我们仍然需要抓取有关即将举行的比赛的数据,以便我们可以对已经收集的数据做一些有用的事情。我为此找到的最佳来源是SofaScore,该应用程序还收集和存储有关比赛和球员的信息,但不仅如此,它们还在Bet365中提供每场比赛的实际赔率。

SofaScore 特别处理 JavaScript 代码,这意味着 html 脚本并不完全可供我们与 BeautifulSoup 一起使用。这意味着我们需要使用另一个框架来抓取他们的信息。我选择了广泛使用的Selenium包,它使我们能够像人类用户一样通过 Python 代码上网冲浪。您实际上可以看到网络驱动程序在您选择的浏览器中点击和导航——我选择了 Chrome。

在下图中,您可以看到 SofaScore 主页以及正在进行或即将进行的比赛,在右侧,您可以看到当您点击特定比赛然后点击“LINEUPS”时会发生什么。

SofaScore 界面

使用 Selenium,正如我所解释的,它的工作方式就像人类用户在网上冲浪一样,您可能会认为这个过程会慢一点,这是事实。因此,我们必须在每个步骤中更加小心,这样我们就不会点击不存在的按钮,一旦 JavaScript 代码仅在用户执行某些操作后呈现,例如当我们点击特定匹配项时,服务器会采取需要一些时间来渲染我们在第二张图片中看到的侧边菜单,如果代码在此期间尝试单击阵容按钮,则会返回错误。现在,让我们来看看代码。

即将到来的Matches Scraper

  1. 打开主页并激活“显示赔率”按钮

def _getDriver(self, path='D:/chromedriver_win32/chromedriver.exe'):
  chrome_options = Options()
  return webdriver.Chrome(executable_path=path, options=chrome_options)  

def getMatchesToday(self):
  self.driver = self._getDriver(path=self.path)
  self.driver.get("https://www.sofascore.com/")
        
  WebDriverWait(self.driver, 20).until(EC.element_to_be_clickable((By.CLASS_NAME, "slider")))
  oddsButton = self.driver.find_element(By.CLASS_NAME, "slider")
  oddsButton.click()

  homeTeam=[]
  awayTeam=[]
  odds=[]
  homeOdds = []
  drawOdds = []
  awayOdds = []

正如我提到的,在启动驱动程序并到达 SofaScore 的 URL 后,我们需要等到赔率按钮呈现后才能单击它。我们还为我们创建了列表来存储抓取的信息。

2.店铺匹配主要信息

WebDriverWait(self.driver, 5).until(EC.visibility_of_element_located((By.CLASS_NAME, 'fvgWCd')))
matches = self.driver.find_elements(By.CLASS_NAME, 'js-list-cell-target')
for match in matches:
  if self._checkExistsByClass('blXay'):
    homeTeam.append(match.find_element(By.CLASS_NAME, 'blXay').text)
    awayTeam.append(match.find_element(By.CLASS_NAME, 'crsngN').text)

    if match.find_element(By.CLASS_NAME, 'haEAMa').text == '-':
      oddsObject = match.find_elements(By.CLASS_NAME, 'fvgWCd')
      for odd in oddsObject:
        odds.append(odd.text)

while(len(odds) > 0):
  homeOdds.append(odds.pop(0))
  drawOdds.append(odds.pop(0))
  awayOdds.append(odds.pop(0))

这里没有什么特别的,但是考虑到在第 8 行我们只过滤还没有开始的匹配是很好的。我这样做是因为处理正在进行的比赛会使赔率变得更加棘手,而且目前还不清楚未来的投注模拟器将如何工作,而且它可能无法在实时结果中正常工作。

3.获得阵容

df = pd.DataFrame({"homeTeam":homeTeam, "awayTeam":awayTeam, "homeOdds":homeOdds, "drawOdds":drawOdds, "awayOdds":awayOdds})
lineups = self._getLineups()

df = pd.concat([df, lineups], axis=1).iloc[:,:-1]

return df

def _getLineups(self):

  matches = self.driver.find_elements(By.CLASS_NAME, "kusmLq")

  nameInPanel = ""

  df = pd.DataFrame(columns=["{team}Player{i}".format(team="home" if i <=10 else "away", i=i+1 if i <=10 else i-10) for i in range(0,22)])
  df["homeTeam"] = []

  for match in matches:
  
    self.driver.execute_script("arguments[0].click()", match)

    #wait until panel is refreshed
    
    waiter = WebDriverWait(driver=self.driver, timeout=10, poll_frequency=1)
    waiter.until(lambda drv: drv.find_element(By.CLASS_NAME, "dsMMht").text != nameInPanel)
    nameInPanel = self.driver.find_element(By.CLASS_NAME, "dsMMht").text
        
    if self._checkExistsByClass("jwanNG") and self.driver.find_element(By.CLASS_NAME, "jwanNG").text == "LINEUPS":
    
      lineupButton = self.driver.find_element(By.CLASS_NAME, "jwanNG")
      lineupButton.click()
      # wait until players are avilable
      WebDriverWait(self.driver, 20).until(EC.visibility_of_element_located((By.CLASS_NAME, "kDQXnl")))
      players = self.driver.find_elements(By.CLASS_NAME, "kDQXnl")
      playerNames=[]
      for player in players:
        playerNames.append(player.find_elements(By.CLASS_NAME, "sc-eDWCr")[2].accessible_name)
      playerNames = [self._isCaptain(playerName) for playerName in playerNames]
      playerNames.append(nameInPanel)
               
      df.loc[len(df)] = playerNames
    else:
      df.loc[len(df), "homeTeam"] = nameInPanel

  return df
  
 def _isCaptain(self, name):
  if name.startswith("(c) "):
  name = name[4:]
  return name

数据框预览

总结上面的代码块,我们等到比赛的侧边菜单加载完毕,单击阵容按钮并获取球员姓名。我们需要注意一下,因为每个团队的队长的名字在网站上都是格式化的,所以我们创建了一个辅助函数来处理它。然后,我们将每场比赛的球员姓名存储在数据框中,最后在整个过程之后,我们将比赛信息与预测阵容连接起来。

结论

那么,今天就到此为止。在这篇文章中,我们构建了两个抓取工具,可以收集过去的足球比赛信息,也可以收集未来的比赛信息。这只是项目的开始,一旦您可以期待有关获取包含玩家信息的数据集、预测器建模和最后的投注策略模拟器的新文章。希望你喜欢它!

推荐书单

《Python从入门到精通(第2版)》

《Python从入门到精通(第2版)》从初学者角度出发,通过通俗易懂的语言、丰富多彩的实例,详细介绍了使用Python进行程序开发应该掌握的各方面技术。全书共分23章,包括初识Python、Python语言基础、运算符与表达式、流程控制语句、列表和元组、字典和集合、字符串、Python中使用正则表达式、函数、面向对象程序设计、模块、异常处理及程序调试、文件及目录操作、操作数据库、GUI界面编程、Pygame游戏编程、网络爬虫开发、使用进程和线程、网络编程、Web编程、Flask框架、e起去旅行网站、AI图像识别工具等内容。所有知识都结合具体实例进行介绍,涉及的程序代码都给出了详细的注释,读者可轻松领会Python程序开发的精髓,快速提升开发技能。除此之外,该书还附配了243集高清教学微视频及PPT电子教案。

《Python从入门到精通(第2版)》【摘要 书评 试读】- 京东图书京东JD.COM图书频道为您提供《Python从入门到精通(第2版)》在线选购,本书作者:明日科技,出版社:清华大学出版社。买图书,到京东。网购图书,享受最低优惠折扣!icon-default.png?t=N4P3https://item.jd.com/13284890.html

精彩回顾

ChatGPT教你如何用Python和Matplotlib绘图(上) 

ChatGPT教你如何用Python和Matplotlib绘图(下) 

《事半功倍,使用ChatGPT编写Python函数》

超快速,使用ChatGPT编写回归和分类算法

《如虎添翼,将ChatGPT和Python结合起来!》

《ChatGPT优化Python代码的小技巧》

《使用ChatGPT提升Python编程效率》 

微信搜索关注《Python学研大本营》

访问【IT今日热榜】,发现每日技术热点

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值