第3章〓网页信息采集和交互式可视化到目前为止,本书专注于使用Jupyter构建可重现的数据分析管道和预测模型。我们将在本章中继续探讨这些主题,但本章主要关注的是数据采集。本章将展示如何使用HTTP请求从网页中获取数据,这些内容涉及通过请求和解析HTML采集网页信息。本章将使用交互式可视化技术研究采集到的数据。
网络在线数据非常庞大且相对容易获取,并且其也在不断成长和发展中,网络在线数据变得越来越重要。网络在线数据持续增长的部分原因是全球性的报纸、杂志和电视等传统信息向网络在线内容的持续转变。如今,人们通过手机、实时在线新闻来源(如Facebook、Reddit、Twitter和YouTube等)获取个性化定制的新闻是可行的,可以想象,经过这种历史转变后的网络在线数据会越来越多。令人惊讶的是,这仅仅是网络中大量增量数据中的一部分。
随着全球转向使用HTTP服务(博客、新闻网站、Netflix等),我们会有更多的机会使用数据驱动分析。例如,Netflix会查看用户观看的电影并预测他们喜欢什么,而此预测将会决定为用户显示的推荐电影。但是,本章不会关注面向业务的数据;相反,我们将会看到如何利用客户端将互联网中的数据作为数据库。在以前,我们从未能如此容易地访问这种数量庞大且类型丰富的数据。本章将使用网络采集技术收集网络数据,然后在Jupyter中使用交互式可视化技术探索这些数据。
交互式可视化是一种可视化的数据表示形式,可以帮助使用者使用图形或图表理解数据,还可以帮助开发人员或分析人员以简单的形式呈现数据,同时也让非技术人员可以更容易地理解数据。
本章结束时,您将能够: 
 分析HTTP请求的工作方式;
 从网页中采集表格数据(tabular data);
 构建和转换Pandas Data Frame;
 创建交互式可视化。3.1采集网页信息
本着将互联网作为数据库的初衷,可以通过采集网页的内容或使用网页API译者注: Web API是网络应用程序接口,包含许多功能,通过API接口,网络应用可以具备存储服务、消息服务、计算服务等能力。在信息采集中,往往可以通过调用API直接获取数据。接口从网页中获取数据。一般来说,采集网页中的内容意味着能够得到以人类可读格式呈现的数据,而网页API接口以机器可读的格式传递数据,最常见的是JSON格式的数据。
本章将重点讨论网页信息的采集,采集的确切过程取决于要采集的页面和所需采集的内容,但是只要理解了最底层的概念和工具,就可以很容易地从HTML页面中获取需要的信息了。本节将使用Wikipedia作为示例,并从其中一篇文章中提取表格内容;然后将使用类似的方法从完全独立的领域中提取数据。首先,本章要花费一些时间介绍HTTP请求。
3.1.1HTTP请求简介
HTTP的全称是Hypertext Transfer Protocol译者注: 超文本传输协议。,是互联网数据通信的基础,它定义了页面如何被获取以及响应请求的方式。例如,客户可以请求Amazon出售笔记本电脑的页面,通过Google搜索当地餐厅或者浏览Facebook。除了URL之外,request请求还在request header译者注: 请求头(request header)包含许多相关的客户端环境和请求正文的有用信息,例如可以声明浏览器所用的语言、请求正文的长度等。中包含用户代理(user agent)和可用的浏览cookie。用户代理会告诉服务器用户正在使用哪种浏览器和设备,通常用于提供对用户最友好的网页相应版本的响应。如果用户最近登录过该网页,那么此类信息将被存储在一个cookie中,以便用户再次访问该网页时实现自动登录。
可以通过Web浏览器了解HTTP请求和响应的详细信息。幸运的是,在使用Python等高级语言进行请求时也是如此。出于许多原因,request请求头(request header)的内容基本上可以忽略。
除非有特别的说明,否则在请求URL时会自动在Python中生成这些内容。但是,为了排除故障和理解针对用户请求而产生的响应,对HTTP有基本的了解是很重要的。
HTTP请求的方法有许多类型,例如GET、HEAD、POST和PUT,前两个用于请求将数据从服务器发送到客户端,后两个用于从客户端将数据发送到服务器。
HTTP方法的总结如表31所示。表31HTTP方法HTTP请求方式相 关 描 述GET从指定的URL中获取信息HEAD从指定URL的HTTP请求头中获取元信息译者注: HTML头元素包含关于文档的概要信息,也称元信息(metainformation)。meta意为“关于某方面的信息”,是关于数据的信息,而元信息是关于信息的信息。引自: https: //baike.baidu.com/item/HTM%E5%A4%B4%E5%85%83%E7%B4%A0/170089。POST发送附加信息,添加信息到指定URL的资源PUT发送附加信息,替换信息到指定URL的资源每当在浏览器中输入网页的URL并按Enter键时,都会向服务器发送GET请求。对于网页信息的获取,这通常是我们最感兴趣的HTTP方法,也是本章使用的唯一方法。
一旦请求被发送,服务器就会返回各种响应数据,这些响应数据被100~500的数字代码标记,代码中的第一个数字表示响应类别,这些标记的描述如下。
 1xx: 信息响应,例如服务器正在处理一个请求,这种情况很少见。 
 2xx: 成功响应,例如成功加载页面。
 3xx: 重定位响应,例如请求的资源已被移动,我们被重定向到一个新的URL。
 4xx: 返回客户端的错误响应,例如请求的资源不存在。
 5xx: 返回服务器的错误响应,例如网站服务器接收了太多的流量,无法满足请求。
