題語:世界杯開始了,大家又重燃了看球的熱情。對于游戲制作來說,經常需要制定一些角色的數(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之家所有文章均包含本聲明。