設置
  • 日夜間
    隨系統(tǒng)
    淺色
    深色
  • 主題色

世界杯到了,寫個爬蟲獲取球員數(shù)據(jù)吧

千猴馬的游戲設計之道 2022/11/25 17:19:11 責編:遠生

題語:世界杯開始了,大家又重燃了看球的熱情。對于游戲制作來說,經常需要制定一些角色的數(shù)據(jù),特別是體育類的游戲。自己去設定工作量大,并且太主觀,這時候就需要去一些權威的網站查詢數(shù)據(jù),,用作參考。筆者結合自己實際經驗,教大家做一個簡單的爬蟲。

前期準備工作

首先確定我們需要爬取的是 FIFA23 的球員數(shù)據(jù),通過 https://sofifa.com/ 這個網站,里面有從 FIFA07 到 FIFA23 所有的球員數(shù)據(jù),非常詳實。打開首頁后,發(fā)現(xiàn)是這樣的::

我們點選其中一個球員,進行分析:

發(fā)現(xiàn)所需要的數(shù)據(jù),都在上面?zhèn)z張圖的位置中。下方是轉會記錄和用戶評論,現(xiàn)在用不上。

經過分析發(fā)現(xiàn),每個球員都有一個唯一 id,顯示在網址 url 中。無論是通過姓名搜索,還是通過球隊搜索后跳轉,球員的頁面都會顯示這個 id。

最后的 230006 這串數(shù)字,應該是某種參數(shù)。在 url 去掉后,依然會打開該球員頁面。

去掉球員名字后,依然可以打開頁面。

所以我們明白了----所有人的只是一段數(shù)字。

到這里,前期的重要準備已經完成了。我們發(fā)現(xiàn)了規(guī)律,下一步需要去運用了。

開始動手

安裝 python,筆者使用的是 3.9.12 版本。然后安裝 requests 庫和 beautiful soup 庫,可以使用 pip install requests,pip install beautifulsoup4 來安裝,或者用 conda 來管理安裝包、關于如何安裝請自行搜索,不再贅述。

先寫用來獲取球員數(shù)據(jù)的最主要的函數(shù):

#通過球員ID,爬取數(shù)據(jù),返回一個列表
deffetchData(id):
    url =f'https://sofifa.com/player/{str(id)}'
    myRequest = requests.get(url)
    soup = BeautifulSoup(myRequest.text,'lxml')

    myList =[]
return myList

我們通過 id 來獲取一個球員的信息,所以參數(shù)是 id。只要遞增 id 就可以來爬取所有球員的信息了。如果查無此人,就返回一個空值。注意 request 如果返回的值是 200,則表示連接成功,至于重試和 http header 怎么設置,請自行搜索。

頁面上取值

按 F12 查看頁面元素,取到所需的值。每個項目都不同,下面的展示是我們所需要的。

meta 數(shù)據(jù)

有一段頁面沒有顯示的 meta 數(shù)據(jù),里面記錄了該球員的描述。我把這個得下來,用來跟同名的球員快速對比。

過濾年份

因為要取最新的 FIFA23 的數(shù)據(jù),所以我過濾了左上角的年份,不是 23 年的就會返回空值。

到目前位置的代碼:

def fetchData(id):
    url = f'https://sofifa.com/player/{str(id)}'
    myRequest = requests.get(url)
    soup =BeautifulSoup(myRequest.text,'lxml')

    meta = soup.find(attrs={'name':'description'})['content']
    years=soup.find(name='span',attrs={'class':'bp3-button-text'})

if meta[:4] !='FIFA'and(str(years.string)) !="FIFA 23"or meta[:4]=='FIFA':
#print(years.string +'    沒有23年的數(shù)據(jù)')
return None
    info = soup.find(name='div',attrs={'class':'info'})
    playerName = info.h1.string

    myList =[id, playerName]

基礎信息

獲取位置 \ 生日 \ 身高 \ 體重等信息,我們可以看出來,這是一個字符串。

這里用到了全篇都在用的,省腦子的做法,就是改變 selector。右鍵選中需要爬取的部分,選擇 copy selector 就可以復制到剪貼板上了。

#獲取小字信息
    rawdata= soup.select("#body > div:nth-child(5) > div > div.col.col-12 > div.bp3-card.player > div > div")

FYI:也可以使用 XPath 來選取,不過需要稍微學習下 XPath 的語法。Chrome 有一個 XPath Helper 插件可以很方便測試 XPath 的語法寫的對不對。

因為球員可能會有多個位置,最多的人我見過有 4 個位置的。所以下面代碼中我做了一個偏移,這樣保證截取的字符串部分是對的。

#多個位置的話,進行平移,要不截取的字符串就錯了
    offset=rawdata[0].find_all("span")
    offset=(len(offset))-1
    temp=rawdata[0].text
    temp=re.split('\s+',temp)
