เปิดโปงบาสเก็ตบอลโดยใช้ Python

Jan 24 2023
ตอนที่ 1: การขูดด้วยซุปที่สวยงาม
ในซีรีส์บล็อกนี้ ไลบรารี Python อันทรงพลังถูกนำไปใช้เพื่อช่วยเปิดเผยความจริงทางสถิติที่ซ่อนอยู่ในบาสเก็ตบอล ขั้นตอนแรกในแนวทางที่ขับเคลื่อนด้วยข้อมูลคือการระบุและรวบรวมข้อมูลที่จำเป็น

ในซีรีส์บล็อกนี้ ไลบรารี Python อันทรงพลังถูกนำไปใช้เพื่อช่วยเปิดเผยความจริงทางสถิติที่ซ่อนอยู่ในบาสเก็ตบอล ขั้นตอนแรกในแนวทางที่ขับเคลื่อนด้วยข้อมูลคือการระบุและรวบรวมข้อมูลที่จำเป็น

โชคดีสำหรับเราBasketball-Reference.comโฮสต์หน้าข้อมูลบาสเก็ตบอลที่สามารถคัดลอกได้ง่าย กระบวนการของคำแนะนำนี้สามารถนำไปใช้กับหน้าเว็บจำนวนเท่าใดก็ได้ แต่สำหรับกรณีนี้ เราวางแผนที่จะรวบรวมสถิติตามฤดูกาลของคลาสมือใหม่หลายๆ คลาส

วัตถุประสงค์

  1. ระบุแหล่งข้อมูล
  2. ดาวน์โหลดหน้า
  3. ระบุองค์ประกอบของหน้าที่สำคัญ
  4. ทำความสะอาดล่วงหน้าและดึงออก
  5. คลังเก็บเอกสารสำคัญ
  • ขอแพ็คเกจ
  • แพ็คเกจซุปที่สวยงาม
  • แพ็คเก็จหมีแพนด้า

การระบุแหล่งข้อมูล

Basketball-Reference.com โฮสต์หน้าเว็บหลายร้อยหน้าเกี่ยวกับสถิติบาสเก็ตบอลซึ่งมีตั้งแต่ค่าเฉลี่ยตามฤดูกาลของหมวดหมู่คะแนนกล่องทั่วไป เช่น คะแนน การรีบาวด์ และเปอร์เซ็นต์การยิง ไปจนถึงการเล่นต่อการเล่นของแต่ละเกมที่เล่นในช่วงหลังสุด 20 ปีหรือมากกว่านั้น เราสามารถหลงทางได้อย่างง่ายดายในสึนามิทางสถิตินี้หากไม่มีเป้าหมายที่ชัดเจนว่าจะมองหาอะไรกันแน่

เป้าหมายในโพสต์นี้เรียบง่าย: รับข้อมูลมือใหม่ที่จะช่วยในการประเมินคุณค่าและศักยภาพที่แท้จริงของผู้เล่นอายุน้อย

ลิงค์ต่อไปนี้เป็นหน้าดังกล่าว มันแสดงสถิติที่เกี่ยวข้องทั้งหมดของมือใหม่ในฤดูกาลใดฤดูกาลหนึ่ง

https://www.basketball-reference.com/leagues/NBA_1990_rookies-season-stats.html

เพื่อที่จะสะสมข้อมูลให้เพียงพอสำหรับการอนุมานทางสถิติที่ชัดเจนเกี่ยวกับผู้เล่น ข้อมูลหนึ่งปีจะไม่ถูกตัดทิ้ง ต้องมีการรวบรวมข้อมูลมูลค่าหลายสิบปีเพื่อช่วยกรองเสียงรบกวนและหาข้อสรุปเกี่ยวกับศักยภาพในอนาคตของผู้เล่น

