Python for Everybody 中文版

Chapter 0 Base Material

| 关于   «  12. PY4E - Python for Everybody   ::   目录   ::   14. PY4E - 面向所有人的 Python  »

13. PY4E - Python 为所有人

切换导航

PY4E

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

13.1. 网络程序

尽管本书中的许多示例都侧重于读取文件并在这些文件中查找数据,但当考虑到互联网时,信息来源多种多样。

在本章中,我们将假装是一个网页浏览器,并使用超文本传输协议 (HTTP) 获取网页。然后我们将阅读网页数据并对其进行解析。

13.1.1. 超文本传输协议 - HTTP

支撑 Web 的网络协议实际上非常简单,Python 内置了名为 socket 的支持,这使得在 Python 程序中建立网络连接并从中检索数据变得非常容易。

一个 socket 类似于一个文件,除了单个 socket 提供两个程序之间的双向连接。你可以从同一个 socket 读取和写入。如果你向 socket 写入内容,它会被发送到 socket 另一端的程序。如果你从 socket 读取,你会获得对方程序发送的数据。

但是如果尝试读取 socket 时,socket 另一端程序尚未发送任何数据,你只会坐在那里等待。如果 socket 两端的程序只是等待某些数据而不发送任何内容,它们会等待很长时间,因此,因特网上通信程序的一个重要组成部分是拥有某种协议。

协议是一组精确的规则,用于确定谁先开始、他们要做什么、对消息的响应是什么、接下来由谁发送,等等。从某种意义上说,套接字两端的应用程序正在跳一支舞,并确保不踩到对方的脚。

有许多文档描述了这些网络协议。超文本传输协议在以下文档中描述:

https://www.w3.org/Protocols/rfc2616/rfc2616.txt

这是一个漫长而复杂的 176 页文档,包含大量细节。如果您觉得它有趣,请随意通读全文。但如果您查看 RFC2616 的第 36 页,您会发现 GET 请求的语法。要从 Web 服务器请求文档,我们连接到端口 80 上的 www.pr4e.org 服务器,然后发送一行如下形式的

GET http://data.pr4e.org/romeo.txt HTTP/1.0

其中第二个参数是我们请求的网页,然后我们还发送一个空行。Web 服务器将响应一些关于该文档的标头信息,随后是一个空行,最后是文档内容。

13.1.2. 世界上最简单的网络浏览器

或许展示 HTTP 协议工作原理最简单的方法是编写一个非常简单的 Python 程序,该程序与 Web 服务器建立连接,遵循 HTTP 协议的规则请求文档,并显示服务器返回的内容。

import socket

mysock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mysock.connect(('data.pr4e.org', 80))
cmd = 'GET http://data.pr4e.org/romeo.txt HTTP/1.0\r\n\r\n'.encode()
mysock.send(cmd)

while True:
    data = mysock.recv(512)
    if len(data) < 1:
        break
    print(data.decode(),end='')

mysock.close()

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

首先,程序与服务器上的 80 端口建立连接 www.py4e.com。由于我们的程序扮演“网络浏览器”的角色,HTTP 协议规定我们必须发送 GET 命令,后跟一个空行。\r\n 表示行尾(end of line),因此 \r\n\r\n 表示两个行尾序列之间没有任何内容。这就是空行的等价形式。

A Socket Connection

一个套接字连接

一旦我们发送了那个空行,我们就编写一个循环,从套接字中以 512 个字符的块接收数据并打印出来,直到没有更多数据可读(即,recv() 返回一个空字符串)。

该程序产生以下输出:

HTTP/1.1 200 OK
Date: Wed, 11 Apr 2018 18:52:55 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Sat, 13 May 2017 11:22:22 GMT
ETag: "a7-54f6609245537"
Accept-Ranges: bytes
Content-Length: 167
Cache-Control: max-age=0, no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Connection: close
Content-Type: text/plain