对于网页采集,通常只关心response响应类,即响应代码的第一个数字。但是,每个类中都有相应的子类别,它们可以提供更详细的内容。例如,401代码表示未授权(unauthorized)响应,404代码表示未找到页面(page not found)响应。
这个区别值得注意,因为404表示请求的页面不存在,而401表示需要登录后才能查看特定资源。
下面看看如何在Python中完成HTTP请求,并使用Jupyter Notebook实现相应的内容。
3.1.2在Jupyter Notebook中实现HTTP请求
3.1.1节讨论了HTTP请求是如何工作的,以及需要哪种类型的响应,现在看看如何在Python中实现HTTP请求。这里将使用一个名为Requests的库译者注: Requests是Python实现的简单易用的HTTP库,使用前需要使用pip等工具完成安装。,它是Python中下载量最多的外部库。可以使用Python的内置工具(如urllib)进行HTTP请求,但是使用Requests要直观得多,而且Python官方文档也建议使用Request。
Request是一个简单高效的网页请求方法,它允许对header、cookie和authorization译者注: authorization(用户凭证)为Web应用程序配置授权,根据用户提供的身份凭证生成权限实体,并为之授予相应的权限。进行各种自定义设置,还能跟踪重定向(redirect),并提供返回特定内容(如JSON)的方法。此外,Request还有一系列便捷高效的功能,但是它不能呈现JavaScript的渲染。
下面将在Jupyter Notebook中使用Python的Request库进行相应的处理。通常,服务器返回包含JavaScript代码片段的HTML,这些代码片段在加载时会自动在浏览器中运行。当使用Python的Requests模块进行请求时,相应的JavaScript代码是可见的,但它不会运行。因此,通过运行此JavaScript代码而出现的页面元素将会丢失。通常这不会影响获取所需信息,但在某些情况下,可能需要获取运行相应JavaScript后的页面元素以正确获取所需的页面信息。为了解决这个问题,可以使用Selenium库,它具有与Requests库类似的API,但支持使用Web驱动程序和呈现JavaScript渲染后的页面效果。在Jupyter Notebook中使用Python处理HTTP请求的方法如下。
(1) 运行Jupyter Notebook,在项目目录中启动Notebook的App。找到chapter 3目录并打开chapter3workbook.ipynb文件。运行靠近顶部用来加载各种包的单元格。
我们将请求一个网页,然后查看响应对象。Python有很多用来发送request请求的库,而且每个库也明确地说明了使用方法,我们可以根据相关文档说明进行多种选择。但是在这里,我们只使用Requests库,因为它提供了详细的说明文档、高级的特性和简单的API。
(2) 向下滚动到副标题为Subtopic A: Introduction to HTTP requests的部分,在该部分运行第一个单元格以导入Requests库,然后运行包含以下代码的单元格,准备实现一个HTTP请求。  url = 'https://jupyter.org/'
  req = requests.Request('GET', url)
  req.headers['User-Agent'] = 'Mozilla/5.0'
  req = req.prepare()使用Request class类库准备一个请求jupyter.org主页的GET请求。将用户代理指定为Mozilla/5.0,这样将会得到一个适合桌面标准浏览器的响应。最后,准备发出request请求。