หากสามารถดำเนินการซ้ำได้ด้วยตนเอง การดำเนินการนั้นเป็นตัวเลือกที่ยอดเยี่ยมสำหรับระบบอัตโนมัติ ในกรณีนี้ ตัวเลขใน URL ด้านบนจะตรงกับปีของคลาสมือใหม่นั้นๆ ขับเคลื่อนด้วยความรู้นั้น มาเริ่มรวบรวมโค้ดบรรทัดแรกของเรากัน

import requests
from bs4 import BeautifulSoup
import pandas as pd

years = list(range(1990, 2017))
url_base = "https://www.basketball-reference.com/leagues/NBA_{}_rookies-season-stats.html"

  1. มีการนำเข้าบรรจุภัณฑ์ที่เหมาะสม
  2. ตัวแปรyearsรายการระบุช่วงของปีที่ต้องการ - 1990 ถึง 2017
  3. url_baseทำหน้าที่จัดเก็บตัวแปรสตริงที่จัดรูปแบบไว้ล่วงหน้าของ URL เป้าหมาย

ในการขูดหน้าเว็บ จำเป็นต้องลบโอเวอร์เฮดออกให้ได้มากที่สุด เนื่องจากไซต์เก็บข้อมูลทั้งหมดไว้ที่ส่วนหน้าของ HTML จึงสามารถดาวน์โหลดหน้านี้ได้อย่างง่ายดายและจัดเก็บไว้ในเครื่องอย่างครบถ้วน

#loop iterates through years
for year in years:
    url = url_base.format(year)
    data = requests.get(url)
    #each page year is locally stored as an HTML file
    with open("notebooks/Rookies/{}.html".format(year), "w+") as f:
        f.write(data. Text)

  1. วงเล็บปีกกาที่พบในurlสตริงของ ' จะทำหน้าที่อนุญาตให้ the formatแทนที่ด้วยปีที่ทำซ้ำในปัจจุบัน
    — ตัวอย่างเช่น ในการวนซ้ำครั้งแรกurlค่าจะเป็นhttps://www.basketball-reference.com/leagues/NBA_1990_rookies-season-stats.html
    — ในการทำซ้ำครั้งที่สอง ปีถัดไปจะถูกอ้างอิงแทน:https://www.basketball-reference.com/leagues/NBA_1991_rookies-season-stats.html
  2. ตัวแปรข้อมูลทำหน้าที่เป็นตัวยึดตำแหน่งสำหรับrequests.get()ฟังก์ชันและการอ้างอิงของurlค่าสตริง ที่ทำซ้ำในปัจจุบัน
  3. จากนั้น เมธอดrequestsจะใช้สตริง URL ที่จัดรูปแบบใหม่เพื่อดึงหน้าที่ต้องการ
  4. with open()การอ่านและเขียนที่ตามมา ( w+) ข้อมูลเพจจากrequests.get( data.text) ของเรา และจัดเก็บไฟล์ HTML ที่สร้างขึ้นใหม่ไว้ในเครื่อง

เพื่อหลีกเลี่ยงความเจ็บปวดที่เพิ่มมากขึ้นในการขูดไซต์ เราจัดเก็บหน้าเหล่านี้เป็นไฟล์ HTML ในเครื่อง ดูสิ เมื่อเยี่ยมชมไซต์ของเพจ เซิร์ฟเวอร์ที่โฮสต์เพจดังกล่าวจะตอบรับคำขอของคุณและส่งข้อมูลที่เหมาะสมกลับไปยังเบราว์เซอร์ของคุณ แต่การมีไคลเอนต์เฉพาะรายเดียวที่ขอข้อมูลเดียวกันซ้ำแล้วซ้ำอีกจะทำให้เซิร์ฟเวอร์ทำงานหนักเกินไป ผู้ดูแลเซิร์ฟเวอร์มีสิทธิ์ในการบล็อกคำขอถาวรเหล่านี้เพื่อให้สามารถให้บริการนี้แก่ผู้อื่นทางออนไลน์ได้อย่างเหมาะสม

