Python for Everybody 中文版

Chapter 0 Base Material

| 关于   «  13. PY4E - Python 为所有人   ::   目录   ::   15. PY4E - 面向所有人的 Python  »

14. PY4E - 面向所有人的 Python

切换导航

PY4E

第 1 章:简介 第 2 章:变量 第 3 章:条件语句 第 4 章:函数 第 5 章:迭代 第 6 章:字符串 第 7 章:文件 第 8 章:列表 第 9 章:字典 第 10 章:元组 第 11 章:正则表达式 第 12 章:网络程序 第 13 章:Python 与 Web 服务 第 14 章:Python 对象 第 15 章:Python 与数据库 第 16 章:数据可视化

14.1. 使用 Web 服务

一旦通过程序检索和解析 HTTP 文档变得容易,我们并没有花很长时间就开发了一种方法,开始生产专门设计供其他程序消费的文档(即,不是用于在浏览器中显示的 HTML)。

我们在跨网络交换数据时有两种常用格式。可扩展标记语言(XML)已使用很长时间,最适合交换文档样式数据。当程序只想彼此交换字典、列表或其他内部信息时,它们使用 JavaScript 对象表示法(JSON)(参见 www.json.org)。我们将查看这两种格式。

14.1.1. 可扩展标记语言 - XML

XML 看起来与 HTML 非常相似,但 XML 比 HTML 更具结构性。下面是一个 XML 文档的示例:

<person>
  <name>Chuck</name>
  <phone type="intl">
    +1 734 303 4456
  </phone>
  <email hide="yes" />
</person>

每对起始标签(例如 <person> )和结束标签(例如 </person> )表示一个 元素 或 节点 ,其名称与标签相同(例如 person )。每个元素可以包含一些文本、一些属性(例如 hide )以及其他嵌套元素。如果 XML 元素为空(即没有内容),则它可以用一个自闭合标签(例如 <email /> )来表示。

通常,将 XML 文档视为一种树结构会有所帮助,其中有一个顶层元素(此处: person ),而其他标签(例如, phone )则被绘制为其 父 元素的 子 元素。

A Tree Representation of XML

XML 的树表示

14.1.2. 解析 XML

这是一个简单的应用程序,用于解析一些 XML 并从中提取数据元素:

import xml.etree.ElementTree as ET

data = '''
<person>
  <name>Chuck</name>
  <phone type="intl">
    +1 734 303 4456
  </phone>
  <email hide="yes" />
</person>'''

tree = ET.fromstring(data)
print('Name:', tree.find('name').text)
print('Attr:', tree.find('email').get('hide'))

# Code: http://www.py4e.com/code3/xml1.py

