题语:世界杯开始了,大家又重燃了看球的热情。对于游戏制作来说,经常需要制定一些角色的数据,特别是体育类的游戏。自己去设定工作量大,并且太主观,这时候就需要去一些权威的网站查询数据,,用作参考。笔者结合自己实际经验,教大家做一个简单的爬虫。

前期准备工作

首先确定我们需要爬取的是 FIFA23 的球员数据,通过 https://sofifa.com/ 这个网站,里面有从 FIFA07 到 FIFA23 所有的球员数据,非常详实。打开首页后,发现是这样的::

1669369275506-1.webp

我们点选其中一个球员,进行分析:

1669369424571-1.webp

1669369363165-1.webp

发现所需要的数据,都在上面俩张图的位置中。下方是转会记录和用户评论,现在用不上。

经过分析发现,每个球员都有一个唯一 id,显示在网址 url 中。无论是通过姓名搜索,还是通过球队搜索后跳转,球员的页面都会显示这个 id。

1669369425220-2.webp

最后的 230006 这串数字,应该是某种参数。在 url 去掉后,依然会打开该球员页面。

1669369283166-1.webp

去掉球员名字后,依然可以打开页面。

1669369305177-1.webp

所以我们明白了----所有人的只是一段数字。

到这里,前期的重要准备已经完成了。我们发现了规律,下一步需要去运用了。

开始动手

安装 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 查看页面元素,取到所需的值。每个项目都不同,下面的展示是我们所需要的。

1669369366862-1.webp

meta 数据

有一段页面没有显示的 meta 数据,里面记录了该球员的描述。我把这个得下来,用来跟同名的球员快速对比。

1669369367325-2.webp

过滤年份

因为要取最新的 FIFA23 的数据,所以我过滤了左上角的年份,不是 23 年的就会返回空值。

1669369289654-1.webp

到目前位置的代码:

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]

基础信息

获取位置 \ 生日 \ 身高 \ 体重等信息,我们可以看出来,这是一个字符串。

1669369290205-2.webp

这里用到了全篇都在用的,省脑子的做法,就是改变 selector。右键选中需要爬取的部分,选择 copy selector 就可以复制到剪贴板上了。

1669369368570-1.webp

#获取小字信息
    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 信息,包括正逆足,技巧动作等级,进攻防守参与度等等。

1669369292618-1.webp

左脚定义为 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)

属性

重头戏来了,这七八十条属性,是手动抄起来最麻烦的,所以才写的这个爬虫。

1669369369047-2.webp

分析发现每个属性的值也写在了类的名字里,例如这个 "class=bp3-tag p p-73",共性就是 "bp3-tag p" 的部分,所以需要用到了正则表达式 (其实上面也用到了,re 就是正则,我认为你们不懂的会自己去搜索,就没多说)

1669369356477-1.webp

就酱,最后把属性作为一个列表返回去,爬虫主体函数就完成了。

#获取属性
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 搜索,和通过俱乐部搜索,分别放在下面。

通过球员名字搜索

我们在这个网站上,通过名字搜索,会出现一个球员列表,例如搜索华伦天奴会出现以下球员:

1669369595478-1.webp

话不多说,直接上代码:

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) 来进行搜索。

1669369361622-1.webp

点击 search 后,发现网址变成了这样,可见 oal 就是 overall low,oah 是 overall high 的意思。

1669369364093-2.webp

代码如下:

# 输入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 和首发阵容。

1669369303426-1.webp

这里写下如何通过 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),作者:千两

请登陆后查看
本内容须登陆后才可以看见(点我即可快速登录)