การดาวน์โหลดไฟล์ HTML เหล่านี้ในเครื่องของคุณ จะเป็นการหลีกเลี่ยงสองสิ่ง:

  1. ต้องรอนานกว่าปกติเพื่อรวบรวมข้อมูลเดียวกัน
  2. โดนบล็อคไม่ให้เข้าเพจ หยุดเก็บข้อมูลไปเลย

มีข้อดีบางประการในการคัดลอกข้อมูลจากรูปแบบ HTML สิ่งที่สำคัญที่สุดประการหนึ่งคือการระบุคุณสมบัติขององค์ประกอบโดยไม่ซ้ำกันได้อย่างไร หากสามารถระบุได้อย่างเฉพาะเจาะจง สามารถขูดโดยใช้ Beautiful Soup แต่การจะทำเช่นนั้นได้ เราต้องรู้วิธีตรวจสอบองค์ประกอบ HTML เหล่านี้ด้วยตัวมันเอง

วิธีการตรวจสอบ

เราจะต้องเจาะลึกลงไปในการทำงานภายในของเอกสารนี้ แต่ฉันสัญญาว่าจะไม่ทำแบบฝึกหัดในการเรียนรู้ HTML

หากคุณรู้วิธีตรวจสอบวัตถุ HTML ก็ข้ามไปได้เลย มิฉะนั้น โปรดติดตามวิธีการตรวจสอบองค์ประกอบของหน้า

  1. คลิกที่จุดแนวตั้งสามจุดบนแถบเมนูด้านบนของ Chrome
  2. เลือก “เครื่องมือเพิ่มเติม”
  3. เลือกเครื่องมือสำหรับนักพัฒนา
  1. คลิกขวาที่หน้าเว็บ
  2. เลือก "ตรวจสอบ" เพื่อเข้าถึงแผงเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์

ตรวจสอบเพจ

เมื่อเห็นว่าเพจเหล่านี้ทั้งหมดถูกจัดเก็บไว้ในเครื่อง เราสามารถเลือกดูได้โดยการเข้าไปในระบบไฟล์เพื่อเปิดในเบราว์เซอร์ที่เราต้องการ หรือเราสามารถสร้างโค้ดต่อไปได้โดยใช้ส่วนย่อยของโค้ดต่อไปนี้

with open("notebooks/Rookies/2000.html") as f:
    page = f.read()

ข้อมูลที่ต้องการทั้งหมดของหน้านี้อยู่ในองค์ประกอบตารางนั้น ก่อนที่จะรีบดูดข้อมูลทั้งหมดนี้อย่างที่เป็นอยู่ ตอนนี้เป็นเวลาที่ดีที่สุดที่จะพิจารณาว่าทุกอย่างในตารางนี้มีค่าควรแก่การรวบรวมหรือไม่

ทำความสะอาดล่วงหน้า

การทำความสะอาดล่วงหน้าอาจไม่ใช่คำที่ใช้บ่อยในคำศัพท์ของคุณ แต่สำหรับคนที่คุณเห็นว่าตัวเองกำลังขูดข้อมูลอยู่เป็นประจำ ก็ควรเป็นเช่นนั้น หากคุณต้องการหลีกเลี่ยงความหงุดหงิดจากการเสียเวลาหลายชั่วโมงในการดำเนินโครงการรวบรวมข้อมูล วิธีที่ดีที่สุดคือแยกแกลบออกจากข้าวสาลีก่อน

ตัวอย่างเช่น สังเกตองค์ประกอบสามอย่างในกล่องสีแดง

แถวหนึ่งทำหน้าที่เป็นส่วนหัวของตาราง "หลัก" อีกสองแถวคืออินสแตนซ์ที่ซ้ำกันของอาร์ติแฟกต์เดียวกันซึ่งอยู่ด้านบนสุด รูปแบบนี้ซ้ำทุกแถวที่ 20