(3) 通过运行包含req?的单元格打印文档字符串Prepared Request,如图31所示。
图31PreparedRequest文档
通过查看其用法,可以看到如何使用session发送请求,类似于打开Web浏览器启动一个session,然后请求URL。
(4) 通过运行以下代码发出request请求,并将响应结果存储在名为page的变量中。  with requests.Session() as sess:
  page = sess.send(req)这段代码返回由page变量存储的HTTP响应。通过使用with语句初始化了一个session,其作用域仅限于缩进的代码块,这意味着我们不必刻意关注是否关闭了session,因为它是自动关闭的。
(5) 在Notebook中运行下面两个单元格以查看响应结果。页面的字符串表示响应状态码为200时的页面响应结果,这与status_code的属性是一致的。
(6) 将响应的文本保存到page_html变量中,并使用page_html[: 1000]查看结果列表的前1000个元素,如图32所示。
图32响应结果
正如我们所料,响应结果是HTML语句,可以在BeautifulSoup的帮助下以易于理解的形式格式化这个结果,这个BeautifulSoup库将在本节后面广泛用于解析HTML页面。
(7) 运行以下单元格,打印处理后的HTML结果。  from bs4 import BeautifulSoup
  print(BeautifulSoup(page_html, 'html.parser').prettify()[:1000])首先导入BeautifulSoup库,然后使用其prettify()函数将结果标准化并打印,其中换行符会根据HTML结构中的层次结构自动缩进。
(8) 通过使用IPython display模块,我们可以在Jupyter中进一步显示HTML的内容,运行以下代码,结果如图33所示。from IPython.display import HTML
HTML(page_html)图33IPython模块处理后的HTML
由于没有JavaScript的渲染和加载外部资源,我们只看到了当前的HTML呈现的页面内容,例如jupyter.org服务器上的图像没有呈现渲染,只能看到circle of programming icons、jupyter logo等。
(9) 与在线加载的网站进行比较,后者可以通过使用IFrame在Jupyter中打开。运行以下代码,结果如图34所示。from IPython.display import IFrame
IFrame(src=url, height=800, width=800)在这里,我们看到了经过JavaScript渲染和加载外部资源之后呈现的页面,甚至可以像浏览普通的请求网页一样单击超链接并将这些页面加载到IFrame中。
(10) 在使用后关闭IFrame是一个很好的做法,这样做可以防止IFrame占用内存和处理器。可以通过选择单元格或从Jupyter Notebook的Cell单元格菜单中单击Current Output | Clear关闭IFrame。
图34IFrame加载后的页面
回想一下我们是如何在Python中使用一个准备好的request和session以字符串的形式请求网页信息的,这通常可以使用shorthand法代替,这个方法的缺点是没有尽可能多的自定义请求头,但这通常也是可以接受的。
(11) 通过运行以下代码向http://www.python.org/发出请求。  url = 'http://www.python.org/'
  page = requests.get(url)
  page
  <Response [200]>页面的字符串显示(如单元格下方所示)为200的状态码,表示响应成功。
