网络编程

目录

1.网络模块

1.1 模块socket

1.2 模块urllib和urllib2

1.2.1 打开远程文件

1.2.2 获取远程文件

1.2.3 SocketServer及相关类

1.3 多个连接

1.3.1 使用SocketServer实现分叉和线程化

1.3.2 使用select和poll实现异步IO

1.4 Twisted

2.屏幕抓取

2.1 Tidy和XHTML解析

2.1.1 Tidy

2.1.2 HTMLParser

2.2 Beautiful Soup


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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章