R 中的 WebScraping:处理网站上的选项卡

Lau*_*ura 3 r web-scraping

这是我的网站:

url<-https://projects.fivethirtyeight.com/2017-nba-predictions/
Run Code Online (Sandbox Code Playgroud)

正如您在这个问题上看到的:R 中的 Web 抓取?

您可以选择不同的日期,然后您的表格会发生变化。

但我的问题是不同的:如何从不同的日子提取表格?

我只能提取与“今天”日期相关的表格。

我知道id="standings-table-wrapper"每次更改日期时都需要使用 id 。

但是我该如何处理呢?

这就是我设法提取有关“今天”日期的表格的方式:

library(rvest)
library(magrittr)

page <- read_html('https://projects.fivethirtyeight.com/2017-nba-predictions/')
df <- setNames(data.frame(cbind(
  html_text(html_nodes(page, 'td.original')),
  html_text(html_nodes(page, 'td.carmelo')),
  html_text(html_nodes(page, '.change')),
  html_text(html_nodes(page, '.team a'))
)),c('elo','carmelo','1wkchange','team'))

print(df)
Run Code Online (Sandbox Code Playgroud)

有什么帮助吗?

QHa*_*arr 5

tl;博士;

该页面依赖于在浏览器中运行的 javascript 来处理选择不同日期时的表格更新。使用 发出请求时不会发生此过程rvest

我将对正在发生的事情和潜在的解决方案进行相当语言不可知的描述。然后,我将展示该解决方案在 Python 中的实现。因为 RI 不确定如何在 R 中进行数据帧操作来管理 1 周的变化计算。我可能有一天会更新它以包含完整的 R 示例。


一些观察:

  • 观察:如果您在浏览器中禁用 javascript 并重新加载页面,您将看到表格不再更新。
    • 结论:为了获取不同日期的数据,javascript 在页面上运行(当您使用 发出请求时,javascript 不会运行rvest)。
  • 观察:如果您通过F12开发工具监控网络流量,同时进行不同的日期选择,页面不会因日期选择而产生额外的流量。
    • 结论:更新表所需的所有数据在页面加载时都存在,并且存在驱动它的 javascript。

数据来源:

基于这两个观察,快速搜索页面的源文档很快就会显示 javascript 源代码,并且整个表是动态构建的。

在此处输入图片说明

此缩小(压缩)js 文件的源链接是:

https://projects.fivethirtyeight.com/2017-nba-predictions/js/bundle.js?v=c1d7d294b039ddcc92b494602a5e337b


表构建和填充:

进一步查看此文件,我们可以看到动态构建和填充表的说明:

在此处输入图片说明

数据填充由一系列函数处理,这些函数从文件中的 javascript 对象中检索信息,例如

和辅助函数,例如,舍入elocar-melo输出:

在此处输入图片说明


了解数据的存储方式:

我们需要的数据都在函数15中;在数组数组中:

在此处输入图片说明

因此,一组预测每个都包含一组团队信息。

如果我们放大某个日期(即外部预测数组中的单个项目),我们将看到:

在此处输入图片说明

如果您查看右侧,您可以看到与特定日期的不同团队相关联的每个块。


检索项目并使用感兴趣的列重新构建表:

可悲的是,如果一个正则表达式不在数组中,这里使用的 javascript 符号并不适合使用 json 库轻松解析。至少在 Python 中,我们有hjsonwhich 可以处理未加引号的键(var 名称),但EOF在这种情况下尝试解析信息仍然会结束错误(尽管我可能需要更改我的正则表达式以提前终止 - 一个沉思下一次)。但是我们可以做的是:

  • 对该文件发出请求并将其内容用作数据源
  • 获取与函数 15 关联的字符串
  • 使用正则表达式按日期生成块(即按 javascript varforecasts存放的日期分组的项目数组):

在此处输入图片说明

  • 循环这些块并提取匹配列表(再次通过正则表达式)为date, elo, car-melo and team. 然后可以将这些列表连接到该日期的数据框中。该date字段必须重复其他列之一的长度(例如elo),因为它每个块只出现一次。
  • 添加额外的列(或直接在elocar-melo列上执行)用于四舍五入elocar-melo数字(您可以在图像中进一步复制原始 js 实现)或实现您自己的。
  • (可选)添加一列,以便您拥有团队的缩写名称和长名称。在我的 Python 实现中,我get_team_dict为此使用了一个辅助函数。
  • 然后,您需要生成 1 周的更改值,这意味着获取最终数据帧并执行GroupByonTEAMSort Byon Date desc
  • 对当前行和下一行之间的组执行差异计算,并从结果生成输出列。
  • 可能Carmelo desc在给定的date时间段内对分组的对象进行排序(如页面那样)。
  • 用结果做一些事情,例如写出到 CSV。