(12) 运行下面两个单元格,打印页面的url和history属性。
可以看到,返回的URL不是我们输入的,看到区别了吗?我们输入的URL http://www.python.org/被重定向到了该页面的安全版本https://www.python.org/。那么它们的区别是什么呢?在协议中,URL的开头有一个附加的s,任何重定向都存储在history属性中。本例中有一个状态码为301(永久重定向)的页面,它与请求的原始URL对应。
现在,我们已经学会了发出HTTP请求的方法,接下来把注意力转向解析HTML,通常有多种方法可以实现HTML的解析,而好的方法通常取决于特定HTML的细节。
3.1.3在Jupyter Notebook中解析HTML
当采集网页页面数据时,在发出请求之后,必须从响应内容中抽取数据。如果内容是HTML,那么最简单的方法就是使用高级解析库,如BeautifulSoup,这并不是唯一的方法。原则上,可以使用正则表达式或Python处理字符串的方法(如split)挑选数据。但是,这两种方法都效率低下,并且很容易导致错误,因此通常不使用这两个方法。建议使用可靠的解析工具对HTML页面进行解析。
为了理解如何从HTML中提取内容,了解HTML的基本知识非常重要。首先,HTML代表超文本标记语言(Hyper Text Markup Language),和Markdown或XML(可扩展标记语言,eXtensible Markup Language)一样,它只是一种用于标记文本的语言。
在HTML中,显示文本包含在HTML元素的内容部分(content section),其中元素的属性指定了该元素在页面上的显示方式,HTML的结构图如图35所示。
图35HTML元素的结构成分
可以看到包含在开始标签(start tag)和结束标签(end tag)之间的内容。在本例中,段落的标签为<p>,其他常见的标签类型有<div>(文本块)、<table>(数据表)、<h1>(标题)、<img>(图像)和<a>(超链接)。标签具有属性,这些属性可以保存重要的元数据译者注: 元数据(metadata)又称中介数据、中继数据,是指描述数据的数据(data about data),主要描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。引自: https: //baike.baidu.com/item/%E5%85%83%E6%95%B0%E6%8D%AE?fromtitle=metadata&fromid=8567615。。最常见的是,一些元数据用于指定元素文本应该如何显示在页面上,这就是CSS译者注: CSS即层叠样式表(Cascading Style Sheets),用来表现HTML或XML等文件样式。文件发挥作用的地方。这些属性还可以存储其他有用的信息,例如<a>标签中的超链接href指定了一个URL链接,或者<img>标签中的具有可替代属性的alt,alt指定了在无法加载图像资源时显示的文本。
现在,让我们回到Jupyter Notebook中并解析一些HTML。虽然本节讲解的HTML基础知识不是必要的,但是在Chrome或Firefox中使用开发人员工具找出所需的HTML元素是非常有用的。下面介绍如何在Jupyter Notebook中使用Python解析HTML。
在Jupyter Notebook中使用Python解析HTML的方法如下。
(1) 在提供的chapter3workbook.ipynb文件中滚动到标题为Subtopic B: Parsing HTML with Python的部分。
这次将采集由维基百科报道的每个国家的央行利率。在深入研究代码之前,让我们先打开包含此数据的Web页面。
(2) 在Web浏览器中打开链接https: //en.wikipedia.org/wiki/list_of_countries_by_central_bank_interest _rates。如果可能,请尽量使用Chrome浏览器进行访问。本节的末尾将展示如何使用Chrome的开发工具查看和搜索HTML。
观察这个页面,能看到的内容非常少,只有许多国家和其央行利率,这就是我们要采集的数据。 
(3) 返回Jupyter Notebook,将HTML作为对象加载到BeautifulSoup中以便于解析,请运行以下代码。from bs4 import BeautifulSoup
soup = BeautifulSoup(page.content, 'html.parser')使用Python默认的html.parser作为解析器,如果需要,也可以使用第三方解析器,如lxml。
通常,当处理这样的BeautifulSoup新对象时,通过执行soup?导出文档字符串是一个不错的想法。但是在这种情况下,文档字符串并不是特别有用。另一个工具是pdir,它列出了对象的所有属性和方法(可以通过pip install pdir2安装),它是Python内置的dir函数的格式化版本。
(4) 通过运行代码段31中的代码显示BeautifulSoup对象的属性和方法。无论pdir外部库是否安装,该命令都会运行。  try:
  import pdir
  dir = pdir
  except:
  print('You can install pdir with:\\npip install pdir2')
  dir(soup)代码段31显示相关属性在这里,可以看到能够在soup上调用的方法和属性列表。最常用的函数是find_all,它可以返回与给定条件匹配的元素列表。
(5) 通过运行以下代码获取页面的h1标题。h1 = soup.find_all('h1')
h1
>> [<h1 class="firstHeading" id="firstHeading" lang="en">
List of countries by central bank interest rates</h1>]通常情况下,页面中只有一个H1元素,所以我们在这里只找到了一个。
(6) 运行下面几个单元格,首先执行代码h1 = h1[0],将H1重新定义为列表的一个(也是唯一的)元素,然后执行代码h1.attrs打印h1的HTML元素属性。>> {'class': ['firstHeading'], 'id': 'firstHeading', 'lang': 'en'}我们看到了这个元素的类(class)和ID,它们都可以被CSS代码引用以定义这个元素的样式。
(7) 通过打印输出h1.text获取HTML元素内容(即可见文本)。
(8) 通过运行以下代码获取页面上的所有图片。  imgs = soup.find_all('img')
  len(imgs)
  >> 91此页面上有很多图片,其中大部分是国旗。
