【问题标题】:WebScraping in R: dealing with tabs on WebSitesR中的网页抓取:处理网站上的标签
【发布时间】:2019-09-14 00:58:40
【问题描述】:

这是我的网站:

url<-https://projects.fivethirtyeight.com/2017-nba-predictions/

正如你在这个问题上看到的那样:Web scraping in R?

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

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

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

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

但是我该如何处理呢?

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

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)

有什么帮助吗?

【问题讨论】:

    标签: r web-scraping


    【解决方案1】:

    tl;dr;

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

    我将对正在发生的事情和潜在的解决方案给出一个与语言无关的描述。然后,我将展示该解决方案在 Python 中的实现。对于 R,我不确定如何在 R 中执行数据框操作来管理 1 周的更改计算。我可能有一天会更新它以包含完整的 R 示例。


    一些观察:

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

    数据来源:

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

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

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


    表构建和填充:

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

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

    还有辅助函数,例如,将 elocar-melo 输出舍入:


    了解数据的存储方式:

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

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

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

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


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

    遗憾的是,如果一个正则表达式不在数组中,则此处使用的 javascript 表示法不适合使用 json 库进行轻松解析。至少在 Python 中,我们有 hjson 可以处理不带引号的键(变量名称),但在这种情况下尝试解析信息时仍然会结束 EOF 错误(尽管我可能需要更改我的正则表达式提前终止 - 再次沉思)。但我们能做的是:

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

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

    理解正则表达式:

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

    然后应该向您提供某种解释。免责声明:我不是正则表达式专家。欢迎提出改进建议。


    比较输出:

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

    网页

    输出:


    阅读:

    1. Regex

    【讨论】:

    • 再一次,我真的无话可说。谢谢!
    【解决方案2】:

    试试这个:

    library(rvest)
    library(tidyverse)
    
        page <- read_html('https://projects.fivethirtyeight.com/2017-nba-predictions/')
        page %>% 
          html_nodes("table") %>% 
          .[[3]] %>% 
          html_table(., fill = TRUE, header = FALSE) %>% 
          dplyr::select(-ncol(.)) %>% 
          dplyr::slice(-c(1:3)) %>% 
          setNames(., .[1, ]) %>% 
          dplyr::slice(-1)
    

    【讨论】:

    • 这并没有解决 OP 对不同日期的请求。
    【解决方案3】:

    我已经使用 RSelenium 执行了一些网络抓取任务,所以让我分享一个适用于您的用例的工作代码。这不是您问题的直接解决方案,而是可能的方法之一。希望这会有所帮助!

    library(RSelenium)
    
    # Create a Selenium Driver Instance; change the chrome version to your installed instance
    
    rd <- rsDriver(browser = c("chrome"),chromever = "76.0.3809.126", port = 9515L)
    
    # Assign the client to remDr variable. This will be the Chrome window to be used for automation
    
    remDr <- rd$client
    
    remDr$navigate("https://projects.fivethirtyeight.com/2017-nba-predictions/")
    
    # Get the options present under 'Forecast from' dropdown
    FDatesRaw <- remDr$findElement(using = "xpath", value = "//div[@id='forecast-selector']")$getElementText()[[1]]
    
    # Simplify the list
    FDatesParsed <- unlist(strsplit(FDatesRaw,"\n",))
    
    # Find the index of the option based on the date. Replace 'June 7' with any option from FDatesParsed as you need
    option <- match("June 7",FDatesParsed)-1
    
    # Generate xpath to be passed with the selected index
    query <-  paste0("//div[@id='forecast-selector']//option[@value = '",option,"']")
    
    # Send the selected value to browser
    remDr$findElement(using = 'xpath', value = query)$clickElement()
    
    # Get the page source as html
    page <- read_html(remDr$getPageSource()[[1]])
    
    # Followed by your code
    
    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'))
    
    # Close the browser, stop the server and collect garbage
    remDr$close()
    rd$server$stop()
    gc() 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-25
      • 2018-10-16
      • 1970-01-01
      • 2020-09-30
      相关资源
      最近更新 更多