引言

这次博文记录的是我之前练习爬虫时做的一个轻量级爬虫项目——对足球赛事射手榜的爬取,使用了一些最基本的爬虫技巧,此外鉴于这个案例爬取工程量很小,另外出于练习正则表达式的考虑,我将不使用现成的解析库,而是用正则表达式进行网页代码解析。

抓取分析

我们需要对多个页面进行抓取,以欧冠比赛为例,我们需要抓取的站点为https://data.huanhuba.com/ucl/goals-410-1-37521-p1/ ,打开之后可以看到详细榜单信息,如下图所示。

1569479759923

页面中显示的有效信息有球员姓名,排名,球队,位置,出场次数,进球数,点球数和平均进球时间等信息。

观察网页的网址,当翻页时网址中的’p1’依次变为’p2’、’p3’等,这意味着我们找到了网址的命名规律——即表达页数时是用字母”p”后接页数实现的,这一规律在接下来爬虫设计过程中将会起到一定用处。

抓取射手榜首页

接下来首先用代码实现抓取过程,首先我们抓取欧冠联赛射手榜。我们实现了get_one_page()方法,并给他传入url参数,然后将抓取结果返回,再通过main()方法调用,初步代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
def get_one_page(url):
# 添加头信息
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.3964.2 Safari/537.36'}
html = requests.get(url, headers=headers)
if html.status_code == 200:
return html.text
return None
def main():
url = 'https://data.huanhuba.com/ucl/goals-410-1-37521-p1/'
html = get_one_page(url)
print(html)
main()

这样运行之后,就能得到欧冠联赛射手榜页面的源代码了,获取源代码后,就要解析页面,提取出我们想要的信息。

正则提取信息

接下来,回到网页看一下页面的真实源码,在开发者模式下的Network监听组件中查看源代码,如下图所示。

1569479931100

注意:这里不要在Elements选项卡中直接查看源代码,因为那里的源码可能经过JavaScript操作而与原始请求不同,而是需要从Network选项卡部分查看原始请求得到的源码。

查看其中一个条目的源代码,如下图所示。

1569480014119

可以看到,一个球员数据信息对应的源代码是一个tr节点,我们用正则表达式来提去这里面的一些数据信息。首先需要提取球员的排名信息,而排名信息是在class为td32 num colorgray9td节点内这里利用费贪婪匹配来提取td节点内的信息,正则表达式写为<tr>.*?td32 num colorgray9">(.*?)</td>

随后需要提取球员姓名与其他数据信息。可以看到后面有两个td节点,td节点内部有a节点,经过检查后发现,这两个a节点里就有球员姓名和所属球队信息,因此正则表达式可以改写如下:

1
<tr>.*?td32 num colorgray9">(.*?)</td>*?colorgray3.*?>(.*?)</a>*?linka.*?>(.*?)</a>

再往后,需要提取球员的位置,出场次数,进球数,点球数和平均进球时间等信息,这些数据在更靠后的td节点里,可以用colorgray9为标志位,进一步提取信息,此时正则表达式可以修改为

1
<tr>.*?td32 num colorgray9">(.*?)</td>*?colorgray3.*?>(.*?)</a>.*?linka.*?>(.*?)</a>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>.*?fnt600">(.*?)</td>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>

这样一个正则表达式可以匹配出一个球员的信息,里面包含了球员的7个信息,接下来通过调用findall()方法提取所有的内容。

接下来,我们再定义解析页面的方法parse_one_page(),主要是通过正则表达式来从结果中提取出我们想要的内容,实现代码如下:

1
2
3
4
def parse_one_page(html):
pattern = re.compile('<tr>.*?td32 num colorgray9">(.*?)</td>*?colorgray3.*?>(.*?)</a>.*?linka.*?>(.*?)</a>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>.*?fnt600">(.*?)</td>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>', re.S)
items = re.findall(pattern, html)
print(items)

