【问题标题】:Need assistance scraping HTML able from basketball-reference需要帮助从篮球参考中抓取 HTML
【发布时间】:2021-04-18 23:55:05
【问题描述】:

我对使用 python/BeautifulSoup/urllib.request 进行网络抓取非常陌生,并且一直在尝试弄清楚如何抓取该表的时间最长。我在网上找到了一些其他代码并进行了尝试,并一直在尝试了解它们的工作原理并对其进行修改,但是它们总是会过滤掉我需要的第一列。

代码:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import pandas as pd
import numpy 

# NBA season we will be analyzing
month = "january"
# URL page we will scrape (see image above)
url = "https://www.basketball-reference.com/leagues/NBA_2021_games-{}.html".format(month)
# this is the HTML for given URL
html = urlopen(url)
soup = BeautifulSoup(html)

# use findALL() to get the column headers
soup.findAll()
# use getText()to extract the text we need into a list
headers = [th.getText() for th in soup.findAll('tr', limit=2)[0].findAll('th')]
# exclude the first column as we will not need the ranking order from Basketball Reference for the analysis
headers=headers[1:]

# avoid the first header row
rows = soup.findAll('tr')[1:]

player_stats = [[td.getText() for td in rows[i].findAll('td')]

for i in range(len(rows))]
df = pd.DataFrame(player_stats, columns = headers)

This is what the HTML table looks like

谁能告诉我如何从这个网站上抓取表格?我这辈子都想不通 https://www.basketball-reference.com/leagues/NBA_2021_games-january.html

【问题讨论】:

  • 如果你不能让它工作也没关系,但请分享你期望工作的代码,并解释你为什么认为它不工作,或者你遇到问题的部分。你说你已经有了一些 Python 代码,所以这是一个很好的起点。
  • 我想我明白了。应该贴在上面。所以我遇到的问题是它不包括第一列。我知道第一列是与其他列不同类型的标签,但我不知道如何在我的数据框中获取它
  • 好的,我们可以看到代码了。 (请在以后修复缩进和格式,我这次做了)。
  • 嘿,非常感谢!我将研究代码以找出我哪里出错以及你做了什么。非常感谢!
  • 啊,现在才看到您的实际问题,第一列有 <th> 而不是 <td> 元素 - 会调整。

标签: python web-scraping beautifulsoup


【解决方案1】:

简单的解决方案是使用pandas:

import pandas as pd

url = "https://www.basketball-reference.com/leagues/NBA_2021_games-january.html"


# Get a list of tables on the page
df_list = pd.read_html(url)


# Print info.
print(f"Number of tables found: {len(df_list)}") # Out: Number of tables found: 1


# Select first dataframe object and go to town.
df = df_list[0]


print(f"Found {df.shape[0]} rows and {df.shape[1]} columns.") # Found 238 rows and 10 columns.

# You can also drop some of those fields, like the ones for Box Score and Notes, which don't contain too much relevant info:

df.drop(["Unnamed: 6", "Unnamed: 7", "Notes"], axis=1, inplace=True)

输出 -

               Date Start (ET)    Visitor/Neutral    PTS       Home/Neutral  PTS.1  Attend.
0  Fri, Jan 1, 2021      7:00p  Memphis Grizzlies  108.0  Charlotte Hornets   93.0      0.0
1  Fri, Jan 1, 2021      7:00p         Miami Heat   83.0   Dallas Mavericks   93.0      0.0
2  Fri, Jan 1, 2021      7:00p     Boston Celtics   93.0    Detroit Pistons   96.0      0.0
3  Fri, Jan 1, 2021      7:30p      Atlanta Hawks  114.0      Brooklyn Nets   96.0      0.0
4  Fri, Jan 1, 2021      8:00p      Chicago Bulls   96.0    Milwaukee Bucks  126.0      0.0

编辑:我看到这里有一些反对者,所以让我们走老路吧:

import re
from urllib.request import urlopen

with urlopen(url) as resp:
    data = resp.read().decode("utf-8")


def clean_data(d):
    """Replace newline, tab, and whitespace with single space."""
    return re.sub("\s{2}", " ", re.sub(r"(\t|\n|\s)+", "  ", d.strip()))


# Capture key segments by element tag and string indexing.
tbl_head = data[data.index("<thead"):data.index("</thead>")]
tbl_body = data[data.index("<tbody"):data.index("</tbody>")]


# Clean our head and body data.
tbl_head = clean_data(tbl_head)
tbl_body = clean_data(tbl_body)


# Simple match to get fields from the table.
# \S gets everything besides whitespace.
th_pat = r">(\S+)<"
p = re.compile(th_pat)
fields = p.findall(tbl_head)


Output of fields:

