题语:世界杯开始了,大家又重燃了看球的热情。对于游戏制作来说,经常需要制定一些角色的数据,特别是体育类的游戏。自己去设定工作量大,并且太主观,这时候就需要去一些权威的网站查询数据,,用作参考。笔者结合自己实际经验,教大家做一个简单的爬虫。
前期准备工作
首先确定我们需要爬取的是 FIFA23 的球员数据,通过 https://sofifa.com/ 这个网站,里面有从 FIFA07 到 FIFA23 所有的球员数据,非常详实。打开首页后,发现是这样的::
我们点选其中一个球员,进行分析:
发现所需要的数据,都在上面俩张图的位置中。下方是转会记录和用户评论,现在用不上。
经过分析发现,每个球员都有一个唯一 id,显示在网址 url 中。无论是通过姓名搜索,还是通过球队搜索后跳转,球员的页面都会显示这个 id。
最后的 230006 这串数字,应该是某种参数。在 url 去掉后,依然会打开该球员页面。
去掉球员名字后,依然可以打开页面。
所以我们明白了----所有人的只是一段数字。
到这里,前期的重要准备已经完成了。我们发现了规律,下一步需要去运用了。
开始动手
安装 python,笔者使用的是 3.9.12 版本。然后安装 requests 库和 beautiful soup 库,可以使用 pip install requests,pip install beautifulsoup4 来安装,或者用 conda 来管理安装包、关于如何安装请自行搜索,不再赘述。
先写用来获取球员数据的最主要的函数:
#通过球员ID,爬取数据,返回一个列表 deffetchData(id): url =f'https://sofifa.com/player/{str(id)}' myRequest = requests.get(url) soup = BeautifulSoup(myRequest.text,'lxml') myList =[] return myList
我们通过 id 来获取一个球员的信息,所以参数是 id。只要递增 id 就可以来爬取所有球员的信息了。如果查无此人,就返回一个空值。注意 request 如果返回的值是 200,则表示连接成功,至于重试和 http header 怎么设置,请自行搜索。
页面上取值
按 F12 查看页面元素,取到所需的值。每个项目都不同,下面的展示是我们所需要的。
meta 数据
有一段页面没有显示的 meta 数据,里面记录了该球员的描述。我把这个得下来,用来跟同名的球员快速对比。
过滤年份
因为要取最新的 FIFA23 的数据,所以我过滤了左上角的年份,不是 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年的数据') 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,这种魔数 (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 了。(这段话打着打着就出现了中英混杂,但 img="图片",url="地址",stream="流",替代之后就会发现很别扭,求大家指导一下,怎么用纯中文打出文化非常自信的代码教程。)
#头像 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)
题外话:很多网页上的图片下载回来发现是 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)
属性
重头戏来了,这七八十条属性,是手动抄起来最麻烦的,所以才写的这个爬虫。
分析发现每个属性的值也写在了类的名字里,例如这个 "class=bp3-tag p p-73",共性就是 "bp3-tag p" 的部分,所以需要用到了正则表达式 (其实上面也用到了,re 就是正则,我认为你们不懂的会自己去搜索,就没多说)
就酱,最后把属性作为一个列表返回去,爬虫主体函数就完成了。
#获取属性 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
写入文件
在开始下一步前,先把写入的函数做好。不然好不容易爬到的数据,只在内存里,很容易就丢失了。很多非程序员可能不了解,这个过程就叫做 "持久化"。正所谓,"不以长短论高下,只凭持久闯天下",说的就是代码。
推荐写入使用 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
如何调用上面的函数?需要的球员 id 从哪里来?这里我用到了 2 种方法,分别介绍一下:
递增 ID
最早用了递增的 id 进行遍历,属于广撒网,多敛鱼的方式。这个方式就很坑,通过这个搜索到了很多网站页面不会显示的球员数据,例如女足球员的数据。
# 实际代码已经不用了,我这里写个例子 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 搜索,和通过俱乐部搜索,分别放在下面。
通过球员名字搜索
我们在这个网站上,通过名字搜索,会出现一个球员列表,例如搜索华伦天奴会出现以下球员:
话不多说,直接上代码:
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
这个函数会获得所有搜索到的结果,没有的话会返回 "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 后,发现网址变成了这样,可见 oal 就是 overall low,oah 是 overall high 的意思。
代码如下:
# 输入OVR最小值,最大值和页数(一页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 和首发阵容。
这里写下如何通过 club id 获得首发阵容:
#获得球队阵容 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 最大的特点。大家可以根据自己的需求类定制这类爬虫,关于爬虫更高级的框架可以使用 scappy 等。对于常用的工具函数,比如写入 csv,写入 \ 读取 excel 等,可以按照自己的需求写在一个 misc.py 里面。实际上,因为经常有新的需求,所以写得很随便,,注释很多都没写。这种力大砖飞得写法是没有任何美感可言的,新的需求接踵而来,又没有时间去重构,能运行起来就谢天谢地了,看到运行完成的这句后 exited with in seconds 后,就再也不想打开了。希望大家以此为戒,能够写出通俗易懂的代码。
本文来自微信公众号:千猴马的游戏设计之道 (ID:baima21th),作者:千两