但是这样还不够,数据比较杂乱,我们将匹配结果再处理一下,遍历结果并生成字典,此时方法改写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def parse_one_page(html):
pattern = re.compile('<tr>.*?num .*?>(.*?)</td>.*?colorgray3.*?>(.*?)</a>.*?linka.*?>(.*?)</a>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>.*?fnt600">(.*?)</td>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>.*?</tr>', re.S)
items = re.findall(pattern, html)
for item in items:
yield {
'index': item[0],
'name': item[1],
'team': item[2],
'position': item[3],
'appearanceNumber': item[4],
'goals': item[5],
'penalty': item[6],
'averageGoalTime': item[7]
}

到此为止,我们就成功提取了单页的球员信息。

写入文件

最后,我们将提取的结果写入文件,这里直接写入到一个文本文件中,通过接送库的dumps()方法实现字典的序列化,并指定ensure_ascii参数为False,这样可以保证输出结果是中文形式而不是Unicode编码,代码如下:

1
2
3
def write_to_file(content):
with open('result.txt', 'a', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False) + '\n')

通过这个方法可以实现将字典写入文本文件,这个content参数就是一个球员的提取结果,是一个字典。

整合代码

最后,实现main()方法来调用前面的方法,将单页球员数据写入文件,相关代码如下

1
2
3
4
5
6
def main(dataPage):
url = 'https://data.huanhuba.com/ucl/goals-410-1-37521-p' + str(dataPage) + '/'
html = get_one_page(url)
for item in parse_one_page(html):
print(item)
write_to_file(item)

到此为止,我们就实现了欧冠球员射手榜的爬取。

分页爬取

因为我们的目标是从第一页抓取到第十六页(最后一页),所以还需要给链接传入一个新的参数来表示不同的足球赛事,此时添加如下调用即可:

1
2
3
if __name__ == '__main__':
for dataPage in range(1,17):
main(dataPage)

到此为止,我们的球赛射手榜爬虫就全部完成了,再稍微整理一下,完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# encoding:utf-8
import json
import requests
from requests.exceptions import RequestException
import re
import time
def get_one_page(url):
# 添加头信息
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.3964.2 Safari/537.36'}
html = requests.get(url, headers=headers)
if html.status_code == 200:
return html.text
return None
def parse_one_page(html):
pattern = re.compile('<tr>.*?num .*?>(.*?)</td>.*?colorgray3.*?>(.*?)</a>.*?linka.*?>(.*?)</a>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>.*?fnt600">(.*?)</td>.*?colorgray9">(.*?)</td>.*?colorgray9">(.*?)</td>.*?</tr>', re.S)
items = re.findall(pattern, html)
for item in items:
yield {
'排名': item[0],
'姓名': item[1],
'队名': item[2],
'位置': item[3],
'出场次数': item[4],
'进球数': item[5],
'点球数': item[6],
'平均进球时间': item[7]
}
def write_to_file(content):
with open('result.txt', 'a', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False) + '\n')
def main(dataPage):
url = 'https://data.huanhuba.com/ucl/goals-410-1-37521-p' + str(dataPage) + '/'
html = get_one_page(url)
for item in parse_one_page(html):
print(item)
write_to_file(item)
if __name__ == '__main__':
for dataPage in range(1,17):
main(dataPage)
time.sleep(1)

运行结果

1569486057184

控制台出现下列输出结果,并保存在了result.txt文件中,如下图所示。

1569486089038

一点碎碎念

由于这里我的练习目的在于练习爬虫与解析的基本技巧与requests库的使用,因此我最后文件保存为了txt格式,其实如果调用第三方库保存为csv或者xls格式,可以更有利于进一步的数据科学研究,也有利于进行数据可视化。

掌握了类似的方法后,我们也可以利用爬虫去爬取我们支持的其它球队的信息啦!~(大雾)

最后,梅西好棒~

最后的最后,本节代码上传到了我的Github上,点击下方按钮即可速览代码,欢迎大家点击与关注。