['Date',
 'Visitor/Neutral',
 'PTS',
 'Home/Neutral',
 'PTS',
 '&nbsp;',
 '&nbsp;',
 'Attend.',
 'Notes']


# Creative pattern to capture nested elements.
body_pat = r"""
    <t(?:h|d) .+?>
    (?:<a.+?>)?
    (.*?)
    (?:</a>)?
    </t\w>
    """
p = re.compile(body_pat, flags = re.X) # Use re.X if doing multiline pattern.


# Further cleaning of body data to remove whitespace.
# (Not super necessary.)
body_data = re.sub(r'("|<t\w) >', r'\1>', tbl_body)
body_data = re.sub(r'>\s+<', '><', body_data)

# Replace <tr> tags with newline character.
body_data = re.sub(r'(<tr>|</tr><tr>)', '\n', body_data)

# Iterate each line, capture our pattern output and add it as a sublist to res.
res = []
for line in body_data.split("\n"):
    tmp = p.findall(line)
    if len(tmp) > 0:
        res.append(tmp)


# First five lines of results
print("\n".join([f"{i}" for i in res[:5]]))

输出 -

['Fri, Jan 1, 2021', '7:00p', 'Memphis Grizzlies', '108', 'Charlotte Hornets', '93', 'Box Score', '', '0', '']
['Fri, Jan 1, 2021', '7:00p', 'Miami Heat', '83', 'Dallas Mavericks', '93', 'Box Score', '', '0', '']
['Fri, Jan 1, 2021', '7:00p', 'Boston Celtics', '93', 'Detroit Pistons', '96', 'Box Score', '', '0', '']
['Fri, Jan 1, 2021', '7:30p', 'Atlanta Hawks', '114', 'Brooklyn Nets', '96', 'Box Score', '', '0', '']
['Fri, Jan 1, 2021', '8:00p', 'Chicago Bulls', '96', 'Milwaukee Bucks', '126', 'Box Score', '', '0', '']

【讨论】:

  • 好的,但是 pandas 只适用于 HTML 的子集(并且没有 JS),不能处理嵌套/格式错误的标签等。最好诊断一下为什么 OP 的 BeautifulSoup 代码不能正常工作。跨度>
  • 使用网页抓取,无论什么工作都是一个好的解决方案,你总是依赖于页面不会改变 - 我认为熊猫解决方案可能会比任何更具体的编码器更强大跟上。它确实有效,所以没有理由不这样做。但是,如图所示,让它与 bs 一起工作也不难。
  • 另外,@smci, bs 也不适用于 JavaScript,除非您引入诸如无头浏览器(硒等)之类的东西,这远远超出了问题的范围。跨度>
  • @Grismar:这不是重点。我已经用熊猫和 BS 完成了刮擦。 pandas 很脆弱且不可扩展,除非 HTML 超级干净,这是不现实的。 BS 更加脾气暴躁,但对现实世界更加宽容。 “放弃BS并使用熊猫”通常是非常糟糕的建议。 OP 询问为什么他们的 BS 代码不起作用,而不是“我应该如何抓取这个?”
  • 正如 Mark 所示,pandas 可以处理这个站点 - 正如我在下面显示的那样,BS 代码根本不起作用,因为它忽略了标题。该网站实际上非常干净。你是对的,它不适用于由 JS 驱动的网站,或者 HTML 不好的网站,但这不是重点。
【解决方案2】:

解决方案:

  1. 第一列使用''标签而不是'',所有其他列都使用''。您使用的代码仅查找 '' (for td in rows[i].findAll('td')...),因此它不包括第一个(日期)列。**
  2. 然后删除您复制的代码中不需要的 hack 行 headers = headers[1:]

这是 BeautifulSoup 的答案(BS 对现实世界的 HTML(嵌套的、格式错误的标签)比 pandas 更宽容,除非 HTML 超级干净,否则它很脆弱,这是不现实的)。

所以您的代码正在尝试解析表格的每一行。让我们通过查看rows[0] 进行调试;精神上忽略 HTML 标签,只看到你所有的玩家统计数据实际上都正确存在:

>>> print(BeautifulSoup.prettify(rows[0]))

