admin管理员组

文章数量:1567449

chardet

字符串编码一直是令人非常头疼的问题,尤其是我们在处理一些不规范的第三方网页的时候。虽然Python提供了Unicode表示的strbytes两种数据类型,并且可以通过encode()decode()方法转换,但是,在不知道编码的情况下,对bytesdecode()不好做。

对于未知编码的bytes,要把它转换成str,需要先“猜测”编码。猜测的方式是先收集各种编码的特征字符,根据特征字符判断,就能有很大概率“猜对”。

当然,我们肯定不能从头自己写这个检测编码的功能,这样做费时费力。chardet这个第三方库正好就派上了用场。用它来检测编码,简单易用。

安装chardet

如果安装了Anaconda,chardet就已经可用了。否则,需要在命令行下通过pip安装:

$ pip install chardet

如果遇到Permission denied安装失败,请加上sudo重试。

使用chardet

当我们拿到一个bytes时,就可以对其检测编码。用chardet检测编码,只需要一行代码:

>>> chardet.detect(b'Hello, world!')

{'encoding': 'ascii', 'confidence': 1.0, 'language': ''}

检测出的编码是ascii,注意到还有个confidence字段,表示检测的概率是1.0(即100%)。

我们来试试检测GBK编码的中文:

>>> data = '离离原上草,一岁一枯荣'.encode('gbk')>>> chardet.detect(data)

{'encoding': 'GB2312', 'confidence': 0.7407407407407407, 'language': 'Chinese'}

检测的编码是GB2312,注意到GBK是GB2312的超集,两者是同一种编码,检测正确的概率是74%,language字段指出的语言是'Chinese'

对UTF-8编码进行检测:

>>> data = '离离原上草,一岁一枯荣'.encode('utf-8')>>> chardet.detect(data)