if offset>0:
for i inrange(offset):
            temp.pop(i)

生日信息并轉換

獲取生日信息,并且轉換成我們所需要的格式。這里提一下,“日 / 月 / 年”的格式被 excel 打開后會自動轉換成日期格式,麻煩的要死。我的做法是:要么用 wps,要么用飛書打開,再粘貼回去。如果大家有更好的辦法歡迎留言。

下面是身高體重,很簡單的截取字符串。

#獲得球員生日,并轉換成所需的格式 (DAY/MONTH/YEAR)
    month=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
# birthday=temp[3][1:]+'-'+temp[4][:-1]+'-'+temp[5][:-1]
    mon=temp[3][1:]
    mon=month.index(mon)+1
    day=temp[4][:-1]
    year=temp[5][:-1]
    birthday =[f"{str(year)}/{str(mon)}/{str(day)}"]
    birthday=eval(str(birthday)[1:-1])
    myList.end(birthday)

#身高體重
    height=int(temp[6][:-2])
    myList.end(height)
    weight=int(temp[7][:-2])
    myList.end(weight)

獲取 Profile

我們需要獲得頁面左邊的 Profile 信息,包括正逆足,技巧動作等級,進攻防守參與度等等。

左腳定義為 1,右腳定義為 2,這種魔數(shù) (Magic Number) 在項目中大量存在... 只能微笑面對 :)

#獲取profile(正逆足,技術動作,國際聲譽等)
    rawdata= soup.select("#body > div:nth-child(5) > div > div.col.col-12 > div:nth-child(2) > div > ul")
    temp=rawdata[0].find_all('li',class_="ellipsis")
    preferred_foot=temp[0].contents[1]
    preferred_foot =1if(preferred_foot =='Left')else2
    myList.end(preferred_foot)

    skill_move_level=temp[2].contents[0]
    myList.end(int(skill_move_level))

    reputation=temp[3].contents[0]
    myList.end(int(reputation))


    todostr=temp[4].text

    workrateString=re.split('\s+',todostr)

    wr_att=workrateString[1][4:-1]
    wr_def=workrateString[2]

    wrList=['Low',"Medium","High"]
    wr_att=wrList.index(wr_att)+1
    wr_def=wrList.index(wr_def)+1
    myList.end(wr_att)
    myList.end(wr_def)

可以看出來,最多的代碼就是用來拆分 \ 拼接字符串而已。

頭像

下面要獲取頭像了,各類圖片都差不多的處理方式,可以說是爬蟲里面最有用的部分了 (誤)。

頭像要獲取 img 的 url 地址,然后用 stream 的方式進行下載。這里最需要注意的是圖片命名,別 down 下來之后自己分不清楚,confused 了。(這段話打著打著就出現(xiàn)了中英混雜,但 img="圖片",url="地址",stream="流",替代之后就會發(fā)現(xiàn)很別扭,求大家指導一下,怎么用純中文打出文化非常自信的代碼教程。)

#頭像
    rawdata=soup.select("#body > div:nth-child(5) > div > div.col.col-12 > div.bp3-card.player > img")
    img_url=rawdata[0].get("data-src")
    img_r=requests.get(img_url,stream=True)
#print(img_r.status_code)
    img_name = f"{id}_{playerName}.png"
    with open(f"X:/這里是路徑,每個人不一樣,我的不能給你們看/{img_name}","wb") as fi:
for chunk in img_r.iter_content(chunk_size=120):
            fi.write(chunk)

題外話:很多網頁上的圖片下載回來發(fā)現(xiàn)是 WebP 格式,也就是谷歌搞得一個格式。大家可以下載 "Save Image as Type" 插件,右鍵可以另存為 PNG 或 JPG。

其他信息:

其他位置信息,俱樂部信息,和國籍信息,都使用了一樣的辦法----哪里不會點哪里,右鍵復制個 selector 就完事。

##獲得位置
    rawdata = soup.select("#body > div:nth-child(5) > div > div.col.col-12 > div.bp3-card.player > div > div > span")
    allPos =''.join(f"{p.text} "for p in rawdata)
    myList.end(allPos)

    rawdata= soup.select("#body > div:nth-child(6) > div > div.col.col-4 > ul > li:nth-child(1) > span")
    bestPos=rawdata[0].text
    myList.end(bestPos)


#獲得俱樂部
    rawdata= soup.select("#body > div:nth-child(5) > div > div.col.col-12 > div:nth-child(4) > div > h5> a")
    club = rawdata[0].text iflen(rawdata)>0else"沒有俱樂部"
    myList.end(club)