But soft what light through yonder window breaks
It is the east and Juliet is the sun
Arise fair sun and kill the envious moon
Who is already sick and pale with grief

之前的输出以服务器发送的用于描述文档的标题开头。例如,Content-Type 标题表明该文档为纯文本文档(text/plain)。

服务器发送标头后,添加一个空行以表示标头的结束,然后发送文件 romeo.txt 的实际数据。

本示例展示了如何使用套接字建立低级网络连接。套接字可用于与 Web 服务器、邮件服务器或许多其他类型的服务器通信。所需的全部工作就是找到描述该协议的文档,并编写代码以按照该协议发送和接收数据。

然而,由于我们最常用的协议是 HTTP 网页协议,Python 拥有一个专门设计用于支持 HTTP 协议的库,以检索网络上的文档和数据。

使用 HTTP 协议的要求之一是必须发送和接收数据作为 bytes 对象,而不是字符串。在前面的示例中,encode() 和 decode() 方法将字符串转换为 bytes 对象并再次转换回来。

之前的示例使用 b'' 记号指定变量应存储为 bytes 对象。encode() 与 b'' 等价。

>>> b'Hello world'
b'Hello world'
>>> 'Hello world'.encode()
b'Hello world'

13.1.3. 通过 HTTP 获取图像

在上述示例中,我们获取了一个包含换行符的纯文本文件,并在程序运行时简单地将数据复制到屏幕上。我们可以使用类似的程序通过 HTTP 获取图像。与在程序运行时将数据复制到屏幕不同,我们将数据累积到一个字符串中,去掉头部信息,然后按如下方式将图像数据保存到文件中:

import socket
import time

HOST = 'data.pr4e.org'
PORT = 80
mysock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mysock.connect((HOST, PORT))
mysock.sendall(b'GET http://data.pr4e.org/cover3.jpg HTTP/1.0\r\n\r\n')
count = 0
picture = b""

while True:
    data = mysock.recv(5120)
    if len(data) < 1: break
    #time.sleep(0.25)
    count = count + len(data)
    print(len(data), count)
    picture = picture + data

mysock.close()

# Look for the end of the header (2 CRLF)
pos = picture.find(b"\r\n\r\n")
print('Header length', pos)
print(picture[:pos].decode())

# Skip past the header and save the picture data
picture = picture[pos+4:]
fhand = open("stuff.jpg", "wb")
fhand.write(picture)
fhand.close()

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

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

$ python urljpeg.py
5120 5120
5120 10240
4240 14480
5120 19600
...
5120 214000
3200 217200
5120 222320
5120 227440
3167 230607
Header length 393
HTTP/1.1 200 OK
Date: Wed, 11 Apr 2018 18:54:09 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Mon, 15 May 2017 12:27:40 GMT
ETag: "38342-54f8f2e5b6277"
Accept-Ranges: bytes
Content-Length: 230210
Vary: Accept-Encoding
Cache-Control: max-age=0, no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Connection: close
Content-Type: image/jpeg

您可以看到,对于此 URL,Content-Type 标头表示文档主体是一个图像(image/jpeg)。一旦程序完成,您可以通过在图像查看器中打开文件 stuff.jpg 来查看图像数据。

随着程序运行,你可以看到每次调用方法``recv()``时,我们得到的字符数并非 5120 个。我们得到的字符数等于在调用方法``recv()``那一刻,Web 服务器通过网络传输给我们的字符数。在此示例中,每次请求最多 5120 个字符的数据时,我们得到的字符数少至 3200 个。

您的结果可能会因网络速度而异。另外请注意,在最后一次对 recv() 的调用中,我们得到 3167 字节,这是流的末尾;而在下一次对 recv() 的调用中,我们得到一个零长度字符串,这表明服务器已在套接字的一端调用了 close(),且不再有更多数据 forthcoming。

