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 对象中检索信息,例如
还有辅助函数,例如,将 elo 和 car-melo 输出舍入:
了解数据的存储方式:
我们需要的数据都在函数15中;在数组数组中:
因此,一组预测包含一组团队信息。
如果我们放大一个日期(即外部预测数组中的单个项目),我们会看到:
如果您查看右侧,您可以看到与该特定日期的不同团队相关联的每个块。
检索项目并用感兴趣的列重新构建一个表:
遗憾的是,如果一个正则表达式不在数组中,则此处使用的 javascript 表示法不适合使用 json 库进行轻松解析。至少在 Python 中,我们有 hjson 可以处理不带引号的键(变量名称),但在这种情况下尝试解析信息时仍然会结束 EOF 错误(尽管我可能需要更改我的正则表达式提前终止 - 再次沉思)。但我们能做的是:
- 对该文件发出请求并将其内容用作数据源
- 获取与函数 15 关联的字符串
- 使用正则表达式按日期生成块(即 javascript var
forecasts 所包含的按日期分组的项目数组):
- 循环这些块并提取匹配列表(再次通过正则表达式)
date, elo, car-melo and team。然后可以将这些列表连接到该日期的数据框中。 date 字段必须重复其他列之一的长度(例如 elo),因为它每个块只出现一次。
- 为舍入的
elo 和car-melo 数字添加额外的列(或直接在elo 和car-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。
然后应该向您提供某种解释。免责声明:我不是正则表达式专家。欢迎提出改进建议。
比较输出:
这是网页和输出的示例比较。
网页
输出:
阅读:
- Regex