{'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}

我们再试试对日文进行检测:

>>> data = '最新の主要ニュース'.encode('euc-jp')>>> chardet.detect(data)

{'encoding': 'EUC-JP', 'confidence': 0.99, 'language': 'Japanese'}

可见,用chardet检测编码,使用简单。获取到编码后,再转换为str,就可以方便后续处理。

chardet支持检测的编码列表请参考官方文档Supported encodings

小结

使用chardet检测编码非常容易,chardet支持检测中文、日文、韩文等多种语言。

psutil

用Python来编写脚本简化日常的运维工作是Python的一个重要用途。在Linux下,有许多系统命令可以让我们时刻监控系统运行的状态,如pstopfree等等。要获取这些系统信息,Python可以通过subprocess模块调用并获取结果。但这样做显得很麻烦,尤其是要写很多解析代码。

在Python中获取系统信息的另一个好办法是使用psutil这个第三方模块。顾名思义,psutil = process and system utilities,它不仅可以通过一两行代码实现系统监控,还可以跨平台使用,支持Linux/UNIX/OSX/Windows等,是系统管理员和运维小伙伴不可或缺的必备模块。

安装psutil

如果安装了Anaconda,psutil就已经可用了。否则,需要在命令行下通过pip安装:

$ pip install psutil

如果遇到Permission denied安装失败,请加上sudo重试。

获取CPU信息

我们先来获取CPU的信息:

>>> import psutil>>> psutil.cpu_count() # CPU逻辑数量4>>> psutil.cpu_count(logical=False) # CPU物理核心2# 2说明是双核超线程, 4则是4核非超线程

统计CPU的用户/系统/空闲时间:

>>> psutil.cpu_times()

scputimes(user=10963.31, nice=0.0, system=5138.67, idle=356102.45)

再实现类似top命令的CPU使用率,每秒刷新一次,累计10次:

>>> for x in range(10):...     print(psutil.cpu_percent(interval=1, percpu=True))...

[14.0, 4.0, 4.0, 4.0]

[12.0, 3.0, 4.0, 3.0]

[8.0, 4.0, 3.0, 4.0]

[12.0, 3.0, 3.0, 3.0]

[18.8, 5.1, 5.9, 5.0]

[10.9, 5.0, 4.0, 3.0]

[12.0, 5.0, 4.0, 5.0]

[15.0, 5.0, 4.0, 4.0]

[19.0, 5.0, 5.0, 4.0]

[9.0, 3.0, 2.0, 3.0]

获取内存信息

使用psutil获取物理内存和交换内存信息,分别使用:

>>> psutil.virtual_memory()

svmem(total=8589934592, available=2866520064, percent=66.6, used=7201386496, free=216178688, active=3342192640, inactive=2650341376, wired=1208852480)>>> psutil.swap_memory()

sswap(total=1073741824, used=150732800, free=923009024, percent=14.0, sin=10705981440, sout=40353792)

返回的是字节为单位的整数,可以看到,总内存大小是8589934592 = 8 GB,已用7201386496 = 6.7 GB,使用了66.6%。

而交换区大小是1073741824 = 1 GB。

获取磁盘信息

可以通过psutil获取磁盘分区、磁盘使用率和磁盘IO信息:

>>> psutil.disk_partitions() # 磁盘分区信息

[sdiskpart(device='/dev/disk1', mountpoint='/', fstype='hfs', opts='rw,local,rootfs,dovolfs,journaled,multilabel')]>>> psutil.disk_usage('/') # 磁盘使用情况

sdiskusage(total=998982549504, used=390880133120, free=607840272384, percent=39.1)>>> psutil.disk_io_counters() # 磁盘IO

sdiskio(read_count=988513, write_count=274457, read_bytes=14856830464, write_bytes=17509420032, read_time=2228966, write_time=1618405)

可以看到,磁盘'/'的总容量是998982549504 = 930 GB,使用了39.1%。文件格式是HFS,opts中包含rw表示可读写,journaled表示支持日志。

获取网络信息

psutil可以获取网络接口和网络连接信息:

>>> psutil_io_counters() # 获取网络读写字节/包的个数

snetio(bytes_sent=3885744870, bytes_recv=10357676702, packets_sent=10613069, packets_recv=10423357, errin=0, errout=0, dropin=0, dropout=0)

>>> psutil_if_addrs() # 获取网络接口信息

{

  'lo0': [snic(family=<AddressFamily.AF_INET: 2>, address='127.0.0.1', netmask='255.0.0.0'), ...],

  'en1': [snic(family=<AddressFamily.AF_INET: 2>, address='10.0.1.80', netmask='255.255.255.0'), ...],

  'en0': [...],

  'en2': [...],

  'bridge0': [...]

}

>>> psutil_if_stats() # 获取网络接口状态

{

  'lo0': snicstats(isup=True, duplex=<NicDuplex.NIC_DUPLEX_UNKNOWN: 0>, speed=0, mtu=16384),

  'en0': snicstats(isup=True, duplex=<NicDuplex.NIC_DUPLEX_UNKNOWN: 0>, speed=0, mtu=1500),

  'en1': snicstats(...),

  'en2': snicstats(...),

  'bridge0': snicstats(...)

}

要获取当前网络连接信息,使用net_connections()

>>> psutil_connections()

Traceback (most recent call last):

  ...

PermissionError: [Errno 1] Operation not permitted

During handling of the above exception, another exception occurred:

Traceback (most recent call last):

  ...

psutil.AccessDenied: psutil.AccessDenied (pid=3847)

你可能会得到一个AccessDenied错误,原因是psutil获取信息也是要走系统接口,而获取网络连接信息需要root权限,这种情况下,可以退出Python交互环境,用sudo重新启动:

$ sudo python3

Password: ******

Python 3.8 ... on darwin

Type "help", ... for more information.

>>> import psutil

>>> psutil_connections()

[

    sconn(fd=83, family=<AddressFamily.AF_INET6: 30>, type=1, laddr=addr(ip='::127.0.0.1', port=62911), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),

    sconn(fd=84, family=<AddressFamily.AF_INET6: 30>, type=1, laddr=addr(ip='::127.0.0.1', port=62905), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),

    sconn(fd=93, family=<AddressFamily.AF_INET6: 30>, type=1, laddr=addr(ip='::', port=8080), raddr=(), status='LISTEN', pid=3725),

    sconn(fd=103, family=<AddressFamily.AF_INET6: 30>, type=1, laddr=addr(ip='::127.0.0.1', port=62918), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),

    sconn(fd=105, family=<AddressFamily.AF_INET6: 30>, type=1, ..., pid=3725),

    sconn(fd=106, family=<AddressFamily.AF_INET6: 30>, type=1, ..., pid=3725),

    sconn(fd=107, family=<AddressFamily.AF_INET6: 30>, type=1, ..., pid=3725),

    ...

    sconn(fd=27, family=<AddressFamily.AF_INET: 2>, type=2, ..., pid=1)

]

获取进程信息

通过psutil可以获取到所有进程的详细信息:

>>> psutil.pids() # 所有进程ID

[3865, 3864, 3863, 3856, 3855, 3853, 3776, ..., 45, 44, 1, 0]>>> p = psutil.Process(3776) # 获取指定进程ID=3776,其实就是当前Python交互环境>>> p.name() # 进程名称'python3.6'>>> p.exe() # 进程exe路径'/Users/michael/anaconda3/bin/python3.6'>>> p.cwd() # 进程工作目录'/Users/michael'>>> p.cmdline() # 进程启动的命令行

['python3']>>> p.ppid() # 父进程ID3765>>> p.parent() # 父进程

<psutil.Process(pid=3765, name='bash') at 4503144040>>>> p.children() # 子进程列表

[]>>> p.status() # 进程状态'running'>>> p.username() # 进程用户名'michael'>>> p.create_time() # 进程创建时间1511052731.120333>>> p.terminal() # 进程终端'/dev/ttys002'>>> p.cpu_times() # 进程使用的CPU时间

pcputimes(user=0.081150144, system=0.053269812, children_user=0.0, children_system=0.0)>>> p.memory_info() # 进程使用的内存

pmem(rss=8310784, vms=2481725440, pfaults=3207, pageins=18)>>> p.open_files() # 进程打开的文件

[]>>> p.connections() # 进程相关网络连接

[]>>> p.num_threads() # 进程的线程数量1>>> p.threads() # 所有线程信息

[pthread(id=1, user_time=0.090318, system_time=0.062736)]>>> p.environ() # 进程环境变量

{'SHELL': '/bin/bash', 'PATH': '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:...', 'PWD': '/Users/michael', 'LANG': 'zh_CN.UTF-8', ...}>>> p.terminate() # 结束进程

Terminated: 15 <-- 自己把自己结束了

和获取网络连接类似,获取一个root用户的进程需要root权限,启动Python交互环境或者.py文件时,需要sudo权限。

psutil还提供了一个test()函数,可以模拟出ps命令的效果:

$ sudo python3Password: ******Python 3.6.3 ... on darwinType "help", ... for more information.

>>> import psutil

>>> psutil.test()USER         PID %MEM     VSZ     RSS TTY           START    TIME  COMMANDroot           0 24.0 74270628 2016380 ?             Nov18   40:51  kernel_taskroot           1  0.1 2494140    9484 ?             Nov18   01:39  launchdroot          44  0.4 2519872   36404 ?             Nov18   02:02  UserEventAgentroot          45    ? 2474032    1516 ?             Nov18   00:14  syslogdroot          47  0.1 2504768    8912 ?             Nov18   00:03  kextdroot          48  0.1 2505544    4720 ?             Nov18   00:19  fseventsd

_appleeven    52  0.1 2499748    5024 ?             Nov18   00:00  appleeventsdroot          53  0.1 2500592    6132 ?             Nov18   00:02  configd

...

小结

psutil使得Python程序获取系统信息变得易如反掌。

psutil还可以获取用户信息、Windows服务等很多有用的系统信息,具体请参考psutil的官网:GitHub - giampaolo/psutil: Cross-platform lib for process and system monitoring in Python

venv

在开发Python应用程序的时候,系统安装的Python3只有一个版本:3.10。所有第三方的包都会被pip安装到Python3的site-packages目录下。

如果我们要同时开发多个应用程序,那这些应用程序都会共用一个Python,就是安装在系统的Python 3。如果应用A需要jinja 2.7,而应用B需要jinja 2.6怎么办?

这种情况下,每个应用可能需要各自拥有一套“独立”的Python运行环境。venv就是用来为一个应用创建一套“隔离”的Python运行环境。

首先,我们假定要开发一个新的项目project101,需要一套独立的Python运行环境,可以这么做:

第一步,创建目录,这里把venv命名为proj101env,因此目录名为proj101env

~$ mkdir proj101env

~$ cd proj101env/

proj101env$

第二步,创建一个独立的Python运行环境:

proj101env$ python3 -m venv .

查看当前目录,可以发现有几个文件夹和一个pyvenv.cfg文件:

proj101env$ ls

bin  include  lib  pyvenv.cfg

命令python3 -m venv <目录>就可以创建一个独立的Python运行环境。观察bin目录的内容,里面有python3pip3等可执行文件,实际上是链接到Python系统目录的软链接。

继续进入bin目录,Linux/Mac用source activate,Windows用activate.bat激活该venv环境:

proj101env$ cd bin

bin$ source activate

(proj101env) bin$

注意到命令提示符变了,有个(proj101env)前缀,表示当前环境是一个名为proj101env的Python环境。

下面正常安装各种第三方包,并运行python命令:

(proj101env) bin$ pip3 install jinja2

...

Successfully installed jinja2-xxx

(proj101env) bin$ python3>>> import jinja2>>> exit()

venv环境下,用pip安装的包都被安装到proj101env这个环境下,具体目录是proj101env/lib/python3.x/site-packages,因此,系统Python环境不受任何影响。也就是说,proj101env环境是专门针对project101这个应用创建的。

退出当前的proj101env环境,使用deactivate命令:

(proj101env) bin$ deactivate

bin$

此时就回到了正常的环境,现在pippython均是在系统Python环境下执行。

完全可以针对每个应用创建独立的Python运行环境,这样就可以对每个应用的Python环境进行隔离。

venv是如何创建“独立”的Python运行环境的呢?原理很简单,就是把系统Python链接或复制一份到venv的环境,用命令source activate进入一个venv环境时,venv会修改相关环境变量,让命令pythonpip均指向当前的venv环境。

如果不再使用某个venv,例如proj101env,删除它也很简单。首先确认该venv没有处于“激活”状态,然后直接把整个目录proj101env删掉就行。

小结

venv为应用提供了隔离的Python运行环境,解决了不同应用间多版本的冲突问题。

图形界面

Python支持多种图形界面的第三方库,包括:

  • Tk
  • wxWidgets
  • Qt
  • GTK

等等。

但是Python自带的库是支持Tk的Tkinter,使用Tkinter,无需安装任何包,就可以直接使用。本章简单介绍如何使用Tkinter进行GUI编程。

Tkinter

我们来梳理一下概念:

我们编写的Python代码会调用内置的Tkinter,Tkinter封装了访问Tk的接口;

Tk是一个图形库,支持多个操作系统,使用Tcl语言开发;

Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。

所以,我们的代码只需要调用Tkinter提供的接口就可以了。

第一个GUI程序

使用Tkinter十分简单,我们来编写一个GUI版本的“Hello, world!”。

第一步是导入Tkinter包的所有内容:

from tkinter import *

第二步是从Frame派生一个Application类,这是所有Widget的父容器:

class Application(Frame):

    def __init__(self, master=None):

        Frame.__init__(self, master)

        self.pack()

        self.createWidgets()

    def createWidgets(self):

        self.helloLabel = Label(self, text='Hello, world!')

        self.helloLabel.pack()

        self.quitButton = Button(self, text='Quit', command=self.quit)

        self.quitButton.pack()

在GUI中,每个Button、Label、输入框等,都是一个Widget。Frame则是可以容纳其他Widget的Widget,所有的Widget组合起来就是一棵树。

pack()方法把Widget加入到父容器中,并实现布局。pack()是最简单的布局,grid()可以实现更复杂的布局。

createWidgets()方法中,我们创建一个Label和一个Button,当Button被点击时,触发self.quit()使程序退出。

第三步,实例化Application,并启动消息循环:

app = Application()# 设置窗口标题:

app.master.title('Hello World')# 主消息循环:

app.mainloop()

GUI程序的主线程负责监听来自操作系统的消息,并依次处理每一条消息。因此,如果消息处理非常耗时,就需要在新线程中处理。

运行这个GUI程序,可以看到下面的窗口:

点击“Quit”按钮或者窗口的“x”结束程序。

输入文本

我们再对这个GUI程序改进一下,加入一个文本框,让用户可以输入文本,然后点按钮后,弹出消息对话框。

from tkinter import *import tkinter.messagebox as messagebox

class Application(Frame):

    def __init__(self, master=None):

        Frame.__init__(self, master)

        self.pack()

        self.createWidgets()

    def createWidgets(self):

        self.nameInput = Entry(self)

        self.nameInput.pack()

        self.alertButton = Button(self, text='Hello', command=self.hello)

        self.alertButton.pack()

    def hello(self):

        name = self.nameInput.get() or 'world'

        messagebox.showinfo('Message', 'Hello, %s' % name)

app = Application()# 设置窗口标题:

app.master.title('Hello World')# 主消息循环:

app.mainloop()

当用户点击按钮时,触发hello(),通过self.nameInput.get()获得用户输入的文本后,使用tkMessageBox.showinfo()可以弹出消息对话框。

程序运行结果如下:

小结

Python内置的Tkinter可以满足基本的GUI程序的要求,如果是非常复杂的GUI程序,建议用操作系统原生支持的语言和库来编写。

海龟绘图

在1966年,Seymour Papert和Wally Feurzig发明了一种专门给儿童学习编程的语言——LOGO语言,它的特色就是通过编程指挥一个小海龟(turtle)在屏幕上绘图。

海龟绘图(Turtle Graphics)后来被移植到各种高级语言中,Python内置了turtle库,基本上100%复制了原始的Turtle Graphics的所有功能。

我们来看一个指挥小海龟绘制一个长方形的简单代码:

# 导入turtle包的所有内容:from turtle import *

# 设置笔刷宽度:

width(4)

# 前进:

forward(200)# 右转90度:

right(90)

# 笔刷颜色:

pencolor('red')

forward(100)

right(90)

pencolor('green')

forward(200)

right(90)

pencolor('blue')

forward(100)

right(90)

# 调用done()使得窗口等待被关闭,否则将立刻关闭窗口:

done()

在命令行运行上述代码,会自动弹出一个绘图窗口,然后绘制出一个长方形:

从程序代码可以看出,海龟绘图就是指挥海龟前进、转向,海龟移动的轨迹就是绘制的线条。要绘制一个长方形,只需要让海龟前进、右转90度,反复4次。

调用width()函数可以设置笔刷宽度,调用pencolor()函数可以设置颜色。更多操作请参考turtle库的说明。

绘图完成后,记得调用done()函数,让窗口进入消息循环,等待被关闭。否则,由于Python进程会立刻结束,将导致窗口被立刻关闭。

turtle包本身只是一个绘图库,但是配合Python代码,就可以绘制各种复杂的图形。例如,通过循环绘制5个五角星:

from turtle import *

def drawStar(x, y):

    pu()

    goto(x, y)

    pd()

    # set heading: 0

    seth(0)

    for i in range(5):

        fd(40)

        rt(144)

for x in range(0, 250, 50):

    drawStar(x, 0)

done()

程序执行效果如下:

使用递归,可以绘制出非常复杂的图形。例如,下面的代码可以绘制一棵分型树:

from turtle import *

# 设置色彩模式是RGB:

colormode(255)

lt(90)

lv = 14

l = 120

s = 45

width(lv)

# 初始化RGB颜色:

r = 0

g = 0

b = 0

pencolor(r, g, b)

penup()

bk(l)

pendown()

fd(l)

def draw_tree(l, level):

    global r, g, b

    # save the current pen width

    w = width()

    # narrow the pen width

    width(w * 3.0 / 4.0)

    # set color:

    r = r + 1

    g = g + 2

    b = b + 3

    pencolor(r % 200, g % 200, b % 200)

    l = 3.0 / 4.0 * l

    lt(s)

    fd(l)

    if level < lv:

        draw_tree(l, level + 1)

    bk(l)

    rt(2 * s)

    fd(l)

    if level < lv:

        draw_tree(l, level + 1)

    bk(l)

    lt(s)

    # restore the previous pen width

    width(w)

speed("fastest")

draw_tree(l, 4)

done()

执行上述程序需要花费一定的时间,最后的效果如下:

网络编程

自从互联网诞生以来,现在基本上所有的程序都是网络程序,很少有单机版的程序了。

计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。

举个例子,当你使用浏览器访问新浪网时,你的计算机就和新浪的某台服务器通过互联网连接起来了,然后,新浪的服务器把网页内容作为数据通过互联网传输到你的电脑上。

由于你的电脑上可能不止浏览器,还有QQ、Skype、Dropbox、邮件客户端等,不同的程序连接的别的计算机也会不同,所以,更确切地说,网络通信是两台计算机上的两个进程之间的通信。比如,浏览器进程和新浪服务器上的某个Web服务进程在通信,而QQ进程是和腾讯的某个服务器上的某个进程在通信。​​​​​​​

网络编程对所有开发语言都是一样的,Python也不例外。用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。

本章我们将详细介绍Python网络编程的概念和最主要的两种网络类型的编程

TCP/IP简介

虽然大家现在对互联网很熟悉,但是计算机网络的出现比互联网要早很多。

计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容,这就好比一群人有的说英语,有的说中文,有的说德语,说同一种语言的人可以交流,不同的语言之间就不行了。

为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。Internet是由inter和net两个单词组合起来的,原意就是连接“网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。

因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。

通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。

IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。

IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。

IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334

TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。

许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。

一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。

端口有什么作用?在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个TCP报文来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。

一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。

了解了TCP/IP协议的基本概念,IP地址和端口的概念,我们就可以开始进行网络编程了。

​​​​​​​

TCP编程

Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。

客户端

大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。

所以,我们要创建一个基于TCP连接的Socket,可以这样做:

# 导入socket库:import socket

# 创建一个socket:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立连接:

s.connect(('www.sina', 80))

创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。

客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。新浪网站的IP地址可以用域名www.sina自动转换到IP地址,但是怎么知道新浪服务器的端口号呢?

答案是作为服务器,提供什么样的服务,端口号就必须固定下来。由于我们想要访问网页,因此新浪提供网页服务的服务器必须把端口号固定在80端口,因为80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。

因此,我们连接新浪服务器的代码如下:

s.connect(('www.sina', 80))

注意参数是一个tuple,包含地址和端口号。

建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:

# 发送数据:

s.send(b'GET / HTTP/1.1\r\nHost: www.sina\r\nConnection: close\r\n\r\n')

TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。

发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:

# 接收数据:

buffer = []while True:

    # 每次最多接收1k字节:

    d = s.recv(1024)

    if d:

        buffer.append(d)

    else:

        break

data = b''.join(buffer)

接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。

当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:

# 关闭连接:

s.close()

接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:

header, html = data.split(b'\r\n\r\n', 1)

print(header.decode('utf-8'))# 把接收的数据写入文件:with open('sina.html', 'wb') as f:

    f.write(html)

现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。

服务器

和客户端编程相比,服务器编程就要复杂一些。

服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。

所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。

但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。

我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。

首先,创建一个基于IPv4和TCP协议的Socket:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

然后,我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。

端口号需要预先指定。因为我们写的这个服务不是标准服务,所以用9999这个端口号。请注意,小于1024的端口号必须要有管理员权限才能绑定:

# 监听端口:

s.bind(('127.0.0.1', 9999))

紧接着,调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量:

s.listen(5)print('Waiting for connection...')

接下来,服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:

while True:

    # 接受一个新连接:

    sock, addr = s.accept()

    # 创建新线程来处理TCP连接:

    t = threading.Thread(target=tcplink, args=(sock, addr))

    t.start()

每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接:

def tcplink(sock, addr):

    print('Accept new connection from %s:%s...' % addr)

    sock.send(b'Welcome!')

    while True:

        data = sock.recv(1024)

        time.sleep(1)

        if not data or data.decode('utf-8') == 'exit':

            break

        sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))

    sock.close()

    print('Connection from %s:%s closed.' % addr)

