NBA 球员数据采集测验
为了完成本关任务,你需要掌握:
数据获取简介
数据采集
本关采用 requests 库完成数据采集。
数据获取简介
url 地址: https://www.basketball-reference.com/players/a/
请求头:
进入网站后,等待网页加载完毕,点击 F12 或右击选择检查,搜索找到页面 a/,设置请求头信息。
请求头图
网页主界面如下图所示:
网页主界面
在本次教学中,我们需要获取所有 A 姓球员的基本数据和一些详细数据。
基本数据就是从网页主界面中获取的数据,如下图所示:
基本数据图
我们需要在网页主界面中获取球员的姓名、位置、身高。由于体重列表栏中部分球员存在空值,我们使用 Xpath 解析后会导致排列顺序混乱,所以体重数据我们在球员详情页中获取。
我们在网页主界面中点击 F12 或右击选择检查,查看球员详情页链接:
球员详情页链接获取图
可以直接获取到球员详情页链接的后缀。
在球员详情页中,我们需要获取球员的详细数据:
详细数据图
所有需要获取的字段信息如下:
字段名 解释 获取信息说明
id 球员 id 解析球员详情页链接获取,如:https://www.basketball-reference.com/players/a/abdelal01.html,则 id 为:abdelal01
info_url 球员详情信息网址 网站首页表格中 Player 列
player_name 球员姓名 网站首页表格中 Player 列
player_pos 战术位置 网站首页表格中 Pos 列
player_ht 身高(英尺) 网站首页表格中 Ht 列
player_wt 体重(磅) 球员详情页(网站首页的体重列中存在空值)
player_age 球员年龄 球员详情页,动态加载数据,需要手动计算。(格式与网址保持一致)
country 国籍 球员详情页(大写)
college 就读大学 球员详情页
high_school 就读高中 球员详情页
rank_year 同届排名 球员详情页
draft 选秀信息 球员详情页
draft_date 选秀日期 球员详情页
work_year 经验 球员详情页
team_count 效力球队数量 球员详情页
last_team_name 最后效力球队 球员详情页
season 赛季 球员详情页
games_count 场次 球员详情页
PTS 场均得分 球员详情页
TRB 场均篮板 球员详情页
AST 场均助攻 球员详情页
FG 投篮命中率 球员详情页
FG3 三分球命中率 球员详情页
FT 罚球命中率 球员详情页
EFG 有效命中率 球员详情页
PER 效率值 球员详情页
WS 胜率 球员详情页
firstTime 首秀时间 球员详情页中表格内的season列,其第一个赛季链接中第一比赛上场时间。
lastTime 退役时间 球员详情页中表格内的season列,其最后一个赛季链接中最后一场比赛上场时间。
数据采集
首先,我们定义相关全局变量,建立初始化函数和入口函数。
全局变量
save_fp = open("./nba_data.csv", "w", encoding="utf-8-sig", newline="") # 创建存储文件对象
csv_writer = csv.writer(save_fp) # 创建 csv 对象
start_time = int(time.time()) # 记录开始时间
main_response = None # 记录主页面内容
main_count = 0 # 记录主页面当前循环次数
total_count = 0 # 记录当前页面总循环次数
初始化函数
写入 csv 文件表头。
def open_spider(csv_writer):
print("--------------------------开始爬取--------------------------")
header = [
"id", "info_url", "player_name", "player_pos", "player_ht", "player_wt", "player_age",
"country", "college", "high_school", "rank_year", "draft", "draft_date",
"work_year", "team_count", "last_team_name", "season", "games_count", "PTS",
"TRB", "AST", "FG", "FG3", "FT", "EFG", "PER", "WS", "firstTime", "lastTime"
]
csv_writer.writerow(header)
入口函数
if __name__ == '__main__':
# 调用初始化函数
open_spider(csv_writer=csv_writer)
# 主界面 url
start_urls = 'https://www.basketball-reference.com/players/a/'
# 请求头,能正常访问则无需设置。
headers = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-encoding": "gzip, deflate, br",
"accept-language":"zh-CN,zh;q=0.9,en;q=0.8",
"cache-control": "max-age=0",
"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36",
"cookie": "__qca=P0-1886065764-1658383903526; hubspotutk=c69f86272b8d6b7951413faad4bbba8e; _gcl_au=1.1.1479764202.1658383906; _fssid=4eb62bce-9135-4575-8c6c-5d8ad0cbf2f4; _cc_id=183aa1de207640f3684240685f5542ec; _gid=GA1.2.241408601.1665543062; fs.session.id=28b82855-63f1-4e4d-91e6-7b89700f6372; _pbjs_userid_consent_data=3524755945110770; cookie=0f478402-4db9-4a70-b701-97f74220b4bc; _lr_env_src_ats=false; _fbp=fb.1.1665624118904.1085698668; meta_more_button=1; sr_note_box_countdown=0; srcssfull=yes; is_live=true; __gpi=UID=000007ecf062bd3b:T=1658383923:RT=1665709635:S=ALNI_Mb2bDIonimZiSivHyVtJXd-izqHOA; fs.bot.check=true; __hssrc=1; __cf_bm=EmPpD3muqzz_31sAtEV0s4gGLvIdFhi6Shpzyu6SgfY-1665729535-0-AWm21C7A17no++kUeumsEqsLsuuBhWdoJfZOSjm84UE4KRKzbBnHrW7CjXx09VK80YmgWIrmVGgGufu+RC7CiiM=; __hstc=180814520.c69f86272b8d6b7951413faad4bbba8e.1658383903774.1665709602912.1665729511448.44; __hssc=180814520.1.1665729511448; _ga_NR1HN85GXQ=GS1.1.1665729503.38.1.1665730082.0.0.0; _ga=GA1.2.1217857762.1658383900; _gat_gtag_UA_1890630_2=1; _gat_gtag_UA_1890630_9=1; __gads=ID=8491b8023e0f5ed5:T=1658383923:S=ALNI_MaR4IK8peM0OjZI9ohiD4Ayvp-YjQ; _lr_retry_request=true; cto_bundle=KErEG19BMG1DTUFvZTB1ZURsWXlUTFc4RTRJUkEwWlI2MlBnYTdkVXhNQ3F2ekNWYkJ3QUhJc3N0V0RqYm9YRkU1T1p2eUZVUXd2MmQ1c1RCJTJCV3ZUNzBVQlFra2VPc0s5amlob1RRZG9yd3JhJTJGOXpyVHhGd1JIaDdvNm1yMVAlMkI0M21RSXI4dU9GU25MY1RkSTdWU2dsSU8wYTZYOTNkUlhUUyUyRlVPZDAxd1Y5Vzd0dyUzRA; cto_bidid=zXPnyV8xOVFMMlJzTHRsak4lMkJiemlZc0tQdG44WTNWdTI3a25qNG9YcU1QTURSJTJGd0dWOHdiWUVXVnp5Tm1qellmWGZUREppVU11Y0FpNDhzQUtJallkd3ViUU5lNkFoTTk5OENDWXpDZEtZY2U1RkV0UjlaRWhvNTNMcmM1dU9XY0RkUUc",
"if-modified-since": "Fri, 14 Oct 2022 06:38:55 GMT",
"sec-ch-ua": '"Chromium";v="106", "Google Chrome";v="106", "Not;A=Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "Windows",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1"
}
# 发起请求
page = requests.get(url=start_urls,headers=None,timeout=30)
# 获取响应内容
page_text = page.text
response = etree.HTML(page_text) # 封装内容,便于使用 Xpath 处理
main_response = response # 记录主界面内容
parse(response, main_count) # 调用主界面解析函数
现在我们来根据获取的字段对主界面进行解析,创建主界面解析函数。
主界面解析函数
解析传入的主界面内容,获取球员详情页链接、名称、位置、身高,由于首页的体重可能为空,所以不在首页这里获取体重。
通过获取的球员详情页链接列表来循环发起请求。
def parse(response, count):
try:
print("【主页面解析】")
# 首页球员详情页链接列表
list_href = response.xpath("//table[@id='players']/tbody/tr/th//a/@href")
# 首页球员名称列表
list_name = response.xpath("//table[@id='players']/tbody/tr/th//a/text()")
# 首页球员位置列表
list_pos = response.xpath("//table[@id='players']/tbody/tr/td[@data-stat='pos']/text()")
# 首页球员身高列表
list_ht = response.xpath("//table[@id='players']/tbody/tr/td[@data-stat='height']/text()")
# 校验列表长度是否一致,如果不一致则说明中间有空值数据,则该列需要去详情页获取数据。
# 长度一致才表明可以做到一一对应。
print("长度校验:")
print(len(list_href))
print(len(list_name))
print(len(list_pos))
print(len(list_ht))
# 存放首页获取到的每一个球员数据列表
list_res = []
# id正则,通过球员详情页链接获取球员 id
rex = re.compile(r".*/(.*).html")
# 循环列表,按顺序添加到球员数据列表中
for i in range(0, len(list_name)):
temp = [rex.findall(list_href[i])[0], "https://www.basketball-reference.com" + list_href[i],
list_name[i],
list_pos[i], list_ht[i]]
list_res.append(temp)
# 循环发起球员详情页数据请求
total_count = len(list_res) # 记录总的循环数
for m in range(count, total_count):
main_count = m # 记录当前请求数
page_text = requests.get(url=list_res[m][1], timeout=20).text
response = etree.HTML(page_text)
# 调用球员详情页解析函数
parse_detail(response, list_res[m])
time.sleep(0.3) # 防止速度过快导致 IP 被封
except Exception as ex:
# 跳过请求发生异常的链接,进入下一次循环
print("---------------发生异常---------------")
print(ex)
parse(main_response, main_count + 1)
球员详情页解析
解析球员详情页数据,根据获取字段信息来获取数据。
def parse_detail(response, data):
try:
print("【球员详情页解析】")
playerData = data
# 定义获取的字段,注意:部分球员中的字段不全。
college = None # 大学
high_school = None # 高中就读地
rank_year = None # 同届排名
draft = None # 选秀信息
draft_date = None # 选秀日期
work_year = None # 经验 Career Length 与 Experience 合为一列
country = None # 国籍
age = None # 年龄
wt = None # 体重
# 通过观察球员详情页数据,可以发现,球员信息标签 <p> 的个数不同,且 <p> 没有任何属性作为标识,直接使用 xpath 解析到的数据肯定有误!
# 解决方法: 可以发现,每个 <p> 标签中都有一个 <strong> 标签,且其内容并不重复,这样我们就可以循环的去判断匹配 <strong> 标签中的内容,
# 如果符合,则通过下标解析出对应数据。
# 观察 <p> 标签的最大数,将循环最大次数设为 15(但不一定要15,给一个相对的最大值即可)
for i in range(1, 15):
print(i)
# 判断 <strong> 标签中值的是否为空,如果为空,则跳过此次循环。
temp = response.xpath(
"//div[@id='info']/div[@id='meta']//p[{}]/strong/text() | //div[@id='info']/div[@id='meta']//p[{}]//text()".format(
i, i))
print(temp)
if temp is None or temp == [] or temp == "":
continue
else:
# 去除多余字符,进行字段匹配。
for j in range(0, len(temp)):
temp[j] = temp[j].replace(" ", "").replace("\n", "").replace(":", "")
# TODO 获取数据
# 获取大学名称
if "Colleges" in temp or "College" in temp:
college = response.xpath(
"//div[@id='info']/div[@id='meta']//p[{}]/a//text()".format(i))