py实现:

import requests, re
import pandas as pd

def get_data(n, response_text): 
    # n is function number within js e.g.  15: [function(s, _, n){}  
    # left slightly abstract so can examine various functions in particular js block    
    pattern = re.compile(f',{n}:(\[.+),{n+1}', re.DOTALL)
    func_string = pattern.findall(response_text)[0]
    return func_string

def get_team_dict(response_text):
    p_teams = re.compile(r'abbrToFull:function\(s\){return{(.*?)}')
    team_info = p_teams.findall(response_text)[0].replace('"','')
    team_dict = dict(team.split(':') for team in team_info.split(','))
    return team_dict

def get_column(block, pattern, length = 0):  #p_block_elos, p_block_carmelos, p_block_teams, p_block_date
    values = pattern.findall(block)
    if length: values = values * length
    return values

def get_df_from_processed_blocks(info_blocks, team_dict):    
    final = pd.DataFrame()
    
    p_block_dates = re.compile(r'last_updated:"(.*?)T')
    p_block_teams = re.compile(r'name:"(.*?)"')
    p_block_elos = re.compile(r',elo:(.*?),')
    p_block_carmelos = re.compile(r'carmelo:(.*?),')

    for block in info_blocks:

        if block == info_blocks[0]: block = block.split('forecasts:')[-1]

        teams = get_column(block, p_block_teams)
        teams_fullnames = [team_dict[team] for team in teams]

        elos = get_column(block, p_block_elos)
        rounded_elos = [round(float(elo)) for elo in elos] # generate rounded values similar to the js func

        carmelos = get_column(block, p_block_carmelos)
        rounded_carmelos = [round(float(carmelo)) for carmelo in carmelos]

        dates = get_column(block, p_block_dates, len(elos)) # determine length of `elos` so can extend single date in block to match length for zipping lists for output
        df = pd.DataFrame(list(zip(dates, teams, teams_fullnames, elos, rounded_elos, carmelos, rounded_carmelos)))

        if final.empty:
            final = df
        else: 
            final = pd.concat([final, df], ignore_index = True)

    return final

def get_date_sorted_df_with_change_column(final):
    grouped_df = final.groupby(['TEAM (Abbr)'])
    grouped_df = grouped_df.apply(lambda _df: _df.sort_values(by=['DATE'], ascending=False ))
    grouped_df['1-WEEK CHANGE'] = pd.to_numeric(grouped_df['CARM-ELO'], errors='coerce').fillna(0).astype(int).diff(periods=-1)
    # Any other desired changes to columns....
    return grouped_df

def write_csv(final, file_name): 
    final.to_csv(f'C:/Users/User/Desktop/{file_name}.csv', sep=',', encoding='utf-8-sig',index = False, header = True)

def main():   
    
    response_text = requests.get('https://projects.fivethirtyeight.com/2017-nba-predictions/js/bundle.js?v=c1d7d294b039ddcc92b494602a5e337b').text   
    
    team_dict = get_team_dict(response_text)
    
    p_info_blocks = re.compile(r'last_updated:".+?Z",teams.+?\]', re.DOTALL)
    info_blocks = p_info_blocks.findall(get_data(15,response_text))    
    final = get_df_from_processed_blocks(info_blocks, team_dict)
    headers = ['DATE', 'TEAM (Abbr)', 'TEAM (Full)', 'ELO', 'Rounded ELO', 'CARM-ELO', 'Rounded CARM-ELO']
    final.columns = headers
    
    grouped_df = get_date_sorted_df_with_change_column(final)
    
    write_csv(grouped_df, 'scores')
    
if __name__ == "__main__":

    main()
Run Code Online (Sandbox Code Playgroud)

理解正则表达式:

我建议将正则表达式模式粘贴到在线正则表达式引擎中并观察描述。也许在浏览器或编辑器中针对源 js 文件进行测试。例如,生成块的正则表达式保存在此处

然后你应该得到某种解释。免责声明:我不是正则表达式专家。随时提出改进建议。

在此处输入图片说明


比较输出:

这是网页和输出的示例比较。

网页

在此处输入图片说明

输出:

在此处输入图片说明


读:

  1. 正则表达式