我们可以通过取消对 time.sleep() 的调用注释来减缓连续 recv() 调用的速度。这样,每次调用后我们会等待四分之一秒,以便服务器能够“领先”我们,在我们再次调用 recv() 之前发送更多数据。有了这个延迟,程序的执行如下:

$ python urljpeg.py
5120 5120
5120 10240
5120 15360
...
5120 225280
5120 230400
207 230607
Header length 393
HTTP/1.1 200 OK
Date: Wed, 11 Apr 2018 21:42:08 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Mon, 15 May 2017 12:27:40 GMT
ETag: "38342-54f8f2e5b6277"
Accept-Ranges: bytes
Content-Length: 230210
Vary: Accept-Encoding
Cache-Control: max-age=0, no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Connection: close
Content-Type: image/jpeg

之前除了第一次和最后一次调用``recv()``外,现在每次请求新数据时都会得到 5120 个字符。

在发出``send()``请求的服务器与发出``recv()``请求的应用程序之间有一个缓冲区。当我们运行带有延迟的程序时,在某个时刻服务器可能会填满套接字的缓冲区,并被迫暂停,直到我们的程序开始清空该缓冲区。发送应用程序或接收应用程序的暂停被称为“流控制”。

13.1.4. 从 urllib 检索网页

虽然我们可以使用 socket 库手动通过 HTTP 发送和接收数据,但在 Python 中执行这一常见任务有一个更简单的方法,即使用 urllib 库。

使用 urllib,您可以将网页像文件一样处理。您只需指明要检索的网页,urllib 便会处理所有 HTTP 协议和标头细节。

读取 Web 上 romeo.txt 文件的等效代码,使用 urllib 如下:

import urllib.request

fhand = urllib.request.urlopen('http://data.pr4e.org/romeo.txt')
for line in fhand:
    print(line.decode().strip())

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

一旦网页已通过 urllib.urlopen 打开,我们可以将其视为一个文件,并使用 for 循环逐行读取。

程序运行时,我们仅能看到文件内容的输出。头部信息仍会被发送,但 urllib 代码会消耗这些头部信息,仅返回数据给我们。

But soft what light through yonder window breaks
It is the east and Juliet is the sun
Arise fair sun and kill the envious moon
Who is already sick and pale with grief

例如,我们可以编写一个程序来获取数据 romeo.txt,并计算文件中每个单词的频率,如下所示:

import urllib.request, urllib.parse, urllib.error

fhand = urllib.request.urlopen('http://data.pr4e.org/romeo.txt')

counts = dict()
for line in fhand:
    words = line.decode().split()
    for word in words:
        counts[word] = counts.get(word, 0) + 1
print(counts)

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

同样,一旦我们打开了网页,就可以像读取本地文件一样读取它。

13.1.5. 使用 urllib 读取二进制文件

有时,您可能希望检索非文本(或二进制)文件,例如图像或视频文件。这些文件中的数据通常不适合打印输出,但您可以使用 urllib 轻松地将 URL 复制到硬盘上的本地文件。

之前的模式是打开 URL,并使用 read 将整个文档内容下载到一个字符串变量 (img),然后按以下方式将该信息写入本地文件:

import urllib.request, urllib.parse, urllib.error

img = urllib.request.urlopen('http://data.pr4e.org/cover3.jpg').read()
fhand = open('cover3.jpg', 'wb')
fhand.write(img)
fhand.close()

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

该程序一次性通过网络读取所有数据,并将其存储在计算机主存的变量 img 中,然后打开文件 cover.jpg,将数据写入您的磁盘。用于 open() 的 wb 参数会打开一个仅用于写入的二进制文件。如果您的计算机内存大小小于文件大小,该程序将无法运行。

然而,如果这是一个大型音频或视频文件,当您的计算机内存耗尽时,该程序可能会崩溃,或者至少运行得极其缓慢。为了避免内存耗尽,我们以块(或缓冲区)为单位检索数据,然后将每个块写入您的磁盘,再检索下一个块。这样,该程序可以读取任何大小的文件,而不会占用您计算机中的所有内存。