连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。

要测试这个服务器程序,我们还需要编写一个客户端程序:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立连接:

s.connect(('127.0.0.1', 9999))# 接收欢迎消息:

print(s.recv(1024).decode('utf-8'))for data in [b'Michael', b'Tracy', b'Sarah']:

    # 发送数据:

    s.send(data)

    print(s.recv(1024).decode('utf-8'))

s.send(b'exit')

s.close()

我们需要打开两个命令行窗口,一个运行服务器程序,另一个运行客户端程序,就可以看到效果了:

┌────────────────────────────────────────────────────────┐
│Command Prompt                                    - □ x │
├────────────────────────────────────────────────────────┤
│$ python echo_server.py                                 │
│Waiting for connection...                               │
│Accept new connection from 127.0.0.1:64398...           │
│Connection from 127.0.0.1:64398 closed.                 │
│                                                        │
│       ┌────────────────────────────────────────────────┴───────┐
│       │Command Prompt                                    - □ x │
│       ├────────────────────────────────────────────────────────┤
│       │$ python echo_client.py                                 │
│       │Welcome!                                                │
│       │Hello, Michael!                                         │
└───────┤Hello, Tracy!                                           │
        │Hello, Sarah!                                           │
        │$                                                       │
        │                                                        │
        │                                                        │
        └────────────────────────────────────────────────────────┘

