查看: 492|回复: 1

[经验分享] 教你如何用Python做网页爬虫

[复制链接]

主题

好友

616

积分

举人

  • TA的每日心情
    擦汗
    昨天 11:15
  • 签到天数: 8 天

    连续签到: 1 天

    [LV.3]偶尔看看II

    发表于 2018-9-28 15:09:43 |显示全部楼层
    本文为 AI 研习社编译的技术博客,原标题 :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 。
    1. [size=2]import pandas as pdimport numpy as npimport matplotlib.pyplot as pltimport seaborn as sns
    2. %matplotlib inline[/size]
    复制代码
    要执行web抓取,还应该导入如下所示的库:urllib.request 模块用于打开url。这个Beautiful Soup用于从html文件中提取数据。模块中 Beautiful Soup 的别名(缩写)是 bs4 ,4 是第四个版本。
    1. [size=2]from urllib.request import urlopenfrom bs4 import BeautifulSoup[/size]
    复制代码
    导入必要的模块后,指定包含数据集的URL, 并将其传递给 urlopen(), 得到返回的html。
    1. [size=2]url = "http://www.hubertiming.com/results/2017GPTR10K"html = urlopen(url)[/size]
    复制代码
    获取页面html后,下一步就是从html 创建一个Beautiful Soup 对象,这很简单,将html 传给构造函数就行。 Beautiful Soup 的方法会解析html,分解为python 对象。第二个参数“lxml” 是html的解析器。只管用吧。
    1. [size=2]soup = BeautifulSoup(html, 'lxml') type(soup)
    2. bs4.BeautifulSoup[/size]
    复制代码
    soup对象允许您提取关于您正在抓取的网站的有趣信息,例如获得如下所示的页面标题。
    1. [size=2]# Get the titletitle = soup.title print(title)
    2. <title>2017 Intel Great Place to Run 10K \ Urban Clash Games Race Results</title>[/size]
    复制代码
    你也可以得到网页的文本,打印出来瞧瞧,检查它是否是你所期望的。
    1. [size=2]# Print texttext = soup.get_text()# Print (soup.text))[/size]
    复制代码
    你可以在网页的任何地方右键单击并选择“查看源代码”来查看网页的html和我们输入的进行对比。
    1.jpg
    您可以使用soup的find_all()方法在网页中提取有用的html标记。有用标记的例子包括< a >用于超链接,<table >用于表行,< tr >用于表标题,< th >用于表单元格,< td >用于表单元格。下面的代码展示了如何提取网页中的所有超链接。
    1. [size=2]soup.find_all('a')

    2. [<a href="/results/2017GPTR" role="button">5K</a>,
    3. <a href="http://hubertiming.com/">Huber Timing Home</a>,
    4. <a href="#individual">Individual Results</a>,
    5. <a href="#team">Team Results</a>,
    6. <a href="mailto:timing@hubertiming.com">[email]timing@hubertiming.com[/email]</a>,
    7. <a href="#tabs-1">Results</a>,
    8. <a name="individual"></a>,
    9. <a name="team"></a>,
    10. <a href="http://www.hubertiming.com/"><img height="65" src="/sites/all/themes/hubertiming/images/clockWithFinishSign_small.png" width="50"/>Huber Timing</a>,
    11. <a href="http://facebook.com/hubertiming/"><img src="/results/FB-f-Logo__blue_50.png"/></a>][/size]
    复制代码
    从上面的输出例子可以看到,html标记有时带有class、src等属性。这些属性提供了关于html元素的附加信息。可以使用for循环和get('"href")方法只提取和打印超链接。
    1. [size=2]all_links = soup.find_all("a")for link in all_links:
    2.     print(link.get("href"))

    3. /results/2017GPTR[url]http://hubertiming.com/#individual#teammailto:timing@hubertiming.com#tabs-1None[/url]
    4. None[url]http://www.hubertiming.com/http://facebook.com/hubertiming/[/url][/size]
    复制代码
    如果仅打印表行,请在soup.find_all()中传递'tr'参数。
    1. [size=2]# Print the first 10 rows for sanity checkrows = soup.find_all('tr')
    2. print(rows[:10])



    3. [<tr><td>Finishers:</td><td>577</td></tr>, <tr><td>Male:</td><td>414</td></tr>, <tr><td>Female:</td><td>163</td></tr>, <tr>
    4. <th>Place</th>
    5. <th>Bib</th>
    6. <th>Name</th>
    7. <th>Gender</th>
    8. <th>City</th>
    9. <th>State</th>
    10. <th>Chip Time</th>
    11. <th>Chip Pace</th>
    12. <th>Gender Place</th>
    13. <th>Age Group</th>
    14. <th>Age Group Place</th>
    15. <th>Time to Start</th>
    16. <th>Gun Time</th>
    17. <th>Team</th>
    18. </tr>, <tr>
    19. <td>1</td>
    20. <td>814</td>
    21. <td>JARED WILSON</td>
    22. <td>M</td>
    23. <td>TIGARD</td>
    24. <td>OR</td>
    25. <td>00:36:21</td>
    26. <td>05:51</td>
    27. <td>1 of 414</td>
    28. <td>M 36-45</td>
    29. <td>1 of 152</td>
    30. <td>00:00:03</td>
    31. <td>00:36:24</td>
    32. <td></td>
    33. </tr>, <tr>
    34. <td>2</td>
    35. <td>573</td>
    36. <td>NATHAN A SUSTERSIC</td>
    37. <td>M</td>
    38. <td>PORTLAND</td>
    39. <td>OR</td>
    40. <td>00:36:42</td>
    41. <td>05:55</td>
    42. <td>2 of 414</td>
    43. <td>M 26-35</td>
    44. <td>1 of 154</td>
    45. <td>00:00:03</td>
    46. <td>00:36:45</td>
    47. <td>INTEL TEAM F</td>
    48. </tr>, <tr>
    49. <td>3</td>
    50. <td>687</td>
    51. <td>FRANCISCO MAYA</td>
    52. <td>M</td>
    53. <td>PORTLAND</td>
    54. <td>OR</td>
    55. <td>00:37:44</td>
    56. <td>06:05</td>
    57. <td>3 of 414</td>
    58. <td>M 46-55</td>
    59. <td>1 of 64</td>
    60. <td>00:00:04</td>
    61. <td>00:37:48</td>
    62. <td></td>
    63. </tr>, <tr>
    64. <td>4</td>
    65. <td>623</td>
    66. <td>PAUL MORROW</td>
    67. <td>M</td>
    68. <td>BEAVERTON</td>
    69. <td>OR</td>
    70. <td>00:38:34</td>
    71. <td>06:13</td>
    72. <td>4 of 414</td>
    73. <td>M 36-45</td>
    74. <td>2 of 152</td>
    75. <td>00:00:03</td>
    76. <td>00:38:37</td>
    77. <td></td>
    78. </tr>, <tr>
    79. <td>5</td>
    80. <td>569</td>
    81. <td>DEREK G OSBORNE</td>
    82. <td>M</td>
    83. <td>HILLSBORO</td>
    84. <td>OR</td>
    85. <td>00:39:21</td>
    86. <td>06:20</td>
    87. <td>5 of 414</td>
    88. <td>M 26-35</td>
    89. <td>2 of 154</td>
    90. <td>00:00:03</td>
    91. <td>00:39:24</td>
    92. <td>INTEL TEAM F</td>
    93. </tr>, <tr>
    94. <td>6</td>
    95. <td>642</td>
    96. <td>JONATHON TRAN</td>
    97. <td>M</td>
    98. <td>PORTLAND</td>
    99. <td>OR</td>
    100. <td>00:39:49</td>
    101. <td>06:25</td>
    102. <td>6 of 414</td>
    103. <td>M 18-25</td>
    104. <td>1 of 34</td>
    105. <td>00:00:06</td>
    106. <td>00:39:55</td>
    107. <td></td>
    108. </tr>][/size]
    复制代码
    本教程的目标是从网页中取出一张表格,并将其转换为dataframe,以便更容易地使用Python进行操作。要做到这一点,您应该首先获得list表单中的所有表行,然后将该列表转换为dataframe。下面是一个for循环,它遍历表行并输出行单元格。
    1. [size=2]for row in rows:
    2.     row_td = row.find_all('td')print(row_td)type(row_td)

    3. [<td>14TH</td>, <td>INTEL TEAM M</td>, <td>04:43:23</td>, <td>00:58:59 - DANIELLE CASILLAS</td>, <td>01:02:06 - RAMYA MERUVA</td>, <td>01:17:06 - PALLAVI J SHINDE</td>, <td>01:25:11 - NALINI MURARI</td>]


    4. bs4.element.ResultSet[/size]
    复制代码
    上面的输出显示每一行都是用嵌入在每一行中的html标记打印出来的。这不是我们想要的。可以使用Beautiful Soup或正则表达式删除html标记。
    1. [size=2]str_cells = str(row_td)
    2. cleantext = BeautifulSoup(str_cells, "lxml").get_text()
    3. print(cleantext)

    4. [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]:正则表达式才是高手风范)。 [/size]
    复制代码
    我们导入re(用于正则表达式)模块,下面的代码展示了如何构建一个正则表达式,该表达式查找< td > html标记中的所有字符,并为每个表行替换为空字符串。

    首先,通过传递与re.compile()匹配的字符串来编译正则表达式。点、星号和问号(.*?) 将匹配一个开头的尖括号,后面跟着任何东西,后面跟着一个结尾的尖括号。它以非贪婪的方式匹配文本,也就是说,它匹配尽可能短的字符串。

    如果省略问号,它将匹配第一个开始尖括号和最后一个结束尖括号之间的所有文本。
    编译正则表达式后,可以使用re.sub()方法查找正则表达式匹配的所有子字符串,并用空字符串替换它们。

    下面的完整代码生成一个空列表,提取每一行html标记之间的文本,并将其附加到指定的列表中。
    1. [size=2]import re
    2. list_rows = []
    3.     for row in rows:     
    4.     cells = row.find_all('td')     
    5.     str_cells = str(cells)     
    6.     clean = re.compile('<.*?>')     
    7.     clean2 = (re.sub(clean, '',str_cells))   
    8.     list_rows.append(clean2)
    9.     print(clean2)
    10.     type(clean2)
    11. [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]

    12. str[/size]
    复制代码
    下一步是将列表转换为dataframe并使用panda快速查看前10行。
    1. [size=2]df = pd.DataFrame(list_rows) df.head(10)
    2. 0
    3. 0        [Finishers:, 577]
    4. 1        [Male:, 414]
    5. 2        [Female:, 163]
    6. 3        []
    7. 4        [1, 814, JARED WILSON, M, TIGARD, OR, 00:36:21...
    8. 5        [2, 573, NATHAN A SUSTERSIC, M, PORTLAND, OR, ...
    9. 6        [3, 687, FRANCISCO MAYA, M, PORTLAND, OR, 00:3...
    10. 7        [4, 623, PAUL MORROW, M, BEAVERTON, OR, 00:38:...
    11. 8        [5, 569, DEREK G OSBORNE, M, HILLSBORO, OR, 00...
    12. 9        [6, 642, JONATHON TRAN, M, PORTLAND, OR, 00:39...[/size]
    复制代码
      数据处理和清理dataframe不是我们想要的格式。为了清理它,您应该在逗号位置将“0”列分割为多个列,通常我们使用str.split()方法实现的。
    1. <font size="2">df1 = df[0].str.split(',', expand=True)
    2. df1.head(10)</font>
    复制代码
    2.jpg



    这看起来清爽多了,但还有工作要做。dataframe的每行周围都有不需要的方括号。可以使用strip()方法删除列“0”上的左方括号。
    1. <font size="2">df1[0] = df1[0].str.strip('[')
    2. df1.head(10)</font>
    复制代码
    3.jpg


    上表缺少表标头。您可以使用find_all()方法获得表标头。
    1. <font size="2">col_labels = soup.find_all('th')</font>
    复制代码
    与处理表类似,您可以使用Beautiful Soup提取表标题的html标记之间的文本。
    1. <font size="2">all_header = []
    2. col_str = str(col_labels)
    3. cleantext2 = BeautifulSoup(col_str, "lxml").get_text()
    4. all_header.append(cleantext2)
    5. print(all_header)

    6. ['[Place, Bib, Name, Gender, City, State, Chip Time, Chip Pace, Gender Place, Age Group, Age Group Place, Time to Start, Gun Time, Team]']</font>
    复制代码
    然后,您可以将标题列表转换为pandas dataframe。
    1. <font size="2">df2 = pd.DataFrame(all_header) df2.head()

    2. 0
    3. 0        [Place, Bib, Name, Gender, City, State, Chip T...</font>
    复制代码
    类似地,您可以在所有行的逗号位置将“0”列分割为多个列。
    1. <font size="2">df3 = df2[0].str.split(',', expand=True)
    2. df3.head()</font>
    复制代码
    4.jpg

    1. <font size="2">frames = [df3, df1]
    2. df4 = pd.concat(frames)
    3. df4.head(10)</font>
    复制代码
    5.jpg
    下面显示了如何将第一行分配为表标头。
    1. <font size="2">df5 = df4.rename(columns=df4.iloc[0])
    2. df5.head()</font>
    复制代码
    6.jpg
    至此,这个表的格式几乎完全正确。对于分析,您可以从以下数据的概述开始。
    1. <font size="2">df5.info()df5.shape</font>
    复制代码
    1. <font size="2"><class 'pandas.core.frame.DataFrame'>
    2. Int64Index: 597 entries, 0 to 595
    3. Data columns (total 14 columns):
    4. [Place              597 non-null object
    5. Bib                596 non-null object
    6. Name               593 non-null object
    7. Gender             593 non-null object
    8. City               593 non-null object
    9. State              593 non-null object
    10. Chip Time          593 non-null object
    11. Chip Pace          578 non-null object
    12. Gender Place       578 non-null object
    13. Age Group          578 non-null object
    14. Age Group Place    578 non-null object
    15. Time to Start      578 non-null object
    16. Gun Time           578 non-null object
    17. Team]              578 non-null object
    18. dtypes: object(14)
    19. memory usage: 70.0+ KB





    20. (597, 14)</font>
    复制代码
    该表有597行和14列。您可以删除所有缺少值的行。
    1. <font size="2">df6 = df5.dropna(axis=0, how='any')</font>
    复制代码
    另外,请注意如何将表头复制为df5中的第一行。 可以使用以下代码行删除它。
    1. <font size="2">df7 = df6.drop(df6.index[0]) df7.head()</font>
    复制代码
    7.jpg
    您可以通过重新命名'[Place' and ' Team]'列来执行更多的数据清理。Python对空格非常挑剔。确保在“Team]”中在引号之后加上空格。
    1. <font size="2">df7.rename(columns={'[Place': 'Place'},inplace=True)
    2. df7.rename(columns={' Team]': 'Team'},inplace=True)
    3. df7.head()</font>
    复制代码
    8.jpg
    最后的数据清理步骤包括删除“Team”列中的单元格的右括号。
    1. <font size="2">df7['Team'] = df7['Team'].str.strip(']')
    2. df7.head()</font>
    复制代码
    9.jpg
    到这里为止我们花了一段很长时间,也得到了我们想要的dataframe。现在您可以进入令人兴奋的部分,开始绘制数据并计算有趣的统计数据。

      数据分析和可视化首先要回答的问题是,跑步者的平均完成时间(以分钟为单位)是多少?您需要将列“Chip Time”转换为几分钟形式。一种方法是首先将列转换为列表进行操作。
    1. <font size="2">time_list = df7[' Chip Time'].tolist()# You can use a for loop to convert 'Chip Time' to minutestime_mins = []for i in time_list:    h, m, s = i.split(':')
    2.     math = (int(h) * 3600 + int(m) * 60 + int(s))/60    time_mins.append(math)#print(time_mins)</font>
    复制代码
    下一步是将列表转换回dataframe,并立即为跑步者  Chip Time 创建一个新的列(“Runner_mins”)。
    1. <font size="2">df7['Runner_mins'] = time_mins
    2. df7.head()</font>
    复制代码
    10.jpg


    下面的代码显示了,在dataframe中计算数字列的统计信息。
    1. <font size="2">df7.describe(include=[np.number])

    2. Runner_mins
    3. count        577.000000
    4. mean        60.035933
    5. std        11.970623
    6. min        36.350000
    7. 25%        51.000000
    8. 50%        59.016667
    9. 75%        67.266667
    10. max        101.300000</font>
    复制代码
    有趣的是,所有跑步者的平均chip time 是大约60分钟。最快的10K跑者跑完36.35分钟,最慢的跑者跑完101.30分钟。
    boxplot是另一个有用的工具,用于可视化汇总统计信息(最大值、最小值、中等值、第一四分位数、第三四分位数,包括异常值)。下面是在箱线图中显示的跑步者的数据汇总统计数据。为了实现数据可视化,可以方便地首先从matplotlib附带的pylab模块导入参数,并为所有图形设置相同的大小,以避免为每个图形设置相同的大小。
    1. <font size="2">from pylab import rcParams
    2. rcParams['figure.figsize'] = 15, 5
    3. df7.boxplot(column='Runner_mins')
    4. plt.grid(True, axis='y')
    5. plt.ylabel('Chip Time')
    6. plt.xticks([1], ['Runners'])

    7. ([<matplotlib.axis.XTick at 0x570dd106d8>],  
    8. <a list of 1 Text xticklabel objects>)</font>
    复制代码
    11.jpg

    要回答的第二个问题是:跑者的完成时间是否服从正态分布?
    下面是使用seaborn库绘制的跑步者chip times分布图。分布看起来几乎是正常的。
    1. <font size="2">x = df7['Runner_mins']
    2. ax = sns.distplot(x, hist=True, kde=True, rug=False, color='m', bins=25, hist_kws={'edgecolor':'black'})
    3. plt.show()</font>
    复制代码
    12.jpg

    第三个问题是关于不同年龄段的男性和女性是否有表现上的差异。
    下面是男性和女性芯片时间的分布图。
    1. <font size="2">f_fuko = df7.loc[df7[' Gender']==' F']['Runner_mins']
    2. m_fuko = df7.loc[df7[' Gender']==' M']['Runner_mins']
    3. sns.distplot(f_fuko, hist=True, kde=True, rug=False, hist_kws={'edgecolor':'black'}, label='Female')
    4. sns.distplot(m_fuko, hist=False, kde=True, rug=False, hist_kws={'edgecolor':'black'}, label='Male')
    5. plt.legend()

    6. <matplotlib.legend.Legend at 0x570e301fd0></font>
    复制代码
    13.jpg
    这一分布表明女性的平均速度比男性慢。您可以使用groupby()方法分别计算男性和女性的汇总统计信息,如下所示。
    1. <font size="2">g_stats = df7.groupby(" Gender", as_index=True).describe()print(g_stats)

    2.         Runner_mins                                                         \
    3.               count       mean        std        min        25%        50%   
    4. Gender                                                                     
    5. F            163.0  66.119223  12.184440  43.766667  58.758333  64.616667   
    6. M            414.0  57.640821  11.011857  36.350000  49.395833  55.791667   


    7.                75%         max  
    8. Gender                        
    9. F       72.058333  101.300000  
    10. M       64.804167   98.516667</font>
    复制代码
    所有女性和男性的平均芯片时间分别为约66分钟和约58分钟。
    那么下面是男性和女性完成时间的并排箱线图比较。
    1. <font size="2">df7.boxplot(column='Runner_mins', by=' Gender')plt.ylabel('Chip Time')plt.suptitle("")

    2. C:\Users\smasango\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\fromnumeric.py:57: FutureWarning: reshape is deprecated and will raise in a subsequent release. Please use .values.reshape(...) instead
    3.   return getattr(obj, method)(*args, **kwds)Text(0.5,0.98,'')</font>
    复制代码
    14.jpg

    结论
    在本教程中,您使用Python执行了Web抓取。 您使用Beautiful Soup库来解析html数据并将其转换为可用于分析的表单。 您在Python中执行了数据清理并创建了有用的图表(箱形图,条形图和分布图),用Python的matplotlib和seaborn库来显示有趣的趋势。 在本教程之后,您应该能够使用Python轻松地从Web抓取数据,应用清理技术并从数据中提取有用的见解。




    回复

    使用道具 举报

    主题

    好友

    758

    积分

    举人

  • TA的每日心情
    开心
    2017-12-29 15:29
  • 签到天数: 2 天

    连续签到: 1 天

    [LV.1]初来乍到

    发表于 6 天前 |显示全部楼层
    Python是个好东西,正在自学中
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    关闭

    站长推荐上一条 /4 下一条

    手机版|爱板网

    GMT+8, 2018-10-16 12:29 , Processed in 0.280938 second(s), 18 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-5   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001056号

    Powered by Discuz!

    返回顶部