import urllib.request, urllib.parse, urllib.error

img = urllib.request.urlopen('http://data.pr4e.org/cover3.jpg')
fhand = open('cover3.jpg', 'wb')
size = 0
while True:
    info = img.read(100000)
    if len(info) < 1: break
    size = size + len(info)
    fhand.write(info)

print(size, 'characters copied.')
fhand.close()

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

在此示例中,我们每次仅读取 100,000 个字符,然后将这些字符写入 cover.jpg 文件,在从网络获取下一批 100,000 个字符之前完成该操作。

该程序的运行方式如下:

python curl2.py
230210 characters copied.

13.1.6. 解析 HTML 并抓取网页

Python 中 `urllib` 能力的一个常见用途是 scrape 网页。网页抓取是指编写一个程序,假装成为网络浏览器并检索页面,然后检查这些页面中的数据以寻找模式。

例如,像 Google 这样的搜索引擎会查看一个网页的源代码,提取指向其他页面的链接并获取这些页面,提取链接,依此类推。使用这种技术,Google 通过其 spiders 遍历了网络上几乎所有的页面。

Google 还使用其找到的页面指向特定页面的链接频率作为衡量该页面“重要性”以及该页面在搜索结果中应排多高的一项指标。

13.1.7. 使用正则表达式解析 HTML

解析 HTML 的一种简单方法是使用正则表达式反复搜索并提取匹配特定模式的子字符串。

这是一个简单的网页:

<h1>The First Page</h1>
<p>
If you like, you can switch to the
<a href="http://www.dr-chuck.com/page2.htm">
Second Page</a>.
</p>

我们可以构建一个格式良好的正则表达式,如上所示,以匹配并提取链接值如下:

href="http[s]?://.+?"

我们的正则表达式查找以 "href="http://" 或 "href="https://" 开头,后跟一个或多个字符 (.+?),再跟另一个双引号的字符串。[s]? 后面的问号表示搜索字符串 "http" 后跟零个或一个 "s"。

之前添加的问号到 .+? 表明匹配应以“非贪婪”方式而非“贪婪”方式进行。非贪婪匹配尝试找到 smallest 可能的匹配字符串,而贪婪匹配尝试找到 largest 可能的匹配字符串。

我们在正则表达式中添加括号,以指示我们想要提取的匹配字符串的哪一部分,并生成以下程序:

# Search for link values within URL input
import urllib.request, urllib.parse, urllib.error
import re
import ssl

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

url = input('Enter - ')
html = urllib.request.urlopen(url, context=ctx).read()
links = re.findall(b'href="(http[s]?://.*?)"', html)
for link in links:
    print(link.decode())

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

ssl 库允许此程序访问严格强制使用 HTTPS 的网站。read 方法返回 HTML 源代码作为字节对象,而不是返回 HTTPResponse 对象。findall 正则表达式方法将返回一个列表,包含所有匹配我们正则表达式的字符串,仅返回双引号之间的链接文本。

当我们运行程序并输入一个 URL 时,我们得到以下输出:

Enter - https://docs.python.org
https://docs.python.org/3/index.html
https://www.python.org/
https://docs.python.org/3.8/
https://docs.python.org/3.7/
https://docs.python.org/3.5/
https://docs.python.org/2.7/
https://www.python.org/doc/versions/
https://www.python.org/dev/peps/
https://wiki.python.org/moin/BeginnersGuide
https://wiki.python.org/moin/PythonBooks
https://www.python.org/doc/av/
https://www.python.org/
https://www.python.org/psf/donations/
http://sphinx.pocoo.org/

正则表达式在您的 HTML 格式良好且可预测时效果很好。但由于存在大量“损坏”的 HTML 页面,仅使用正则表达式的解决方案可能会遗漏一些有效链接,或者最终导致数据错误。