需要注意的是,客户端程序运行完毕就退出了,而服务器程序会永远运行下去,必须按Ctrl+C退出程序。

小结

用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。

同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。

UDP编程

TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。

使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。

虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。

我们来看看如何通过UDP协议传输数据。和TCP类似,使用UDP的通信双方也分为客户端和服务器。服务器首先需要绑定端口:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# 绑定端口:

s.bind(('127.0.0.1', 9999))

创建Socket时,SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据:

print('Bind UDP on 9999...')while True:

    # 接收数据:

    data, addr = s.recvfrom(1024)

    print('Received from %s:%s.' % addr)

    s.sendto(b'Hello, %s!' % data, addr)

recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。

注意这里省掉了多线程,因为这个例子很简单。

客户端使用UDP时,首先仍然创建基于UDP的Socket,然后,不需要调用connect(),直接通过sendto()给服务器发数据:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)for data in [b'Michael', b'Tracy', b'Sarah']:

    # 发送数据:

    s.sendto(data, ('127.0.0.1', 9999))

    # 接收数据:

    print(s.recv(1024).decode('utf-8'))

s.close()

从服务器接收数据仍然调用recv()方法。

仍然用两个命令行分别启动服务器和客户端测试,结果如下:

┌────────────────────────────────────────────────────────┐
│Command Prompt                                    - □ x │
├────────────────────────────────────────────────────────┤
│$ python udp_server.py                                  │
│Bind UDP on 9999...                                     │
│Received from 127.0.0.1:63823...                        │
│Received from 127.0.0.1:63823...                        │
│Received from 127.0.0.1:63823...                        │
│       ┌────────────────────────────────────────────────┴───────┐
│       │Command Prompt                                    - □ x │
│       ├────────────────────────────────────────────────────────┤
│       │$ python udp_client.py                                  │
│       │Welcome!                                                │
│       │Hello, Michael!                                         │
└───────┤Hello, Tracy!                                           │
        │Hello, Sarah!                                           │
        │$                                                       │
        │                                                        │
        │                                                        │
        └────────────────────────────────────────────────────────┘