(9) 通过运行以下代码输出每张图片的源地址,结果如图36所示。[element.attrs['src'] for element in imgs
if 'src' in element.attrs.keys()]图36图片来源地址列表
使用列表生成式译者注: 列表生成式(List Comprehensions)是Python内置的非常简单强大的可以用来创建list的生成式。循环遍历每一个元素,并选择每个元素的src属性(只要该属性可用即可),以生成关于图片链接的列表。
现在采集表格数据,使用Chrome的开发工具查找其中包含的元素。
(10) 如果尚未完成以上内容,请打开在Chrome中查看的Wikipedia页面。然后在浏览器的View(视图)菜单中选择Developer Tools(开发者工具)选项。可以在侧边栏的Developer Tools(开发者工具)选项的Elements选项卡中查看HTML。
(11) 单击工具侧栏左上角的小箭头,如图37所示,该功能可以将鼠标悬停在页面上,并查看HTML元素在侧栏的Elements(元素)部分中的位置。
图37工具栏箭头选择
(12) 将鼠标悬停在页面上,如图38所示,查看表格是如何包含在id="bodyContent"的div中的。
图38表格元素结构
(13) 通过运行以下代码选择该div标签。body_content = soup.find('div', {'id': 'bodyContent'})我们现在可以在完整HTML的子集中查找表。通常情况下,表格包含headers标题<th>、rows行<tr>、data entries数据条目<td>。
(14) 通过运行代码段32中的代码获取table headers(表头)。table_headers = body_content.find_all('th')[:3]
table_headers
>>> [<th>Country or<br/>
currency union</th>, <th>Central bank<br/>
interest rate (%)</th>, <th>Date of last<br/>
change</th>]代码段32获取表头属性这里可以看到3个表头。在每个元素的内容中,都有一个中断元素<br/>,这会增加文本清晰解析的难度。
(15) 通过运行代码段33中的代码获取文本。  table_headers = [element.get_text().replace('\\n', ' ')
  for element in table_headers]
  table_headers
  >> ['Country or currency union',
  'Central bank interest rate (%)',
  'Date of last change']代码段33获取文本属性使用get_text方法获取内容,然后运行replace译者注: replace是Python中的函数,可执行字符替换操作。 字符串方法以删除由<br/>元素生成的换行。为了获得数据,首先进行一些测试,然后将所有数据收集到一个单元格中。
(16) 通过运行以下代码获取第2个<tr> (行)元素中每个单元格的数据。row_number = 2
d1, d2, d3 =body_content.find_all('tr')[row_number]\\.find_all('td')找到所有的行数据,找出第3个,然后找到其中的3个数据。
查看结果数据,并观察如何解析每行的文本。
(17) 运行下面几个单元格并打印d1及其text属性,结果如图39所示。
图39显示d1及其文本可以看到,结果中有很多我们不想要的标签及其内容,可以通过仅搜索<a>标签的文本解决这个问题。
(18) 运行d1.find('a').text返回该单元格内正确处理的数据文本。
(19) 运行下面两个单元格以打印d2及其文本。从输出结果中可以看到,d2的文本内容非常标准,甚至不用对它进行数据处理。
(20) 运行下面两个单元格以打印d3及其文本,如图310所示。
图310显示d3及其文本
与d1类似,可以只获取span标签中的文本。
(21) 通过运行以下代码正确地解析这个表格的日期。  d3.find_all('span')[0].text
  >> '30 June 2016'(22) 下面通过循环语句迭代行标签<th>采集全部数据。运行代码段34中的代码。data = []
for i, row in enumerate(body_content.find_all('tr')):
...
...
>> Ignoring row 101 because len(data) != 3
> > Ignoring row 102 because len(data) != 3代码段34通过循环迭代采集数据对行标签进行迭代,忽略包含3个以上数据的行,这些数据与我们所需的表中的数据不对应。如果表中有3个数据的行,则解析在测试期中标记的行。
文本解析是在try/except语句块中完成的,该语句块用于捕获异常,若发现异常,则允许跳过这一行且不停止迭代,我们应该及时查看由该语句引起错误的任何行,这些数据可以手工记录,也可以通过更改循环并重新运行记录。本例为了节省时间,就不对错误信息进行处理了。
(23) 通过执行print(data[: 10])语句打印所采集数据的前10个元素。  >> [['Albania', 1.25, '4 May 2016'],
  ['Angola', 16.0, '30 June 2016'],
  ['Argentina', 26.25, '11 April 2017'],
  ['Armenia', 6.0, '14 February 2017'],
  ['Australia', 1.5, '2 August 2016'],
  ['Azerbaijan', 15.0, '9 September 2016'],
  ['Bahamas', 4.0, '22 December 2016'],
  ['Bahrain', 1.5, '14 June 2017'],
  ['Bangladesh', 6.75, '14 January 2016'],
  ['Belarus', 12.0, '28 June 2017']](24) 我们将在本章后面可视化这些数据。现在,运行代码段35中的代码,将数据保存到CSV文件中。f_path = '../data/countries/interest-rates.csv'