这可以通过使用一个健壮的 HTML 解析库来解决。

13.1.8. 使用 BeautifulSoup 解析 HTML

尽管 HTML 看起来像 XML:sup:`1 <https://www.py4e.com/html3/12-network#fn1>`__,且某些页面被精心构建为 XML,但大多数 HTML 通常以各种方式损坏,导致 XML 解析器将整个 HTML 页面视为格式不正确而拒绝。

有若干 Python 库可帮助你解析 HTML 并从页面中提取数据。每个库都有其优缺点,你可以根据需求选择一个。

例如,我们只需解析一些 HTML 输入并使用 BeautifulSoup 库提取链接。BeautifulSoup 能够容忍高度有缺陷的 HTML,仍让您轻松提取所需数据。您可以从以下位置下载并安装 BeautifulSoup 代码:

https://pypi.python.org/pypi/beautifulsoup4

关于使用 Python 包索引工具安装 BeautifulSoup 的信息 pip 可在以下地址获取:

https://packaging.python.org/tutorials/installing-packages/

我们将使用 urllib 读取页面,然后使用 BeautifulSoup 从锚点(a)标签中提取 href 属性。

# To run this, download the BeautifulSoup zip file
# http://www.py4e.com/code3/bs4.zip
# and unzip it in the same directory as this file

import urllib.request, urllib.parse, urllib.error
from bs4 import BeautifulSoup
import ssl

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

url = input('Enter - ')
html = urllib.request.urlopen(url, context=ctx).read()
soup = BeautifulSoup(html, 'html.parser')

# Retrieve all of the anchor tags
tags = soup('a')
for tag in tags:
    print(tag.get('href', None))

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

该程序提示输入网址,然后打开网页,读取数据并将其传递给 BeautifulSoup 解析器,随后获取所有锚标签并打印每个标签的 href 属性。

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

Enter - https://docs.python.org
genindex.html
py-modindex.html
https://www.python.org/
#
whatsnew/3.6.html
whatsnew/index.html
tutorial/index.html
library/index.html
reference/index.html
using/index.html
howto/index.html
installing/index.html
distributing/index.html
extending/index.html
c-api/index.html
faq/index.html
py-modindex.html
genindex.html
glossary.html
search.html
contents.html
bugs.html
about.html
license.html
copyright.html
download.html
https://docs.python.org/3.8/
https://docs.python.org/3.7/
https://docs.python.org/3.5/
https://docs.python.org/2.7/
https://www.python.org/doc/versions/
https://www.python.org/dev/peps/
https://wiki.python.org/moin/BeginnersGuide
https://wiki.python.org/moin/PythonBooks
https://www.python.org/doc/av/
genindex.html
py-modindex.html
https://www.python.org/
#
copyright.html
https://www.python.org/psf/donations/
bugs.html
http://sphinx.pocoo.org/

此列表要长得多,因为某些 HTML 锚点标签是相对路径(例如,tutorial/index.html)或页面内引用(例如,'#'),这些路径不包含"http://"或"https://",而这正是我们正则表达式的要求。

您也可以使用 BeautifulSoup 提取每个标签的各个部分:

# To run this, download the BeautifulSoup zip file
# http://www.py4e.com/code3/bs4.zip
# and unzip it in the same directory as this file

from urllib.request import urlopen
from bs4 import BeautifulSoup
import ssl

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

url = input('Enter - ')
html = urlopen(url, context=ctx).read()
soup = BeautifulSoup(html, "html.parser")

# Retrieve all of the anchor tags
tags = soup('a')
for tag in tags:
    # Look at the parts of a tag
    print('TAG:', tag)
    print('URL:', tag.get('href', None))
    print('Contents:', tag.contents[0])
    print('Attrs:', tag.attrs)