小结

UDP的使用与TCP类似,但是不需要建立连接。此外,服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。

电子邮件

Email的历史比Web还要久远,直到现在,Email也是互联网上应用非常广泛的服务。

几乎所有的编程语言都支持发送和接收电子邮件,但是,先等等,在我们开始编写代码之前,有必要搞清楚电子邮件是如何在互联网上运作的。

我们来看看传统邮件是如何运作的。假设你现在在北京,要给一个香港的朋友发一封信,怎么做呢?

首先你得写好信,装进信封,写上地址,贴上邮票,然后就近找个邮局,把信仍进去。

信件会从就近的小邮局转运到大邮局,再从大邮局往别的城市发,比如先发到天津,再走海运到达香港,也可能走京九线到香港,但是你不用关心具体路线,你只需要知道一件事,就是信件走得很慢,至少要几天时间。

信件到达香港的某个邮局,也不会直接送到朋友的家里,因为邮局的叔叔是很聪明的,他怕你的朋友不在家,一趟一趟地白跑,所以,信件会投递到你的朋友的邮箱里,邮箱可能在公寓的一层,或者家门口,直到你的朋友回家的时候检查邮箱,发现信件后,就可以取到邮件了。

电子邮件的流程基本上也是按上面的方式运作的,只不过速度不是按天算,而是按秒算。

现在我们回到电子邮件,假设我们自己的电子邮件地址是me@163,对方的电子邮件地址是friend@sina(注意地址都是虚构的哈),现在我们用Outlook或者Foxmail之类的软件写好邮件,填上对方的Email地址,点“发送”,电子邮件就发出去了。这些电子邮件软件被称为MUA:Mail User Agent——邮件用户代理。

Email从MUA发出去,不是直接到达对方电脑,而是发到MTA:Mail Transfer Agent——邮件传输代理,就是那些Email服务提供商,比如网易、新浪等等。由于我们自己的电子邮件是163,所以,Email首先被投递到网易提供的MTA,再由网易的MTA发到对方服务商,也就是新浪的MTA。这个过程中间可能还会经过别的MTA,但是我们不关心具体路线,我们只关心速度。

Email到达新浪的MTA后,由于对方使用的是@sina的邮箱,因此,新浪的MTA会把Email投递到邮件的最终目的地MDA:Mail Delivery Agent——邮件投递代理。Email到达MDA后,就静静地躺在新浪的某个服务器上,存放在某个文件或特殊的数据库里,我们将这个长期保存邮件的地方称之为电子邮箱。

同普通邮件类似,Email不会直接到达对方的电脑,因为对方电脑不一定开机,开机也不一定联网。对方要取到邮件,必须通过MUA从MDA上把邮件取到自己的电脑上。

所以,一封电子邮件的旅程就是:

发件人 -> MUA -> MTA -> MTA -> 若干个MTA -> MDA <- MUA <- 收件人

有了上述基本概念,要编写程序来发送和接收邮件,本质上就是:

  1. 编写MUA把邮件发到MTA;
  2. 编写MUA从MDA上收邮件。
  3. 发邮件时,MUA和MTA使用的协议就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一个MTA也是用SMTP协议。​​​​​​​
  4. 收邮件时,MUA和MDA使用的协议有两种:POP:Post Office Protocol,目前版本是3,俗称POP3;IMAP:Internet Message Access Protocol,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱,等等。

邮件客户端软件在发邮件时,会让你先配置SMTP服务器,也就是你要发到哪个MTA上。假设你正在使用163的邮箱,你就不能直接发到新浪的MTA上,因为它只服务新浪的用户,所以,你得填163提供的SMTP服务器地址:smtp.163,为了证明你是163的用户,SMTP服务器还要求你填写邮箱地址和邮箱口令,这样,MUA才能正常地把Email通过SMTP协议发送到MTA。

类似的,从MDA收邮件时,MDA服务器也要求验证你的邮箱口令,确保不会有人冒充你收取你的邮件,所以,Outlook之类的邮件客户端会要求你填写POP3或IMAP服务器地址、邮箱地址和口令,这样,MUA才能顺利地通过POP或IMAP协议从MDA取到邮件。

在使用Python收发邮件前,请先准备好至少两个电子邮件,如xxx@163xxx@sinaxxx@qq等,注意两个邮箱不要用同一家邮件服务商。

最后特别注意,目前大多数邮件服务商都需要手动打开SMTP发信和POP收信的功能,否则只允许在网页登录。

​​​​​​​

SMTP发送邮件

SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。

Python对SMTP支持有smtplibemail两个模块,email负责构造邮件,smtplib负责发送邮件。

首先,我们来构造一个最简单的纯文本邮件:

from email.mime.text import MIMEText

msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')

注意到构造MIMEText对象时,第一个参数就是邮件正文,第二个参数是MIME的subtype,传入'plain'表示纯文本,最终的MIME就是'text/plain',最后一定要用utf-8编码保证多语言兼容性。

然后,通过SMTP发出去:

# 输入Email地址和口令:

from_addr = input('From: ')

password = input('Password: ')# 输入收件人地址:

to_addr = input('To: ')# 输入SMTP服务器地址:

smtp_server = input('SMTP server: ')

import smtplib

server = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25

server.set_debuglevel(1)

server.login(from_addr, password)

server.sendmail(from_addr, [to_addr], msg.as_string())

server.quit()

