UNPKG

basketball-reference-player-scraper

Version:

A simple web scraper for NBA player data via Basketball-Reference

387 lines (325 loc) 11.7 kB
import { getDocument } from './Puppet' import { rowsToArray, trimWhitespace } from './Utils' import { findPlayer } from './Search' const BASE_URL = 'https://www.basketball-reference.com/' const CONTRACT_SELECTOR = 'table[id^="contracts_"]' const BIO_SELECTOR = 'div[itemtype="https://schema.org/Person"]' const PLAYER_NAME_SELECTOR = 'h1[itemprop="name"]' const LEADERBOARD_SELECTOR = 'div_leaderboard' const VALID_TABLE_IDS = [ 'advanced', 'all_college_stats', 'all_salaries', 'all_star', 'pbp', 'per_game', 'per_minute', 'per_poss', 'playoffs_advanced', 'playoffs_pbp', 'playoffs_per_game', 'playoffs_per_minute', 'playoffs_per_poss', 'playoffs_shooting', 'playoffs_totals', 'shooting', 'sim_career', 'sim_thru', 'totals', 'year-and-career-highs-po', 'year-and-career-highs' ] export const getPlayerHonors = (document: Document) => { const leaderboard = document.getElementById(LEADERBOARD_SELECTOR) if (leaderboard) { const sections = Array.from(leaderboard.children) const tables = sections.map((section) => section.children[0]) const honors = tables.map((table) => { const children = Array.from(table.children) const label = children![0].textContent!.trim() const results = Array.from(children[1].children).map((row) => { const nodes = row.children[0].childNodes const honor = nodes![0].textContent!.trim() const rank = nodes[nodes.length - 1]?.textContent?.trim() ?? -1 return { honor, rank } }) return { label, results } }) return honors } return [] } export const getPlayerPage = async (query: string) => { return await getDocument(`${BASE_URL}/players/${query}`) } export const getPlayerStats = (document: Document, tableID: string) => { const table = document.querySelector(`#${tableID}`) const hasTable = table !== null if (!hasTable) { return null } const tableChildren = Array.from(table.children) const tableBody = tableChildren.find((node) => node.tagName === 'TBODY') const hasTableBody = tableBody !== undefined if (!hasTableBody) { throw new Error('Table does not have a table body element') } const tableRows = Array.from(tableBody.children) const stats = tableRows.map(rowsToArray) return stats } export const getPlayerContract = (document: Document) => { const contractTable = document.querySelector(CONTRACT_SELECTOR) const hasContractTable = contractTable !== null if (!hasContractTable) { return null } const tableChildren = Array.from(contractTable.children) const tableBody = tableChildren.find((element) => element.tagName === 'TBODY') const tableHead = tableChildren.find((element) => element.tagName === 'THEAD') const hasTableBody = tableBody !== undefined const hasTableHead = tableHead !== undefined const hasTableContents = hasTableBody && hasTableHead if (!hasTableContents) { throw new Error('Table Body does not exist for player contract') } const headerRow = tableHead.children[0] const headerCells = Array.from(headerRow.children) const contractKeys = headerCells.map( (cell, index) => cell.textContent ?? `unknownCell_${index + 1}` ) const bodyRow = tableBody.children[0] const bodyRowCells = Array.from(bodyRow.children) const results = bodyRowCells.reduce((output, cell, index) => { const key = contractKeys[index] output[key] = cell.textContent return output }, {} as Record<string, unknown>) return results } export const getPlayerAccolades = (document: Document) => { const accoladesList = document.querySelector('#bling') const accoladesItems = Array.from(accoladesList?.children ?? []) const accolades = accoladesItems.map((item) => item.textContent) return accolades } export const getPlayerAge = (str: string) => { const ageRegExp = /([0-9]+-[0-9]+d)/g const age = str.match(ageRegExp)?.[0] ?? '' return age } export const getPlayerName = (document: Document) => { return document.querySelector(PLAYER_NAME_SELECTOR)?.textContent ?? '' } export const getPlayerBirthplace = (str: string) => { const endAtRelativesExp = /(?=Born:)(.*)(?=Relatives:)/ const endAtCollegeExp = /(?=Born:)(.*)(?=College:)/ const endAtHSExp = /(?=Born:)(.*)(?=High School:)/ const endAtDraft = /(?=Born:)(.*)(?=College:)/ const potentialMatchA = str.match(endAtRelativesExp) const potentialMatchB = str.match(endAtCollegeExp) const potentialMatchC = str.match(endAtHSExp) const potentialMatchD = str.match(endAtDraft) const matchResults = potentialMatchA || potentialMatchB || potentialMatchC || potentialMatchD const hasMatch = matchResults !== null if (!hasMatch) { return '' } const birthLocation = matchResults[0].split('in')[1] const trimmedOutput = birthLocation.trim() return trimmedOutput } export const getPlayerHighSchool = (str: string) => { const regExps = [ /(?=High School:)(.*)(?=Recruiting Rank)/, /(?=High School:)(.*)(?=Draft:)/, /(?=High Schools:)(.*)(?=Recruiting Rank:)/, /(?=High Schools:)(.*)(?=Draft:)/ ] const matchedString = regExps .map((regExp) => str.match(regExp)) .find((match) => match !== null) ?? [] const hasMatch = matchedString.length > 0 if (!hasMatch) { return null } const matches = matchedString[0] const strsToSplitby = ['High School: ', 'High Schools: '] const highSchool = strsToSplitby.map((str) => matches.split(str)[1]) const results = highSchool?.[1] ?.split(',') ?.reduce((output, str, index, input) => { const isOdd = index % 2 === 1 const prevVal = input[index - 1] const canJoin = isOdd if (canJoin) { output[index - 1] = `${prevVal},${str}`.trim() return output } return output }, [] as string[]) .filter((val) => val) ?? [highSchool[0].trim()] return results } // why? some players have a slightly different description // thus, we need a slightly different regular expression export const altGetPlayerCollege = (str: string) => { const collegeRegExp = /(?=College:)(.*)(?=High Schools:)/ const matchedString = str.match(collegeRegExp) const hasMatch = matchedString !== null if (!hasMatch) { return null } const matches = matchedString[0] const college = matches.split('College: ')[1] return [college.trim()] } export const getPlayerCollege = (str: string) => { const collegeRegExp = /(?=College:)(.*)(?=High School:)/ const matchedString = str.match(collegeRegExp) const hasMatch = matchedString !== null if (!hasMatch) { return null } const matches = matchedString[0] const college = matches.split('College: ')[1] return [college.trim()] } export const getPlayerPosition = (str: string) => { const positionRegExp = /(?=Position:).*(?=▪)/ const matchedStr = str.match(positionRegExp) const hasMatch = matchedStr !== null if (!hasMatch) { return null } const parsedPositionA = matchedStr[0].split('Position: ')[1] const positions = parsedPositionA.split(' and ') const trimmedPositions = positions.map((position) => position.trim()) return trimmedPositions } export const getPlayerAttributes = (str: string) => { const attrRegExp = /(?=Shoots).*(?=\) [BT])/ const heightRegExp = /\d*(?=cm)/ const weightRegExp = /\d*(?=kg)/ const handRegExp = /(?=Shoots: ).*(?=[0-9]-)/ const matchedStr = str.match(attrRegExp) const hasMatch = matchedStr !== null if (!hasMatch) { return null } const parsedAttributes = matchedStr[0] const weight = parsedAttributes!.match(weightRegExp)![0] const height = parsedAttributes!.match(heightRegExp)![0] const handStr = parsedAttributes!.match(handRegExp)![0] const hand = handStr.split('Shoots: ')[1].trim() return { hand, height, weight } } export const getPlayerNicknames = (str: string) => { const nicknameRegExp = /\(((?!born)(?!Age:)(?![0-9])(?!Full)[^)]+)\)/ const noParensRegExp = /([^(^)])/g const nicknameStr = str.match(nicknameRegExp) const hasNickname = nicknameStr !== null const parsedNickname = hasNickname ? nicknameStr[0]!.match(noParensRegExp)!.join('') : '' const nicknameArr = hasNickname ? parsedNickname.split(',') : [] return nicknameArr } export const getPlayerDraft = (str: string) => { const draftRegExp = /(?=Draft:)(.*)(?=NBA Debut:)/ const draftPosRegExp = /(?=\().*(?=pick)/ const draftStr = str.match(draftRegExp) const wasDrafted = draftStr !== null if (!wasDrafted) { return null } const splitStr = wasDrafted ? draftStr[0].split(',') : [] const draftClass = splitStr[3].trim() const draftYear = parseInt(draftClass, 0) const draftTeam = splitStr[0].split('Draft: ')[1].trim() const draftRound = parseInt(splitStr[1].trim(), 0) const draftPos = splitStr.length > 0 ? parseInt(splitStr[1]!.match(draftPosRegExp)![0][1].trim(), 0) : '' return { team: draftTeam, year: draftYear, position: { round: draftRound, position: draftPos } } } export const getPlayerBirthdate = (str: string) => { const dateRegExp = /([A-Z][a-z]+ +[0-9]+, +[0-9]+)/ const dateOfBirth = str.match(dateRegExp)?.[0] ?? '' return dateOfBirth } export const getPlayerBio = (document: Document) => { const bioElement = document.querySelector(BIO_SELECTOR) const bioString = bioElement?.textContent ?? '' const trimmedBioString = trimWhitespace(bioString) const accolades = getPlayerAccolades(document) const age = getPlayerAge(trimmedBioString) const name = getPlayerName(document) const birthplace = getPlayerBirthplace(trimmedBioString) const highSchool = getPlayerHighSchool(trimmedBioString) const college = getPlayerCollege(trimmedBioString) || altGetPlayerCollege(trimmedBioString) const position = getPlayerPosition(trimmedBioString) const attributes = getPlayerAttributes(trimmedBioString) const nicknames = getPlayerNicknames(bioString) const draft = getPlayerDraft(trimmedBioString) const dob = getPlayerBirthdate(trimmedBioString) const education = { college, highSchool } return { accolades, attributes, age, birthplace, draft, education, name, nicknames, position, dob } } export const playerStatTableToObject = (tableIDs: string[]) => { const ids = tableIDs.reduce((output, id) => { const matchedID = VALID_TABLE_IDS.find((validID) => validID === id) const hasMatch = matchedID !== undefined if (!hasMatch) { return output } return [...output, id] }, [] as string[]) return ids } type Options = { tableIDs?: string[] bio?: boolean contract?: boolean honors?: boolean } export const getPlayer = async ( query: string, options: Options = { tableIDs: ['per_game'], bio: true, contract: false, honors: true } ) => { const document = await findPlayer(query) const baseOutput = { bio: options.bio ? getPlayerBio(document) : null, contract: options.contract ? getPlayerContract(document) : null, honors: getPlayerHonors(document) } as Record<string, unknown> const results = (options?.tableIDs ?? []).reduce((output, category) => { output[category] = getPlayerStats(document, category) return output }, baseOutput) return results }