#獲得國籍
    rawdata= soup.select("#body > div:nth-child(5) > div > div.col.col-12 > div.bp3-card.player > div > div > a")
    nation = rawdata[0].get("title")iflen(rawdata)>0else"國家"
    myList.end(nation)

屬性

重頭戲來了,這七八十條屬性,是手動抄起來最麻煩的,所以才寫的這個爬蟲。

分析發(fā)現(xiàn)每個屬性的值也寫在了類的名字里,例如這個 "class=bp3-tag p p-73",共性就是 "bp3-tag p" 的部分,所以需要用到了正則表達式 (其實上面也用到了,re 就是正則,我認為你們不懂的會自己去搜索,就沒多說)

就醬,最后把屬性作為一個列表返回去,爬蟲主體函數(shù)就完成了。

#獲取屬性
rawdata=soup.select('#body>div:nth-child(6)>divdiv.col.col-12')
data=rawdata[0].find_all(class_=re.compile('bp3-tagp'))
#print(data)
myList.extend(allatt.textforallattindata)
returnmyList

寫入文件

在開始下一步前,先把寫入的函數(shù)做好。不然好不容易爬到的數(shù)據(jù),只在內存里,很容易就丟失了。很多非程序員可能不了解,這個過程就叫做 "持久化"。正所謂,"不以長短論高下,只憑持久闖天下",說的就是代碼。

推薦寫入使用 csv,其他格式也一樣,如果要寫 excel,推薦使用 openpyxl 庫,以下時代碼部分,最長的那里是表格的頭。