我们用set_debuglevel(1)就可以打印出和SMTP服务器交互的所有信息。SMTP协议就是简单的文本命令和响应。login()方法用来登录SMTP服务器,sendmail()方法就是发邮件,由于可以一次发给多个人,所以传入一个list,邮件正文是一个stras_string()MIMEText对象变成str

如果一切顺利,就可以在收件人信箱中收到我们刚发送的Email:

仔细观察,发现如下问题:

  1. 邮件没有主题;
  2. 收件人的名字没有显示为友好的名字,比如Mr Green <green@example>
  3. 明明收到了邮件,却提示不在收件人中。

这是因为邮件主题、如何显示发件人、收件人等信息并不是通过SMTP协议发给MTA,而是包含在发给MTA的文本中的,所以,我们必须把FromToSubject添加到MIMEText中,才是一封完整的邮件:

from email import encodersfrom email.header import Headerfrom email.mime.text import MIMETextfrom email.utils import parseaddr, formataddr

import smtplib

    def _format_addr(s):

    name, addr = parseaddr(s)

    return formataddr((Header(name, 'utf-8').encode(), addr))

from_addr = input('From: ')

password = input('Password: ')

to_addr = input('To: ')

smtp_server = input('SMTP server: ')

msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')

msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)

msg['To'] = _format_addr('管理员 <%s>' % to_addr)

msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()

    

server = smtplib.SMTP(smtp_server, 25)

server.set_debuglevel(1)

server.login(from_addr, password)

server.sendmail(from_addr, [to_addr], msg.as_string())

server.quit()

我们编写了一个函数_format_addr()来格式化一个邮件地址。注意不能简单地传入name <addr@example>,因为如果包含中文,需要通过Header对象进行编码。

msg['To']接收的是字符串而不是list,如果有多个邮件地址,用,分隔即可。

再发送一遍邮件,就可以在收件人邮箱中看到正确的标题、发件人和收件人:

​​​​​​​

你看到的收件人的名字很可能不是我们传入的管理员,因为很多邮件服务商在显示邮件时,会把收件人名字自动替换为用户注册的名字,但是其他收件人名字的显示不受影响。

如果我们查看Email的原始内容,可以看到如下经过编码的邮件头:

From: =?utf-8?b?UHl0aG9u54ix5aW96ICF?= <xxxxxx@163>To: =?utf-8?b?566h55CG5ZGY?= <xxxxxx@qq>Subject: =?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg==?=

这就是经过Header对象编码的文本,包含utf-8编码信息和Base64编码的文本。如果我们自己来手动构造这样的编码文本,显然比较复杂。

发送HTML邮件

如果我们要发送HTML邮件,而不是普通的纯文本文件怎么办?方法很简单,在构造MIMEText对象时,把HTML字符串传进去,再把第二个参数由plain变为html就可以了:

msg = MIMEText('<html><body><h1>Hello</h1>' +

    '<p>send by <a href="http://www.python">Python</a>...</p>' +

    '</body></html>', 'html', 'utf-8')

再发送一遍邮件,你将看到以HTML显示的邮件

发送附件

如果Email中要加上附件怎么办?带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可:

# 邮件对象:

msg = MIMEMultipart()

msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)

msg['To'] = _format_addr('管理员 <%s>' % to_addr)

msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()

# 邮件正文是MIMEText:

msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))

# 添加附件就是加上一个MIMEBase,从本地读取一个图片:with open('/Users/michael/Downloads/test.png', 'rb') as f:

    # 设置附件的MIME和文件名,这里是png类型:

    mime = MIMEBase('image', 'png', filename='test.png')

    # 加上必要的头信息:

    mime.add_header('Content-Disposition', 'attachment', filename='test.png')

    mime.add_header('Content-ID', '<0>')

    mime.add_header('X-Attachment-Id', '0')

    # 把附件的内容读进来:

    mime.set_payload(f.read())

    # 用Base64编码:

    encoders.encode_base64(mime)

    # 添加到MIMEMultipart:

    msg.attach(mime)

然后,按正常发送流程把msg(注意类型已变为MIMEMultipart)发送出去,就可以收到如下带附件的邮件

发送图片

如果要把一个图片嵌入到邮件正文中怎么做?直接在HTML邮件中链接图片地址行不行?答案是,大部分邮件服务商都会自动屏蔽带有外链的图片,因为不知道这些链接是否指向恶意网站。

要把图片嵌入到邮件正文中,我们只需按照发送附件的方式,先把邮件作为附件添加进去,然后,在HTML中通过引用src="cid:0"就可以把附件作为图片嵌入了。如果有多个图片,给它们依次编号,然后引用不同的cid:x即可。

把上面代码加入MIMEMultipartMIMETextplain改为html,然后在适当的位置引用图片:

msg.attach(MIMEText('<html><body><h1>Hello</h1>' +

    '<p><img src="cid:0"></p>' +

    '</body></html>', 'html', 'utf-8'))

再次发送,就可以看到图片直接嵌入到邮件正文的效果

同时支持HTML和Plain格式

如果我们发送HTML邮件,收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的,但是,如果收件人使用的设备太古老,查看不了HTML邮件怎么办?

办法是在发送HTML的同时再附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。

利用MIMEMultipart就可以组合一个HTML和Plain,要注意指定subtype是alternative

msg = MIMEMultipart('alternative')

msg['From'] = ...

msg['To'] = ...

msg['Subject'] = ...

msg.attach(MIMEText('hello', 'plain', 'utf-8'))

msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))

# 正常发送msg对象...

加密SMTP

使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上就是先创建SSL安全连接,然后再使用SMTP协议发送邮件。

某些邮件服务商,例如Gmail,提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全SMTP发送邮件。

必须知道,Gmail的SMTP端口是587,因此,修改代码如下:

smtp_server = 'smtp.gmail'

smtp_port = 587

server = smtplib.SMTP(smtp_server, smtp_port)

server.starttls()# 剩下的代码和前面的一模一样:

server.set_debuglevel(1)

...

只需要在创建SMTP对象后,立刻调用starttls()方法,就创建了安全连接。后面的代码和前面的发送邮件代码完全一样。

如果因为网络问题无法连接Gmail的SMTP服务器,请相信我们的代码是没有问题的,你需要对你的网络设置做必要的调整。

小结

使用Python的smtplib发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出。

构造一个邮件对象就是一个Messag对象,如果构造一个MIMEText对象,就表示一个文本邮件对象,如果构造一个MIMEImage对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart对象,而MIMEBase可以表示任何对象。它们的继承关系如下:

Message

+- MIMEBase

   +- MIMEMultipart

   +- MIMENonMultipart

      +- MIMEMessage

      +- MIMEText

      +- MIMEImage

