目录
Python提供了强大的网络编程支持,有很多库实现了常见的网络协议以及基于这些协议的抽象层。
1.网络模块
1.1 模块socket
网络编程中的一个基本组件是套接字,套接字基本上是一个信息通道,两端各有一个程序。这些程序可能位于不同的计算机上,通过套接字向对方发送消息。在Python中,大多数网络编程都隐藏了模块socket的基本工作原理,不与套接字直接交互。
套接字是模块socket中socket类的实例,实例化套接字时最多指定三个参数:一个地址族(默认为socket.AF_INET);流套接字(socket.SOCK_STREAM,默认设置)还是数据报套接字(socket.SOCK_DGRAM);协议(使用默认值0就好)。创建普通套接字时,可以不用提供任何参数。方法listen接收一个参数,即最多有多少个客户端连接在队列中等待连接,到达这个数量以后,服务端就可以拒绝连接。服务端套接字开始监听后,就可以使用accept接收客户端的连接了。
为了传输数据,套接字提供两个方法:send和recv。要发送数据,可调用方法send并提供一个字符串;要接收数据,可调用recv并指定接收多少个字节的数据。实例代码如下:
服务端
import socket
s = socket.socket()
host = socket.gethostname()
port = 8080
s.bind((host, port))
s.listen(5)
while True:
c, addr = s.accept()
print('get connect from:', addr)
c.send('hello!'.encode())
c.close()
客户端:
import socket
s = socket.socket()
host = socket.gethostname()
port = 8080
s.connect((host, port))
print(s.recv(1024).decode())
1.2 模块urllib和urllib2
urllib和urlib2让我们能够通过网络访问文件,就像这些文件位于我们本地计算机一样。urllib2相比于urllib1功能更加强大一些,它提供了HTTP身份认证和cookie。
1.2.1 打开远程文件
打开远程文件就像打开本地文件一样,差别是只能使用只读模式。实例代码如下:
from urllib.request import urlopen
webpage = urlopen('http://www.python.org')
print(webpage)
变量webpage包含一个类似于文件的对象,该对象与网页http://www.python.org 相关联。urlopen返回的类似于文件的对象支持方法close、read、readline和readlines,还支持迭代等。如果我们要提取网页中的链接About的相对URL,可使用正则表达式匹配:
from urllib.request import urlopen
import re
webpage = urlopen('http://www.python.org')
text = webpage.read()
m = re.search(b'<a href="([^"]+)".*?>about</a>', text, re.IGNORECASE)
print(m.group(1))
当然,如果网页发生了变化,我们需要修改使用的正则表达式。
1.2.2 获取远程文件
如果要让urllib下载文件,并保存到本地文件中,可使用urlretrieve。实现代码如下:
from urllib.request import urlretrieve
urlretrieve('http://www.python.org', r'D:\text\python.html')
除了这些模块以外,Python库还包含了很多其他与网络相关的模块:
1.2.3 SocketServer及相关类
前面我们编写了一个简单的套接字服务器,这对我们来说相当容易。如果我们实现的服务器非常复杂,此时我们需要使用服务器模块。模块SocketServer是标准库提供的服务器框架的基石,该框架包括BasedHTTPServer、SimpleHTTPServer、CGIHTTPServer、SimpleXMLRPCServer和DocXMLRPCServer等服务器,它们在基本服务器的基础上添加了很多功能。如下代码我们实现了简单服务器的SocketServer版本。
from socketserver import TCPServer, StreamRequestHandler
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('get connection from', addr)
self.wfile.write(b'hello!')
server = TCPServer(('', 8080), Handler)
server.serve_forever()
1.3 多个连接
前面实现的服务器都是同步的,不能同时处理多个客户端的连接请求。如果连接持续的时间较长,就需要我们同时处理多个连接。处理多个连接的主要方式有三种:分叉(forking)、线程化和异步I/O。通过结合使用SocketServer中的混合类和服务器类,很容易实现分叉和线程化。然而,它们确实存在缺点,分叉占用的资源很多,且在客户端很多时可伸缩性不佳;而线程化可能带来同步问题。
分叉是一个UNIX术语,windows不支持分叉。对进程进行分叉时,基本上是复制它,而这样得到的两个进程都将从当前位置开始继续往下执行,且每个进程都有自己的内存副本(变量等)。原来的进程为父进程,复制的进程为子进程。进程能判断它们是原始进程还是子进程(通常查看函数fork的返回值),因此能够执行不同的操作。
在分叉服务器中,对于每个客户端连接,都将通过分叉创建一个子进程。父进程继续监听新连接,而子进程负责处理客户端请求。客户端请求结束后,子进程直接退出。由于分叉出来的进程并行的运行,因此客户端无需等待。
鉴于分叉占用的资源较多(每个分叉出来的进程都必须有自己的内存),还有一种解决方案:线程化。线程是轻量级的进程,都位于同一个进程中共享内存。这减少了占用的资源,但也带来了一个缺点:由于线程共享内存,所以必须解决同步问题。本章将介绍基于函数select的其他解决方案。
1.3.1 使用SocketServer实现分叉和线程化
使用框架SocketServer创建分叉服务器非常简单,代码如下:
from socketserver import TCPServer, ForkingMixIn, StreamRequestHandler
class Server(ForkingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('get connection from', addr)
self.wfile.write('hello!')
server = Server(('', 8080), Handler)
server.serve_forever()
在windows执行报错,这是因为windows是不支持分叉的。
ImportError: cannot import name 'ForkingMixIn' from 'socketserver' (F:\Python3.7.3\lib\socketserver.py)
线程化服务器执行如下:
from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler
class Server(ThreadingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('get connection from', addr)
self.wfile.write(b'hello!')
server = Server(('', 8080), Handler)
server.serve_forever()
1.3.2 使用select和poll实现异步IO
当服务器与客户端通信时,来自客户端的数据可能时断时续。如果使用了分叉和线程化,这就不是问题:因为一个进程(线程)等待数据时,其他进程(线程)可继续处理其他客户端的请求。另外一种做法是只处理当前正在通信的客户端,无需不断监听,只需要监听后将客户端加入队列即可。这就是框架asyncore/asynchat和Twisted采取的方法。这种功能的基石是系统函数select或poll。这两个函数都是位于模块select中,其中poll的可伸缩性更好,但是只有Unix系统支持它。
下面的实例代码展示了select来为多个连接提供服务。该服务器是一个简单的日志程序,将来自客户端的数据都打印出来。
import socket, select
s = socket.socket()
host = socket.gethostname()
port = 8080
s.bind((host, port))
s.listen(5)
inputs = [s]
while True:
rs, ws, es = select.select(inputs, [], [])
for r in rs:
if r is s:
c, addr = s.accept()
print('get connection from', addr)
inputs.append(c)
else:
try:
data = r.recv(1024)
disconnected = not data
except socket.error:
disconnected = True
if disconnected:
print(r.getpeername(), 'disconnected')
inputs.remove(r)
else:
print(data)
方法poll使用起来比select容易。调用poll时,将返回一个轮询对象。我们使用方法register向这个对象注册文件描述符,注册后可使用方法unregister将它们删除。注册对象后,可调用方法poll,该方法返回一个包含(fd,event)元组的列表,其中fd为文件描述符,而event是发生的事件。event是一个位掩码,这意味着它是一个整数,其各个位对应于不同的事件。各个事件使用select模块中的常量表示。要检查指定的位是否为1,可以直接使用按位与运算符(&)。select模块中的轮询事件常量如下:
实例代码如下:
import socket, select
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
fdmap = {s.fileno(): s}
s.listen(5)
p = select.poll()
p.register(s)
while True:
events = p.poll()
for fd, event in events:
if fd in fdmap:
c, addr = s.accept()
print('Got connection from', addr)
p.register(c)
fdmap[c.fileno()] = c
elif event & select.POLLIN:
data = fdmap[fd].recv(1024)
if not data: # 没有数据 --连接已关闭
print(fdmap[fd].getpeername(), 'disconnected')
p.unregister(fd)
del fdmap[fd]
else:
print(data)
1.4 Twisted
Twisted是一个事件驱动的Python网络框架,最初是为编写网络游戏开发的,但是现在被各种网络软件使用。Twisted支持许多常见的传输及应用层协议,包括TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC以及FTP。Twisted对于其支持的所有协议都带有客户端和服务器实现,同时附带有基于命令行的工具,使得配置和部署产品级的Twisted应用变得非常方便。如果想要深入地了解,可参阅网站http://twistedmatrix.com的在线文档。
2.屏幕抓取
屏幕抓取是通过程序下载网页并从中提取信息的过程,这种技术很有用,在网页中有我们在程序中使用的信息时,就可以使用它。例如,我们可以使用前面介绍的urllib来获取网页的HTML代码,再使用正则表达式或其他技术从中提取信息,示例代码如下:
from urllib.request import urlopen
import re
p = re.compile('<a href="(/jobs/\\d+)/">(.*?)</a>')
text = urlopen('http://python.org/jobs').read().decode()
for url, name in p.findall(text):
print('{} ({})'.format(name, url))
上面的代码中,正则表达式依赖于HTML代码的细节,而不是更抽象的结构。这意味着只要网页的结构发生细微的变化,改程序可能就不管用。针对正则表达式存在的问题,有两种解决方法:一是结合使用程序Tidy和XHTML解析;二是使用专门为屏幕抓取而设计的Beautiful Soup库。
2.1 Tidy和XHTML解析
XHTML和旧式HTML的主要区别在于,XHTML非常严格,要求显式地结束所有的元素。因此在HTML中,可以通过一个标签开始与结束段落,但在XHTML中,必须显式地开始与结束当前段落。这让XHTML解析起来非常方便,而且XHTML是一种标准的XML格式,可以直接使用XML工具进行解析。
当然,如果对于格式不正确且不严谨的HTML,Tidy可以帮助我们进行修复。
2.1.1 Tidy
假设我们有一个混乱的HTML文件messy.html,格式如下:
<h1>Pet Shop
<h2>Complaints</h3>
<p>There is <b>no <i>way</b> at all</i> we can accept returned
parrots.
<h1><i>Dead Pets</h1>
<p>Our pets may tend to rest at times, but rarely die within the
warranty period.
<i><h2>News</h2></i>
<p>We have just received <b>a really nice parrot.
<p>It's really nice.</b>
<h3><hr>The Norwegian Blue</h3>
<h4>Plumage and <hr>pining behavior</h4>
<a href="#norwegian-blue">More information<a>
<p>Features:
<body>
<li>Beautiful plumage
使用Tidy进行修复,修复代码如下:
from subprocess import Popen, PIPE
text = open(r'd:\test\mess.html').read()
tidy = Popen('tidy', stdin=PIPE, stdout=PIPE, stderr=PIPE)
tidy.stdin.write(text.encode())
tidy.stdin.close()
print(tidy.stdout.read().decode())
2.1.2 HTMLParser
要对Tidy生成的格式良好的XHTML进行解析,一种非常简单的方式是使用标准库模块html.parser中的HTMLParser类
2.2 Beautiful Soup
Beautiful Soup是一个小巧而出色的模块,用于解析我们在Web上可能遇到的不严谨且格式糟糕的HTML。实例代码如下:
from urllib.request import urlopen
from bs4 import BeautifulSoup
text = urlopen('https://www.python.org/jobs/').read()
soup = BeautifulSoup(text, 'html.parser')
jobs = set()
for job in soup.body.section('h2'):
jobs.add('{}({})'.format(job.a.string, job.a['href']))
print('\n'.join(sorted(jobs, key=str.lower)))
我们使用要从中抓取文本的HTML代码实例化Beautiful Soup类,然后提取解析树的不同部位。例如,使用soup.body来获取文档体,再访问其中的第一个section。使用参数‘h2’调用返回的对象,这与find_all等效——返回其中的所有h2元素。每个h2都表示一个职位,而我们提取的是它包含的第一个链接job.a。属性string是链接的文本内容,而a['href']为属性href。