# Code: http://www.py4e.com/code3/urllink2.py
python urllink2.py
Enter - http://www.dr-chuck.com/page1.htm
TAG: <a href="http://www.dr-chuck.com/page2.htm">
Second Page</a>
URL: http://www.dr-chuck.com/page2.htm
Content: ['\nSecond Page']
Attrs: [('href', 'http://www.dr-chuck.com/page2.htm')]

html.parser 是标准 Python 3 库中包含的 HTML 解析器。有关其他 HTML 解析器的信息可在以下位置获取:

http://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser

这些示例仅开始展示 BeautifulSoup 在解析 HTML 时的强大功能。

13.1.9. Unix / Linux 用户奖励部分

如果您拥有一台 Linux、Unix 或 Macintosh 计算机,您的操作系统中可能已内置了一些命令,这些命令可以使用 HTTP 或文件传输(FTP)协议检索纯文本和二进制文件。这些命令之一就是``curl``:

$ curl -O http://www.py4e.com/cover.jpg

命令 curl 是“复制 URL"的简写,因此前文列出的两个用于通过 urllib 获取二进制文件的示例被巧妙地命名为 curl1.py 和 curl2.py``(见 `www.py4e.com/code3 <https://www.py4e.com/code3>`__),因为它们实现了与 ``curl 命令类似的功能。此外,还有一个 curl3.py 示例程序能更有效地完成此任务,以防你确实想在所编写的程序中使用此模式。

另一个功能非常相似的命令是 wget:

$ wget http://www.py4e.com/cover.jpg

这两个命令都使获取网页和远程文件变得简单。

13.1.10. 术语表

BeautifulSoup 一个用于解析 HTML 文档并从 HTML 文档中提取数据的 Python 库,它弥补了浏览器通常忽略的 HTML 中的大多数缺陷。您可以从 www.crummy.com 下载 BeautifulSoup 代码。port 一个通常指示当您向服务器建立套接字连接时正在联系哪个应用程序的数字。例如,网络流量通常使用端口 80,而电子邮件流量使用端口 25。scrape 当一个程序假装成 Web 浏览器并检索网页,然后查看网页内容时。通常程序会跟随一页中的链接以找到下一页,从而遍历一页网络或社交网络。socket 两个应用程序之间的网络连接,其中应用程序可以双向发送和接收数据。spider Web 搜索引擎检索一页,然后检索从该页链接的所有页面,依此类推,直到它们拥有互联网上几乎所有的页面,并使用这些页面构建其搜索索引的行为。

13.1.11. 练习

练习 1:修改套接字程序 ``socket1.py``,使其提示用户输入 URL,以便读取任何网页。你可以使用 ``split('/')`` 将 URL 分解为其组成部分,以便从中提取主机名用于套接字 ``connect`` 调用。使用 ``try`` 和 ``except`` 添加错误检查,以处理用户输入格式不正确或 URL 不存在的情况。

练习 2:修改你的套接字程序,使其统计接收到的字符数量,并在显示 3000 个字符后停止显示任何文本。该程序应检索整个文档,统计总字符数,并在文档末尾显示字符计数。

练习 3:使用 ``urllib`` 重复上一题的练习,即 (1) 从 URL 检索文档,(2) 显示最多 3000 个字符,以及 (3) 统计文档中的总字符数。本练习无需关心页眉,只需显示文档内容的前 3000 个字符。

练习 4:修改 ``urllinks.py`` 程序以从检索到的 HTML 文档中提取并统计段落 (p) 标签,并将段落的计数作为程序的输出显示。不要显示段落文本,只需统计它们。在几个小型网页以及一些大型网页上测试你的程序。

练习 5:(进阶)修改套接字程序,使其仅在接收到头部和空行后才显示数据。记住,``recv``接收的是字符(包括换行符),而不是行。


  1. 上章将描述 XML 格式。 ↩︎


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

   «  12. PY4E - Python for Everybody   ::   目录   ::   14. PY4E - 面向所有人的 Python  »

关闭窗口