这种嵌套关系就可以构造出任意复杂的邮件。你可以通过email.mime文档查看它们所在的包以及详细的用法。

POP3收取邮件

SMTP用于发送邮件,如果要收取邮件呢?

收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议,目前版本号是3,俗称POP3

Python内置一个poplib模块,实现了POP3协议,可以直接用来收邮件。

注意到POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。

要把POP3收取的文本变成可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。

所以,收取邮件分两步:

第一步:用poplib把邮件的原始文本下载到本地;

第二部:用email解析原始文本,还原为邮件对象。

通过POP3下载邮件

POP3协议本身很简单,以下面的代码为例,我们来获取最新的一封邮件内容:

import poplib

# 输入邮件地址, 口令和POP3服务器地址:

email = input('Email: ')

password = input('Password: ')

pop3_server = input('POP3 server: ')

# 连接到POP3服务器:

server = poplib.POP3(pop3_server)# 可以打开或关闭调试信息:

server.set_debuglevel(1)# 可选:打印POP3服务器的欢迎文字:print(server.getwelcome().decode('utf-8'))

# 身份认证:

server.user(email)

server.pass_(password)

# stat()返回邮件数量和占用空间:print('Messages: %s. Size: %s' % server.stat())# list()返回所有邮件的编号:

resp, mails, octets = server.list()# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...]print(mails)

# 获取最新一封邮件, 注意索引号从1开始:

index = len(mails)

resp, lines, octets = server.retr(index)

# lines存储了邮件的原始文本的每一行,# 可以获得整个邮件的原始文本:

msg_content = b'\r\n'.join(lines).decode('utf-8')# 稍后解析出邮件:

msg = Parser().parsestr(msg_content)

# 可以根据邮件索引号直接从服务器删除邮件:# server.dele(index)# 关闭连接:

server.quit()

用POP3获取邮件其实很简单,要获取所有邮件,只需要循环使用retr()把每一封邮件内容拿到即可。真正麻烦的是把邮件的原始内容解析为可以阅读的邮件对象。

解析邮件

解析邮件的过程和上一节构造邮件正好相反,因此,先导入必要的模块:

from email.parser import Parserfrom email.header import decode_headerfrom email.utils import parseaddr

import poplib

只需要一行代码就可以把邮件内容解析为Message对象:

msg = Parser().parsestr(msg_content)

但是这个Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。

所以我们要递归地打印出Message对象的层次结构:

# indent用于缩进显示:def print_info(msg, indent=0):

    if indent == 0:

        for header in ['From', 'To', 'Subject']:

            value = msg.get(header, '')

            if value:

                if header=='Subject':

                    value = decode_str(value)

                else:

                    hdr, addr = parseaddr(value)

                    name = decode_str(hdr)

                    value = u'%s <%s>' % (name, addr)

            print('%s%s: %s' % ('  ' * indent, header, value))

    if (msg.is_multipart()):

        parts = msg.get_payload()

        for n, part in enumerate(parts):

            print('%spart %s' % ('  ' * indent, n))

            print('%s--------------------' % ('  ' * indent))

            print_info(part, indent + 1)

    else:

        content_type = msg.get_content_type()

        if content_type=='text/plain' or content_type=='text/html':

            content = msg.get_payload(decode=True)

            charset = guess_charset(msg)

            if charset:

                content = content.decode(charset)

            print('%sText: %s' % ('  ' * indent, content + '...'))

        else:

            print('%sAttachment: %s' % ('  ' * indent, content_type))

邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode:

def decode_str(s):

    value, charset = decode_header(s)[0]

    if charset:

        value = value.decode(charset)

    return value

decode_header()返回一个list,因为像CcBcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码我们偷了个懒,只取了第一个元素。

文本邮件的内容也是str,还需要检测编码,否则,非UTF-8编码的邮件都无法正常显示:

def guess_charset(msg):

    charset = msg.get_charset()

    if charset is None:

        content_type = msg.get('Content-Type', '').lower()

        pos = content_type.find('charset=')

        if pos >= 0:

            charset = content_type[pos + 8:].strip()

    return charset

把上面的代码整理好,我们就可以来试试收取一封邮件。先往自己的邮箱发一封邮件,然后用浏览器登录邮箱,看看邮件收到没,如果收到了,我们就来用Python程序把它收到本地

运行程序,结果如下:

+OK Welcome to coremail Mail Pop3 Server (163coms[...])

Messages: 126. Size: 27228317

From: Test <xxxxxx@qq>

To: Python爱好者 <xxxxxx@163>

Subject: 用POP3收取邮件

part 0

--------------------

  part 0

  --------------------

    Text: Python可以使用POP3收取邮件……...

  part 1

  --------------------

    Text: Python可以<a href="...">使用POP3</a>收取邮件……...

part 1

--------------------

  Attachment: application/octet-stream

我们从打印的结构可以看出,这封邮件是一个MIMEMultipart,它包含两部分:第一部分又是一个MIMEMultipart,第二部分是一个附件。而内嵌的MIMEMultipart是一个alternative类型,它包含一个纯文本格式的MIMEText和一个HTML格式的MIMEText

小结

用Python的poplib模块收取邮件分两步:第一步是用POP3协议把邮件获取到本地,第二步是用email模块把原始邮件解析为Message对象,然后,用适当的形式把邮件内容展示给用户即可。​​​​​​​

访问数据库

程序运行的时候,数据都是在内存中的。当程序终止的时候,通常都需要将数据保存到磁盘上,无论是保存到本地磁盘,还是通过网络保存到服务器上,最终都会将数据写入磁盘文件。

而如何定义数据的存储格式就是一个大问题。如果我们自己来定义存储格式,比如保存一个班级所有学生的成绩单:

名字

成绩

Michael

99

Bob

85

Bart

59

Lisa

87

你可以用一个文本文件保存,一行保存一个学生,用,隔开:

Michael,99

Bob,85

Bart,59

Lisa,87

你还可以用JSON格式保存,也是文本文件:

[

    {"name":"Michael","score":99},

    {"name":"Bob","score":85},

    {"name":"Bart","score":59},

    {"name":"Lisa","score":87}

]

你还可以定义各种保存格式,但是问题来了:

存储和读取需要自己实现,JSON还是标准,自己定义的格式就各式各样了;

不能做快速查询,只有把数据全部读到内存中才能自己遍历,但有时候数据的大小远远超过了内存(比如蓝光电影,40GB的数据),根本无法全部读入内存。

为了便于程序保存和读取数据,而且,能直接通过条件快速查询到指定的数据,就出现了数据库(Database)这种专门用于集中存储和查询的软件。