การตรวจสอบเพิ่มเติมพบว่าแถวเหล่านี้ทั้งหมดมีtrแท็ก HTML (แถวตาราง) เหมือนกัน สิ่งที่ทำให้แต่ละองค์ประกอบเหล่านี้แตกต่างจากองค์ประกอบอื่นๆ คือชื่อคลาส

  1. แถวส่วนหัวหลัก
    — คลาส =over_header
  2. ทำซ้ำแถวส่วนหัว
    — คลาส =over_header thead
  3. สถิติ หมวดหมู่ แถว
    — ชั้น =thead
  4. #array to house list of dataframes
    dfs = []
    #unnecessary table rows to be removed
    classes = ["over_header", "over_header thead", "thead"]
    

  5. วัตถุclassesอาร์เรย์จะเก็บชื่อคลาสขององค์ประกอบแถวตารางทั้งหมดที่ไม่ได้รับการเปิดเผย

เมธอดdecomposeทำหน้าที่ลบองค์ประกอบที่ไม่ต้องการดังกล่าวในเพจ ตามเอกสาร Beautiful Soup อย่างเป็นทางการ ...

decompose()

Tag.decompose()ลบแท็กออกจากแผนผัง จากนั้นทำลายแท็กและเนื้อหาทั้งหมด

ด้านล่างนี้เป็นตัวอย่างของโค้ดที่เราสามารถเพิ่มประสิทธิภาพdecomposeวิธีการด้วยความช่วยเหลือของforลูป

#for loop to iterate through the years 
for year in years:
    with open("notebooks/Rookies/{}.html".format(year)) as f:
        page = f.read()
    soup = BeautifulSoup(page, "html.parser")
    #for loop cleans up unnecessary table headers from reappearing in rows
    for i in classes:
        for tr in soup.find_all("tr", {"class":i}):
            tr.decompose()

  1. เมธอด นี้withให้โค้ดของเราเป็นโครงสร้างสำหรับpageตัวแปรเพื่ออ่านไฟล์ HTML ที่จัดเก็บไว้ในเครื่องเมื่อเรียกใช้
  2. คลาสตัวแยกวิเคราะห์ HTML เริ่มต้นได้โดยการยกตัวอย่างคลาส Beautiful Soup และส่งผ่านทั้งpageอ็อบเจกต์สตริง และhtml.parser.
  3. ลูป ที่สองวนforซ้ำผ่านค่าในclassesอาร์เรย์
  4. ลูป ที่สามforใช้วิธีการของ Beautiful Soup find_allเพื่อระบุองค์ประกอบที่มีทั้ง trแท็กและชื่อคลาสที่ตรงกับองค์ประกอบเหล่านั้นclasses
  5. tr.decomposeทำหน้าที่ละองค์ประกอบแถวตารางที่ระบุออกจากหน้าทั้งหมด

แยกข้อมูล

ในที่สุดเราก็สามารถเริ่มทำงานในส่วนของโค้ดที่ดึงข้อมูลจากตารางได้จริง โปรดจำไว้ว่าตารางที่มีข้อมูลที่เกี่ยวข้องทั้งหมดมี ID เฉพาะของrookiesHTML การเพิ่มโค้ดของเราต่อไปนี้จะทำหน้าที่แยกวิเคราะห์ข้อมูลของตารางนี้

#the years we wish to parse for
years = list(range(1990, 2017))
#array to house list of dataframes
dfs = []
#unnecessary table headers to be removed
classes = ["over_header","over_header thead", "thead"]
 
for year in years:
    #url = url_base.format(year)
    #data = requests.get(url)
    ##page is saved as an html and placed in Rookies folder
    #with open("notebooks/Rookies/{}.html".format(year), "w+") as f:
        #f.write(data.text)
     #with open("notebooks/Rookies/2000.html") as f:
    with open("notebooks/Rookies/{}.html".format(year)) as f:
        page = f.read()
      soup = BeautifulSoup(page, "html.parser")   #for loop cleans up unnecessary table headers from reappearing in rows
    for i in classes:
        for tr in soup.find_all("tr", {"class":i}):
        tr.decompose()
        