三单引号 (''') 以及三双引号 (""") 允许创建跨越多行的字符串。

调用 fromstring 将 XML 的字符串表示转换为 XML 元素的“树”。当 XML 以树的形式存在时,我们拥有一系列可以调用的方法,用于从 XML 字符串中提取部分数据。 find 函数遍历 XML 树并检索与指定标签匹配的元素。

Name: Chuck
Attr: yes

使用像 ElementTree 这样的 XML 解析器有一个优势:虽然本例中的 XML 非常简单,但实际上有许多关于有效 XML 的规则,而使用 ElementTree 使我们能够从 XML 中提取数据,而无需担心 XML 语法的规则。

14.1.3. 遍历节点

通常,XML 包含多个节点,我们需要编写循环来处理所有节点。在下面的程序中,我们循环遍历所有 user 节点:

import xml.etree.ElementTree as ET

input = '''
<stuff>
  <users>
    <user x="2">
      <id>001</id>
      <name>Chuck</name>
    </user>
    <user x="7">
      <id>009</id>
      <name>Brent</name>
    </user>
  </users>
</stuff>'''

stuff = ET.fromstring(input)
lst = stuff.findall('users/user')
print('User count:', len(lst))

for item in lst:
    print('Name', item.find('name').text)
    print('Id', item.find('id').text)
    print('Attribute', item.get('x'))

# Code: http://www.py4e.com/code3/xml2.py

The findall 方法获取一个 Python 列表,其中包含代表 XML 树中 user 结构的子树。然后我们可以编写一个 for 循环来检查每个用户节点,并打印 name 和 id 文本元素以及来自 user 节点的 x 属性。

User count: 2
Name Chuck
Id 001
Attribute 2
Name Brent
Id 009
Attribute 7

重要的是在 findall 语句中包含所有父级元素,但不包括顶层元素(例如, users/user )。否则,Python 将无法找到任何所需的节点。

import xml.etree.ElementTree as ET

input = '''
<stuff>
  <users>
    <user x="2">
      <id>001</id>
      <name>Chuck</name>
    </user>
    <user x="7">
      <id>009</id>
      <name>Brent</name>
    </user>
  </users>
</stuff>'''

stuff = ET.fromstring(input)

lst = stuff.findall('users/user')
print('User count:', len(lst))

lst2 = stuff.findall('user')
print('User count:', len(lst2))

lst 存储所有嵌套在其 user 父节点内的 users 元素。 lst2 查找不在顶层 stuff 元素内嵌套的 user 元素,但此处没有。

User count: 2
User count: 0

14.1.4. JavaScript 对象表示法 - JSON

JSON 格式灵感来源于 JavaScript 语言中使用的对象和数组格式。但由于 Python 发明于 JavaScript 之前,Python 中字典和列表的语法影响了 JSON 的语法。因此,JSON 的格式几乎等同于 Python 列表和字典的组合。

以下是与上述简单 XML 大致等价的 JSON 编码:

{
  "name" : "Chuck",
  "phone" : {
    "type" : "intl",
    "number" : "+1 734 303 4456"
   },
   "email" : {
     "hide" : "yes"
   }
}

您将注意到一些差异。首先,在 XML 中,我们可以像“intl”这样的属性添加到“phone”标签。在 JSON 中,我们简单地有键值对。此外,XML“person”标签不见了,被一组外部大括号替换。

通常,JSON 结构比 XML 更简单,因为 JSON 的功能比 XML 少。但 JSON 的优势在于它能将数据直接映射到字典和列表的某些组合。并且由于几乎所有编程语言都有与 Python 的字典和列表等价的东西,因此 JSON 是两台协同程序交换数据的一种非常自然的格式。

JSON 正迅速成为几乎所有应用程序间数据交换的首选格式,这得益于其相对于 XML 的相对简单性。

14.1.5. 解析 JSON

我们通过嵌套字典和列表来构建 JSON。在这个例子中,我们表示一个用户列表,其中每个用户都是一组键值对(即一个字典)。因此,我们有一个字典列表。

在以下程序中,我们使用内置的 json 库来解析 JSON 并遍历数据。请将其与上述等价的 XML 数据和代码进行仔细对比。JSON 提供的细节较少,因此我们必须预先知道获取的是一个列表,该列表包含用户,且每个用户是一组键值对。JSON 更为简洁(这是一个优势),但也更缺乏自描述性(这是一个劣势)。

import json

data = '''
[
  { "id" : "001",
    "x" : "2",
    "name" : "Chuck"
  } ,
  { "id" : "009",
    "x" : "7",
    "name" : "Brent"
  }
]'''

info = json.loads(data)
print('User count:', len(info))

for item in info:
    print('Name', item['name'])
    print('Id', item['id'])
    print('Attribute', item['x'])

# Code: http://www.py4e.com/code3/json2.py

如果您比较从解析后的 JSON 和 XML 中提取数据的代码,您将看到我们从 json.loads() 获得的是一个 Python 列表,我们使用 for 循环遍历它,且该列表中的每个项目都是一个 Python 字典。一旦 JSON 被解析,我们就可以使用 Python 索引运算符来提取每个用户的各种数据。我们不必使用 JSON 库来挖掘已解析的 JSON,因为返回的数据仅仅是原生的 Python 结构。

该程序的输出与上面的 XML 版本完全相同。

User count: 2
Name Chuck
Id 001
Attribute 2
Name Brent
Id 009
Attribute 7

通常,行业趋势是从 XML 转向 JSON 用于 Web 服务。因为 JSON 更简单且更直接地映射到编程语言中我们已有的原生数据结构,所以使用 JSON 时解析和数据提取代码通常更简单、更直接。但 XML 比 JSON 更具自描述性,因此有些应用中 XML 仍具有优势。例如,大多数文字处理程序内部使用 XML 而非 JSON 存储文档。

14.1.6. 应用程序编程接口

我们现在能够使用超文本传输协议 (HTTP) 在应用程序之间交换数据,并且能够使用可扩展标记语言 (XML) 或 JavaScript 对象表示法 (JSON) 来表示在这些应用程序之间来回发送的复杂数据。

下一步是开始定义和记录使用这些技术的应用程序之间的“契约”。这些应用程序间契约的通用名称是 应用程序编程接口 (APIs)。当我们使用 API 时,通常一个程序提供一组 服务 供其他应用程序使用,并发布必须遵循以访问该程序所提供服务的 API(即“规则”)。

当我们的程序开始构建,且程序的功能包括访问其他程序提供的服务时,我们将这种方法称为 面向服务架构 (SOA)。SOA 方法是指我们的整体应用程序利用其他应用程序的服务。非 SOA 方法是指应用程序是一个独立的单一应用程序,其中包含实现该应用程序所需的所有代码。

当我们使用互联网时,可以看到许多面向服务的架构(SOA)的例子。我们可以访问单个网站并预订航空旅行、酒店和汽车,所有这些都在一个网站上完成。酒店的数据并不存储在航空公司的计算机上。相反,航空公司的计算机联系酒店计算机上的服务,检索酒店数据并将其呈现给用户。当用户同意通过航空公司网站进行酒店预订时,该网站使用酒店系统上的另一个网络服务来实际完成预订。当需要为您的整个交易收取信用卡费用时,其他计算机也会参与到该过程中。

Service-oriented architecture

面向服务架构

面向服务的架构具有许多优势,包括:(1) 我们总是只维护一份数据副本(这对于酒店预订之类的事情尤其重要,因为我们不希望过度承诺)和 (2) 数据的所有者可以设定关于其数据使用的规则。凭借这些优势,面向服务的架构系统必须经过精心设计,以具有良好的性能并满足用户的需求。

当应用程序将其 API 中的一组服务在网络上提供时,我们称之为 Web 服务 。

14.1.7. 安全与 API 使用

使用供应商的 API 通常需要 API 密钥,这种情况很常见。其基本思路是,他们希望了解谁在使用他们的服务,以及每个用户的使用量。也许他们对服务提供免费和付费层级,或者制定了政策,限制单个个人在特定时间段内可以发出的请求数量。

有时一旦你获取了 API 密钥,你只需将密钥作为 POST 数据的一部分包含在内,或者在调用 API 时将其作为 URL 上的参数。

其他时候,供应商希望增加对请求来源的确认,因此他们期望您使用共享密钥和秘密发送经过密码学签名的消息。一种常用于在互联网上对请求进行签名的技术称为 OAuth 。您可以在 www.oauth.net 中阅读更多关于 OAuth 协议的详细信息。

幸运的是,有许多方便且免费的 OAuth 库,因此您可以通过阅读规范来避免从头编写 OAuth 实现。这些库的复杂程度各不相同,丰富程度也有差异。OAuth 网站提供了各种 OAuth 库的信息。

14.1.8. 术语表

API 应用程序编程接口——定义两个应用程序组件之间交互模式的协议。ElementTree 用于解析 XML 数据的内置 Python 库。JSON JavaScript 对象表示法——一种允许基于 JavaScript 对象语法对结构化数据进行标记的格式。SOA 面向服务架构——当应用程序由通过网络连接的组件构成时。XML 可扩展标记语言——一种允许对结构化数据进行标记的格式。

14.1.9. 应用 1: Google 地理编码 Web 服务

Google 拥有一个出色的网络服务,允许我们利用其庞大的地理信息数据库。我们可以向 Google 的地理编码 API 提交地理搜索字符串(如"Ann Arbor, MI"),让 Google 返回其对我们可能在地图上找到该搜索字符串的位置的最佳猜测,并告知我们附近的地标。

地理编码服务是免费的,但有限制速率,因此您不能在商业应用中无限制地使用 API。但是,如果您有一些调查数据,其中最终用户在自由格式的输入框中输入了位置,您可以使用此 API 相当出色地清理您的数据。

当使用免费 API(如 Google 的地理编码 API)时,您需要在使用这些资源时保持尊重。如果太多人滥用该服务,Google 可能会停止或大幅削减其免费服务。

您可以阅读该服务的在线文档,但它非常简单,甚至可以通过浏览器测试,只需在浏览器中输入以下 URL:

http://maps.googleapis.com/maps/api/geocode/json?address=Ann+Arbor%2C+MI

确保在将 URL 粘贴到浏览器之前,解除 URL 的包装并从 URL 中删除任何空格。

以下为简单应用,提示用户输入搜索字符串,调用 Google 地理编码 API,并从返回的 JSON 中提取信息。

import urllib.request, urllib.parse, urllib.error
import json
import ssl

api_key = False
# If you have a Google Places API key, enter it here
# api_key = 'AIzaSy___IDByT70'
# https://developers.google.com/maps/documentation/geocoding/intro

if api_key is False:
    api_key = 42
    serviceurl = 'http://py4e-data.dr-chuck.net/json?'
else :
    serviceurl = 'https://maps.googleapis.com/maps/api/geocode/json?'

# Ignore SSL certificate errors
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

while True:
    address = input('Enter location: ')
    if len(address) < 1: break

    parms = dict()
    parms['address'] = address
    if api_key is not False: parms['key'] = api_key
    url = serviceurl + urllib.parse.urlencode(parms)

    print('Retrieving', url)
    uh = urllib.request.urlopen(url, context=ctx)
    data = uh.read().decode()
    print('Retrieved', len(data), 'characters')

    try:
        js = json.loads(data)
    except:
        js = None

    if not js or 'status' not in js or js['status'] != 'OK':
        print('==== Failure To Retrieve ====')
        print(data)
        continue

    print(json.dumps(js, indent=4))

    lat = js['results'][0]['geometry']['location']['lat']
    lng = js['results'][0]['geometry']['location']['lng']
    print('lat', lat, 'lng', lng)
    location = js['results'][0]['formatted_address']
    print(location)

# Code: http://www.py4e.com/code3/geojson.py

该程序接收搜索字符串,并构建一个 URL,其中搜索字符串作为正确编码的参数,然后使用 urllib 从 Google 地理编码 API 检索文本。与固定网页不同,我们获得的数据取决于我们发送的参数以及存储在 Google 服务器上的地理数据。

一旦我们检索到 JSON 数据,我们就用 json 库对其进行解析,并执行一些检查以确保我们收到了良好的数据,然后提取我们要查找的信息。

程序的输出如下(部分返回的 JSON 已被移除):

$ python3 geojson.py
Enter location: Ann Arbor, MI
Retrieving http://py4e-data.dr-chuck.net/json?address=Ann+Arbor%2C+MI&key=42
Retrieved 1736 characters
{
    "results": [
        {
            "address_components": [
                {
                    "long_name": "Ann Arbor",
                    "short_name": "Ann Arbor",
                    "types": [
                        "locality",
                        "political"
                    ]
                },
                {
                    "long_name": "Washtenaw County",
                    "short_name": "Washtenaw County",
                    "types": [
                        "administrative_area_level_2",
                        "political"
                    ]
                },
                {
                    "long_name": "Michigan",
                    "short_name": "MI",
                    "types": [
                        "administrative_area_level_1",
                        "political"
                    ]
                },
                {
                    "long_name": "United States",
                    "short_name": "US",
                    "types": [
                        "country",
                        "political"
                    ]
                }
            ],
            "formatted_address": "Ann Arbor, MI, USA",
            "geometry": {
                "bounds": {
                    "northeast": {
                        "lat": 42.3239728,
                        "lng": -83.6758069
                    },
                    "southwest": {
                        "lat": 42.222668,
                        "lng": -83.799572
                    }
                },
                "location": {
                    "lat": 42.2808256,
                    "lng": -83.7430378
                },
                "location_type": "APPROXIMATE",
                "viewport": {
                    "northeast": {
                        "lat": 42.3239728,
                        "lng": -83.6758069
                    },
                    "southwest": {
                        "lat": 42.222668,
                        "lng": -83.799572
                    }
                }
            },
            "place_id": "ChIJMx9D1A2wPIgR4rXIhkb5Cds",
            "types": [
                "locality",
                "political"
            ]
        }
    ],
    "status": "OK"
}
lat 42.2808256 lng -83.7430378
Ann Arbor, MI, USA
Enter location:

您可以下载 www.py4e.com/code3/geoxml.py 来探索 Google 地理编码 API 的 XML 变体。

练习 1:将 **geojson.py** 或 **geoxml.py** 改为输出检索到的两字符国家代码。添加错误检查,以便当国家代码不存在时程序不会抛出异常。一旦运行正常,搜索“大西洋”,并确保它能处理不属于任何国家的位置。

14.1.10. 应用 2: Twitter

随着 Twitter API 变得越来越有价值,Twitter 从一个开放且公开的 API 转变为一个需要在每个 API 请求中使用 OAuth 签名的 API。

对于接下来的示例程序,请从 www.py4e.com/code 下载文件 twurl.py 、 hidden.py 、 oauth.py 和 twitter1.py ,并将它们全部放在您计算机上的一个文件夹中。

要使用这些程序,您需要拥有一个 Twitter 账户,并将您的 Python 代码授权为应用程序,设置密钥、密钥、令牌和令牌密钥。您将编辑文件 hidden.py 并将这四个字符串放入文件中的适当变量中:

# Keep this file separate

# https://apps.twitter.com/
# Create new App and get the four strings

def oauth():
    return {"consumer_key": "h7Lu...Ng",
            "consumer_secret": "dNKenAC3New...mmn7Q",
            "token_key": "10185562-eibxCp9n2...P4GEQQOSGI",
            "token_secret": "H0ycCFemmC4wyf1...qoIpBo"}

# Code: http://www.py4e.com/code3/hidden.py

Twitter 网络服务通过如下 URL 进行访问:

https://api.twitter.com/1.1/statuses/user_timeline.json

但一旦所有安全信息都已添加,URL 将更像这样:

https://api.twitter.com/1.1/statuses/user_timeline.json?count=2
&oauth_version=1.0&oauth_token=101...SGI&screen_name=drchuck
&oauth_nonce=09239679&oauth_timestamp=1380395644
&oauth_signature=rLK...BoD&oauth_consumer_key=h7Lu...GNg
&oauth_signature_method=HMAC-SHA1

如果您想了解更多关于为满足 OAuth 安全要求而添加的各种参数的含义,可以阅读 OAuth 规范。

对于我们要运行 Twitter 的程序,我们在 oauth.py 和 twurl.py 文件中隐藏了所有复杂性。我们只需在 hidden.py 中设置秘密,然后将所需的 URL 发送到 twurl.augment() 函数,库代码会为我们添加 URL 所需的所有参数。

该程序检索特定 Twitter 用户的时间线,并以字符串形式的 JSON 格式返回给我们。我们只需打印该字符串的前 250 个字符:

import urllib.request, urllib.parse, urllib.error
import twurl
import ssl

# https://apps.twitter.com/
# Create App and get the four strings, put them in hidden.py

TWITTER_URL = 'https://api.twitter.com/1.1/statuses/user_timeline.json'

# Ignore SSL certificate errors
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

while True:
    print('')
    acct = input('Enter Twitter Account:')
    if (len(acct) < 1): break
    url = twurl.augment(TWITTER_URL,
                        {'screen_name': acct, 'count': '2'})
    print('Retrieving', url)
    connection = urllib.request.urlopen(url, context=ctx)
    data = connection.read().decode()
    print(data[:250])
    headers = dict(connection.getheaders())
    # print headers
    print('Remaining', headers['x-rate-limit-remaining'])

# Code: http://www.py4e.com/code3/twitter1.py

当程序运行时,它产生以下输出:

Enter Twitter Account:drchuck
Retrieving https://api.twitter.com/1.1/ ...
[{"created_at":"Sat Sep 28 17:30:25 +0000 2013","
id":384007200990982144,"id_str":"384007200990982144",
"text":"RT @fixpert: See how the Dutch handle traffic
intersections: http:\/\/t.co\/tIiVWtEhj4\n#brilliant",
"source":"web","truncated":false,"in_rep
Remaining 178

Enter Twitter Account:fixpert
Retrieving https://api.twitter.com/1.1/ ...
[{"created_at":"Sat Sep 28 18:03:56 +0000 2013",
"id":384015634108919808,"id_str":"384015634108919808",
"text":"3 months after my freak bocce ball accident,
my wedding ring fits again! :)\n\nhttps:\/\/t.co\/2XmHPx7kgX",
"source":"web","truncated":false,
Remaining 177

Enter Twitter Account:

与返回的时间线数据一起,Twitter 还在 HTTP 响应头中返回关于请求的元数据。其中一个特定的响应头 x-rate-limit-remaining 告知我们,在短暂时间内被限制之前,我们还能发起多少次请求。您可以看到,每当我们向 API 发起一次请求,剩余的请求次数就会减少一次。

在下面的例子中,我们检索用户的 Twitter 朋友,解析返回的 JSON,并提取一些关于朋友的信息。我们也在解析后输出 JSON 并用缩进为四个字符的方式对其进行“美化打印”,以便我们在想要提取更多字段时可以仔细查看数据。

import urllib.request, urllib.parse, urllib.error
import twurl
import json
import ssl

# https://apps.twitter.com/
# Create App and get the four strings, put them in hidden.py

TWITTER_URL = 'https://api.twitter.com/1.1/friends/list.json'

# Ignore SSL certificate errors
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

while True:
    print('')
    acct = input('Enter Twitter Account:')
    if (len(acct) < 1): break
    url = twurl.augment(TWITTER_URL,
                        {'screen_name': acct, 'count': '5'})
    print('Retrieving', url)
    connection = urllib.request.urlopen(url, context=ctx)
    data = connection.read().decode()

    js = json.loads(data)
    print(json.dumps(js, indent=2))

    headers = dict(connection.getheaders())
    print('Remaining', headers['x-rate-limit-remaining'])

    for u in js['users']:
        print(u['screen_name'])
        if 'status' not in u:
            print('   * No status found')
            continue
        s = u['status']['text']
        print('  ', s[:50])

# Code: http://www.py4e.com/code3/twitter2.py

由于 JSON 变成了嵌套的 Python 列表和字典集合,我们可以使用索引操作和 for 循环的组合,用非常少的 Python 代码来遍历返回的数据结构。

程序的输出如下所示(部分数据项被缩短以适应页面):

Enter Twitter Account:drchuck
Retrieving https://api.twitter.com/1.1/friends ...
Remaining 14
{
  "next_cursor": 1444171224491980205,
  "users": [
    {
      "id": 662433,
      "followers_count": 28725,
      "status": {
        "text": "@jazzychad I just bought one .__.",
        "created_at": "Fri Sep 20 08:36:34 +0000 2013",
        "retweeted": false,
      },
      "location": "San Francisco, California",
      "screen_name": "leahculver",
      "name": "Leah Culver",
    },
    {
      "id": 40426722,
      "followers_count": 2635,
      "status": {
        "text": "RT @WSJ: Big employers like Google ...",
        "created_at": "Sat Sep 28 19:36:37 +0000 2013",
      },
      "location": "Victoria Canada",
      "screen_name": "_valeriei",
      "name": "Valerie Irvine",
    }
  ],
 "next_cursor_str": "1444171224491980205"
}
leahculver
   @jazzychad I just bought one .__.
_valeriei
   RT @WSJ: Big employers like Google, AT&amp;T are h
ericbollens
   RT @lukew: sneak peek: my LONG take on the good &a
halherzog
   Learning Objects is 10. We had a cake with the LO,
scweeker
   @DeviceLabDC love it! Now where so I get that "etc

Enter Twitter Account:

输出的最后一部分是我们要看到 for 循环读取 *@drchuck* 推特账号最近的五个“朋友”并为每个朋友打印最新状态的地方。返回的 JSON 中还有大量的数据可用。如果你查看程序的输出,你还可以看到查找某个特定账号的“朋友”与每个时间段允许运行的时间线查询数量具有不同的速率限制。

这些安全的 API 密钥允许 Twitter 对其知道谁在使用其 API 和数据以及使用级别具有坚定的信心。速率限制方法允许我们进行简单的个人数据检索,但不允许我们构建一个每天从其 API 拉取数百万次数据的产品。


如果您在本书中发现错误,欢迎使用 Github 发送修正。

   «  13. PY4E - Python 为所有人   ::   目录   ::   15. PY4E - 面向所有人的 Python  »

关闭窗口