with open(f_path, 'w') as f:
f.write('{};{};{}\\n'.format(table_headers))
for d in data:
f.write('{};{};{}\\n'.format(d))代码段35数据的持久化操作这里需要注意的是,需要使用分号分隔字段数据。
3.1.4实践: 在Jupyter Notebook中实现网页信息采集
通过上面的操作,我们将得到每个国家的人口数据。然后,本节我们将把这些数据与在3.1.3节中收集的利率数据一起进行可视化。
我们在本实践中看到的页面可以在以下网址找到: http: //www.worldometers.info/worldpopulation/populbycountry/。既然我们已经了解了网页信息采集的基本知识,那么下面就将这些技术应用到一个新的网页中,并采集更多的数据。当访问该网址时可能会发现此页面已经和本书所描述的不一样了。如果此URL不再显示国家和地区的人口统计表,请使用以下网址: https://en.wikipedia.org/wiki/List_of_countries_by_population (联合国)。(1) 在这个网页中,可以使用代码段36进行数据采集与处理。data = []
for i, row in enumerate(soup.find_all('tr')):
row_data = row.find_all('td')
try:
d1, d2, d3 = row_data[1], row_data[5], row_data[6]
d1 = d1.find('a').text
d2 = float(d2.text)
d3 = d3.find_all('span')[1].text.replace('+', '')
data.append([d1, d2, d3])
except:
print('Ignoring row {}'.format(i))代码段36完成对相关信息的处理(2) 找到chapter3workbook.ipynb文件中的Activity Web scraping with Python部分。
(3) 设置url变量,运行以下代码,在notebook中加载页面的IFrame。url ='http://www.worldometers.info/world-population/
population-bycountry/'
IFrame(url, height=300, width=800)(4) 通过选择单元格或从Jupyter Notebook的单元格菜单中单击Current output |Clear按钮清除数据,关闭IFrame。
(5) 通过运行以下代码请求页面,并将其加载为BeautifulSoup对象。  page = requests.get(url)
  soup = BeautifulSoup(page.content,'html.parser')将页面内容导入BeautifulSoup中的构造函数。回想一下,之前我们使用的是page.text方法解决此问题。这两种方法的不同之处在于,上述的page.content方法返回原始二进制响应的内容,而page.text方法则返回UTF8编码的内容。通常,最好的做法是传递bytes型对象并让BeautifulSoup对其进行解码,而不是使用Requests的page.text函数进行处理。
(6) 运行以下代码,打印页面的H1标签的内容。soup.find_all('h1')
>> [<h1>Countries in the world by population (2017)</h1>]和3.1.3节的方法一样,通过搜索<th>、<tr>和<td>标签采集表格数据。
(7) 通过运行以下代码获取和打印表格头部信息。  table_headers = soup.find_all('th')
  table_headers
  >> [<th>#</th>,
   <th>Country (or dependency)</th>,
   <th>Population<br/> (2017)</th>,
   <th>Yearly<br/> Change</th>,
   <th>Net<br/> Change</th>,
   <th>Density<br/> (P/Km?)</th>,
   <th>Land Area<br/> (Km?)</th>,
   <th>Migrants<br/> (net)</th>,
   <th>Fert.<br/> Rate</th>,
   <th>Med.<br/> Age</th>,
   <th>Urban<br/> Pop %</th>,
   <th>World<br/> Share</th>](8) 我们只需要前三列的信息。选择这些信息,并通过以下代码完成解析。   table_headers = table_headers[1:4]
   table_headers = [t.text.replace('\\n', '') for t in table_headers]选择需要的表头的子集后,解析每个表头的文本内容,并删除所有换行符。现在,我们将获取数据。和3.1.3节描述的处理方法一样,首先将测试如何解析样本行的数据。
(9) 通过运行以下代码获取样本行的数据。    row_number = 2
   row_data = soup.find_all('tr')[row_number]\\.find_all('td')(10) 我们究竟有多少列数据呢?通过执行print (len(row_data))语句可以打印row_data的长度。