### Scraping Block ###
    #identifies, scrapes, and loads rookie tables into one dataframe
    rookie_table = soup.find(id="rookies")
    rookies = pd.read_html(str(rookie_table))[0]
    rookies["Year"] = year
    dfs.append(rookies)

#new variable turns list of dataframes into single dataframe
all_rookies = pd.concat(dfs)

  1. rookie_tableยกตัวอย่างวิธีการของ Beautiful Soup findเพื่อระบุองค์ประกอบ HTML ด้วย IDrookies
  2. เมื่อเห็นว่า Pandas สามารถอ่านตาราง HTML ได้ ตารางมือใหม่จะถูกโหลดลงใน Pandas โดยใช้read_htmlวิธีส่งผ่านrookie_tableเป็นสตริง
  3. การตรึงเพื่อสิ้นสุด[0]การเปลี่ยนrookiesจากรายการของ data frames เป็น data frame เดียว
  4. คอลัมน์ "ปี" คือจุดสิ้นสุดของrookiesกรอบข้อมูล
  5. dfs.append(rookies)ทำหน้าที่จัดเก็บตารางทั้งหมดของทุก ๆ ปีใหม่ตามลำดับที่พวกเขาวนซ้ำในรายการของเฟรมข้อมูล
  6. วิธี Pandas concatใช้เพื่อรวมรายการของ data frames ให้เป็น data frame เดียว:all_rookies

ขั้นตอนสุดท้ายของเราคือการนำข้อมูลที่มีประโยชน์และสะอาดทั้งหมดมาจัดเก็บในรูปแบบ CSV ที่มนุษย์และเครื่องอ่านได้ง่าย การตรึงบรรทัดนี้ไว้ท้ายโค้ดของเราจะมีประโยชน์เมื่อตัดสินใจกลับมาและอ้างอิงข้อมูลที่รวบรวม

#a single dataframe archived as a local CSV using Pandas
all_rookies.to_csv("archive/NBA_Rookies_1990-2016.csv")

import requests
import pandas as pd
from bs4 import BeautifulSoup

years = list(range(1990, 2017))
dfs = []
classes = ["over_header","over_header thead", "thead"] #unnecessary table headers to be removed

for year in years:
    with open("C:/Users/cisco/OneDrive/python/Projects/Basketball/Rookies/{}.html".format(year), encoding = "utf-8") as f:
        page = f.read()
    soup = BeautifulSoup(page, "html.parser")
    #second for loop cleans up unnecessary table headers from reappearing in rows
    for i in classes:
        for tr in soup.find_all("tr", {"class":i}):
            tr.decompose()
    #identifies, scrapes, and loads rookie tables into one dataframe
    table_rookies = soup.find(id="rookies")
    rookies = pd.read_html(str(table_rookies))[0]
    rookies["Year"] = year
    dfs.append(rookies)

#new variable turns list of dataframes into single dataframe
all_rookies = pd.concat(dfs)

#dataframe archived as local CSV
all_rookies.to_csv("archive/NBA_Rookies_1990-2016.csv")

อีกครั้ง กระบวนการที่ตามมาในคำแนะนำนี้จะนำไปใช้กับหน้าอื่นๆ เกือบทั้งหมดบน Basketball-Reference.com อย่างไม่ต้องสงสัย มีห้าขั้นตอนง่ายๆ ที่ควรทำในแต่ละกรณี

  1. ระบุ URL ของหน้า
  2. ดาวน์โหลดหน้า
  3. ระบุองค์ประกอบ
  4. ทำความสะอาดล่วงหน้าและดึงออก
  5. คลังเก็บเอกสารสำคัญ

ถัดไปในซีรีส์นี้จะใช้ข้อมูลนี้จริง ๆ เพื่อทำความเข้าใจเกี่ยวกับศักยภาพของผู้เล่นในอนาคต ดังนั้นโปรดระวังงวดหน้า!