本文为 AI 研习社编译的技术博客,原标题 :
Web Scraping Using Python
翻译 | 余杭 校对 | 志豪 整理 | 志豪
https://www.datacamp.com/community/tutorials/web-scraping-using-python
在本教程中,您将学习如何从web提取数据、使用Python的Pandas库操作和清理数据,以及使用Python的Matplotlib库来实现数据可视化。
Web抓取是一个用来描述使用程序或算法从Web中提取和处理大量数据的术语。无论您是数据科学家、工程师还是分析大数据集的人,从web上获取数据的能力都是一项有用的技能。假设您从web中找到数据,并且没有直接的方法来下载它,使用Python进行web抓取是一种技巧,您可以使用它将数据提取到并导入到有用的表单中。
在本教程中,您将了解以下内容:
使用Python Beautiful Soup模块从web中提取数据
使用Python的panda库进行数据操作和清理
使用Python的Matplotlib库进行数据可视化
本教程中使用的数据集取自于发生在希尔斯伯勒2017年6月的10公里比赛。你将分析跑者的表现,并回答以下问题:
跑步者的平均完成时间是多少?
跑者的完成时间是否服从正态分布?
不同年龄段的男性和女性在表现上有什么不同吗?
使用Beautiful Soup进行网页抓取
在使用Jupyter Notebook 之前,你需要导入以下模块:
pandas, numpy, matplotlib.pyplot, seaborn。如果您还没有安装Jupyter笔记本,我建议您下载安装Anaconda Python发行版,因为这个确实太好用了,而且自带了很多模块。为了方便在程序中显示图表,请确保按如下所示的方式包含%mattplotlib 。
importpandasaspdimport numpyasnpimport matplotlib.pyplotaspltimport seabornassns
%matplotlib inline
要执行web抓取,还应该导入如下所示的库:urllib.request 模块用于打开url。这个Beautiful Soup用于从html文件中提取数据。模块中 Beautiful Soup 的别名(缩写)是 bs4 ,4 是第四个版本。
fromurllib.requestimporturlopenfrombs4importBeautifulSoup
导入必要的模块后,指定包含数据集的URL, 并将其传递给 urlopen(), 得到返回的html。
url="http://www.hubertiming.com/results/2017GPTR10K"html = urlopen(url)
获取页面html后,下一步就是从html 创建一个Beautiful Soup 对象,这很简单,将html 传给构造函数就行。 Beautiful Soup 的方法会解析html,分解为python 对象。第二个参数“lxml” 是html的解析器。只管用吧。
soup=BeautifulSoup(html, 'lxml')type(soup)
bs4.BeautifulSoup
soup对象允许您提取关于您正在抓取的网站的有趣信息,例如获得如下所示的页面标题。
# Get the titletitle = soup.titleprint(title)
2017Intel Great Place to Run10K \ Urban Clash Games Race Results
你也可以得到网页的文本,打印出来瞧瞧,检查它是否是你所期望的。
#Print texttext = soup.get_text()# Print (soup.text))
你可以在网页的任何地方右键单击并选择“查看源代码”来查看网页的html和我们输入的进行对比。
您可以使用soup的find_all()方法在网页中提取有用的html标记。有用标记的例子包括用于超链接,用于表行,用于表标题,用于表单元格,用于表单元格。下面的代码展示了如何提取网页中的所有超链接。
soup.find_all('a')
[5K, Huber Timing Home, Individual Results, Team Results, timing@hubertiming.com, Results, , , Huber Timing, ]
从上面的输出例子可以看到,html标记有时带有class、src等属性。这些属性提供了关于html元素的附加信息。可以使用for循环和get('"href")方法只提取和打印超链接。
all_links = soup.find_all("a")forlinkinall_links: print(link.get("href"))
如果仅打印表行,请在soup.find_all()中传递'tr'参数。
# Print the first 10 rows for sanity checkrows = soup.find_all('tr')
print(rows[:10])
[Finishers:577,Male:414,Female:163,
Place
Bib
Name
Gender
City
State
Chip Time
Chip Pace
Gender Place
Age Group
Age Group Place
Time to Start
Gun Time
Team
,
1
814
JARED WILSON
M
TIGARD
OR
00:36:21
05:51
1 of 414
M 36-45
1 of 152
00:00:03
00:36:24
,
2
573
NATHAN A SUSTERSIC
M
PORTLAND
OR
00:36:42
05:55
2 of 414
M 26-35
1 of 154
00:00:03
00:36:45
INTEL TEAM F
,
3
687
FRANCISCO MAYA
M
PORTLAND
OR
00:37:44
06:05
3 of 414
M 46-55
1 of 64
00:00:04
00:37:48
,
4
623
PAUL MORROW
M
BEAVERTON
OR
00:38:34
06:13
4 of 414
M 36-45
2 of 152
00:00:03
00:38:37
,
5
569
DEREK G OSBORNE
M
HILLSBORO
OR
00:39:21
06:20
5 of 414
M 26-35
2 of 154
00:00:03
00:39:24
INTEL TEAM F
,
6
642
JONATHON TRAN
M
PORTLAND
OR
00:39:49
06:25
6 of 414
M 18-25
1 of 34
00:00:06
00:39:55
]
本教程的目标是从网页中取出一张表格,并将其转换为dataframe,以便更容易地使用Python进行操作。要做到这一点,您应该首先获得list表单中的所有表行,然后将该列表转换为dataframe。下面是一个for循环,它遍历表行并输出行单元格。
forrowinrows: row_td = row.find_all('td')print(row_td)type(row_td)
[14TH, INTEL TEAM M, 04:43:23, 00:58:59 - DANIELLE CASILLAS, 01:02:06 - RAMYA MERUVA, 01:17:06 - PALLAVI J SHINDE, 01:25:11 - NALINI MURARI]bs4.element.ResultSet
上面的输出显示每一行都是用嵌入在每一行中的html标记打印出来的。这不是我们想要的。可以使用Beautiful Soup或正则表达式删除html标记。
str_cells = str(row_td)cleantext = BeautifulSoup(str_cells,"lxml").get_text()print(cleantext)
[14TH, INTEL TEAM M,04:43:23,00:58:59- DANIELLE CASILLAS,01:02:06- RAMYA MERUVA,01:17:06- PALLAVIJ SHINDE,01:25:11- NALINI MURARI]:正则表达式才是高手风范)。
我们导入re(用于正则表达式)模块,下面的代码展示了如何构建一个正则表达式,该表达式查找 html标记中的所有字符,并为每个表行替换为空字符串。
首先,通过传递与re.compile()匹配的字符串来编译正则表达式。点、星号和问号(.*?) 将匹配一个开头的尖括号,后面跟着任何东西,后面跟着一个结尾的尖括号。它以非贪婪的方式匹配文本,也就是说,它匹配尽可能短的字符串。
如果省略问号,它将匹配第一个开始尖括号和最后一个结束尖括号之间的所有文本。
编译正则表达式后,可以使用re.sub()方法查找正则表达式匹配的所有子字符串,并用空字符串替换它们。
下面的完整代码生成一个空列表,提取每一行html标记之间的文本,并将其附加到指定的列表中。
importre
list_rows = []
for row in rows:
cells = row.find_all('td')
str_cells = str(cells)
clean = re.compile('')
clean2 = (re.sub(clean, '',str_cells))
list_rows.append(clean2)
print(clean2)
type(clean2)
[14TH, INTEL TEAM M,04:43:23,00:58:59- DANIELLE CASILLAS,01:02:06- RAMYA MERUVA,01:17:06- PALLAVIJ SHINDE,01:25:11- NALINI MURARI]str
下一步是将列表转换为dataframe并使用panda快速查看前10行。
df = pd.DataFrame(list_rows)df.head(10)
[Finishers:,577]
1[Male:,414]
2[Female:,163]
3[]
4[1,814, JARED WILSON, M, TIGARD, OR,00:36:21...
5[2,573, NATHAN A SUSTERSIC, M, PORTLAND, OR, ...
6[3,687, FRANCISCO MAYA, M, PORTLAND, OR,00:3...
7[4,623, PAUL MORROW, M, BEAVERTON, OR,00:38:...
8[5,569, DEREK G OSBORNE, M, HILLSBORO, OR,00...
9[6,642, JONATHON TRAN, M, PORTLAND, OR,00:39...
数据处理和清理
dataframe不是我们想要的格式。为了清理它,您应该在逗号位置将“0”列分割为多个列,通常我们使用str.split()方法实现的。
df1= df[].str.split(',', expand=True)
df1.head(10)
这看起来清爽多了,但还有工作要做。dataframe的每行周围都有不需要的方括号。可以使用strip()方法删除列“0”上的左方括号。
df1[] = df1[].str.strip('[') df1.head(10)
上表缺少表标头。您可以使用find_all()方法获得表标头。
col_labels= soup.find_all('th')
与处理表类似,您可以使用Beautiful Soup提取表标题的html标记之间的文本。
['[Place, Bib, Name, Gender, City, State, Chip Time, Chip Pace, Gender Place, Age Group, Age Group Place, Time to Start, Gun Time, Team]']
然后,您可以将标题列表转换为pandas dataframe。
df2= pd.DataFrame(all_header) df2.head()
[Place, Bib, Name, Gender, City, State, Chip T...
类似地,您可以在所有行的逗号位置将“0”列分割为多个列。
df3 = df2[].str.split(',',expand=True)df3.head()
frames= [df3, df1]
df4 = pd.concat(frames)
df4.head(10)
下面显示了如何将第一行分配为表标头。
df5= df4.rename(columns=df4.iloc[])
df5.head()
至此,这个表的格式几乎完全正确。对于分析,您可以从以下数据的概述开始。
df5.info()df5.shape
Int64Index:597entries,to595
Data columns (total14columns):
[Place597non-nullobject
Bib596non-nullobject
Name593non-nullobject
Gender593non-nullobject
City593non-nullobject
State593non-nullobject
Chip Time593non-nullobject
Chip Pace578non-nullobject
Gender Place578non-nullobject
Age Group578non-nullobject
Age Group Place578non-nullobject
Time to Start578non-nullobject
Gun Time578non-nullobject
Team]578non-nullobject
dtypes: object(14)
memory usage:70.0+ KB
(597,14)
该表有597行和14列。您可以删除所有缺少值的行。
df6 = df5.dropna(axis=0,how='any')
另外,请注意如何将表头复制为df5中的第一行。 可以使用以下代码行删除它。
df7= df6.drop(df6.index[0])df7.head()
您可以通过重新命名'[Place' and ' Team]'列来执行更多的数据清理。Python对空格非常挑剔。确保在“Team]”中在引号之后加上空格。
df7.rename(columns={'[Place':'Place'},inplace=True)
df7.rename(columns={' Team]':'Team'},inplace=True)
df7.head()
最后的数据清理步骤包括删除“Team”列中的单元格的右括号。
df7['Team'] = df7['Team'].str.strip(']')
df7.head()
到这里为止我们花了一段很长时间,也得到了我们想要的dataframe。现在您可以进入令人兴奋的部分,开始绘制数据并计算有趣的统计数据。
数据分析和可视化
首先要回答的问题是,跑步者的平均完成时间(以分钟为单位)是多少?您需要将列“Chip Time”转换为几分钟形式。一种方法是首先将列转换为列表进行操作。
time_list = df7[' Chip Time'].tolist()# You can useaforlooptoconvert'Chip Time'tominutestime_mins = []fori in time_list:h,m, s = i.split(':') math = (int(h) *3600+int(m) *60+int(s))/60time_mins.append(math)#print(time_mins)
下一步是将列表转换回dataframe,并立即为跑步者 Chip Time 创建一个新的列(“Runner_mins”)。
df7['Runner_mins'] = time_mins df7.head()
下面的代码显示了,在dataframe中计算数字列的统计信息。
df7.describe(include=[np.number])
Runner_mins
count577.000000
mean60.035933
std11.970623
min36.350000
25%51.000000
50%59.016667
75%67.266667
max101.300000
有趣的是,所有跑步者的平均chip time 是大约60分钟。最快的10K跑者跑完36.35分钟,最慢的跑者跑完101.30分钟。
boxplot是另一个有用的工具,用于可视化汇总统计信息(最大值、最小值、中等值、第一四分位数、第三四分位数,包括异常值)。下面是在箱线图中显示的跑步者的数据汇总统计数据。为了实现数据可视化,可以方便地首先从matplotlib附带的pylab模块导入参数,并为所有图形设置相同的大小,以避免为每个图形设置相同的大小。
from pylab import rcParams rcParams['figure.figsize'] =15,5
df7.boxplot(column='Runner_mins')
plt.grid(True, axis='y')
plt.ylabel('Chip Time')
plt.xticks([1], ['Runners'])
([], )
要回答的第二个问题是:跑者的完成时间是否服从正态分布?
下面是使用seaborn库绘制的跑步者chip times分布图。分布看起来几乎是正常的。
x = df7['Runner_mins']
ax = sns.distplot(x, hist=True, kde=True, rug=False, color='m', bins=25, hist_kws={'edgecolor':'black'})
plt.show()
第三个问题是关于不同年龄段的男性和女性是否有表现上的差异。
下面是男性和女性芯片时间的分布图。
f_fuko = df7.loc[df7[' Gender']==' F']['Runner_mins']m_fuko = df7.loc[df7[' Gender']==' M']['Runner_mins']sns.distplot(f_fuko, hist=True, kde=True, rug=False, hist_kws={'edgecolor':'black'}, label='Female')sns.distplot(m_fuko, hist=False, kde=True, rug=False, hist_kws={'edgecolor':'black'}, label='Male')plt.legend()
这一分布表明女性的平均速度比男性慢。您可以使用groupby()方法分别计算男性和女性的汇总统计信息,如下所示。
g_stats = df7.groupby(" Gender",as_index=True).describe()print(g_stats)
Runner_mins \ count mean std min25%50% Gender F163.066.11922312.18444043.76666758.75833364.616667M414.057.64082111.01185736.35000049.39583355.79166775% max Gender F72.058333101.300000M64.80416798.516667
所有女性和男性的平均芯片时间分别为约66分钟和约58分钟。
那么下面是男性和女性完成时间的并排箱线图比较。
df7.boxplot(column='Runner_mins', by=' Gender')plt.ylabel('Chip Time')plt.suptitle("")
C:\Users\smasango\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\fromnumeric.py:57: FutureWarning: reshapeisdeprecatedandwillraiseina subsequent release. Please use .values.reshape(...) instead return getattr(obj,method)(*args, **kwds)Text(0.5,0.98,'')
结论
在本教程中,您使用Python执行了Web抓取。 您使用Beautiful Soup库来解析html数据并将其转换为可用于分析的表单。 您在Python中执行了数据清理并创建了有用的图表(箱形图,条形图和分布图),用Python的matplotlib和seaborn库来显示有趣的趋势。 在本教程之后,您应该能够使用Python轻松地从Web抓取数据,应用清理技术并从数据中提取有用的见解。
如果您想了解有关Python的更多信息,请参阅DataCamp的免费Intro to Python for Data Science课程。
想要继续查看该篇文章更多代码、链接和参考文献?
戳链接:
http://www.gair.link/page/TextTranslation/831
领取专属 10元无门槛券
私享最新 技术干货