数据库软件诞生的历史非常久远,早在1950年数据库就诞生了。经历了网状数据库,层次数据库,我们现在广泛使用的关系数据库是20世纪70年代基于关系模型的基础上诞生的。

根据某个年级的ID就可以查找所有班级的行,这种查询语句在关系数据库中称为SQL语句,可以写成:

SELECT * FROM classes WHERE grade_id = '1';

结果也是一个表:

---------+----------+----------

grade_id | class_id | name---------+----------+----------

1        | 11       | 一年级一班---------+----------+----------

1        | 12       | 一年级二班---------+----------+----------

1        | 13       | 一年级三班---------+----------+----------

类似的,Class表的一行记录又可以关联到Student表的多行记录

由于本教程不涉及到关系数据库的详细内容,如果你想从零学习关系数据库和基本的SQL语句,如果你想从零学习关系数据库和基本的SQL语句,请参考SQL教程。

NoSQL

你也许还听说过NoSQL数据库,很多NoSQL宣传其速度和规模远远超过关系数据库,所以很多同学觉得有了NoSQL是否就不需要SQL了呢?千万不要被他们忽悠了,连SQL都不明白怎么可能搞明白NoSQL呢?

数据库类别

既然我们要使用关系数据库,就必须选择一个关系数据库。目前广泛使用的关系数据库也就这么几种:

付费的商用数据库:

  • Oracle,典型的高富帅;
  • SQL Server,微软自家产品,Windows定制专款;
  • DB2,IBM的产品,听起来挺高端;
  • Sybase,曾经跟微软是好基友,后来关系破裂,现在家境惨淡。

这些数据库都是不开源而且付费的,最大的好处是花了钱出了问题可以找厂家解决,不过在Web的世界里,常常需要部署成千上万的数据库服务器,当然不能把大把大把的银子扔给厂家,所以,无论是Google、Facebook,还是国内的BAT,无一例外都选择了免费的开源数据库:

  • MySQL,大家都在用,一般错不了;
  • PostgreSQL,学术气息有点重,其实挺不错,但知名度没有MySQL高;
  • sqlite,嵌入式数据库,适合桌面和移动应用。

作为Python开发工程师,选择哪个免费数据库呢?当然是MySQL。因为MySQL普及率最高,出了错,可以很容易找到解决方法。而且,围绕MySQL有一大堆监控和运维的工具,安装和使用很方便。

为了能继续后面的学习,你需要从MySQL官方网站下载并安装MySQL Community Server 5.6,这个版本是免费的,其他高级版本是要收钱的(请放心,收钱的功能我们用不上)。

使用SQLite

SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。

Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。

在使用SQLite前,我们先要搞清楚几个概念:

表是数据库中存放关系数据的集合,一个数据库里面通常都包含多个表,比如学生的表,班级的表,学校的表,等等。表和表之间通过外键关联。

要操作关系数据库,首先需要连接到数据库,一个数据库连接称为Connection

连接到数据库后,需要打开游标,称之为Cursor,通过Cursor执行SQL语句,然后,获得执行结果。

Python定义了一套操作数据库的API接口,任何数据库要连接到Python,只需要提供符合Python标准的数据库驱动即可。

由于SQLite的驱动内置在Python标准库中,所以我们可以直接来操作SQLite数据库。

我们在Python交互式命令行实践一下:

# 导入SQLite驱动:>>> import sqlite3# 连接到SQLite数据库# 数据库文件是test.db# 如果文件不存在,会自动在当前目录创建:>>> conn = sqlite3.connect('test.db')# 创建一个Cursor:>>> cursor = conn.cursor()# 执行一条SQL语句,创建user表:>>> cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')

<sqlite3.Cursor object at 0x10f8aa260># 继续执行一条SQL语句,插入一条记录:>>> cursor.execute('insert into user (id, name) values (\'1\', \'Michael\')')

<sqlite3.Cursor object at 0x10f8aa260># 通过rowcount获得插入的行数:>>> cursor.rowcount1# 提交事务:>>> connmit()# 关闭Cursor:>>> cursor.close()# 关闭Connection:>>> conn.close()

我们再试试查询记录:

>>> conn = sqlite3.connect('test.db')>>> cursor = conn.cursor()# 执行查询语句:>>> cursor.execute('select * from user where id=?', ('1',))

<sqlite3.Cursor object at 0x10f8aa340># 获得查询结果集:>>> values = cursor.fetchall()>>> values

[('1', 'Michael')]>>> cursor.close()>>> conn.close()

使用Python的DB-API时,只要搞清楚ConnectionCursor对象,打开后一定记得关闭,就可以放心地使用。

使用Cursor对象执行insertupdatedelete语句时,执行结果由rowcount返回影响的行数,就可以拿到执行结果。

使用Cursor对象执行select语句时,通过fetchall()可以拿到结果集。结果集是一个list,每个元素都是一个tuple,对应一行记录。

如果SQL语句带有参数,那么需要把参数按照位置传递给execute()方法,有几个?占位符就必须对应几个参数,例如:

cursor.execute('select * from user where name=? and pwd=?', ('abc', 'password'))

SQLite支持常见的标准SQL语句以及几种常见的数据类型。具体文档请参阅SQLite官方网站。

小结

在Python中操作数据库时,要先导入数据库对应的驱动,然后,通过Connection对象和Cursor对象操作数据。

要确保打开的Connection对象和Cursor对象都正确地被关闭,否则,资源就会泄露。

如何才能确保出错的情况下也关闭掉Connection对象和Cursor对象呢?请回忆try:...except:...finally:...的用法。

练习

请编写函数,在Sqlite中根据分数段查找指定的名字:

# -*- coding: utf-8 -*-

import os, sqlite3

db_file = os.path.join(os.path.dirname(__file__), 'test.db')

if os.path.isfile(db_file):

    os.remove(db_file)

# 初始数据:

conn = sqlite3.connect(db_file)

cursor = conn.cursor()

cursor.execute('create table user(id varchar(20) primary key, name varchar(20), score int)')

cursor.execute(r"insert into user values ('A-001', 'Adam', 95)")

cursor.execute(r"insert into user values ('A-002', 'Bart', 62)")

cursor.execute(r"insert into user values ('A-003', 'Lisa', 78)")

connmit()

cursor.close()

conn.close()

def get_score_in(low, high):

    ' 返回指定分数区间的名字,按分数从低到高排序 '

----

    pass

----

# 测试:

assert get_score_in(80, 95) == ['Adam'], get_score_in(80, 95)

assert get_score_in(60, 80) == ['Bart', 'Lisa'], get_score_in(60, 80)

assert get_score_in(60, 100) == ['Bart', 'Lisa', 'Adam'], get_score_in(60, 100)

print('Pass')

本文标签: 教程Python