<tr>
 <th class="left" csk="202101010CHO" data-stat="date_game" scope="row">
  <a href="/boxscores/index.fcgi?month=1&amp;day=1&amp;year=2021">
   Fri, Jan 1, 2021
  </a>
 </th>
 <td class="right" data-stat="game_start_time">
  7:00p
 </td>
 <td class="left" csk="MEM.202101010CHO" data-stat="visitor_team_name">
  <a href="/teams/MEM/2021.html">
   Memphis Grizzlies
  </a>
 </td>
 <td class="right" data-stat="visitor_pts">
  108
 </td>
 <td class="left" csk="CHO.202101010CHO" data-stat="home_team_name">
  <a href="/teams/CHO/2021.html">
   Charlotte Hornets
  </a>
 </td>
 <td class="right" data-stat="home_pts">
  93
 </td>
 <td class="center" data-stat="box_score_text">
  <a href="/boxscores/202101010CHO.html">
   Box Score
  </a>
 </td>
 <td class="center iz" data-stat="overtimes">
 </td>
 <td class="right iz" data-stat="attendance">
  0
 </td>
 <td class="left iz" data-stat="game_remarks">
 </td>
</tr>

所以,我们需要重写:

player_stats = [[td.getText() for td in rows[i].findAll('td')]

作为:

player_stats = [[tc.getText() for tc in rows[0].findAll(['th','td'])]]

请注意,findAll() 将接受标签列表 ['th','td']

我将变量tc 重命名为“表格单元格”,因为它可以是“th”或“td”。

那么这给我们带来了第二个错误:

ValueError: 9 columns passed, passed data had 10 columns
>>> headers
['Start (ET)', 'Visitor/Neutral', 'PTS', 'Home/Neutral', 'PTS', '\xa0', '\xa0', 'Attend.', 'Notes']

columns 8 和 9 为空白,其中一个未被读取。

这是因为您复制的代码中不需要的 hack 行 headers = headers[1:]

但定义headers 的更简洁的方法是:

>>> [th.text for th in soup.findAll('thead')[0].findAll('th')]

['Date', 'Start (ET)', 'Visitor/Neutral', 'PTS', 'Home/Neutral', 'PTS', '\xa0', '\xa0', 'Attend.', 'Notes']

【讨论】:

    【解决方案3】:

    @MarkMoretto 说得对,让pandas 完成这项工作。但是,如果您想让 bs 工作,那也不难。

    您正在做的部分事情可能会更简短,但您的工作也一样:

    # specific to this page, you want the first table ([0]) and skip the first header ([1:])
    headers = [elem.text for elem in soup.findAll('thead')[0].findAll('th')][1:]
    

    还有数据:

    # now the data matches the headers, again, getting the data from the first table ([0])
    data = [[elem.text for elem in row.findAll('td')] for row in soup.findAll('tbody')[0].findAll('tr')]
    # getting the dates from the headers on the rows
    index = [datetime.strptime(elem.text, '%a, %b %d, %Y').date() for elem in soup.findAll('tbody')[0].findAll('th')]
    
    df = pd.DataFrame(data, columns=headers, index=index)
    
    

    现在,你当然需要导入datetime,结果:

    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import pandas as pd
    from datetime import datetime
    
    month = "january"
    url = "https://www.basketball-reference.com/leagues/NBA_2021_games-{}.html".format(month)
    html = urlopen(url)
    soup = BeautifulSoup(html, "html.parser")
    
    headers = [elem.text for elem in soup.findAll('thead')[0].findAll('th')][1:]
    
    data = [[elem.text for elem in row.findAll('td')] for row in soup.findAll('tbody')[0].findAll('tr')]
    index = [datetime.strptime(elem.text, '%a, %b %d, %Y').date() for elem in soup.findAll('tbody')[0].findAll('th')]
    
    df = pd.DataFrame(data, columns=headers, index=index)
    print(df)
    

    输出:

               Start (ET)       Visitor/Neutral  PTS            Home/Neutral  PTS               Attend. Notes
    2021-01-01      7:00p     Memphis Grizzlies  108       Charlotte Hornets   93  Box Score          0
    2021-01-01      7:00p            Miami Heat   83        Dallas Mavericks   93  Box Score          0
    2021-01-01      7:00p        Boston Celtics   93         Detroit Pistons   96  Box Score          0
    2021-01-01      7:30p         Atlanta Hawks  114           Brooklyn Nets   96  Box Score          0
    2021-01-01      8:00p         Chicago Bulls   96         Milwaukee Bucks  126  Box Score          0
    ...               ...                   ...  ...                     ...  ...        ... ..     ...   ...
    2021-01-31      5:00p    Philadelphia 76ers               Indiana Pacers
    2021-01-31      5:00p  Los Angeles Clippers              New York Knicks
    2021-01-31      6:00p         Orlando Magic              Toronto Raptors
    2021-01-31      7:00p         Brooklyn Nets           Washington Wizards
    2021-01-31      7:30p   Cleveland Cavaliers       Minnesota Timberwolves
    
    [238 rows x 9 columns]
    

    【讨论】:

      猜你喜欢
      • 2018-07-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-02
      • 1970-01-01
      相关资源
      最近更新 更多