#寫入文件
def dealWithData(dataToWrite):     
    header_list = ['id','name','birthday','height','weight','preferred_foot',"skill_move_level","reputation","wr_att","wr_def",'Positions','Best Position','Club',"nation",'Crossing','Finishing','Heading Accuracy', 'Short Passing','Volleys','Dribbling','Curve', 'FK Accuracy','Long Passing','Ball Control','Acceleration','Sprint Speed','Agility','Reactions','Balance','Shot Power','Jumping','Stamina','Strength','Long Shots','Aggression','Interceptions','Positioning','Vision','Penalties','Composure','Defensive Awareness','Standing Tackle','Sliding Tackle','GK Diving','GK Handling','GK Kicking','GK Positioning','GK Reflexes']
    with open('./目錄隨便寫/不推薦中文名.csv', 'a+', encoding='utf-8-sig', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(header_list)
        writer.writerows(dataToWrite)

另外關于寫入的幾種模式:w,a 和 + 的用法,請自行搜索 (寫教程好容易啊)。

搜索 id

如何調用上面的函數(shù)?需要的球員 id 從哪里來?這里我用到了 2 種方法,分別介紹一下:

遞增 ID

最早用了遞增的 id 進行遍歷,屬于廣撒網,多斂魚的方式。這個方式就很坑,通過這個搜索到了很多網站頁面不會顯示的球員數(shù)據(jù),例如女足球員的數(shù)據(jù)。

# 實際代碼已經不用了,我這里寫個例子
soData = []
for s in range(20000,40000):
    l=fetchData(s)
    if l!=None:
        soData.end(l)
dealWithData(soData)

這樣如果搜一條寫入一條,效率是非常差的,可以分批次來搜索,比如一次 100 條,然后整體寫入。寫入 CSV 時可以把 header_list 那條注釋掉,不需要寫入那么多次 header。

id 列表

我們使用一個 csv 文件,將需要搜索的 id 添加進去,然后讀取該列表進行靶向搜索!

#搜索列表
searchList=[]

with open('./目錄看自己/需要去搜索的id.CSV',"r",encoding='utf-8-sig') as f:
    f_csv=csv.reader(f,dialect='excel',delimiter=',')
    searchList.extend(iter(f_csv))
# print(len(searchList))

#進行搜索
soData =[]
for p in searchList:
    #因為ID讀進來是個字符串,所以要截取
    soid=str(p)[2:-2]
    l =fetchData(soid)
if l!=None:
        soData.end(l)
dealWithData(soData)

這樣就可以了,我們需要得到球員的 sofia 網站上的 id。這里我有通過名字搜索,通過 ovr 搜索,和通過俱樂部搜索,分別放在下面。

通過球員名字搜索

我們在這個網站上,通過名字搜索,會出現(xiàn)一個球員列表,例如搜索華倫天奴會出現(xiàn)以下球員:

話不多說,直接上代碼:

defgetPlayerID(key):
    url =f"https://sofifa.com/players?keyword={str(key)}"
    myRequest=requests.get(url)
    soup=BeautifulSoup(myRequest.text,'lxml')
    playerTable=soup.select("#body>div.center>div>div.col.col-12>div>table>tbody")

# print(len(playerTable[0].contents))
    data=playerTable[0].contents

    playersCandicate=[]
iflen(data)>0:
for p in data:
id=p.find("img")["id"]
            name=p.find("a")["aria-label"]
            ovr=p.find(attrs={"data-col":"oa"}).get_text()
            playersCandicate.end([id,name,ovr])
else:
print("not found")
        playersCandicate.end(["not found","the name you're searching is >>",keyword])
return playersCandicate

這個函數(shù)會獲得所有搜索到的結果,沒有的話會返回 "not found",需要注意的是會搜索到很多名字類似的球員,至于真正需要的是哪個,需要自己去過濾了。

同樣的,把要搜索的名字放在一個 csv 里面,方便使用。

#讀取要搜索的名單
searchList=[]
with open('toSearchByName.CSV',"r",encoding='utf-8-sig') as f:
    f_csv=csv.reader(f,dialect='excel',delimiter=',')
    searchList.extend(iter(f_csv))

#進行搜索,注意同名球員會全部搜索出來
idata = []
for p in searchList:
    keyword=str(p)[2:len(p)-3]
    l = getPlayerID(keyword)
    if l!=None:
        idata.end(l)
dealWithData(idata)

通過 OVR 搜索

搜索時通過球員的總屬性值 (OVR) 來進行搜索。

點擊 search 后,發(fā)現(xiàn)網址變成了這樣,可見 oal 就是 overall low,oah 是 overall high 的意思。

代碼如下:

# 輸入OVR最小值,最大值和頁數(shù)(一頁60個)
def searchByOVR(min,max,pages):
    i=min
    p=0

    playersCandicate=[]
    while i<=max:
        while p<=pages:
            url = f"https://sofifa.com/players?type=all&oal={str(min)}&oah={str(i)}&offset={str(60 * p)}"

            myRequest=requests.get(url)
            soup=BeautifulSoup(myRequest.text,'lxml')
            playerTable=soup.select("#body > div.center > div > div.col.col-12 > div > table > tbody")
            data=playerTable[0].contents

            if len(data)>0:
                for all in data:
                    id=all.find("img")["id"]
                    name=all.find("a")["aria-label"]
                    ovr=all.find(attrs={"data-col":"oa"}).get_text()
                    playersCandicate.end([id,name,ovr])
                p+=1
        p=0
        i+=1

#調用時,例如搜索65到75的,共搜索10頁.
searchByOVR(65,75,10)

通過球隊來搜索

球隊搜索的話,需要知道 club 的 id,我們選擇 teams,可以看到它唯一的 club id 和首發(fā)陣容。

這里寫下如何通過 club id 獲得首發(fā)陣容:

#獲得球隊陣容
def getLineup(id):
    url = f'https://sofifa.com/teams/{str(id)}'
    myRequest=requests.get(url)
    soup=BeautifulSoup(myRequest.text,'lxml')
    clubName=soup.find("h1").text
    if clubName == 'Teams':
        return None
    lineup=soup.select("#body > div:nth-child(4) > div > div.col.col-12 > div > div.block-two-third > div > div")
    data=lineup[0].find_all("a")
    field_player=[]

    if len(data)>0:
        for p in data:
            temp=str(p.attrs["href"])
            temp=temp.lstrip("/player/")
            temp=temp.rstrip("/")
            id=temp[:temp.find("/")]
            field_player.end([clubName, id, p.attrs["title"], p.text[:2]])
    return field_player

至于如何獲得 club id,跟之前球員一樣,或者用遞增的 id 去記錄下來,或者通過搜索球隊名字,不再贅述。

總結

常言道,人生苦短,我用 python。作為一個腳本語言,快和簡單就是 python 最大的特點。大家可以根據(jù)自己的需求類定制這類爬蟲,關于爬蟲更高級的框架可以使用 scappy 等。對于常用的工具函數(shù),比如寫入 csv,寫入 \ 讀取 excel 等,可以按照自己的需求寫在一個 misc.py 里面。實際上,因為經常有新的需求,所以寫得很隨便,,注釋很多都沒寫。這種力大磚飛得寫法是沒有任何美感可言的,新的需求接踵而來,又沒有時間去重構,能運行起來就謝天謝地了,看到運行完成的這句后 exited with  in  seconds 后,就再也不想打開了。希望大家以此為戒,能夠寫出通俗易懂的代碼。

本文來自微信公眾號:千猴馬的游戲設計之道 (ID:baima21th),作者:千兩

廣告聲明:文內含有的對外跳轉鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時間,結果僅供參考,IT之家所有文章均包含本聲明。

相關文章

關鍵詞:爬蟲python,FIFA23

軟媒旗下網站: IT之家 最會買 - 返利返現(xiàn)優(yōu)惠券 iPhone之家 Win7之家 Win10之家 Win11之家

軟媒旗下軟件: 軟媒手機APP應用 魔方 最會買 要知