(11) 通过执行print (row_data[: 4])语句打印前四个列表元素。   >> [<td>2</td>,
   <td style="font-weight: bold; font-size:15px; text-align:left"><a
   href="/world-population/india-population/">India</a></td>,
   <td style="font-weight: bold;">1,339,180,127</td>,
   <td>1.13 %</td>]很明显,我们需要选择list下标为1,2,3的元素。第1个数据是一个简单的索引值,可以忽略。
(12) 通过运行以下代码选择需要解析的数据。d1, d2, d3 = row_data[1:4](13) 通过查看row_data的输出,可以知道是否能正确地解析数据。我们希望在第1个数据元素中选择<a>标签中的内容,然后从其他标签中获取文本,可以通过运行以下代码完成测试。   print(d1.find('a').text)
   print(d2.text)
   print(d3.text)
   >> India
   >> 1,339,180,127
   >> 1.13 %很棒!看起来效果不错。现在,我们要开始采集整个表格的数据了。
(14) 通过运行代码段37中的代码采集和解析表格数据。ata = []
for i, row in enumerate(soup.find_all('tr')):
try:
d1, d2, d3 = row.fid_all('td')[1:4]
d1 = d1.fid('a').text
d2 = d2.text
d3 = d3.text
data.append([d1, d2, d3])
except:
print('Error parsing row {}'.format(i))
>> Error parsing row 0代码段37采集和解析表格数据这一步与之前的操作非常相似,首先尝试解析文本,如果有错误,则跳过行。
(15) 通过执行print(data[: 10])语句打印被获取数据的前10个元素。   >> [['China', '1,409,517,397', '0.43 %'],
   ['India', '1,339,180,127', '1.13 %'],
   ['U.S.', '324,459,463', '0.71 %'],
   ['Indonesia', '263,991,379', '1.10 %'],
   ['Brazil', '209,288,278', '0.79 %'],
   ['Pakistan', '197,015,955', '1.97 %'],
   ['Nigeria', '190,886,311', '2.63 %'],
   ['Bangladesh', '164,669,751', '1.05 %'],
   ['Russia', '143,989,754', '0.02 %'],
   ['Mexico', '129,163,276', '1.27 %']]看起来我们已经成功地获取了数据。需要注意的是,这个表的处理过程与Wikipedia表的处理过程非常相似,尽管这个Web页面是完全不同的。当然,数据并不总是包含在一个表中,但是无论如何,通常都使用find_all作为解析网页内容的主要方法。
(16) 最后,运行代码段38中的代码,将数据保存到CSV文件中供以后使用。f_path = '../data/countries/populations.csv'
with open(f_path, 'w') as f:
f.write('{};{};{}\\n'.format(table_headers))
for d in data:
f.write('{};{};{}\\n'.format(d))代码段38数据持久化操作总之,我们已经看到了如何利用Jupyter Notebook采集网页中的数据。本章从学习HTTP方法和状态代码开始讲解,然后介绍了如何使用Python的Request库发出HTTP请求,以及如何使用BeautifulSoup库解析HTML响应。
Jupyter Notebook是用于完成这类工作的一个很好的工具,它能够探索网页Request请求的结果,并尝试各种有关HTML的解析技术,还能够很好地渲染HTML内容,甚至能够在Jupyter Notebook中加载一个实时网页。
3.2节将介绍一个全新的主题——交互可视化,我们将了解如何在Jupyter Notebook中创建和显示交互式图表,并使用这些图表进一步探索我们刚刚收集到的数据。
3.2交互可视化
可视化是从数据集中提取信息的非常实用的方法,例如使用柱状图(bar graph)区分值的分布要比查看表格中的数据直观很多。当然,正如本书前面所写的,它们可以用于研究数据集中的模式(patterns),否则这些模式将很难被识别。此外,它们还可用于向不熟悉数据的一方更好地解释数据集。例如,如果将可视化技术应用在博客中,则可以提高读者的阅读兴趣,并使文章的布局更美观。
当探讨交互可视化时,其优势类似于静态可视化,因为它们允许观看者从其自身角度进行主动探索,这不仅允许观看者回答他们对数据可能有的疑问,而且会在探索时考虑新的问题。可以使博客读者或同事等单独一方受益,当然也包括创建者,因为借助可视化技术可以轻松地对数据进行即时探索,且无须更改任何代码。
本节将讨论并展示如何使用Bokeh在Jupyter中构建交互可视化。在此之前,首先将简单回顾一下Pandas的Data Frames,它在使用Python进行数据可视化方面发挥着重要作用。
3.2.1构建DataFrame以存储和组织数据
正如我们在本书中一次又一次地看到的那样,Pandas是使用Python和Jupyter Notebook进行数据科学分析不可或缺的一部分。DataFrame提供了一种存储和组织标记数据的方法,但更重要的是,Pandas提供了在DataFrame中转换数据的省时方法。我们在本书中看到的示例有删除重复值项、将字典映射到列、在列上应用函数以及填充缺失值等。
关于可视化,DataFrame提供了使用Matplotlib译者注: Matplotlib是一个Python的2D绘图库,它以各种硬拷贝格式和跨平台的交互式环境生成图像。创建各种图的方法,包括df.plot.barch()和df.plot.hist()等。依赖于Pandas和DataFrame,交互式可视化库Bokeh可用于高级图表的制作,这些工作类似于Seaborn译者注: Seaborn是在Matplotlib的基础上封装的API,可使作图变得更加方便。。正如第2章所述,DataFrame被传递给绘图函数以及要绘制的特定列。然而,最新版本的Bokeh已经不再支持DataFrame。相反,现在的绘图方式与Matplotlib大致相同,其中,数据可以存储在简单列表或NumPy数组中。讨论这个问题的重点是说明DataFrame并不是非常重要,但在可视化之前,组织和操作数据仍然非常有用。
下面演示构建和合并Pandas DataFrame的步骤。
本节将继续研究之前收集的国家信息数据。回想一下,我们已经提取了每个国家的央行利率和人口信息,并将结果保存到了CSV文件中。下面将从这些文件中加载数据,并将它们合并到DataFrame中,然后将其作为实现交互可视化的数据源。
(1) 在名为chapter3workbook.ipynb的Jupyter Notebook文件中,滚动到副标题为Building a DataFrame to store and organize data的位置。
首先从CSV文件中加载数据,以便回到数据采集后的状态。下面要练习在Python对象中(而不是使用pd.read_csv函数的方法)构建DataFrames。当使用pd.read_csv方法时,每列的数据类型将被视为字符串类型。而在使用pd.DataFrame时,其数据类型将被视为当前输入变量类型。正如本节中的例子,在读取文件和实例化DataFrame之前,无须将变量转换为数字或日期时间。(2) 通过运行代码段39中的代码将CSV文件加载到列表list中。with open('../data/countries/interest-rates.csv', 'r') as f:
int_rates_col_names = next(f).split(',')
int_rates = [line.split(',') for line in f.read().splitlines()]
with open('../data/countries/populations.csv', 'r') as f:
populations_col_names = next(f).split(',')
populations = [line.split(',') for line in f.read().splitlines()]代码段39数据加载操作(3) 运行下面两个单元格,检查生成的列表list,应该会看到类似以下的输出。print(int_rates_col_names)
int_rates[:5]
>> ['Country or currency union', 'Central bank interest ...
...
['Indonesia', '263', '991', '379', '1.10 %'],
['Brazil', '209', '288', '278', '0.79 %']]现在,数据采用标准的Python列表list结构,这就和前面部分从网页中采集的信息一致了。现在将创建两个DataFrame并合并它们,以便所有数据都能组织在一个对象中。
(4) 运行以下代码,使用标准的DataFrame构造函数创建两个DataFrame。df_int_rates = pd.DataFrame(int_rates,columns=int_rates_col_names)
df_populations = pd.DataFrame(populations,
   columns=populations_col_names)这已经不是我们第一次在本书中使用此构造函数了。在这里,我们传递数据列表(如上文所示)和相应的列名到构造函数中,输入的数据也可以是字典类型,当每列包含在单独的列表时,这可能很有用。
接下来,我们将清理每个DataFrame。从利率开始,我们打印其头部(head)和尾部(tail),并列出其数据类型。
(5) 显示整个DataFrame时,默认的最大行数为60(对于版本0.18.1)。通过运行以下代码,可设定最大行数为10。pd.options.display.max_rows = 10(6) 通过运行以下代码显示利率DataFrame的头部和尾部,结果如图311所示。df_int_rates图311利率DataFrame中的数据
(7) 通过运行以下代码打印数据类型。df_int_rates.dtypes
>> Country or currency union object
>> Central bank interest rate (%) object