Python-网络自动化入门指南-三-

80 阅读1小时+

Python 网络自动化入门指南(三)

原文:Introduction to Python Network Automation

协议:CC BY-NC-SA 4.0

八、Linux 基本管理

有很多关于 Linux 主题的书,其中一些书虽然长达数百页,但只关注一个 Linux 发行版。从上一章继续,我们将在这一章讨论最基本的 Linux 主题,这样你就可以看完这本书。如果你是 Linux 的新手,想了解更多关于 Linux 操作系统(OS)的知识,那么使用一本专门的 Linux 书籍是个不错的主意。您将学习查找 Linux 系统信息,使用 Linux 网络命令,然后在 CentOS 8.1 服务器上安装 TFTP、FTP、SFTP 和 NTP 服务。

img/492721_1_En_8_Figa_HTML.jpg

Linux 信息:内核和发行版本

大多数新的 Linux 用户都会提出第一个问题,“我如何检查确切的 Linux 发行版本和内核?”Linux 内核是 Linux 操作系统的主要组成部分,是计算机硬件和进程之间的核心接口。你如何在你的操作系统上检查 Linux 内核和发行版本?本章中的练习基于 CentOS(一种基于 Red Hat 的操作系统),由于不同 Linux 发行版的命令语法,使用的一些命令可能无法在基于 Debian 的操作系统(Ubuntu)上工作。如果是这样的话,那么你就会被提醒这样的区别。然而,跨不同 Linux 发行版的大多数 Linux 命令都是相似的。

| ① | 如何检查 Linux 内核版本?使用带有不同选项的`uname`命令;CentOS 和 Ubuntu 上使用的命令与`uname`命令相同。`For CentOS 8.1``[root@centos8s1 ~]# uname``Linux``[root@centos8s1 ~]# uname -o``GNU/Linux``[root@centos8s1 ~]# uname -r``4.18.0-193.14.2.el8_2.x86_64`[root@centos8s1 ~]# `uname -or``4.18.0-193.14.2.el8_2.x86_64 GNU/Linux``For Ubuntu 20.04 LTS``root@ubuntu20s1:~# uname``Linux``root@ubuntu20s1:~# uname -o``GNU/Linux``root@ubuntu20s1:~# uname -r``5.4.0-47-generic``root@ubuntu20s1:~# uname -or``5.4.0-47-generic GNU/Linux` | | ② | 下面的练习是一个额外的`uname`命令练习,它通过名称将空终止的操作系统信息字符串存储到结构化引用中。使用不同的选项运行`uname`命令。注意,同样的命令可以在 Ubuntu 20.04 LTS 服务器上使用。`[root@centos8s1 ~]# uname –a` `# -a option for all``Linux centos8s1 4.18.0-193.14.2.el8_2.x86_64 #1 SMP Sun Jul 26 03:54:29 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux``[root@centos8s1 ~]# uname -n` `# -n for name or hostname``centos8s1``[root@centos8s1 ~]# uname -r` `# -r for Linux kernel version``4.18.0-193.14.2.el8_2.x86_64``[root@centos8s1 ~]# uname -m` `# -m for system architecture``x86_64``[root@centos8s1 ~]# uname -v` `# -v for system time and timezone``#1 SMP Sun Jul 26 03:54:29 UTC 2020``[root@centos8s1 ~]# uname -rnmv` `# combined options``centos8s1 4.18.0-193.14.2.el8_2.x86_64 #1 SMP Sun Jul 26 03:54:29 UTC 2020 x86_64` | | ③ | 在基于 CentOS 和 Red Hat 的 Linux 操作系统上,您还可以使用`grep`命令来检查 Grub 环境块。使用`grep saved_entry /boot/grub2/grubenv`命令。这个命令在基于 Debian 的 Linux 发行版上不起作用。`For CentOS 8.1``[root@centos8s1 ~]# grep saved_entry /boot/grub2/grubenv``saved_entry=e53c131bb54c4fb1a835f3aa435024e2-4.18.0-193.14.2.el8_2.x86_64` | | ④ | 使用`hostnamectl`来检查主机名、操作系统、内核版本、架构等等。`For CentOS 8.1``[root@centos8s1 ~]# hostnamectl``Static hostname: centos8s1``Icon name: computer-vm``Chassis: vm``Machine ID: e53c131bb54c4fb1a835f3aa435024e2``Boot ID: 648950d070634d3e856b015f4a7cf7cd``Virtualization: vmware``Operating System: CentOS Linux 8 (Core)``CPE OS Name: cpe:/o:centos:centos:8``Kernel: Linux 4.18.0-193.14.2.el8_2.x86_64``Architecture: x86-64``For Ubuntu 20.04 LTS``root@ubuntu20s1:~# hostnamectl``Static hostname: ubuntu20s1``Icon name: computer-vm``Chassis: vm``Machine ID: ea202235f9834d979436db10d89b1347``Boot ID: 9bcbe4f6d00e407a81634dd61d74ff39``Virtualization: vmware``Operating System: Ubuntu 20.04.1 LTS``Kernel: Linux 5.4.0-47-generic``Architecture: x86-64` | | ⑤号 | 您也可以使用`lsb_release –d`命令来检查您的操作系统。与 Ubuntu OS 不同,CentOS 没有预装`lsb`。你可以通过使用`yum install –y redhat-lsb`命令或者运行`lsb_release –d`来安装它,当出现提示时,说 Y 来安装。`For CentOS 8.1``[root@centos8s1 ~]# lsb_release -d``[root@centos8s1 ~]# yum install -y redhat-lsb``[root@centos8s1 ~]# lsb_release -d``Description:    CentOS Linux release 8.2.2004 (Core)`您还可以使用`–sc`和`–a`选项来查看关于您的 Linux 操作系统的更多信息。`For Ubuntu 20.04 LTS``root@ubuntu20s1:~# lsb_release -d``Description:    Ubuntu 20.04.1 LTS``root@ubuntu20s1:~# lsb_release -sc``focal``root@ubuntu20s1:~# lsb_release -a``No LSB modules are available.``Distributor ID: Ubuntu``Description:    Ubuntu 20.04.1 LTS``Release:        20.04``Codename:       focal` | | ⑥ | 也可以使用`cat`命令输出`/etc/`目录下的信息。注意,这在 CentOS 上有效,但在 Ubuntu 上*无效,所以查看系统信息的更好方法是使用带有通配符`*`的`cat`。见下一个练习。*`For CentOS 8.1``[root@centos8s1 ~]# cat /etc/centos-release``CentOS Linux release 8.2.2004 (Core)``[root@centos8s1 ~]# cat /etc/system-release``CentOS Linux release 8.2.2004 (Core)` | | ① | 要检查您的 Linux 发行版的版本信息,请使用`cat /etc/*release`命令。这个`cat`命令是一个值得记住的 Linux 命令,因为它提供了关于您的 Linux 操作系统的大量信息。这个命令在 CentOS 和 Ubuntu Linux 操作系统上都有效。`[root@centos8s1 ~]# cat /etc/*release``CentOS Linux release 8.2.2004 (Core)``NAME="CentOS Linux"``VERSION="8 (Core)"``ID="centos"``ID_LIKE="rhel fedora"``VERSION_ID="8"``PLATFORM_ID="platform:el8"``PRETTY_NAME="CentOS Linux 8 (Core)"``ANSI_COLOR="0;31"``CPE_NAME="cpe:/o:centos:centos:8"``HOME_URL="``https://www.centos.org/``BUG_REPORT_URL="``https://bugs.centos.org/``CENTOS_MANTISBT_PROJECT="CentOS-8"``CENTOS_MANTISBT_PROJECT_VERSION="8"``REDHAT_SUPPORT_PRODUCT="centos"``REDHAT_SUPPORT_PRODUCT_VERSION="8"``CentOS Linux release 8.2.2004 (Core)``CentOS Linux release 8.2.2004 (Core)`此外,尝试运行`cat /etc/os-release`命令并检查差异。 |

关于 Linux 的信息:使用 netstat 命令验证 TCP/UDP 端口

为了让您为 IP 服务安装做好准备,让您的 Linux 服务器对我们的实验室有用,并在生产中应用您所学的知识,您首先必须知道如何使用一些 Linux 网络工具。所有设备都运行在某个网段上,并连接在网络上;因此,我们必须知道哪些端口是打开的,哪些是关闭的。换句话说,哪些 IP 服务正在运行并可供您的用户使用是服务器网络安全的一部分,它可以是网络访问控制的一部分。让我们看看如何在 Linux 系统上检查网络配置。

| ① | 首先,要获得 Linux 服务器上所有网络接口的列表,使用`netstat –i`。`[root@centos8s1 ~]# netstat -i``Kernel Interface table` | 国际会计师联合会 | 移动式测试装置(Mobile Test Unit) | RX-正常 | 接收错误 | RX-DRP | rx-UFO | tx-好的 | TX-ERR | TX-DRP | TX-OVR(消歧义) | Flg | | ens160 | One thousand five hundred | Five thousand six hundred and one | Zero | Zero | Zero | Two thousand five hundred and eighty-seven | Zero | Zero | Zero | BMRU | | 我会的 | Sixty-five thousand five hundred and thirty-six | Zero | Zero | Zero | Zero | Zero | Zero | Zero | Zero | 最近最少使用算法 | | virbr0 | One thousand five hundred | Zero | Zero | Zero | Zero | Zero | Zero | Zero | Zero | 电池管理单元|

| | ② | 为了练习我们的netstat命令,让我们在您的 CentOS 服务器上安装 nginx web 服务服务器并启动 web 服务。启动 Nginx web 服务后,运行netstat –tulpn : grep :80命令确认您的 CentOS 服务器正在本地接口的端口 80 上进行监听。对于 CentOS 8.1,安装并启动 nginx web 服务器。[root@centos8s1 ~]# yum install -y nginx``[root@centos8s1 ~]# whatis nginx``nginx (3pm)          - Perl interface to the nginx HTTP server API``nginx (8)            - "HTTP and reverse proxy server, mail proxy server"``[root@centos8s1 ~]# whereis nginx``nginx: /usr/sbin/nginx /usr/lib64/nginx /etc/nginx /usr/share/nginx /usr/share/man/man3/nginx.3pm.gz /usr/share/man/man8/nginx.8.gz``[root@centos8s1 ~]# systemctl start nginx``[root@centos8s1 ~]# netstat -tulpn &#124; grep :80``tcp     0      0 0.0.0.0:80       0.0.0.0:*        LISTEN      6046/nginx: master``tcp6    0      0 :::80                 :::*        LISTEN      6046/nginx: masterAt this point, if you log into your CentOS desktop through VMware’s main console, you will be able to open the Nginx home page in the Firefox web browser using http://localhost or http://192.168.183.130. See Figure 8-1.img/492721_1_En_8_Fig1_HTML.jpg图 8-1。CenOS 桌面,打开 http://192.168.183.130 | | ③ | 要永久启用端口 80 上的 HTTP 连接并允许其他机器通过 HTTP 连接,请将 HTTP 服务添加到 CentOS 防火墙的服务列表中。然后验证 HTTP 防火墙服务是否正常运行。要应用更改,您必须使用firewall-cmd --reload命令重新加载防火墙服务。下面列出了完整的命令:[root@centos8s1 ~]# firewall-cmd --permanent --add-service=http``success``[root@centos8s1 ~]# firewall-cmd --permanent --list-all``public``target: default``icmp-block-inversion: no``interfaces:``sources:``services: cockpit dhcpv6-client ftp http ntp ssh tftp``ports: 40000-41000/tcp``protocols:``masquerade: no``forward-ports:``source-ports:``icmp-blocks:``rich rules:``[root@centos8s1 ~]# firewall-cmd --reload``success我们的网络中没有 DNS 服务器,所以我们必须使用 CentOS 服务器的 IP 地址。我们已经知道我们的服务器 IP 是 192.168.183.130,但是为了验证这个信息,让我们运行下面的命令。或者,您可以使用ip add命令浏览信息。[root@centos8s1 ~]# ip addr show ens160 &#124; grep inet &#124; awk '{ print $2; }' &#124; sed 's/\/.*$//'``192.168.183.130``fe80::f49c:7ff4:fb3f:bf32``[root@centos8s1 ~]# ip add``[...omitted for brevity]``2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000``link/ether 00:0c:29:85:a6:36 brd ff:ff:ff:ff:ff:ff``inet 192.168.183.130/24 brd 192.168.183.255 scope global noprefixroute ens160``valid_lft forever preferred_lft forever``inet6 fe80::f49c:7ff4:fb3f:bf32/64 scope link noprefixroute``valid_lft forever preferred_lft forever``[...omitted for brevity]At this point, you can open a web browser from your Windows 10 host PC/laptop and open the Nginx home page using http://192.168.183.130. Now you have successfully installed Nginx and opened port 80 on your server. See Figure 8-2.img/492721_1_En_8_Fig2_HTML.jpg图 8-2。Windows 主机,打开 http://192.168.183.130 | | ④ | 要列出您的 Linux 服务器上的所有 TCP 或 UDP 端口,您可以使用netstat –a命令;-a选项代表all。您可以仅为 TCP 端口指定-t选项,或者为正在使用的 UDP 端口指定-u选项。对于 TCP:[root@centos8s1 ~]# netstat -atActive Internet connections (servers and established)

| 普罗托呼吸装置 | 接收-Q | 发送队列 | 本地地址 | 国外地址 | 状态 | | 传输控制协议 | Zero | Zero | centos8s1:smux | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:主机 | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:sunrpc | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:http | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | centos8s1:域 | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:ssh | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | centos8s1:ipp | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Sixty-four | Centi8s1:Ssh | 192.168.183.1:54572 | 确定的 | | tcp6 | Zero | Zero | [::]:主持人 | [::]:* | 听 | | tcp6 | Zero | Zero | [::sun RPC | [::]:* | 听 | | tcp6 | Zero | Zero | [::]:http | [::]:* | 听 | | tcp6 | Zero | Zero | [::]:ftp | [::]:* | 听 | | tcp6 | Zero | Zero | [::]:ssh | [::]:* | 听 | | tcp6 | Zero | Zero | centos8s1:ipp | [::]:* | 听|

For UDP``[root@centos8s1 ~]# netstat -auActive Internet connections (servers and established)

| 普罗托呼吸装置 | 接收-Q | 发送队列 | 本地地址 | 国外地址 | 状态 | | 用户数据报协议 | Zero | Zero | 0.0.0.0:48026 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | centos8s1:58365 | _ 网关:域 | 确定的 | | 用户数据报协议 | Zero | Zero | centos8s1:域 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 127.0.0.53:域 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:引导盘 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | centos8s1:bootpc | 192.168.183.254:引导区 | 确定的 | | 用户数据报协议 | Zero | Zero | 0.0.0.0:tftp | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:sunrpc | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:ntp | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0:snmp | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:mdns | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:主机 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | centos8s1:323 | 0.0.0.0:* |   | | udp6 | Zero | Zero | [::]:tftp | [::]:* |   | | udp6 | Zero | Zero | [::sun RPC | [::]:* |   | | udp6 | Zero | Zero | [::]:mdns | [::]:* |   | | udp6 | Zero | Zero | [::]:主持人 | [::]:* |   | | udp6 | Zero | Zero | centos8s1:323 | [::]:* |   | | udp6 | Zero | Zero | [::]:53619 | [::]:* |  |

| | ⑤号 | 如果你想只列出监听端口,那么可以使用netstat –l。列表太长,所以下面的输出只显示了监听端口信息的顶部,但是也许您可以在 CentOS VM 上运行这个命令并研究返回的结果。[root@centos8s1 ~]# netstat -lActive Internet connections (only servers)

| 普罗托呼吸装置 | 接收-Q | 发送队列 | 本地地址 | 国外地址 | 状态 | | 传输控制协议 | Zero | Zero | centos8s1:smux | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:主机 | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:sunrpc | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:http | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | centos8s1:域 | 0.0.0.0:* | 听|

[...omitted for brevity] | | ⑥ | 像netstat –a命令一样,netstat –l也可以与–t–u选项一起使用,用于仅 TCP 和 UDP 信息。netstat –lt只会列出 TCP 监听端口,而netstat –lu只会列出 UDP 监听端口。netstat -lt``netstat -lu在 Linux concole 上运行前面的命令,看看 CentOS 服务器会输出什么。 | | ① | 如果你想检查哪个进程使用了一个特定的端口,你可以通过键入netstat –an结合一个管道(&#124;)和grep ':port_number'来检查。因此,如果您想检查谁在使用 SSH(端口 22),您可以发出netstat –an &#124; grep ':22'命令。netstat –an &#124; grep ':22' <<< for SSH service``[root@centos8s1 ~]# netstat -an &#124; grep ':22'``tcp        0     0 0.0.0.0:22                0.0.0.0:*                LISTEN``tcp        0     64 192.168.183.130:22       192.168.183.1:54572      ESTABLISHED``tcp6       0     0 :::22``netstat –an &#124; grep ':80'  <<< for http service``[root@centos8s1 ~]# netstat -an &#124; grep ':80'``tcp        0     0 0.0.0.0:80                0.0.0.0:*          LISTEN``tcp6       0     0 :::80                     :::*               LISTEN | | ⑧ | 要检查哪些 IP 网络服务正在您的服务器上运行,您可以发出netstat –ap命令。下面是一个netstat –ap &#124; grep ssh命令的例子:[root@centos8s1 ~]# netstat -ap &#124; grep ssh``tcp   0      0 0.0.0.0:ssh     0.0.0.0:*            LISTEN       1257/sshd``tcp   0     64 centos8s1:ssh   192.168.183.1:54572  ESTABLISHED  1609/sshd: root [pr``tcp6  0     0 [::]:ssh         [::]:*               LISTEN       1257/sshd``unix  2      [ ]         STREAM     CONNECTED     42663    1609/sshd: root [pr``unix  2      [ ]         STREAM     CONNECTED     35797    1609/sshd: root [pr``unix  2      [ ]         STREAM     CONNECTED     43450    2204/sshd: root@pts``unix  2      [ ]         DGRAM                    43437    1609/sshd: root [pr``unix  3      [ ]         STREAM     CONNECTED     43441    1609/sshd: root [pr``unix  3      [ ]         STREAM     CONNECTED     43440    2204/sshd: root@pts``unix  3      [ ]         STREAM     CONNECTED     32383    1257/sshd``unix  2      [ ]         STREAM     CONNECTED     32584    1257/sshd | | ⑵ | 在输出中显示进程 ID (PID)和程序名称;您可以使用netstat –pt命令。它将显示协议、IP 服务和 IP 地址信息供您参考。以下命令显示活动 SSH 连接的netstat –pt输出:[root@centos8s1 ~]# netstat -pt``Active Internet connections (w/o servers)``Proto Recv-Q Send-Q Local Address  Foreign Address    State      PID/Program name``tcp   0   64 centos8s1:ssh   192.168.183.1:54572     ESTABLISHED 1609/sshd: root [pr | | ⑩ | 最后一组命令由统计命令组成;由于输出很长,您可以从 CentOS VM 控制台运行以下命令。你对–s句柄的提示是单词统计。请从 CentOS Linux 服务器运行以下命令,按照您自己的速度研究输出:[root@centos8s1 ~]# netstat –s``[root@centos8s1 ~]# netstat –st``[root@centos8s1 ~]# netstat -su |

安装 TFTP、FTP、SFTP 和 NTP 服务器

这里有一堂针对非 CCNA 读者的快速词汇课:

FTP: 文件传输协议

SFTP: 安全文件传输协议

TFTP: 琐碎文件传输协议

NTP: 网络时间协议

在商业领域,PC 最终用户和 IT 工程师都使用其网络中支持的各种网络服务。在众多 IP 服务中,目前网络和系统工程师使用和支持的最流行的网络服务包括 FTP、SFTP、TFTP 和 NTP 服务。每个服务都可以安装在多个分布式服务器上,如果安装和配置受支持,甚至可以安装在网络平台上。但是在实验室中,没有理由将网络服务分布在四个独立的虚拟服务器上来支持四种不同的网络服务。因为我们已经安装了 Nginx 服务,所以在一台服务器上有五个 IP 服务。出于测试目的,您可以将所有服务安装在一台 Linux 服务器上,但是在实际的生产环境中,这些服务可以分布在多台服务器上。

在生产环境中,FTP、SFTP 和 TFTP 是需要大量存储空间的文件服务。大容量存储要求意味着这些服务通常几乎总是安装在企业 Linux 或 Windows 服务器上,而不是路由器和交换机等网络设备上。NTP 是一种时间服务,因此该服务可以在 Cisco 路由器上运行。在 NTP 的情况下,提供时间同步服务,以便服务器和网络设备都可以在标准时间运行。为了方便起见,所有这四种 IP 服务都可以捆绑到一台服务器中,并在我们的实验室中用作 IP 服务服务器。如果您来自一个纯粹的网络背景,您可能没有很多构建和支持 Linux 服务器的企业 IP 服务的经验。更好地理解网络和系统管理技能集不是很好吗?知道的多就是自由。出于实验目的,让我们在 CentOS 8 服务器上安装 FTP、SFTP、TFTP 和 NTP 服务器。请记住,我们将 Ubuntu 服务器保留为 Python + Docker 服务器,供以后的实验使用。

FTP 服务器安装

文件传输协议(FTP)是一种安全稳定的网络服务,旨在服务器和客户端之间发送和接收许多文件。换句话说,简单的优点是,与较慢的 TFTP 相比,你可以快速地同时发送和接收文件。默认情况下,FTP 使用 TCP 端口 21,其安全性低于其安全版本 SFTP。有几种开源 FTP 服务器可用于 Linux,例如 PureFTPd、ProFTPD 和 vsftpd。在我们的例子中,我们将安装和使用非常安全的 FTP 守护程序(vsftpd),因为它安全、稳定、快速。首先,让我们检查 FTP 服务是否在我们的 CentOS 8 服务器上运行。

CentOS 8 中 FTP 服务器的安装和验证如下:

| ① | 在安装 CentOS 8 虚拟机时,我们选择了安装 FTP 服务器,但我们不知道具体安装了哪个软件版本。默认情况下,当您选择从安装介质安装 FTP 服务器时,CentOS 将安装`vsftpd`。运行`vsftpd –version`命令检查 CentOS 服务器上`vsftpd`的当前版本。`As expected, vsftpd version 3.0.3 is available.``[root@centos8s1 ~]# vsftpd -version``vsftpd: version 3.0.3`如果 CentOS 服务器上没有安装 FTP 软件,请使用以下两个命令完成安装:`[root@centos8s1 ~]# sudo yum makecache``[root@centos8s1 ~]# yum install -y vsftpd` | | ② | 通过键入`systemctl status vsftpd`检查`vsftpd`是否正在您的服务器上运行。`[root@centos8s1 ~]# systemctl status vsftpd`●t0]`Loaded: loaded (/usr/lib/systemd/system/vsftpd.service; enabled; vendor preset: disabled)``Active: active (running) since Mon 2021-01-04 19:15:34 AEDT; 1h 43min ago``Process: 1238 ExecStart=/usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf (code=exited, status=0/SUCCESS)``Main PID: 1248 (vsftpd)``Tasks: 1 (limit: 11166)``Memory: 932.0K``CGroup: /system.slice/vsftpd.service``ice248 /usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf``Jan 04 19:15:33 centos8s1 systemd[1]: Starting Vsftpd ftp daemon...``Jan 04 19:15:34 centos8s1 systemd[1]: Started Vsftpd ftp daemon.`使用`Ctrl+C`退出`vsftpd`状态文件。 | | ③ | 您希望`vsftpd`守护进程在操作系统启动时自动启动,因此您必须使用`systemctl enable`命令启用该服务。启用`vsftpd`服务后,使用之前使用的相同状态命令检查运行状态。当活动状态为`active (running)`时,则`vsftpd`服务处于活动状态并正常工作。`[root@centos8s1 ~]# systemctl enable vsftpd --now``[root@centos8s1 ~]# systemctl status vsftpd` | | ④ | 要在实验室中使用 FTP,需要更新几行`vsftpd.conf`文件。在 nano 中打开`vsftpd.conf`文件,检查现有配置,添加几行配置。`[root@centos8s1 ~]# nano /etc/vsftpd/vsftpd.conf`以下配置应该是现成的,但是如果它们不同,请按如下方式配置设置:`anonymous_enable=NO``local_enable=YES``write_enable=YES`要使用`user_list`控制对 FTP 服务器的访问,请在`userlist_enable=YES`后添加以下行:`userlist_file=/etc/vsftpd/user_list``userlist_deny=NO`要授予您的用户对其主目录的可写访问权限,请使用以下命令:`allow_writeable_chroot=YES`另外,`vsftpd`可以使用任何端口范围进行被动 FTP 连接。最佳做法是为此用途指定端口范围。将以下配置附加到`vsftpd.conf`文件的末尾,并保存文件:`pasv_enable=YES``pasv_min_port=40000``pasv_max_port=41000`要更改文件上传和下载的位置,请更新`local_root`配置。在这个例子中,`$USER`将从`/home/pynetauto/ftp`目录获取你的用户 ID 并打开 FTP 会话。`user_sub_token=$USER``local_root=/home/$USER/ftp`检查并更新之前的设置后,在`/etc/vsftpd/vsftpd.conf`文件的末尾添加以下几行:`### ADDED by Admin ###``# Use user_list``userlist_file=/etc/vsftpd/user_list``userlist_deny=NO``# Allow writeable_chroot to the user``allow_writeable_chroot=YES``# Control passive port range to use between 40000-41000``pasv_enable=YES``pasv_min_port=40000``pasv_max_port=41000``# Use ftp directory under /home/user/``user_sub_token=$USER``local_root=/home/$USER/ftp`记住最后一行:`local_root = /home/$USER/ftp`。确保为 FTP 访问创建一个名为`ftp`的本地文件夹。我们将使用标准用户登录 FTP 服务器,在我的例子中,是`/home/pynetauto/ftp`目录。`[pynetauto@centos8s1 ~]$ mkdir ftp` | | ⑤号 | 使用 vi 编辑器更改 FTP 服务器配置文件,以便所有用户都可以访问它。现在将您的用户 ID 添加到`/etc/vsftpd`下的`user_list`文件中。`[root@centos8s1 ~]# vi /etc/vsftpd/user_list``# vsftpd userlist``# If userlist_deny=NO, only allow users in this file``# If userlist_deny=YES (default), never allow users in this file, and``# do not even prompt for a password.``# Note that the default vsftpd pam config also checks /etc/vsftpd/ftpusers``# for users that are denied.``root``bin``[...omitted for brivety]``nobody``pynetauto``~``"/etc/vsftpd/user_list" 21L, 371C` | | ⑥ | 一旦更改保存到文件中,使用`firewall-cmd`命令打开 FTP 和被动端口。要允许防火墙访问 FTP 端口 20 和 21,请运行以下命令:`firewall-cmd --add-service=ftp --permanent``(OR firewall-cmd --permanent --add-port=20-21/tcp)``firewall-cmd --permanent --add-port=40000-41000/tcp``setsebool -P ftpd_full_access on``firewall-cmd --reload` | | ① | 确保发出`systemctl enable vsftpd`使`vsftpd`在系统启动时自动启动。此外,这里有 FTP 故障排除命令供您使用。根据需要使用这些命令来保持服务正常运行。`systemctl enable vsftpd.service``systemctl start vsftpd.service``systemctl restart vsftpd.service``systemctl stop vsftpd.service``systemctl status vsftpd.service`我们在上一节已经学习了`netstat`命令,您可以使用`netstat`命令来检查 FTP 服务是否运行顺畅。`netstat -ap | grep ftp``netstat -tupan | grep 21``netstat -na | grep tcp6` | | ⑧ | 如果 FTP 服务工作正常,您应该打开您最喜欢的 web 浏览器,并使用服务器的`ftp://SERVER_IP_ADDRESS`打开 FTP 页面。如果提示您输入用户 ID 和密码,请输入您的详细信息。见图 8-3 。`ftp://192.168.183.130/`![img/492721_1_En_8_Fig3_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/703b5a838d1a49febaa07598c63dc493~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771051471&x-signature=SkLd%2Fhdl1bwa5UscvmdZayN%2FHFk%3D)图 8-3。CentOS FTP 服务器,FTP 在 Firefox web 浏览器中打开 | | ⑵ | 您还可以从您的 Windows 主机 PC 或 Linux 桌面下载 FileZilla 客户端,并使用端口 21 连接到您的新 FTP 服务器。URL: [`https://filezilla-project.org/download.php?platform=win64`](https://filezilla-project.org/download.php%253Fplatform%253Dwin64) |

Tip

在主机字段输入ftp://192.168.183.130。见图 8-4 。

img/492721_1_En_8_Fig6_HTML.jpg

图 8-6。

WinSCP,连接到 CentOS FTP 服务器

img/492721_1_En_8_Fig5_HTML.jpg

图 8-5。

WinSCP,连接到 CentOS FTP 服务器

| ⑩ | WinSCP 也是另一个在连接到 FTP 服务器进行文件管理时很方便的 Windows 应用。在主机的 Windows 操作系统和 CentOS 8 Linux 服务器之间上传或下载。参见图 8-5 和图 8-6 。您可以从以下 URL 免费下载 WinSCP:URL: [`https://winscp.net/eng/download.php`](https://winscp.net/eng/download.php) |

img/492721_1_En_8_Fig4_HTML.jpg

图 8-4。

FileZilla 客户端连接到 FTP 服务器

您已经完成了设置和 FTP 服务器登录测试。现在,您可以使用 FTP 服务器在服务器和其他网络设备之间共享各种文件,如 IOS 和配置文件。当然,您也可以使用 FTP 来备份路由器和交换机配置。

img/492721_1_En_8_Figc_HTML.gif使用find命令确定包含文件或目录名的目录或文件位置。在下面的例子中,我们寻找的是pub目录,它通常是./var/ftp/目录下的ftp默认目录。

[root @ localhost ~] # cd /

[root @ localhost /] # find . -name "pub"

安装 SFTP 服务器

安全文件传输协议(SFTP)是指通过网络安全共享文件的安全 FTP 方法。SFTP 和 FTP 的区别在于通过网络发送的数据是加密的还是未加密的。SFTP 使用 TCP/UDP 端口 22 作为默认端口。在大多数通过 SFTP 协议的文件传输场景中,将使用 TCP 22 端口。应开发人员的要求,UDP 22 包含在 TCP/IP 开发过程中,但没有完全实现。所以,可以肯定地说,SFTP 主要使用 TCP 端口 22。

vsftpd被配置为 FTP,但它也是一个 s FTP 服务器。因为我们已经安装了 3.0.3 版本的vsftpd,我们也将把它配置为一个 SFTP 服务器,与 FTP 服务器一起运行。

| ① | 首先,通过 SSH 客户端 PuTTY 以 root 用户身份登录 CentOS 8 VM。您不需要为 SFTP 安装新的软件,但是首先,您需要创建一个特定于 SFTP 的用户帐户。使用`adduser`和`passwd`为 SFTP 文件传输创建一个新用户;这里创建的用户名是`sftpuser`。`[root@centos8s1 ~]# adduser sftpuser``[root@centos8s1 ~]# passwd sftpuser``Changing password for user sftpuser.``New password: **********``Retype new password: **********``passwd: all authentication tokens updated successfully.`快速提示:如果您想删除并重新创建一个用户,您可以使用`userdel`命令。要删除用户的主目录和邮件池,添加`–r`。`[root@centos8s1 ~]# userdel -r sftpuser` | | ② | 在`/var/`目录下创建一个目录`sftp`,然后在`/var/sftp/`目录下创建另一个子目录`sftpdata`,你将在那里存储和共享你的文件。这里给出的目录名是`sftpdata`。`[root@centos8s1 ~]# mkdir -p /var/sftp/``[root@centos8s1 ~]# mkdir -p /var/sftp/sftpdata` | | ③ | 更改`sftp`目录的所有权,以便`sftpuser`可以更改`/var/sftp`的内容。让 root 用户成为`/var/sftp/`目录的所有者。将权限更改为 755,以便`sftpuser`可以修改目录和文件。让`sftpuser`用户成为`/var/sftp/sftpdata`目录的所有者。`[root@centos8s1 ~]# chown root:root /var/sftp``[root@centos8s1 ~]# chmod 755 /var/sftp``[root@centos8s1 ~]# chown sftpuser:sftpuser /var/sftp/sftpdata` | | ④ | 为了限制 SSH 对`/var/sftp/sftpdata`目录的访问,您必须修改`/etc/ssh/`目录中的`sshd_config`文件。打开`sshd_config`文件,添加/修改以下配置。将此处指定的内容复制到配置文件的底部并保存。`[root@centos8s1 ~]# vi /etc/ssh/sshd_config``# Add the following to the end of the file.``### ADDED by Admin ###``Match User sftpuser``ForceCommand internal-sftp``PasswordAuthentication yes``ChrootDirectory /var/sftp``PermitTunnel no``AllowAgentForwarding no``AllowTcpForwarding no``X11Forwarding no` | | ⑤号 | 重新启动`sshd`服务以使之前的更改生效。`[root@centos8s1 ~]# systemctl restart sshd` | | ⑥ | 如果您尝试使用`sftpuser`帐户 SSH 进入 CentOS 服务器,连接将被拒绝并断开。尽管 SSH 和 SFTP 使用相同的连接端口,但是`sftpuser`帐户只能用于 SFTP 连接,而不能用于 SSH 连接。试试看。`[root@centos8s1 ~]# ssh sftpuser@192.168.183.130``The authenticity of host '192.168.183.130 (192.168.183.130)' can't be established.``ECDSA key fingerprint is SHA256:b17PV5polHEKIcl+cFUCGA1KHGcl7xJkw/jhTf0kfZY.``Are you sure you want to continue connecting (yes/no/[fingerprint])? yes``Warning: Permanently added '192.168.183.130' (ECDSA) to the list of known hosts.``sftpuser@192.168.183.130's password: ********``This service allows sftp connections only.``Connection to 192.168.183.130 closed.` | | ① | 这一次,尝试使用 sftp 命令连接到 SFTP 服务器。输入登录密码,然后使用 Ctrl+Z 断开与 SFTP 服务器的连接。`[root@centos8s1 ~]# sftp sftpuser@192.168.183.130``sftpuser@192.168.183.130's password: ********``Connected to sftpuser@192.168.183.130.``sftp>`按 Ctrl+Z 断开与 sftp 服务器的连接。`[1]+  Stopped                 sftp sftpuser@192.168.183.130` | | ⑧ | 使用以下命令检查端口 22 和`sshd`是否正常工作(参见图 8-7 ):`[root@centos8s1 ~]# netstat -ap | grep sshd``[root@centos8s1 ~]# netstat -tupan | grep 22``[root@centos8s1 ~]# netstat -na | grep tcp6`![img/492721_1_En_8_Fig7_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4237e9151d4b4e92bf3659c41e804bbe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771051471&x-signature=wVTeEbcZDix6BNpp9dEIBNakoL0%3D)图 8-7。CentOS,netstat -tupan | grep 22 示例 | | ⑵ | Finally, verify that your SFTP server is working correctly. You can use Windows applications such as FileZilla or WinSCP with user ID `sftpuser`, your password, and connecting port 22\. See Figure 8-8 and Figure 8-9.![img/492721_1_En_8_Fig8_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3f998e9aae1f487395d08e3be350dc01~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771051471&x-signature=AQud9sZgKD%2B9Ea2FvYv9VrO%2FAto%3D)图 8-8。FileZilla 客户端连接示例![img/492721_1_En_8_Fig9_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e7f37151a5894062bb59e3929f30eea9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771051471&x-signature=wcAYxXg5qDo4TKgYSyw3tXvfnWU%3D)图 8-9。WinSCP,SFTP 连接示例 |

img/492721_1_En_8_Figd_HTML.gif 如何打印出 Linux 服务器上配置的所有用户名?

只需运行以下命令:

[root@centos8s1 ~]# awk -F ':' '{print $ 1}' /etc/passwd

root

bin

daemon

adm

[... omitted for brevity]

tcpdump

pynetauto

nginx

sftpuser

tftpuser

现在,您的 CentOS 服务器正在运行 SFTP 服务,通过网络实现安全文件共享。只要有可能,SFTP 必须通过 FTP 或 TFTP 用于两个节点(设备)之间的安全文件传输。接下来是 TFTP 的安装及其配置。

安装 TFTP 服务器

普通文件传输协议(TFTP)使用 UDP 69 作为其默认通信端口。因为 TFTP 使用 UDP,所以它的文件传输操作比 FTP 或 SFTP 简单得多。TFTP 是最常用的文件共享方法之一,用于通过内部网络发送和接收 Cisco IOS 或 NX-OS 映像或配置文件。它仍然在许多网络中使用,但是它仍然使用纯文本进行通信,并且通过 UDP 的文件传输并不总是保证完整的文件传输或安全性。

如前所述,在真实的生产环境中,您不会同时在同一台服务器上运行所有的 FTP、SFTP 和 TFTP 服务,但是我们在实验室环境中将所有三台服务器合二为一。在 CentOS 8 上安装 TFTP 类似于安装vsftpd。让我们继续安装实验室使用的 TFTP 服务器。重要的是要记住,当你自己建造实验室时,你总是学到更多,带走更多。

| ① | 首先,使用 root 用户帐户登录 CentOS 8.1,并使用 PuTTY 通过 SSH 登录服务器。登录后,运行`systemctl status firewalld`命令检查防火墙状态。`[root@centos8s1 ~]# systemctl status firewalld``●ifirewalld.service - firewalld - dynamic firewall daemon``Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; vendor preset: enabled)``Active: active (running) since Mon 2021-01-04 19:15:30 AEDT; 3h 18min ago``Docs: man:firewalld(1)``Main PID: 1111 (firewalld)``Tasks: 3 (limit: 11166)``Memory: 33.8M``CGroup: /system.slice/firewalld.service``ervicewausr/libexec/platform-python -s /usr/sbin/firewalld --nofork --nopid` | | ② | 使用以下命令更改服务器防火墙设置,使 TFTP 流量能够与您的服务器通信:`[root@centos8s1 ~]# firewall-cmd --permanent --zone=public --add-service=tftp``[root@centos8s1 ~]# firewall-cmd --reload` | | ③ | 使用 yum install 命令安装 tftp-server、xinetd 服务守护程序和 tftp 客户端软件包。运行以下命令集,在 CentOS 服务器上完成安装:`[root@centos8s1 ~]# yum install xinetd tftp-server tftp``[root@centos8s1 ~]# systemctl enable xinetd tftp``[root@centos8s1 ~]# systemctl start xinetd tftp``[root@centos8s1 ~]# systemctl status xinetd``●ixinetd.service - Xinetd A Powerful Replacement For Inetd``Loaded: loaded (/usr/lib/systemd/system/xinetd.service; enabled; vendor preset: enabled)``Active: active (running) since Mon 2021-01-04 19:15:34 AEDT; 3h 20min ago``Docs: man:xinetd``man:xinetd.conf``man:xinetd.log``Process: 1245 ExecStart=/usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid (code=exited, status=0/SUCCESS)``Main PID: 1281 (xinetd)``Tasks: 1 (limit: 11166)``Memory: 1.3M``CGroup: /system.slice/xinetd.service``ice281 /usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid` | | ④ | 现在我们知道 TFTP 服务器服务运行正常,我们想创建一个名为`tftpuser`的用户帐户,分配足够的权限来完成它的工作,即通过 UDP 端口 69 传输文件。此处创建的`tftp`帐户仅供系统使用,因此您不能将其用作用户帐户。`[root@centos8s1 ~]# useradd -s /bin/false -r tftpuser` | | ⑤号 | 在`/var`目录下创建一个名为`tftpdir`的目录。所有未来的 TFTP 文件共享将通过这个目录。`[root@centos8s1 ~]# mkdir /var/tftpdir` | | ⑥ | 更改`tftpdir`目录的权限,以便`tftp`系统帐户拥有该目录的权限。为了允许一些自由,我们将使用`chmod 777`修改目录。`[root@centos8s1 ~]# chown tftpuser:tftpuser /var/tftpdir``[root@centos8s1 ~]# chmod 777 /var/tftpdir` | | ① | 接下来,在`/etc/xinetd.d/`中创建一个名为`tftp`的服务文件,复制以下内容,并保存文件:`[root@centos8s1 ~]# nano /etc/xinetd.d/tftp``service tftp``{``socket_type = dgram``protocol = udp``wait = yes``user = root``server = /usr/sbin/in.tftpd``server_args = -c -s /var/tftpdir -v -v -v -u tftpuser -p``disable = no``per_source = 11``cps = 100 2``flags = IPv4``}` | | ⑧ | 为了让 TFTP 在 SELinux 中很好地工作,我们必须更新我们的`tftp`目录文件`tftpdir`的变更上下文(`chcon`)。`[root@centos8s1 ~]# chcon -t tftpdir_rw_t /var/tftpdir`让我们再次重启`tftp`和`xinetd`服务。`[root@centos8s1 ~]# systemctl restart xinetd tftp` | | ⑵ | 在对 TFTP 服务器执行验证任务之前,让我们检查两个配置,它们对于 CentOS 服务器上的 TFTP 操作非常重要。首先,确保调整了`/etc/selinux/config`配置,其次,检查`setsebool`设置以允许`tftp`写和目录访问。首先,将`SELINUX = enforcing`改为`SELINUX = permissive`后保存文件。见图 8-10 。`[root@centos8s1 ~]# nano /etc/selinux/config`![img/492721_1_En_8_Fig10_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d4184c918f5e437995a6b9788b492b54~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771051471&x-signature=TIPrWgdspvJhaFIX%2FnGCRF%2FEv7Q%3D)图 8-10。CentOS、/etc/selinux/config 更新其次,我们必须检查 SELinux 的布尔值`tftp_anon_write`和`tftp_home_dir`,这样 SELinux 就允许 TFTP 文件传输。如果`tftp_anon_write`和`tftp_home_dir`都显示为`off`,使用下面的`getsebool`命令检查`tftp`权限。您必须使用后续命令来启用它们。[root@centos8s1 ~]# `getsebool -a | grep tftp``tftp_anon_write --> off``tftp_home_dir --> off`更新`tftp_anon_write`和`tftp_home_dir`的 SELinux 布尔值。`[root@centos8s1 ~]# setsebool -P tftp_anon_write 1``[root@centos8s1 ~]# setsebool -P tftp_home_dir 1``[root@centos8s1 ~]# getsebool -a | grep tftp``tftp_anon_write --> on``tftp_home_dir --> on`最后,在 CentOS 服务器上最后一次重启`xinetd`和`tftp`服务。`[root@centos8s1 ~]# systemctl restart xinetd tftp` | | ⑩ | 在另一台 Linux 机器上,`ubuntu20s1` (192.168.183.129)服务器,使用`apt install –y tftp`命令安装`tftp`客户端,并执行以下验证任务。首先,在`ubuntu20s1`服务器上安装`tftp`客户端。`root@ubuntu20s1:~# apt install tftp`其次,创建`transfer_file01`并添加一些文本,如下所示:`root@ubuntu20s1:~# nano transfer_file01``This is a file transfer test file only.``Please contact the administrator if you have any issues.``root@ubuntu20s1:~# cat transfer_file01``This is a file transfer test file only.``Please contact the administrator if you have any issues.`第三,为了测试 CentOS `tftp`服务器的`tftp`登录,运行`tftp 192.168.183.130`。连接后,使用`put`命令将`transfer_file01`上传到 TFTP 服务器(192.168.183.130)。`root@ubuntu20s1:~# tftp 192.168.183.130``tftp> put transfer_file01``Sent 99 bytes in 0.0 seconds``tftp>`现在回到`centos8s1`服务器,使用`ls /var/tftpdir`命令检查文件是否已经正确上传。`[root@centos8s1 ~]# ls -lh /var/tftpdir``total 4.0K``-rw-rw-r--. 1 tftpuser  tftpuser    97 Jan  4 22:54 transfer_file01`第四,现在反过来,让我们从 TFTP 服务器(`cenos8s1: 192.168.183.130`)下载一个文件名为`transfer_file77`的文件到文件客户端(`ubuntu20s1: 192.168.183.132`)。在 CentOS 服务器上,我们将`transfer_file01`文件复制为`transfer_file77`。`[root@centos8s1 ~]# cp /var/tftpdir/transfer_file01 /var/tftpdir/transfer_file77``[root@centos8s1 ~]# ls /var/tftpdir``transfer_file01  transfer_file77`在 Ubuntu 服务器上,发出`get file_name`命令从 CentOS TFTP 服务器下载`transfer_file05`文件。`root@ubuntu20s1:~# tftp 192.168.183.130``tftp> get transfer_file77``Received 99 bytes in 0.0 seconds``tftp> ^Z` `# Use Ctrl+Z to exit tftp mode``[1]+  Stopped                 tftp 192.168.183.130``root@ubuntu20s1:~# ls transfer*``transfer_file01  transfer_file77` |

您已经在 CentOS 8 服务器上成功安装了 TFTP 软件,并验证了服务器和客户端之间的 TFTP 操作。到目前为止,FTP、SFTP 和 TFTP 都已在 CentOS 服务器上启动并运行,接下来,您将在 CentOS 上快速安装时间服务(NTP 服务器)软件。当我们在实验室或学习或工作中测试各种场景时,这些服务器会派上用场。

img/492721_1_En_8_Fige_HTML.gif您可以使用net-tool来检查 TFTP 是否正常工作。如果您的 CentOS VM 不识别netstat命令,您可以使用yum install -y net-tool来安装它,并运行以下一组命令来检查与 TFTP 相关的端口操作:

netstat -na | grep udp6

netstat –lu

netstat –ap | grep tftp

netstat -tupan

netstat –tupan | grep 69

安装 NTP 服务器

网络时间协议(NTP)用于同步服务器、防火墙、路由器和交换机等企业设备的时间。NTP 服务使用 UDP 端口 123 和 IP 网络服务,该服务必须存在于公司网络中以保持时间正确。运行在同一网段或时区的设备需要有一致的时间,NTP 服务器可以为其他设备提供这项服务。如果网络上成百上千的联网设备都按照它们的硬件时钟时间运行,那么日志和系统文件就不会有一致的时间戳。NTP 服务器充当时间助手,帮助网络设备商定一个单一的参考时间。让我们继续在 CentOS 8 服务器上安装 NTP 服务器,并执行一次快速测试以进行确认。在 CentOS 的上一版本 CentOS 7.5 中,可以安装 NTP,但在 CentOS 8 中,chrony守护程序同时提供 NTP 服务器和 NTP 客户端服务。

| ① | 首先,使用 SSH 客户端通过 root 用户凭据登录 CentOS 8 虚拟机。登录后,检查服务器时间和时区是否正确。如果这些信息有任何错误,请按照以下说明正确设置您的时钟和时区:1a .检查服务器上的时间。`[root@centos8s1 ~]# clock``2021-01-05 09:09:56.710758+11:00`1b .检查时区、NTP 状态和其他时间详细信息。[root@centos8s1 ~]# `timedatectl1c`。如果您的时区不正确,请搜索时区列表并找到您的国家/城市。按 Ctrl+Z 退出列表。`[root@centos8s1 ~]# timedatectl list-timezones`1d。现在设置正确的时区。如果你在另一个城市和国家,你的时区会和我的不同,所以根据你的地理位置调整时间。`[root@centos8s1 ~]# timedatectl set-timezone Australia/Sydney`1e。确认您的服务器在正确的当地时间运行。`[root@centos8s1 ~]# clock``2021-01-05 09:16:05.039324+11:00``[root@centos8s1 ~]# date``Tue Jan  5 09:16:13 AEDT 2021` | | ② | 接下来,如果还没有安装`chrony`,使用`dnf`命令`dnf install chrony`安装 NTP 服务器和客户端。`[root@centos8s1 ~]# dnf install chrony` | | ③ | 要使`chronyd`在启动时运行,通过键入`systemctl enable chronyd`启用`chrony`服务。`[root@centos8s1 ~]# systemctl enable chronyd` | | ④ | 打开`/etc/chrony.conf`下的`chrony.conf`文件,允许您的服务器和实验室联网。配置文件允许子网后,`chrony`将作为本地网络的 NTP 服务器。确保更新子网以反映您的虚拟机 NAT 子网。此外,为了使这个时间服务器更可信,我们将改变当地的地层为 3。大多数网络设备和服务器将乐于与等于或小于 stratum 5 的 NTP 服务器同步时间。`[root@centos8s1 ~]# vi /etc/chrony.conf`在 vi 的`--INSERT--`模式下更新以下粗体行,然后用`:wq`命令保存更改:`[...omitted for brevity]``# Allow NTP client access from local network.``#allow 192.168.0.0/16``# ADDED by pynetauto``allow 192.168.183.0/24` `# Add this line to allow local communication``# Serve time even if not synchronized to a time source.``# local stratum 10``# ADDED by pynetauto``local stratum 5` `# Add this line to make this as a stratum 3 server``[...omitted for brevity]``:wq`重启`chronyd`服务以使更改生效。`[root@centos8s1 ~]# systemctl stop chronyd``[root@centos8s1 ~]# systemctl start chronyd``[root@centos8s1 ~]# systemctl status chronyd` | | ⑤号 | 打开防火墙端口以允许网络上的传入 NTP 请求,并重新加载防火墙。`[root@centos8s1 ~]# firewall-cmd --permanent --add-service=ntp``success``[root@centos8s1 ~]# firewall-cmd --reload``Success` | | ⑥ | 最后,为了应用更改,重启`chronyd`守护进程。`[root@centos8s1 ~]# systemctl restart chronyd` | | ① | 转到您的`Ubuntu 20 LTS`服务器,设置时区,安装`ntpdate`,并使用`ntpdate 192.168.183.130`与该服务器同步时间。如果您的 NTP 服务器 IP 不同,请不要忘记更新 IP 地址以反映您的 NTP 服务器 IP。7a .使用`timedatectl`命令更新时区。检查 Ubuntu 服务器上的当前时间和日期。`root@ubuntu20s1:~# timedatectl``Local time: Mon 2021-01-04 22:32:42 UTC``Universal time: Mon 2021-01-04 22:32:42 UTC``RTC time: Mon 2021-01-04 22:32:42``Time zone: Etc/UTC (UTC, +0000)``System clock synchronized: yes``NTP service: active``RTC in local TZ: no`更新时区并再次检查时间和日期。`root@ubuntu20s1:~# timedatectl set-timezone Australia/Sydney``root@ubuntu20s1:~# timedatectl``Local time: Tue 2021-01-05 09:33:29 AEDT``Universal time: Mon 2021-01-04 22:33:29 UTC``RTC time: Mon 2021-01-04 22:33:29``Time zone: Australia/Sydney (AEDT, +1100)``System clock synchronized: yes``NTP service: active``RTC in local TZ: no`7b .检查日期、硬件时钟和时区。`root@ubuntu20s1:~# date``Tue 05 Jan 2021 09:35:23 AM AEDT``root@ubuntu20s1:~# hwclock --show``2021-01-05 09:35:33.118208+11:00``root@ubuntu20s1:~# cat /etc/timezone``Australia/Sydney`7c .在你的 Ubuntu 服务器上安装`ntpdate`。`root@ubuntu20s1:~# apt-get install ntpdate`![img/492721_1_En_8_Figf_HTML.gif](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/472ea379e3f04344b59c80eae901c3f0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771051471&x-signature=YRU43PwW2FDbBILLXDuKXv%2BL%2FoA%3D)注意这里`apt-get`的用法。apt (Advanced Package Tool)合并了`apt-get`和`apt-cache`的功能,但是对于某些包,你仍然需要运行`apt-get`来安装某些包。`apt`命令应该在大多数情况下都能工作,但并不总是如此。谷歌上有大量关于这种差异的文章;你可以在谷歌上快速查找这个区别。URL: [`https://askubuntu.com/questions/445384/what-is-the-difference-between-apt-and-apt-get`](https://askubuntu.com/questions/445384/what-is-the-difference-between-apt-and-apt-get)URL: [`https://phoenixnap.com/kb/apt-vs-apt-get`](https://phoenixnap.com/kb/apt-vs-apt-get)7d。将 Ubuntu 服务器时间与 CentOS 8 NTP 服务器时间同步。如果 NTP 服务运行正常,Ubuntu 服务器将同步其时钟,并开始参考 NTP 服务器的时间请求。`root@ubuntu20s1:~# ntpdate –d 192.168.183.130``5 Jan 10:26:42 ntpdate[5667]: ntpdate 4.2.8p12@1.3728-o (1)``Looking for host 192.168.183.130 and service ntp``host found : 192.168.183.130``transmit(192.168.183.130)``receive(192.168.183.130)``[...omitted for brevity]``server 192.168.183.130, port 123``stratum 5, precision -26, leap 00, trust 000``[...omitted for brevity]``0.000000 0.000000 0.000000 0.000000``delay 0.02592, dispersion 0.00006``offset 5.987462``5 Jan 10:26:58 ntpdate[5667]: step time server 192.168.183.130 offset 5.987462 sec``root@ubuntu20s1:~# date``Tue 05 Jan 2021 10:27:04 AM AEDT`检查 NTP 服务器上的时间(`centos8s1`)。如果时间同步如前所示正确进行,您的 NTP 客户端的时间将与 NTP 服务器的时间同步。`[root@centos8s1 ~]# date``Tue Jan  5 10:27:10 AEDT 2021`或者,您可以使用`ntpdate 192.168.183.130`或`ntpdate –u 192.168.183.130`命令来更新和重新同步时间。 | | ⑧ | If you break this IP services server, now is the perfect time to take a snapshot of your CentOS server to preserve the server’s working state. See Figure 8-11.![img/492721_1_En_8_Fig11_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e29f8e3f4193418da273e2069eae87b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771051471&x-signature=UKoZk6M%2F71GulKHHuIoECTlwplI%3D)图 8-11。CentOS,拍摄快照 |

您已经成功安装了 NTP 服务器,这就完成了 NTP、FTP、SFTP 和 TFTP 的安装和验证。这些服务器将使您的实验室场景变得更加有趣,而且,您将能够研究、破坏和重建您的实验室,就像您小时候最喜欢的玩具一样。

Linux TCP/IP 故障排除练习

这是本章的最后一个练习。您将学习一些 Linux 网络基本故障排除技巧,以测试 TCP 和 IP 相关的问题。当您遇到服务器网络问题时,您可以自己解决,而不是交给 Linux 管理员。

| ① | 第一个练习是检查您的服务器是否允许 ping (ICMP)。使用`cat /proc/sys/net/ipv4/icmp_echo_ignore_all`,您可以检查您的服务器是否会响应 ICMP 请求。输出为 0 或 1。0 表示 ICMP 已启用并且可以响应;1 表示 ICMP 被禁用,这意味着 ICMP 请求将被忽略。对于 CentOS 服务器:`[root@centos8s1 ~]# cat /proc/sys/net/ipv4/icmp_echo_ignore_all``0`对于 Ubuntu 服务器:`root@ubuntu20s1:~# cat /proc/sys/net/ipv4/icmp_echo_ignore_all``0`出于测试目的,让我们将 Ubuntu 服务器的值更新为 1(从 0 开始),这样 ICMP 响应在 Ubuntu 服务器上被禁用。然后在两台服务器之间 ping。测试之后,确保将这个值更新回 0。`root@ubuntu20s1:~# nano /proc/sys/net/ipv4/icmp_echo_ignore_all``root@ubuntu20s1:~# cat /proc/sys/net/ipv4/icmp_echo_ignore_all``1``[root@centos8s1 ~]# ping 192.168.183.130``root@ubuntu20s1:~# ping 192.168.183.130 -c 4``PING 192.168.183.130 (192.168.183.130) 56(84) bytes of data.``64 bytes from 192.168.183.130: icmp_seq=1 ttl=64 time=0.386 ms``64 bytes from 192.168.183.130: icmp_seq=2 ttl=64 time=0.602 ms``64 bytes from 192.168.183.130: icmp_seq=3 ttl=64 time=0.576 ms``64 bytes from 192.168.183.130: icmp_seq=4 ttl=64 time=0.607 ms``--- 192.168.183.130 ping statistics ---``4 packets transmitted, 4 received, 0% packet loss, time 3053ms``rtt min/avg/max/mdev = 0.386/0.542/0.607/0.091 ms`不出所料,Ubuntu 服务器没有响应,因为来自 CentOS 服务器的 ICMP 请求被忽略。如果从 CentOS 服务器 ping Ubuntu 服务器,会导致 100%的数据包丢失,如下所示:`[root@centos8s1 ~]# ping 192.168.183.132 -c 4``PING 192.168.183.132 (192.168.183.132) 56(84) bytes of data.``--- 192.168.183.132 ping statistics ---``4 packets transmitted, 0 received, 100% packet loss, time 79ms` | | ② | 要从远程机器测试服务器的开放端口,可以使用`telnet` +远程服务器的 IP 地址 TCP 端口号。Telnet 可用于测试简单的网络套接字连接,但仅适用于 TCP 端口(不适用于 UDP 端口)。例如,如果您想要测试从 Ubuntu 服务器到 CentOS FTP/SFTP 服务器的 FTP (21)和 SFTP (22) TCP 连接,您可以执行以下`telnet`测试并确认端到端连接。连接后,要退出,键入`QUIT`并按回车键。2a .下面是 FTP 连接测试:`root@ubuntu20s1:~# telnet 192.168.183.130 21``Trying 192.168.183.130...``Connected to 192.168.183.130.``Escape character is '^]'.``220 (vsFTPd 3.0.3)``QUIT``221 Goodbye.``Connection closed by foreign host.`2b。下面是 SSH/SFTP 连接测试:`root@ubuntu20s1:~# telnet 192.168.183.130 22``Trying 192.168.183.130...``Connected to 192.168.183.130.``Escape character is '^]'.``SSH-2.0-OpenSSH_8.0``QUIT``Invalid SSH identification string.``Connection closed by foreign host.` | | ③ | 要查看一个程序或进程是否正在监听一个端口,准备接受一个包,使用`netstat`命令。`netstat`这里列出了参数(选项),所以混合搭配并尝试在你的 Ubuntu 服务器上运行它们:`t`—显示 TCP 端口`u`—显示 UDP 端口`a`—显示全部`l`—仅显示监听过程`n`—不解析网络 IP 地址名称或端口号`p`—显示监听不同端口的进程名 3a .从你的 Ubuntu 服务器运行`netstat –tulnp`命令。`root@ubuntu20s1:~# netstat -tulnp`Here are the active Internet connections (only servers): | 普罗托呼吸装置 | 接收-Q | 发送队列 | 本地地址 | 国外地址 | 状态 | PID/程序名称 | | 传输控制协议 | Zero | Zero | 127.0.0.1:45715 | 0.0.0.0:* | 听 | 939/容器 d | | 传输控制协议 | Zero | Zero | 127.0.0.53:53 | 0.0.0.0:* | 听 | 843/systemd-解决 | | 传输控制协议 | Zero | Zero | 0.0.0.0:22 | 0.0.0.0:* | 听 | 1015/sshd: /usr/sbi | | 传输控制协议 | Zero | Zero | 127.0.0.1:631 | 0.0.0.0:* | 听 | 3221/cupsd | | tcp6 | Zero | Zero | :::22 | :::* | 听 | 1015/sshd: /usr/sbi | | tcp6 | Zero | Zero | ::1:631 | :::* | 听 | 3221/cupsd | | 用户数据报协议 | Zero | Zero | 127.0.0.53:53 | 0.0.0.0:* |   | 843/systemd-解决 | | 用户数据报协议 | Zero | Zero | 0.0.0.0:5353 | 0.0.0.0:* |   | 859/avahi 守护程式 | | 用户数据报协议 | Zero | Zero | 0.0.0.0:43413 | 0.0.0.0:* |   | 859/avahi 守护程式 | | 用户数据报协议 | Zero | Zero | 0.0.0.0:631 | 0.0.0.0:* |   | 3252/cups-已浏览 | | udp6 | Zero | Zero | :::51143 | :::* |   | 859/avahi 守护程式 | | udp6 | Zero | Zero | :::5353 | :::* |   | 859/avahi 守护程式|

3b。从你的 Ubuntu 服务器运行netstat –tuna命令。root@ubuntu20s1:~# netstat -tunaHere are the active Internet connections (servers and established):

| 普罗托呼吸装置 | 接收-Q | 发送队列 | 本地地址 | 国外地址 | 状态 | | 传输控制协议 | Zero | Zero | 127.0.0.1:45715 | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 127.0.0.53:53 | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 0.0.0.0:22 | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 127.0.0.1:631 | 0.0.0.0:* | 听 | | 传输控制协议 | Zero | Zero | 192.168.183.132:22 | 192.168.183.1:56036 | 确定的 | | tcp6 | Zero | Zero | :::22 | :::* | 听 | | tcp6 | Zero | Zero | ::1:631 | :::* | 听 | | 用户数据报协议 | Zero | Zero | 127.0.0.53:53 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:5353 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:43413 | 0.0.0.0:* |   | | 用户数据报协议 | Zero | Zero | 0.0.0.0:631 | 0.0.0.0:* |   | | udp6 | Zero | Zero | :::51143 | :::* |   | | udp6 | Zero | Zero | :::5353 | :::* |  |

| | ④ | 您还可以使用带有参数的ss命令来查看监听端口或端口准备情况。要查看一个程序或进程是否正在监听一个端口,准备接受一个包,使用ssss命令可以在 Ubuntu 和 CentOS 服务器上运行。参见图 8-12 。t—显示 TCP 套接字u—显示 UDP 套接字l—显示监听插座n—不解析名称p—使用套接字显示过程[root@centos8s1 ~]# ss –nutlp运筹学root@ubuntu20s1:~# ss -nutlpimg/492721_1_En_8_Fig12_HTML.jpg图 8-12。Ubuntu–ss–nutlp 示例 | | ⑤号 | 使用lsof列出 Linux 操作系统上开放的端口。要列出进程名和 PID 号以及打开的端口,请键入lsof –i。您可以在大多数 Linux 操作系统上使用这个命令。见图 8-13 。[root@centos8s1 ~]# lsof –i运筹学root@ubuntu20s1:~# lsof -iimg/492721_1_En_8_Fig13_HTML.jpg图 8-13。Ubuntu–lsof–I 示例 | | ⑥ | 使用iptables命令列出更多的 TCP/IP 通信信息。运行iptables -xvn –L显示数据包、接口、源和目的 IP 地址以及正在使用的端口。见图 8-14 。root@ubuntu20s1:~# iptables -xvn –L运筹学[root@centos8s1 ~]# iptables -xvn -Limg/492721_1_En_8_Fig14_HTML.jpg图 8-14。CentOS,iptables-xvn–L 示例 | | ① | 也许您正在从 Windows 客户端连接到您的 FTP 服务器。那么,你如何从你的 Windows 电脑与你的 FTP 服务器建立 FTP 连接呢?您可以使用 Windows 命令行提示符或 Windows PowerShell。Test-NetConnection是一个本地 PowerShell 命令,可用于测试简单的连接,如 FTP 端口 21。让我们从您的 Windows 10 主机 PC/笔记本电脑启动 PowerShell,快速测试与 CentOS FTP 服务器的 FTP 连接是否正常工作。参见图 8-15 。PS C:\Users\brendan> test-NetConnection -ComputerName 192.168.183.130 -Port 21img/492721_1_En_8_Fig15_HTML.jpg图 8-15。Windows 10 主机 PC,PowerShell 测试-网络连接示例 |

摘要

在本章中,您学习了更多的 Linux 管理,从如何检查系统信息和如何检查 Linux 系统上的 TCP 和 UDP 端口开始。然后,指导您在 CentOS 服务器上安装 IP 服务,以制作适用于 TFTP、FTP、SFTP 和 NTP 服务的一体化实验室服务器。在本书的后半部分,这个服务器将用于 Python 网络自动化实验室。最后,您学习了一些基本的 TCP/IP 和 Linux 上的连接故障排除,因此您可以在移动中排除连接问题。我希望这是每个人都感兴趣的一章。在下一章,Chapter 9 中,你将处理另一个具有挑战性的主题,正则表达式(又名 regex)。理解 regex 的内部工作方式可以提高您的 Python 编码能力。你必须坚持下去,完成下一章。

Storytime 4:成为一名精明的 Linux 管理员,或许是每个 IT 工程师的梦想?

IT 行业的每个人都会同意,在 IT 行业工作时,投入时间和资源学习 enterprise Linux 是一项值得的未来投资。成为一名精明的 Linux 管理员是许多 IT 工程师的梦想,如果不是每个工程师的话。至少这是我一段时间以来的梦想。你还记得你的第一次 Linux 体验吗?以下是 Linux 用户在尝试学习 Linux 时可能会经历的一些常见经历,尤其是如果你一直是 Windows 用户的话。

  • 大家都说 Ubuntu Desktop 是最适合初学者的 Linux,于是你下载了最新的 Linux .iso文件,安装在了自己的 PC 上。没过几天,你就用 Windows 10 把它抹掉了,你又回到了 Windows 操作系统上,就像什么都没有发生过一样。

  • 双引导听起来是一件很酷的事情,所以你在同一台主机上安装了 Windows 和 Linux,并在你的笔记本电脑上尝试了多引导选项。几周后,您注意到 Windows 分区管理器上有一个相当大的未使用的分区,心想,真是浪费存储空间。然后你想起来了:这是你的 Linux 操作系统的分区。

  • 随着时间的推移,你登录 Windows 的次数越来越多,尽管你的电脑有多引导选项。

  • Ubuntu 或 Gnome 桌面体验看起来是一个连接到互联网的不错的用户界面,但是你仍然渴望 Windows 图形用户界面。您已经被 Windows GUI 所吸引。

  • 大家都说你要学习 Linux 命令,但是没人告诉你怎么学习 Linux 命令。所有的网站和视频都说你必须背下来,所以你尽最大努力尽可能多地记忆,把每个命令都塞进你小小的大脑,第二天,你甚至记不起前一天学习的一个命令。

  • 在你的工作中总会有一些 Linux 专家,他会给你施加压力,确保你再次使用 Linux,即使你已经离开它几年了。永不放弃!

也许你从一开始就喜欢 Linux,并享受学习 Linux 的经历。对于一些人来说,Linux 章节可能是 Python 网络自动化之旅的第一个障碍,但是我们必须继续。根据一些快速的互联网研究,在 IT 行业中仍然只有一小部分真正的 Linux 管理员。在我上一个工作场所,80 名 IT 工程师中有 3 名 Linux 工程师,而在我目前的团队中,只有两名成员(20 名中)精通 Linux。几乎每个会写编程代码的工程师都会同意,只在 Windows 服务器上开发代码并作为服务运行是不可能的。如果你是微软的专家,一年中可能会出现几次蓝屏死机,你对 Windows Server 最好的解决办法就是按下重启键。换句话说,作为一个网络自动化工程师新手,我们必须熟悉 Linux 系统,并开始在 Linux 上使用 Python。毕竟 Python 和 Linux 是齐头并进的。如果你想精通 Python 或任何编码,你必须很好地覆盖 Linux 基础知识。我同意,学习和掌握 Linux 管理是有挑战性的,但是我们总是可以一步一个脚印,随着时间的推移,我们会慢慢地但肯定地到达那里。尽管如此,我还是希望本章和上一章的内容足以让你看完这本书,并保持你对 Linux 的兴趣。自从学习 Python 之后,我用 Linux 改进了我的游戏,Linux 让我重新思考我的职业生涯,走上了一条不同的 IT 职业道路。我强烈建议每个人都尝试一下 Linux。让我们继续努力!

九、网络自动化的正则表达式

本章致力于学习正则表达式的基础知识。Python 提供了处理和解析文本字符串的本地方法,但是需要许多行代码来定位准确的字符串。尽管 Python 提供了强大的字符串索引方法,但在现实生活中,每次需要处理大型字符串时,您都会面临挑战。我发现很有趣的是,没有其他 Python 网络自动化书籍真正强调用正则表达式处理数据的重要性。许多书掩盖了正则表达式,因为它不是最令人兴奋的讨论话题。我意识到我必须掌握正则表达式才能使用任何编程语言。尽管如此,一开始我也不敢认真对待它。只有当我开始从事真正的项目时,我才意识到我对正则表达式的无知。我明白这一章的内容对许多读者来说消化起来会很痛苦,但你应该坚持下去,集中精力完成这一章;你以后会感谢我的。

img/492721_1_En_9_Figa_HTML.jpg

正则表达式普遍适用于所有计算机编程语言,并不是 Python 所独有的。一旦你掌握了它们,你就可以把它们应用到所有的编程语言中,比如 Java、C++、JavaScript、Ruby 和 Perl。在 Python 中,re模块为您提供了通向正则表达式的通道。通过深入理解正则表达式,并通过re模块将这些知识应用到您的 Python 程序中,您将成为一名更好的 Python 程序员。您在这里学到的知识将应用于实验室中的网络自动化应用开发。掌握正则表达式是任何 Python 程序员都不应该跳过的话题。虽然这是本书中较短的章节之一,但你将获得终生受用的宝贵技能。

在学习了基本的 Python 语法和概念之后,你会想要流利地编写 Python 应用。正如我将在整本书中提到的,用 Python 写代码不仅仅是关于语法和概念;与计算中的所有事情一样,编写代码与正确的数据处理息息相关。这些数据可能来自用户和应用变量、读取文件、浏览网页或两台计算机之间的日志。用任何编程语言编写代码几乎总是涉及到处理数据。写了一段时间 Python 代码后,我很快意识到,如果不完全理解正则表达式,就不可能开发任何网络自动化应用。

如果你对 Python 编码很认真,你必须掌握正则表达式和re模块的艺术。您将学习 regex 的基础知识,并将正则表达式应用于真实的 Cisco 路由器和交换机文本文件,以便您可以将信息与真实的生产示例相关联。

对于章节要求,您需要访问运行在 VMware Workstation 15 Pro 上的 Windows 10 主机 PC 和 Ubuntu Server 虚拟机,如图 9-1 所示。

img/492721_1_En_9_Fig1_HTML.jpg

图 9-1。

第九章中要求的装置

为什么是 Regex?

文字。正则表达式( regex )是计算中用来处理复杂文本字符串的标准方法,并不是 Python 独有的;在执行数据处理时,它被用在任何地方。乍一看,学习正则表达式可以看作是学习 Python 的一门遥远的学科。尽管如此,当您进入中级水平时,正则表达式的真正威力将会显现出来。在 Python 中,正则表达式也通过一个名为re模块的内置 Python 模块来支持。一些读者可能认为学习正则表达式不是学习 Python 的基本部分,但是当您处理实际问题以解决自动化问题时,您将需要从源文件或大字符串中提取特定的字符串(数据)。在编码中,在处理数据以创建变量和运行应用以避免数据处理错误时,每个逗号、句号和空格都很重要。如果有人问我 C、C++、Java、Perl、Ruby、R 和 Python 等编程语言是做什么的,我会毫不犹豫地直接回答是数据处理。用计算机编程语言编写的每一个程序都必须处理数据以服务于它们的目的。换句话说,我们需要一个工具来改进我们的数据处理和加工。正如您在第二章中所了解的,有一些内置的方法可以对字符串中的数据进行切片、连接和索引。当处理一组更广泛的数据时,您将会遇到许多不便,因为您可能不得不提出您的函数来更好地处理大数据。re模块来帮忙了,因为它被视为基于 20 世纪 70 年代的旧正则表达式的超级数据处理方法。知道何时何地使用不同的 Python 模块可以节省您重新发明轮子的时间。首先,您需要使用 Python 原生方法来处理数据。其次,使用内置的re模块探索数据处理。第三,使用广泛使用的 Python 库(如 NumPy 和 Pandas)探索更高级的数据处理方法。NumPy 库为多维数组提供对象,而 Pandas 提供一个内存中的 2D 表对象,称为 dataframe 。每个 Python 程序员都必须很好地学习正则表达式,并利用它们来处理数据以提取特定的字符串。

将 regex 主题作为一个专门章节的另一个原因是基于我最近从事网络自动化的经验。正则表达式在编码中起着至关重要的作用,在各种网络自动化项目中使用 Python 代码时,我怎么强调它们的有用性都不为过。一旦你达到 Python 编码的中级水平,你将不可避免地面对数据处理的挑战,如果你拒绝学习如何使用正则表达式,你就不会进步。在完成本章的所有练习后,你将会很好地掌握发明的最有用的编码工具之一。本章中展示的例子对某些人来说可能不够全面,但涵盖了足够多的场景来帮助你通过 Python 的re模块掌握数据处理。每个字符串匹配的要求都是独特的,所以我强烈建议您超越这本书,花时间研究更多的正则表达式教程。有大量免费的在线培训材料来练习适用于您的数据处理情况的用例。

响还是不响

首先,让我们考虑一个简单的例子。您申请了一份具有网络背景的 Python 开发人员的工作。在你通过第一次技术面试后,招聘经理已经给了你一个带回家的任务,分析一串要分析和转换的文本。您的任务是提取交换机名称及其 MAC 地址,以特定格式显示信息。

您获得了以下字符串。您的任务是打印交换机名称,后跟 12 个十六进制字符的 MAC 地址,其中必须保留前六位数字(OUI)以识别制造商的 ID。但是最后六位数字被用*屏蔽了,以隐藏真实的 MAC 地址。

下面是一个给定的字符串:

sw_mac = '''pynetauto-sw01 84:3d:c6:05:09:11
pynetauto-sw17 80:7f:f8:80:71:1b
pynetauto-sw05 f0:62:81:5a:53:cd'''

此外,包括十六进制 a–f 在内的任何字母都必须大写,因此结果必须与此处显示的预期输出相匹配:

PYNETAUTO-SW01 843DC6******
PYNETAUTO-SW17 807FF8******
PYNETAUTO-SW05 F06281******

如果你没有学习过正则表达式,你可以采取以下步骤来编写你的程序:

  1. 删除冒号并将字符串转换为大写字母。

  2. 在空格处拆分字符串,并将它们添加到列表中。

  3. 使用strip()方法删除空白,并制作一个更新的列表。

  4. 使用len()方法根据列表的项目长度来识别名称和 MAC 地址。如果该项有 14 个字符,那么它是一个开关名称;添加到sw列表。如果该项有 12 个字符,那么它是一个 MAC 地址;将其添加到 MAC 列表中。MAC 追加时,添加******替换 MAC 地址的后半部分。

  5. 使用 Python 字典的 zip 方法将两个列表转换成一个字典。

  6. 使用for循环打印字典的键-值对中的键和值。

在编程语言中,他们说,“剥一只猫的皮有不止一种方法”,但是其中一种方法(通过代码)将在不使用re模块的情况下获得想要的结果,看起来类似于清单 9-1 。

img/492721_1_En_9_Figb_HTML.gif这里有一个代码和示例字符串下载提示:本书中的所有代码都可以从 GitHub 上的pynetauto下载。对于本章中使用的代码,请查找名为chapter9_codes.zip的 zip 文件。

URL: github。com/pyneto/apess _ pyneto

>>> sw_mac = '''pynetauto-sw01 84:3d:c6:05:09:11
... pynetauto-sw17 80:7f:f8:80:71:1b
... pynetauto-sw05 f0:62:81:5a:53:cd'''
>>>
>>> sw_mac = sw_mac.replace(":", "").upper()          # 1
>>> sw_mac
'\nPYNETAUTO-SW01 843DC6050911 \nPYNETAUTO-SW17 807FF880711B \nPYNETAUTO-SW05 F062815A53CD\n'
>>> list1 = sw_mac.split(" ")          # 2
>>> list1
['\nPYNETAUTO-SW01', '843DC6050911', '\nPYNETAUTO-SW17', '807FF880711B', '\nPYNETAUTO-SW05', 'F062815A53CD\n']
>>> list2 =           # 3
>>> for i in list1:          # 4
...     list2.append(i.strip())          # 5
...
>>> list2
['PYNETAUTO-SW01', '843DC6050911', 'PYNETAUTO-SW17', '807FF880711B', 'PYNETAUTO-SW05', 'F062815A53CD']
>>>
>>> sw_list =           # 6
>>> mac_list =           # 7
>>> for i in list2:          # 8
...     if len(i) == 14:          # 9
...         sw_list.append(i)          # 10
...     if len(i) == 12:          # 11
...         i = i[:6] + "******"           # 12
...         mac_list.append(i)          # 13
...
>>> sw_list
['PYNETAUTO-SW01', 'PYNETAUTO-SW17', 'PYNETAUTO-SW05']
>>> mac_list
['843DC6******', '807FF8******', 'F06281******']
>>> sw_mac_dict = dict(zip(sw_list, mac_list))          # 14
>>> for k,v in sw_mac_dict.items():          # 15
...     print(k, v)          # 16 lines
...
PYNETAUTO-SW01 843DC6******
PYNETAUTO-SW17 807FF8******
PYNETAUTO-SW05 F06281******

Listing 9-1.Native Python Methods

你不需要担心或理解如何阅读前面的代码。重要的是,您可以计算出达到预期结果所需的代码行数。不算换行和数据行,仅使用 Python 方法(不使用任何模块)打印结果就需要 16 行代码。是的,它可以工作,但是需要许多行代码来实现目标。

在你学会了如何使用正则表达式之后,你可以使用 Python 的re模块来做同样的任务,数据操作变得轻而易举。代码行数下降,读写更容易。统计清单 9-2 中的代码行数;打印出预期的结果只需要四行代码(不包括换行符和数据线)。因此,当您比较两个清单之间的代码行数时,您可以看到所使用的代码行数显著减少。这是一个简单的例子,但是想想使用大数据的更复杂的任务。编写冗长的定制代码来实现最终目标会花费大量的时间和精力。使用 Python 代码中的正则表达式模块,您的应用代码将变得更简洁、更易于编写、更易于阅读。

>>> sw_mac = '''pynetauto-sw01 84:3d:c6:05:09:11
... pynetauto-sw17 80:7f:f8:80:71:1b
... pynetauto-sw05 f0:62:81:5a:53:cd'''
>>> import re           # 1
>>> sw_mac = sw_mac.replace(":", "").upper()          # 2
>>> pattern = re.compile("([0-9A-F]{6})" "([0-9A-F]{6})")           # 3
>>> print(pattern.sub("\g<1>******", sw_mac))          # 4
PYNETAUTO-SW01 843DC6******
PYNETAUTO-SW17 807FF8******
PYNETAUTO-SW05 F06281******
>>>

Listing 9-2.re Example

这个阶段不需要自己写代码。要下载并运行代码,你可以去我的pynetauto GitHub 站点,下载清单 9-1 和 9-2 中的代码。

使用 Python 研究正则表达式

用 Python 学习正则表达式有很多方法,这里我们将讨论如何用几种不同的方法学习。尝试一种最适合自己的学习方法,坚持下去,直到完成本章的练习。

img/492721_1_En_9_Figc_HTML.gif如果你想跟随清单 9-3 和 9-4 ,你可以从我的 GitHub 站点下载sh_ver.txt文件,并在你的 Linux 目录下保存为sh_ver.txt

URL: github。com/pyneto/apess _ pyneto

方法 1:使用记事本++

你已经在第二章安装了 Notepad++,它是一个很好的工具,可以用来探索正则表达式。在 Notepad++中,有两种学习正则表达式的方法,根据数据或文本的大小,您可以选择任何一种方法。你可以对一个短字符串使用单个 Python 文件,如图 9-2 所示,在 Notepad++中使用 Ctrl+F6 或 Ctrl+F5 执行脚本。

img/492721_1_En_9_Fig2_HTML.jpg

图 9-2。

使用 Notepad++和一个脚本学习正则表达式

如果文本数据很长或者要读取多个文件,可以使用文件读取方法将文件读入 Python 脚本并运行您编写的 Python 代码。该示例如图 9-3 和 9-4 所示。

img/492721_1_En_9_Fig4_HTML.jpg

图 9-4。

使用 Notepad++的正则表达式,读取文件方法数据文件

img/492721_1_En_9_Fig3_HTML.jpg

图 9-3。

使用 Notepad++的正则表达式,读取文件方法 re 脚本

方法 2:使用 Linux Shell

像 Notepad++方法一样,您可以运行一个 Linux VM 服务器,并通过 SSH 连接到服务器来练习 Python 上的正则表达式。如果你想测试一个简单的正则表达式,你可以用 Python 解释器启动一个交互式会话(参见清单 9-3 )。在您键入表达式并按 Enter 键后,它会立即返回结果,或者如果您的表达式不匹配任何字符串,则不返回结果。您也可以在 Windows 10 主机上使用 Python 3 来使用这种方法。与 Notepad++中一样,您也可以先编写一个 Python 代码,然后从终端控制台运行它来运行re匹配脚本,如清单 9-4 所示。这种方法听起来可能很难,但是当您练习几个例子时,您会熟悉它,并且在 Python 中使用正则表达式会变得更容易。

pynetauto@ubuntu20s1:~/ex_regex$ pwd
/home/pynetauto/ex_regex
pynetauto@ubuntu20s1:~/ex_regex$ ls
sh_ver.txt
pynetauto@ubuntu20s1:~/ex_regex$ nano ex9.4_sh_ver.py
pynetauto@ubuntu20s1:~/ex_regex$ cat ex9.4_sh_ver.py
import re
with open("/home/pynetauto/ex_regex/sh_ver.txt") as f:
    read_file = f.read()

# Only match Cisco router model number from show version output.
rt_model = re.findall("[A-Z]{3}\d{4}\w+", read_file)
print(rt_model)

my_router = rt_model[0]
print(my_router)
pynetauto@ubuntu20s1:~/ex_regex$ python3 ex9.4_sh_ver.py
['ISR4351/K9']
ISR4351/K9

Listing 9-4.Regular Expression Writing Python Code on Linux

pynetauto@ubuntu20s1:~$ pwd
/home/pynetauto
pynetauto@ubuntu20s1:~$ mkdir ex_regex
pynetauto@ubuntu20s1:~$ cd ex_regex
pynetauto@ubuntu20s1:~/ex_regex$ nano sh_ver.txt
pynetauto@ubuntu20s1:~/ex_regex$ ls
sh_ver.txt
pynetauto@ubuntu20s1:~/ex_regex
pynetauto@ubuntu20s1:~/ex_regex$ python3
Python 3.8.2 (default, Jul 16 2020, 14:00:26)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import re
>>> with open("/home/pynetauto/ex_regex/sh_ver.txt") as f:
...     read_file = f.read()
...
>>> # Only match Cisco router model number from show version output.
>>> rt_model = re.findall("[A-Z]{3}\d{4}\w+", read_file)
>>> print(rt_model)
['ISR4351/K9']
>>> my_router = rt_model[0]
>>> my_router
'ISR4351/K9'

Listing 9-3.Regular Expression on Python Interpreter

正则表达式细分:[A-Z]{3}\d{4}[/]\w+

|

[A-Z]{3}

|

\d{4}

|

[/]

|

\w+

| | --- | --- | --- | --- | | 三个大写字母 | 四位数 | 字符/ | 至少出现一次的任何字符串 |

方法 3:使用互联网学习正则表达式

学习正则表达式最有效的方法之一是使用 web 浏览器并查看 Internet 上的内容。有很多网站提供免费的正则表达式练习。一些网站偏向于一种编程语言而不是另一种,但是试着找到中性的编程语言网站并多加练习。一旦建立了对正则表达式的信心,就可以开始使用前面展示的前两个 Python re方法之一。在 Python 中练习re模块很重要,因为这是你在 Python 中使用正则表达式的方式。比较好的正则表达式练习站点之一是 regex101 ( https://regex101.com )。见图 9-5 。

img/492721_1_En_9_Fig5_HTML.jpg

图 9-5。

正则表达式练习网站

如果你一直在忙,但仍然想练习正则表达式,你可以在 Android 设备或苹果 iOS 设备上练习。在移动设备上下载 RegexPal(适用于 Android 手机)或 RegEx Lab(适用于 iOS 设备)或类似的应用。这些应用对于随时随地进行简单的正则表达式练习非常有用。参见图 9-6 。

img/492721_1_En_9_Fig6_HTML.jpg

图 9-6。

Android 正则表达式应用示例

正则表达式操作:基础

正则表达式使用元字符来匹配静态或动态数据字符串。 meta 这个词是一个拉丁(希腊语)词,意思是之后的*、之后的或者之后的。当在编程语言中使用时,元字符从它们的实际表示中具有特殊的含义,因此元字符可以被定义为具有隐藏含义的字符。元字符包括以下内容:*

  . ^ $ * + ? \ | ( ) [ ] { }

当在正则表达式中使用这些元字符之一时,它具有一些特殊的含义。最后六个字符成对出现,所以在正则表达式中必须成对使用括号。先从最直白的正则表达式开始,慢慢积累知识。此外,如果您想将这些字符表示为单个文字字符,那么元字符可以用一组方括号([ ])括起来,或者使用反斜杠(\)进行转义。但是,要特别注意元字符^\;使用方括号方法无法精确匹配这两个字符。见表 9-1 。

表 9-1。

正则表达式元字符

|

元字符

|

术语

|

用\转义

|

使用[ ]进行字面匹配

| | --- | --- | --- | --- | | . | 点 | \. | [.] | | ^ | 脱字号 | \^ | [^] | | $ | 美元 | \$ | [$] | | * | 星星 | \* | [*] | | + | 加 | \+ | [+] | | ? | 问号 | \? | [?] | | \ | 反斜线符号 | \\ | [\] | | &#124; | 管 | \&#124; | [|] | | ( ) | 圆括号 | \( \) | [(] [)] | | [ ] | 方括号 | \[ \] | [[][]] | | { } | 波形括号 | \{ \} | [{] [}] |

为了避开与^\的字面匹配方法,您可以在方括号内的^\前添加一个反斜杠。见清单 9-5 。

与所有其他章节一样,为了全面理解正则表达式的概念,你需要打开你的 Python 解释器,在每个练习中输入所有用粗体标记的内容。这包括三个大于号(>>>)之后的所有内容。

>>> import re
>>> expr = " . ^ $ * + ? \ | ( ) [ ] { }"
>>> re.search(r'[\^]', expr)
<re.Match object; span=(3, 4), match='^'>
>>> re.search(r'[\\]', expr)
<re.Match object; span=(13, 14), match='\\'>

Listing 9-5.Matching Metacharacter ^ and \ Using Square Brackets ([ ])

如果在方括号组中使用了^\,则需要在元字符前添加转义反斜杠。这是有充分理由的,当用在方括号内时,^用来否定它后面的字符。在你的 Python 解释器中输入清单 9-6 并仔细研究它。[^a]意味着匹配除以外的所有字符。在清单 9-6 中,除了字母 a 之外,所有字符都匹配。

>>> import re
>>> re.findall('[^a]', 'abracadabra')
['b', 'r', 'c', 'd', 'b', 'r']

Listing 9-6.Meaning of [^a]

方括号内的反斜杠[\]也有一个小问题,因为正则表达式将反斜杠识别为右方括号]的否定字符。所以,我们必须通过添加另一个反斜杠来否定反斜杠。看看清单 9-7 ,其中反斜杠必须用来匹配反斜杠和一个右方括号]

>>> re.search(r'[\\]', 'match \ or ]')
<re.Match object; span=(6, 7), match='\\'>
>>> re.search(r'[\]]', 'match \ or ]')
<re.Match object; span=(11, 12), match=']'>

Listing 9-7.Meaning of [\]]

字符类([])

我们将学习的第一个元字符是字符类元字符。通常在左开方括号([)和右闭方括号(])之间表示一个由字符类组成的正则表达式。字符类[ ]中允许使用所有字符。

例如,[aei]的正则表达式将匹配元音字母 aei 中的任何一个。我们来看看和闹铃相关的词的真实用法:铿锵

|

正则表达式

|

单词(字符串)

|

相配的

|

说明

| | --- | --- | --- | --- | | [aei] | ding | ding | 逐字匹配字符 i (区分大小写) | | buzz | 嗡嗡声 | 不匹配;嗡嗡声不包含 aei | | beep | 哔哔声 | 匹配字符 e 两次 | | clang | 叮当声 | 逐字匹配字符 a |

当连字符(-)用在带有字母或数字的方括号内时,它缩写了一个范围的表达式。例如,[a-z]采用全小写字母的含义。[A-Z]取从 AZ 所有大写字母的含义。你已经猜到了:要同时用小写和大写表示所有字母,要匹配的正则表达式就变成了[a-zA-Z]。如果你想表达一个十六进制数,你将使用[0-9a-fA-F],意思是从 af 开始的所有整数和大小写字母。在正则表达式中,大小写很重要。常见的例子[0-9]取 0 到 9 的意思,意为[0 1 2 3 4 5 6 7 8 9]

几乎所有的字符都可以用在方括号[ ]之间,但是有一个例外,那就是^(脱字符)符号(^元字符)。当^用在方括号内时,所用的表达取与NOT相反的意思或意义。比如想用一个表达式排除任何整数,可以用[⁰-9]。如果不想匹配任何大写字母,可以使用[^A-Z]

对于常用的正则表达式,如[0-9][a-zA-Z],有一些特殊的表达式可以节省时间,使正则表达式更加简洁易读。以下列表供您参考:

|

正则表达式

|

可互换表达式

|

说明

| | --- | --- | --- | | \d | [0-9] | 匹配从 0 到 9 的整数。 | | \D | [⁰-9] | 匹配除整数以外的所有字符。 | | \s | [ \t\n\r\f\v] | 匹配所有空格。包括一个空格。 | | \S | [^ \t\n\r\f\v] | 匹配除空白字符以外的所有字符。 | | \w | [a-zA-Z0-9] | 匹配所有字母数字。 | | \W | [^a-zA-Z0-9] | 与字母数字不匹配。匹配所有符号,如% # @。 |

在编程领域,反斜杠(\)通常用于否定反斜杠后面的表达式的含义。例如,d是字母表中的字母 d ,但如果和反斜杠组合成\d,这就呈现出不同的含义。此外,大写字母的特殊表达总是与小写字母表达相反的意思。

点(。):单个字符匹配

正则表达式.(点)匹配除了行终止符如\n之外的所有字符。有趣的是,正则表达式提供了包含\n的方法,我们将在后面的例子中看到,但是通过使用re.DOTALL选项,.(点)也可以包含换行符\n

让我们考虑下面的正则表达式例子。

|

正则表达式

|

说明

| | --- | --- | | d.g | 匹配字母 dg 之间的任意单个字符 |

任何指定的字符都必须匹配。例如,字母 d 必须在第一个位置匹配,字母 g 必须在第三个位置匹配。中间字符可以匹配除新字符\n之外的任何字符。

|

正则表达式

|

单词(字符串)

|

相配的

|

说明

| | --- | --- | --- | --- | | d.g | 狗 | 狗 | 匹配所有字符 dog 。 | | d%g | d%g | 匹配所有字符 d 、% sign 和 g 。 | | d \ ng | 没有人 | 不匹配, \n 被忽略,不匹配。 dg 匹配,但是 \n 被忽略,所以 d\ng 不匹配。 |

如果点字符在方括号之间,[ ]怎么办?

|

正则表达式

|

说明

| | --- | --- | | d[.]g | 方括号之间的点字符将其字面意思作为一个点(.)。所以,它只会匹配d.g,而不会匹配dogd%g。 |

星号(*):重复

让我们放大这个,看看下面的正则表达式。

|

正则表达式

|

说明

| | --- | --- | | zo*m | 如果星号前面的字符出现零次、一次或多次,则匹配。匹配前面的表达式。在这种情况下,字母 o 。 |

元字符*有重复的意思,它匹配前面的表达式 o 零次、一次或几乎无限次。*在尝试匹配字符可能出现或可能不出现的不可预测事件时变得方便;因此,它可能匹配,也可能不匹配。

所有的例子都由元字符*匹配。

|

正则表达式

|

单词(字符串)

|

相配的

|

说明

| | --- | --- | --- | --- | | zo*m | 赞比亚 | 赞比亚 | 匹配零次 o*。即使没有 ozm也是旗鼓相当。* | | 播放器 | 播放器 | 匹配 o 一次。 | | 嗡嗡声 | 嗡嗡声 | 匹配中间的 oo 。 |

加号(+):重复

加号(+)是与重复相关联的另一个元字符。它与星号有点相似,但又略有不同,因为零次不被视为匹配。使用+元字符时,必须至少匹配一个字符。让我们再次使用相同的字符进行解释。

|

正则表达式

|

说明

| | --- | --- | | zo+m | 如果字母 o 出现一次或多次,则匹配 |

+元字符匹配一些单词。

|

正则表达式

|

单词(字符串)

|

相配的

|

说明

| | --- | --- | --- | --- | | zo+m | 赞比亚 | 没有人 | 中间没有 o,所以不匹配 | | 播放器 | 播放器 | 匹配 zom | | 嗡嗡声 | 嗡嗡声 | 匹配 zoom |

{m,n}:重复

使用元字符{m, n },您可以匹配重复的次数。字母m是比赛计数的开始,n是比赛计数的结束。比如o{1, 3}取匹配字符 o 一到三次的意思。再比如o{3, },这意味着前置字符 o 的重复至少要匹配三次以上。再比如o{, 3}会匹配同一个字符 o 多达三次。所以,{0,}的正则表达式等价于+{1,}等价于*。我们先通过看一些例子来学习{m, n}

{m}

参考字母或m是匹配前置字符所需的精确重复次数。看一个简单的例子和解释。

|

正则表达式

|

说明

| | --- | --- | | Zo{1}e | 匹配第一个字母 Z ,然后匹配字母 o m多次(m =1),在本例中只匹配一次,然后匹配字母 e 。所以,期望的匹配字母是Zoe。注意,正则表达式是区分大小写的。 |

让我们看更多的例子来帮助你理解。如果你能在阅读这本书的同时在电脑键盘上输入这些,你会学到更多。

|

正则表达式{m}

|

单词(字符串)

|

相配的

|

说明

| | --- | --- | --- | --- | | o{2} | 面向对象的 | 面向对象的 | 匹配字符 oo | | 动物园 | 动物园 | 匹配字符 oo | | 繁荣 | 繁荣 | 匹配字符 oo | | zo{2}m | 赞比亚 | 没有人 | 缺少 oo ,所以不匹配 | | 播放器 | 没有人 | 缺少 oo ,所以不匹配。 | | 嗡嗡声 | 嗡嗡声 | 匹配字符 zoom |

{m,n}

第一个参考数字或m是最小重复数,第二个参考数字或n是最大重复数。首先,简单解释一下{m, n}是如何工作的。

|

正则表达式

|

说明

| | --- | --- | | o{2, 5} | 匹配字符 oooooooooooo。匹配出现两到五次的字母 o 。 |

现在让我们看一些简单的例子。

|

正则表达式{m,n}

|

单词(字符串)

|

相配的

|

说明

| | --- | --- | --- | --- | | zo{1,3}m | 赞比亚 | 没有人 | 不匹配, o 匹配 0 次 | | 嗡嗡声 | 嗡嗡声 | 匹配 zoom | | 祖姆 | 没有人 | 不匹配,多了一个 o |

?(问号:重复)

元字符与{0, 1}的含义相同,可以互换使用。再次使用一个简单的例子,这里有更多关于?如何在正则表达式中工作的解释。

|

正则表达式

|

说明

| | --- | --- | | Zoe? | 匹配佐伊佐伊。问号前的字符 e 变成可选。所以,连弦都是 Zo。正则表达式仍将匹配该单词。这个可以写成Zoe{0, 1}。 |

使用与之前相同的示例来更好地理解?

|

正则表达式

|

单词(字符串)

|

相配的

|

说明

| | --- | --- | --- | --- | | zo?m | 赞比亚 | 赞比亚 | 匹配zm。字母 o 是可选的,所以不必匹配。 | | 播放器 | 播放器 | 匹配 zom 。 | | 嗡嗡声 | 没有人 | 不匹配,因为只需要一个 o 。 |

如前所示,*+?元字符可以用{m, n}方法替换,但是使用*+?使得正则表达式更容易阅读和理解,所以只要有可能,尽量使用*+?而不是{m, n}格式。

Python 的 re 模块

如前所述,Python 提供了开箱即用的正则表达式特性,它由一个名为re(正则表达式)的标准模块支持。当 Python 安装在您的操作系统上时,此模块将作为标准 Python 库的一部分预安装。

为了开始您的 Python re模块之旅,值得一提的是,使用re模块编写 Python 代码的方式或风格有所不同。您可以选择保持脚本结构简单、标准或结构化。在简单风格中,您可以用一行代码编写正则表达式语句,如表 9-2 中的简单风格示例所示。当您使用标准样式时,您不必使用re.compiler语句,但是您仍然可以通过分离字符串和正则表达式语句来添加一些样式。如果您选择在代码中有更多的一致性,您可以使用编译器风格,使用re.compiler语句。任何编码风格都可以,但是在代码中使用re.compiler语句的优势是显而易见的。它为您的代码提供了更多的控制和结构,因此也增加了样式的一致性。

让我们通过查看每个示例来快速比较问题中的三种风格。所有三个例子将返回相同的结果,但是以两种不同的风格编写。

表 9-2。

Python 中 re 模块的不同使用方法

|

风格

|

例子

| | --- | --- | | 简单的 | import re``m = re.findall('\dx\d{4}', "Configuration register is 0x2102")``print(m) | | 标准 | import re``expr = "Configuration register is 0x2102"``m = re.findall('\dx\d{4}', expr)``print(m) | | 编译程序 | import re``expr for expression``expr for expression``p for pattern``p for pattern``m for match``m for match``print(m) | | 结果 | ['0x2102'] |

在第三种编译器风格中,我们给编译器语句一个单独的变量p,它包含我们想要匹配的正则表达式。然后变量m使用变量exprp执行匹配功能。如果编译后的正则表达式必须使用两次以上,那么使用这种方法比使用更简单的方法更有利。你必须注意 Python 允许你写短代码,但是你失去了结构和风格,而更多的代码行给你更好的控制和结构,但是你必须写更多的代码行来达到同样的结果。在我看来,这里没有正确或错误的答案;这完全取决于你的风格和个人喜好。

Python re 字符串方法

我们必须掌握四种基本类型的正则表达式搜索方法。让我们快速浏览一下这四种方法,然后通过在 Python 解释器上做一些练习来回顾每一种方法。参见表 9-3 。

表 9-3。

关于字符串方法

|

方法

|

说明

| | --- | --- | | re.match() | 在第一行搜索正则表达式模式并返回 match 对象。(仅搜索第一行。)如果没有找到匹配,则返回None。 | | re.search() | 搜索正则表达式模式并返回第一个匹配项。与re.match()方法不同,该方法将检查所有行。如果没有找到匹配,则返回None。 | | re.findall() | 搜索正则表达式模式并匹配所有匹配项。与re.match()re.search()方法不同,该方法将一个字符串中所有不重叠的模式匹配作为一个字符串列表返回。 | | re.finditer() | 搜索正则表达式模式,这个方法返回一个迭代器,在字符串的re模式的所有不重叠匹配中产生MatchObject实例。 |

重新匹配()

清单 9-8 、清单 9-9 和清单 9-10 将返回相同的匹配目标,但写法不同。仔细研究每一行的写法。同样,这里没有正确或错误的答案,只有不同的方法来编写相同的代码以实现相同的目标。

>>> import re
>>> expr = '0x2142 Configuration register is 0x2102'
>>> p = re.compile(r'\d\w\d{4}')
>>> m = p.match(expr)
>>> print(m)
<re.Match object; span=(0, 6), match='0x2142'>

Listing 9-10.re.match() method 3

>>> import re
>>> expr = '0x2142 Configuration register is 0x2102'
>>> re.match(r'\d\w\d{4}', expr)
<re.Match object; span=(0, 6), match='0x2142'>

Listing 9-9.re.match() method 2

>>> import re
>>> re.match(r'\d\w\d{4}', '0x2142 Configuration register is 0x2102')
<re.Match object; span=(0, 6), match='0x2142'>

Listing 9-8.re.match() method 1

让我们再做一个比赛练习。在清单 9-11 中,match对象返回包含match字符串开始和结束位置的跨度。相比之下,如果清单 9-12 中没有匹配的对象,则返回值为None。在 Python 中,None的作用与其他语言中的null相同,这意味着它不匹配任何东西。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "5 regular expression"
>>> m = p.match(expr)
>>> print(m)
Result: None

Listing 9-12.re.match()

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "five regular expression"
>>> m = p.match(expr)
>>> print(m)
<re.Match object; span=(0, 4), match='five'>

Listing 9-11.re.match()

前面的练习对于我们构建 Python 正则表达式脚本流是必不可少的。查看返回的值,最佳实践可能是以下面的格式构建我们的 Python re脚本,这样只有在找到匹配时脚本才会继续运行:

|

本章正则表达式的推荐脚本流

| | --- | | import re``p = re.compile("Enter_re_here")``expr = 'string_to_search_here'``m = p.match(expr)``if m:``print('Match found: ', m.group())``else:``print('Match not found') | ←导入 re 模块←编译一个模式←数据或表达式←使用模式匹配模式←仅在找到匹配时执行 |

重新搜索()

如前所示,让我们使用re.search()方法来执行匹配函数,在清单 9-13 中,返回的结果与正则表达式匹配字符串并返回匹配对象的re.match()相同。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "five regular expression"
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(0, 4), match='five'

Listing 9-13.re.search()

另一方面,在清单 9-14 中,与re.match()方法不同的是,re.search()方法正则表达式与数字 5 不匹配,因此它跳转到下一个匹配项并返回与正则表达式匹配的单词 months 。因此,如果从字符串的开头开始搜索,通常使用match()方法,而search()方法可以用于匹配第一个实例,搜索整个字符串。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "5 regular expression"
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(2, 9), match='regular'>

Listing 9-14.re.search()

re . findall()

这次让我们使用相同的正则表达式流来练习一下findall()方法。在 Python 解释器控制台中输入每一行以获得更多练习。与前两种方法不同,findall()方法将匹配的对象作为列表返回。在清单 9-15 中,每个单词都以字符串的形式返回到列表中。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "five regular expression"
>>> m = p.findall(expr)
>>> print(m)
['five', 'regular', 'expression']

Listing 9-15.re.findall() exercise 1

在清单 9-16 中,findall()匹配所有满足正则表达式条件的字符串,但忽略数字 5。该方法将尝试匹配并以列表格式返回所有匹配的对象。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "5 regular expression"
>>> m = p.findall(expr)
>>> print(m)
['regular', 'expression']

Listing 9-16.re.findall() exercise 2

re.finditer()

最后一个练习是关于finditer()方法的。像以前一样,让我们键入代码,并通过做一些练习来学习(参见清单 9-17 和清单 9-18 )。finditer()方法返回与finditer()方法相同的结果,但是它返回字符串正则表达式模式的所有不重叠匹配的迭代器。对于文本处理来说,它可能是一个强大的工具,但是finditer()方法的用例非常狭窄。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "5 regular expression"
>>> m = p.finditer(expr)
>>> print(m)
<callable_iterator object at 0x000001E581F1B5E0>
>>> for r in m: print(r)
...
<re.Match object; span=(2, 9), match='regular'>
<re.Match object; span=(10, 20), match='expression'>

Listing 9-18.re.finditer() exercise 2

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "five regular expression"
>>> m = p.finditer(expr)
>>> print(m)
<callable_iterator object at 0x000001E581F1B5E0>
>>> for r in m:
...     print(r)
...
<re.Match object; span=(0, 4), match='five'>
<re.Match object; span=(5, 12), match='regular'>
<re.Match object; span=(13, 23), match='expression'>

Listing 9-17.re.finditer() exercise 1

匹配对象方法

前面,我们看到了使用 match 和 search 方法从正则表达式匹配中返回的对象,但是我们仍然对匹配的字符串以及它们是如何工作的有一些疑问。为了更好地理解这些返回对象的属性,在运行 match 和 search 方法之后,我们可以使用 match 对象方法来回答一些问题。首先,让我们快速回顾一下表 9-4 。

表 9-4。

重新匹配对象方法

|

匹配方法

|

说明

| | --- | --- | | group() | 返回匹配的字符串 | | start() | 返回匹配字符串的起始位置 | | end() | 返回匹配字符串的结束位置 | | span() | 以元组格式返回匹配字符串的开始和结束位置 |

现在我们通过练习来学习匹配对象方法,从匹配的返回对象中确认返回的对象。在使用匹配方法的第一个练习中,我们可以使用groupstartendspan匹配方法分别获得match对象的属性。将清单 9-19 中的代码输入解释器,并交互检查结果。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "automation"
>>> m = p.match(expr)
>>> print(m)
<re.Match object; span=(0, 10), match='automation'>
>>> m.group()
'automation'
>>> m.start()
0
>>> m.end()
10
>>> m.span()
(0, 10

Listing 9-19.re.match()

在清单 9-20 所示的搜索匹配方法中,如果我们想找出match对象在内存中的确切位置,可以发出groupstartendspan而不在末尾设置圆括号。无论如何,我们应该更关注match对象属性的group()start()end()span()结果。

>>> import re
>>> p = re.compile('[a-z]+')
>>> expr = "5\. regular expression"
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(3, 10), match='regular'>
>>> m.group
<built-in method group of re.Match object at 0x000001E581F00C00>
>>> m.group()
'regular'
>>> m.start
<built-in method start of re.Match object at 0x000001E581F00C00>
>>> m.start()
3
>>> m.end
<built-in method end of re.Match object at 0x000001E581F00C00>
>>> m.end()
10
>>> m.span
<built-in method span of re.Match object at 0x000001E581F00C00>
>>> m.span()
(3, 10)

Listing 9-20.re.search()

编译选项

编译正则表达式时,还可以向表达式添加选项。一、快速复习选项。使用这些正则表达式选项时,可以使用完整的描述性选项名称,如re.DOTALLre.IGNORECASEre.MULTILINEre.VERBOSE,也可以使用缩写形式,如re.Sre.Ire.Mre.X。参见表 9-5 。

表 9-5。

重新编译选项

|

选择

|

缩写

|

说明

| | --- | --- | --- | | DOTALL | S | 匹配任何字符,包括换行符\n。 | | IGNORECASE | 我 | 使 regex 不区分大小写或忽略大小写。所有主要的正则表达式引擎都以区分大小写的方式匹配;I 模式禁用区分大小写。 | | MULTILINE | M | ^$将匹配一行的开始和结束,而不是整个字符串。它使正则表达式引擎能够处理由多行组成的输入字符串。 | | VERBOSE | X | 允许使用详细模式。空白被忽略。空格、制表符和回车符与空格、制表符和回车符不匹配。 |

再说一次,没有付出就没有收获,所以让我们将下面的练习输入到 Python 解释器中,并从示例中学习。

re。DOTALL (re。s)

DOTALL选项用于在匹配字符串中包含一个换行符,让我们看看DOTALL选项在实际使用中是如何工作的。见清单 9-21 和清单 9-22 ,

>>> import re
>>> expr = 'a\nb'
>>> p = re.compile('a.b', re.DOTALL)
>>> m = p.match(expr)
>>> print(m)
<re.Match object; span=(0, 3), match='a\nb'>  ← Matches object

Listing 9-22.re.DOTALL()

>>> import re
>>> expr = 'a\nb'
>>> p = re.compile('a.b')
>>> m = p.match(expr)
>>> print(m)
None                                  ← No match found due to newline

Listing 9-21.Without re.DOTALL()

在清单 9-21 中,\n字面上表示一个换行符,由于这个原因,正则表达式a.b不能单独匹配a\nb。在清单 9-22 中,当我们使用re.DOTALL启用DOTALL选项时,我们可以确认甚至\n字符也作为字符串的一部分被匹配。在正常情况下,您不会使用DOTALL选项来匹配换行符。相反,它将通过忽略换行符来匹配多行中的字符串。

re。IGNORECASE(关于。(一)

IGNORCASE选项被启用时,它使正则表达式不区分大小写,因此大写和小写字母都匹配,而不管字母字符的大小写。同样,让我们做一些练习来看看re.IGNORECASE ( re.I)是如何工作的。参见清单 9-23 。

>>> import re
>>> expr1 = 'automation'
>>> expr2 = 'Automation'
>>> expr3 = 'AUTOMATION'
>>> p = re.compile('[a-z]+', re.IGNORECASE)
>>> m1 = p.match(expr1)
>>> print(m1)
<re.Match object; span=(0, 10), match='automation'>
>>> m2 = p.match(expr2)
>>> print(m2)
<re.Match object; span=(0, 10), match='Automation'>
>>> m3 = p.match(expr3)
>>> print(m3)
<re.Match object; span=(0, 10), match='AUTOMATION'>

Listing 9-23.re.IGNORECASE()

仅仅一个练习就足以理解re.IGNORECASEre.I的用法。在清单 9-23 中,相同的模式被使用了三次,以匹配三个不同表达式(字符串)中的模式。三种大小写类型(全部小写、首字母大写和全部大写表达式)被用作我们的目标字符串。正则表达式将匹配所有三个字符串,因为在正则表达式模式中启用了re.I选项。

re。多行(重。m)

re.MULTILINEre.M选项与^$正则表达式元字符一起使用。如前所述,^元字符用于指示字符串的开始,而$用于标记字符串的结束。通俗地说,^Network的正则表达式意味着第一个字符串必须以单词 network 开头,而automation$意味着最后一个字符串必须以单词 automation 结尾。还是那句话,我们边做边学MULTILINE。参见清单 9-24 和清单 9-25 。

>>> import re
>>> expr = '''Regular Engineers
... Regular Network Engineers
... Regular but not so Regular Engineers'''
>>> p = re.compile('^R\w+\S', re.MULTILINE)
>>> m = p.findall(expr)
>>> print(m)
['Regular', 'Regular', 'Regular']

Listing 9-25.^ and re.MULTILINE()

>>> import re
>>> expr = '''Regular Engineers
... Regular Network Engineers
... Regular but not so regular Engineers'''
>>> p = re.compile('^R\w+\S')
>>> m = p.findall(expr)
>>> print(m)
['Regular']

Listing 9-24.without re.MULTILINE()

在这两个练习中,数据(表达式)由三行组成,以常用词Regular开头。每一行都以同一个单词开始,正则表达式^R\w+\S只匹配第一个单词,以大写字母 R 开始,后面是字母数字字母到非白色空间(\S)。在清单 9-24 中,不带 re。MULTILINE 选项,搜索只匹配单词 Regular 的第一个匹配项。而在清单 9-25 中,用 re。多行选项启用时,搜索匹配单词,正则在多行中的新行的开头。

re。详细(回复。十)

尽可能多地打字,尽可能准确地打字。如果你需要交叉检查你的错别字,你可以从我的下载网站下载本章使用的练习。您可以根据需要复制并粘贴练习信息。

URL: github。com/pynetato/apess _ pynetato/

与真实的、生产就绪的脚本中使用的正则表达式相比,本章中介绍的大多数正则表达式示例都是基本的。

现在看下面两个例子(清单 9-26 和清单 9-27);然而,它们是返回相同值的相同脚本。对于对正则表达式了解有限的 Python 编码新手来说,第一个例子(r'1-9(?:,\d{3})*(?:\.\d*[1-9])?|0?\.\d*[1-9]|0')中编译后的正则表达式看起来就像胡言乱语一样。编译后的正则表达式似乎有些过于复杂。它用于匹配与日期相关的数字,并有目的地匹配数字或在年份中使用逗号的数字。第一个例子是我们到目前为止所熟悉的,但是第二个例子充分利用了正则表达式re.VERBOSE选项来添加注释并解释正则表达式是如何工作的。虽然两者返回相同的结果,但是后者增加了可读性,甚至对于编写这个正则表达式的人来说也是如此。使用re.VERBOSEre.X选项,您可以解码并添加可选注释以增加可读性。

import re

expr = 'I was born in 2,009 and I am 15 years old. I started my primary school in 2,010'
p = re.compile(r"""
[1-9]           # Match a single digit between 1-9
(?:\d{0,2})     # Match digit equatl to [0-9] between 0 and 2 times
(?:,\d{3})*     # Match the character ,(Comma) literally, match a digit equal to [0-9] \
                  exactly 3 times) Zero and unlimited times
(?:\.\d*[1-9])? # Match the character. (Dot) literally, match a digit equal to [0-9] \
                  zero and unlimited times, match a single digit between [1-9]) \
                  zero and one time.
|               # OR
0?\.\d*[1-9]    # Match 0 zero or one time, match . (Dot) literally, match a digit \
                  equal to [0-9] zero or unlimited times, and match a digit between [1-9]
|               # OR
0               # Match one 0
""", re.VERBOSE)

m = p.findall(expr)

print(m)

Result:  ['2,009', '15', '2,010']

Listing 9-27.With re.VERBOSE

import re

expr = 'I was born in 2,009 and I am 15 years old. I started my primary school in 2,010'
p = re.compile(r'1-9(?:,\d{3})*(?:\.\d*[1-9])?|0?\.\d*[1-9]|0')

m = p.findall(expr)

print(m)

Listing 9-26.Without re.VERBOSE

:令人困惑的反斜杠字符

\在 Python 中赋予元字符特殊的含义;它在 Python 字符串和 regex 引擎中都是一个转义字符,您最终会将您的模式传递给它。当在 Python re引擎中使用斜杠字符时,这可能会导致一些混乱,即您必须使用多少转义字符来匹配您想要从数据中提取的确切字符串。为了消除混淆,在用 Python 编译正则表达式时,r(原始字符串表示法)被附加在正则表达式之前。让我们输入下面的两个例子(清单 9-28 和清单 9-29 ),第一个没有使用原始字符串符号r,第二个使用原始字符串符号r。在下面的例子中,我们试图匹配字符串\scored

>>> import re
>>> expr = 'Our team \scored three goals\\'
>>> p1 = re.compile('\scored')
>>> p2 = re.compile('\\scored')
>>> p3 = re.compile('\\\scored')
>>> p4 = re.compile('\\\\scored')
>>> p5 = re.compile('\\\\\scored')
>>> print(p1.findall(expr))
[]
>>> print(p2.findall(expr))
[]
>>> print(p3.findall(expr))
['\\scored']
>>> print(p4.findall(expr))
['\\scored']
>>> print(p5.findall(expr))
[]

Listing 9-28.Backslashes Without Raw String Notation

在清单 9-29 中,编译后的带有三个反斜杠和四个反斜杠的正则表达式匹配并返回相同的结果。这可能会令人困惑,因为您可能不确定是使用三个还是四个反斜杠来匹配单词和字面反斜杠字符\scored

>>> import re
>>> expr = 'Our team \scored three goals\\'
>>> p1 = re.compile(r'\scored')
>>> p2 = re.compile(r'\\scored')
>>> p3 = re.compile(r'\\\scored')
>>> p4 = re.compile(r'\\\\scored')
>>> print(p1.findall(expr))
[]
>>> print(p2.findall(expr))
['\\scored']
>>> print(p3.findall(expr))
[]
>>> print(p4.findall(expr))
[]

Listing 9-29.Backslash with Raw String Notation

在清单 9-30 中,我们使用了原始字符串符号r,很明显,结果表明您必须使用两个反斜杠来匹配目标字符串\scored。在原始字符串符号中使用一个、三个和四个反斜杠将与目标字符串不匹配。

>>> import re
>>> expr = 'Our team \scored three goals\\'
>>> p2 = re.compile(r'\\scored')
>>> m = p2.findall(expr)
>>> print(m)
['\\scored']
>>> n = m[0]
>>> n
'\\scored'
>>> for x in n:
...     print(x, end=")
...
\scored

Listing 9-30.Backslash with Raw String Notation

在清单 9-30 中,使用了原始字符串匹配方法,在本例中,原始字符串匹配方法匹配两个(反斜杠)并返回匹配的字符串,在列表中得分。若要转换列表中的项目,可以使用索引方法。

正则表达式:一点修改加更多

在本节中,让我们快速回顾一下到目前为止我们所学的内容,并涵盖更多元字符和正则表达式方法,如分组、前瞻搜索等。

更多元字符

到目前为止,我们已经介绍了最常用的元字符,但是我们还必须介绍一些更常见的元字符。我们在这里讨论的元字符的特征与本章前面讨论的有些不同。我们已经学习了元字符,如+*[ ]{ }。当这些元字符在匹配字符串中改变位置时,它们将只搜索字符串一次。现在,让我们通过引入一些新的元字符来扩展我们的元字符词汇表,然后我们将看看分组、前视和后视示例。

OR 运算符(|)

在正则表达式中,|(管道)元字符与or的含义相同。a|b的正则表达式与[ab]的含义相似,但它们的操作方式不同。|[ ]都是 OR 运算符,它们会尝试匹配字符串中的指定字符,但匹配和返回的结果略有不同。让我们看看下面的例子,准确理解|[ ]的真正区别。参见清单 9-31 和清单 9-32 。

>>> re.findall('a(b|c)', 'a, ab, ac, abc, acb, ad')
['b', 'c', 'b', 'c']

Listing 9-32.a(b|c)

>>> import re
>>> re.findall('a[bc]', 'a, ab, ac, abc, acb, ad')
['ab', 'ac', 'ab', 'ac']

Listing 9-31.a[bc]

在清单 9-31 中,您已经使用了[ ]正则表达式来匹配bc;正则表达式匹配的返回值也包含前导的a。但是在清单 9-32 中,你用|替换了[ ],返回的值只有bc的匹配值,没有前导a。所以,[ ]|的区别在于它匹配前导字符的方式,但是一个返回前导字符,一个不返回。

现在,看一下清单 9-33 和清单 9-34 ,这用一个正则表达式的范围选项来说明,更明显的是,一个打印前导字符,一个不打印。

>>> re.findall('3(a|b|c|d|e|f)', '3, 3a, 3c, 3f, 3g')
['a', 'c', 'f']

Listing 9-34.3(a|b|c|d|e|f)

>>> re.findall('3[a-f]', '3, 3a, 3c, 3f, 3g')
['3a', '3c', '3f']

Listing 9-33.3[a-f]

清单 9-35 和清单 9-36 是带有苹果树莓字样的|(或)的简单例子。在清单 9-36 中,re.findall 匹配方法匹配两个单词, rasberryapple 。匹配的单词作为列表返回。

>>> print(re.findall('apple|raspberry', 'raspberry and apple pie'))
['rasberry', 'apple']

Listing 9-36.apple|rasberry

>>> re.match('apple|raspberry', 'raspberry pie')
<re.Match object; span=(0, 8), match='rasberry'>

Listing 9-35.apple|rasberry

^和$(主播)

^(插入符号)将从字符串的第一个字符开始查找匹配。当您将它与re.MULTILINEre.M选项一起使用时,您可以在数据或字符串的每个换行符处启用正则表达式。让我们使用 Python 解释器快速做一些练习。列表 9-37 和列表 9-38 。

>>> re.findall('^Start', 'Start to finish')
['Start']

Listing 9-37.^Start

在清单 9-38 中,^指示正则表达式^Start匹配字符串开头的单词 start

>>> re.findall('finish$', 'Start to finish')
['finish']

Listing 9-38.finish$

在清单 9-38 中,$指示正则表达式匹配字符串末尾的单词 finish

在清单 9-39 中,结合^$,正则表达式匹配以 S 开始并以 sh 结束的精确字符串。

>>> re.findall('^S.+sh$', 'Start to finish\nSpecial fish\nSuper fresh', re.MULTILINE)
['Start to finish', 'Special fish', 'Super fresh']

Listing 9-41.^S.+sh$' and re.M

>>> re.findall('^S.+sh$', 'Start to finish')
['Start to finish']

Listing 9-39.^S.+sh$

在清单 9-40 中,使用re.M模块,您已经匹配了以 S 开始并以 sh 结束的三行字符串。

现在让我们通过另一个练习来看一个简化的实际例子。代码可以从我的 GitHub 网站下载,这个练习的文件名是5.7.1.2_5.py(https://github.com/pynetauto/apress_pynetauto)。见清单 9-41 。

>>> import re

>>> expr = '''SYDCBDPIT-ST01#sh ip int brief
... Interface              IP-Address      OK? Method Status                Protocol
... Vlan1                  unassigned      YES NVRAM  up                    up
... Vlan50                 10.50.50.11     YES NVRAM  up                    up
... FastEthernet0          unassigned      YES NVRAM  down                  down
... GigabitEthernet1/0/1   unassigned      YES unset  down                  down
... GigabitEthernet1/0/2   unassigned      YES unset  up                    up
... GigabitEthernet1/0/3   unassigned      YES unset  up                    up
... '''

>>> p = re.compile('^Gig.+down$', re.MULTILINE)
>>> m = p.findall(expr)
>>> print(m)
['GigabitEthernet1/0/1   unassigned      YES unset  down                  down']

Listing 9-41.^Gig.+up$ and re.M

您可以很容易地将它应用到真实的场景中,从您的网络设备show命令中提取一些接口信息。在清单 9-41 中,我们使用思科交换机的show ip interface简短命令的前几行来演示这一点。使用正则表达式^Gig.+up$,我们可以快速匹配具有关闭状态的接口。出于演示的目的,这个例子过于简单了。尽管如此,在您必须管理成百上千个 switchport 接口的真实环境中,您需要正则表达式的能力来处理收集的数据。

\A 和\Z

在单行字符串匹配中,^\A的工作方式相同,但是在尝试匹配多行字符串时,行为会有所不同(参见清单 9-42 和清单 9-43 )。同样适用于$\Z;尝试匹配多行字符串时,匹配行为会发生变化(参见清单 9-44 )。当re.Mre.MULTILINE选项被启用时,^可以在字符串的开头和每个换行符之后匹配,但是\A只在字符串的开头匹配(参见清单 9-45 )。另外,$可以在字符串末尾和每个换行符之前匹配(参见清单 9-46 ),但是\Z只在字符串末尾匹配(参见清单 9-47 )。

>>> re.findall('S.+sh\Z', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
['Super smelly fish']

Listing 9-47.S.+sh\Z with re.MULTILINE

>> re.findall('S.+sh$', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
['Start to finish', 'Super special fish', 'Super fresh fish', 'Super smelly fish']

Listing 9-46.S.+sh$ with re.MULTILINE

>>> re.findall('\AS.+sh', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
['Start to finish']

Listing 9-45.\AS.+sh with re.MULTILINE

>>> re.findall('^S.+sh', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
['Start to finish', 'Super special fish', 'Super fresh fish', 'Super smelly fish']

Listing 9-44.^S.+sh with re.MULTILINE

>>> re.findall('\AS.+sh', 'Start to finish')
['Start to finish']

Listing 9-43.\AS.+sh

>>> re.findall('^S.+sh', 'Start to finish')
['Start to finish']

Listing 9-42.^S.+sh

以下练习演示了混合使用^\A$\Z如何影响匹配的返回结果。我们通过用\n分隔每一行来人为地添加新行,以创建新行效果。拿出您的例子并测试这些元字符来扩展您的理解。参见清单 9-48 ,清单 9-49 ,清单 9-50 ,清单 9-51 。

>>> re.findall('\AS.+sh\Z', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
[]

Listing 9-51.\AS.+sh\Z with re.M

>>> re.findall('^S.+sh\Z', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
['Super smelly fish']

Listing 9-50.^S.+sh\Z with re.M

>>> re.findall('\AS.+sh$', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
['Start to finish']

Listing 9-49.\AS.+sh$ with re.M

>>> re.findall('^S.+sh$', 'Start to finish\nSuper special fish\nSuper fresh fish\nSuper smelly fish', re.M)
['Start to finish', 'Super special fish', 'Super fresh fish', 'Super smelly fish']

Listing 9-48.^S.+sh$ with re.M

\b 和\B

在正则表达式中,\b代表单词边界。在一个句子中,首词和下一个词之间的空白由通常的词边界表示。

|

单词边界

|

描述

| | --- | --- | | \b | 代表像^(类似于$^)这样的主播匹配位置,其中一边是字符(像\w),另一边不是字符;例如,它可能是字符串的开头或空格字符。 | | \B | 它伴随着它的否定,\B。这将匹配所有与\b不匹配的位置,如果我们想要找到一个由单词字符包围的搜索模式,那么就可能出现这种情况。 |

根据正则表达式字面规则,\b代表退格;因此,当您在正则表达式中使用它时,您总是必须使用原始字符串符号r来指定这不是退格字符。

在清单 9-52 中,您试图匹配表达式中的单词 computers。在编译的模式中,我们使用前导的\b和结尾的\b作为我们试图匹配前导空格和结尾空格的单词,所以这个单词匹配并返回结果。

>>> import re
>>> expr = "Small computers include smartphones."
>>> p = re.compile(r'\bcomputers\b')
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(6, 15), match='computers'>

Listing 9-52.\b(word)\b matched

在清单 9-53 中,单词微型计算机有单词计算机但是以微型开始,所以空格不能匹配单词微型后面的单词计算机。结果果然是None

>>> import re
>>> expr = "Microcomputers include smartphones."
>>> p = re.compile(r'\bcomputers\b')
>>> m = p.search(expr)
>>> print(m)
None

Listing 9-53.\b(word)\b not match

在清单 9-54 中,第一个\b在我们的正则表达式编译器中被删除,现在匹配目标词 computers 被匹配并返回预期结果。

>>> import re
>>> expr = "Microcomputers include smartphones."
>>> p = re.compile(r'computers\b')
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(5, 14), match='computers'>

Listing 9-54.(word)\b matched

\B\b意思相反,因此可以用来实现\b的反转。在清单 9-55 中,您尝试匹配单词 computer 而不在末尾添加复数 s 。因此,使用开头的\B和结尾的\B来精确匹配单词。

>>> import re
>>> expr = "Microcomputers include smartphones."
>>> p = re.compile(r'\Bcomputer\B')
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(5, 13), match='computer'>

Listing 9-55.\B(word)\B matched

分组

假设您有一个由路由器上摆动链路的 up 和 down 状态组成的字符串,并且您想要执行匹配来搜索连续的 up 状态。搜索并匹配连续上升状态后,可以使用group()选项打印输出(列表 9-56 )。要在正则表达式中编译一个组,必须使用( )。这是一个不切实际的例子,只是为了向您介绍正则表达式分组的概念。下一个例子将会在真实的环境中进行。

>>> import re
>>> expr = "downupupupdowndownupdowndown"
>>> p = re.compile("(up)+")
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(4, 10), match='upupup'>
>>> print(m.group(0))
upupup

Listing 9-56.Grouping exercise 1

现在让我们考虑一个技术援助中心的国家名称和美国的全限定号码。首先,我们要编译一个正则表达式来匹配整个字符串,以国家名称开始,然后是技术助理中心的电话号码。参见清单 9-57 。

>>> import re
>>> expr = "United States 1 408 526 1234"
>>> p = re.compile(r"\w+\s\w+\s\d\s\d{3}\s\d{3}\s\d+")
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(0, 28), match='United States 1 408 526 1234'>

Listing 9-57.Grouping exercise 2

在清单 9-57 中,您混合使用了字母数字速记字符\w、空格速记字符\s和重复数字速记字符\d``{n}来匹配所有字符。如果我们在这里停止字符串匹配,这个例子中捕获的数据就没有得到很好的利用。通过使用分组,我们可以从匹配的字符串中提取特定的信息。假设我们只想提取国家名称(参见清单 9-58 )。

>>> import re
>>> expr = "United States 1 408 526 1234"
>>> p = re.compile(r"(\w+\s\w+)\s\d?\s\d{3}\s\d{3}\s\d+")
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(0, 28), match='United States 1 408 526 1234'>
>>> country = m.group(1)
>>> country
'United States'

Listing 9-58.Grouping exercise 3

在清单 9-58 中,我们可以使用group(1)方法从整个匹配的字符串中分离出信息,现在我们可以在脚本中将这些数据作为变量或数据使用。现在让我们快速回顾分组方法索引,以理解正则表达式分组是如何编号的。见表 9-6 。

表 9-6。

分组方法索引含义

|

分组指数

|

说明

| | --- | --- | | group(0) | 完整匹配字符串 | | group(1) | 匹配的第一组 | | group(2) | 匹配的第二组 | | group(3) | 匹配的第三组 | | group(n) | 匹配第 n 个 |

使用实例可以更好地解释分组方法索引,如清单 9-59 所示。

>>> import re
>>> expr = "United States 1 408 526 1234"
>>> p = re.compile(r"(\w+\s\w+)\s(\d?\s\d{3}\s\d{3}\s\d+)")
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(0, 28), match='United States 1 408 526 1234'>
>>> phone_number = m.group(2)
>>> phone_number
'1 408 526 1234'

Listing 9-59.Grouping Method Indexing exercise 1

在清单 9-60 中,group(1)(\w+\s\w+),与清单 9-59 中的group(1)相同,(\d?\s\d{3}\s\d{3}\s\d+)变成了group(2),捕获电话号码。参见图 9-7 。

img/492721_1_En_9_Fig7_HTML.jpg

图 9-7。

正则表达式示例中的组索引

>>> import re
>>> expr = "United States 1 408 526 1234"
>>> p = re.compile(r"(\w+\s\w+)\s((\d?)\s(\d{3})\s(\d{3}\s\d+))")
>>> m = p.search(expr)
>> m.group(0)
'United States 1 408 526 1234'
>>> m.group(1)
'United States'
>>> m.group(2)
'1 408 526 1234'
>>> m.group(3)
'1'
>>> m.group(4)
'408'
>>> m.group(5)
'526 1234'

Listing 9-60.Grouping Method Indexing exercise 2

在清单 9-61 中,这些组被细分成更小的组,现在我们可以将国家代码 1、区号 408 和本地号码 526 1234 彼此分开。想象一下,在真实的网络自动化场景中,我们可以用正则表达式分组做所有可能的事情。想象一下,您可以利用从实际生产路由器和交换机中捕获的数据做些什么,并根据收集和分析的数据控制您的设备。

>>> import re
>>> expr = "Did you know that that 'that', that that person used in that sentence, is wrong."
>>> p = re.compile(r'(\bthat)\s+\1')
>>> m = p.search(expr)
>>> print(m)
<re.Match object; span=(13, 22), match='that that'>
>>> m = p.search(expr).group()
>>> print(m)
that that

Listing 9-61.Referencing Grouped String

使用组索引的另一个优点是,您可以使用类似于+\1的简写来重新引用第一个组。在(\bthat)\s+\1的正则表达式中,+有匹配与前一组相同的字符串的意思,\1有重新引用组号 1 的意思。如果有第二组并且你想引用它,可以使用\2

正则表达式命名组

让我们假设我们正在进行一个项目,编写一个正则表达式来处理数据,并充分利用re分组方法。这将使您的正则表达式极难解码,并在试图理解正则表达式试图匹配的内容时带来一些混乱。在网络中,我们有访问控制列表(ACL)和命名访问控制列表(NACL);类似地,正则表达式还提供了命名组,以便为组附加一个名称,从而简化数据操作。当正则表达式中有大量组时,命名组就变得很方便。命名组是为了在高级搜索和替换中查找和替换时更加方便。

我们必须使用从路由器的show version命令中提取的一行,它包含路由器名称和正常运行时间。我们希望使用正则表达式组来提取路由器名称、设备运行的精确年数、周数、天数、小时数和分钟数。

expr = "SYD-GW1 uptime is 1 year, 9 weeks, 2 days, 5 hours, 26 minutes"

为了将它们按组进行匹配,一个可行的分组示例将类似于正则表达式。尽管如此,由于存在多个分组,并且您正在阅读其他人的代码,您可能会对仅仅一行正则表达式感到不知所措。

(\w+[-]\w+)\s.+((\d+\sy\w+),\s(\d+\sw\w+),\s(\d+\sd\w+),\s(\d+\sh\w+),\s(\d+\sm\w+))

仔细研究正则表达式 10 分钟后,你终于得出结论,字符串就是这样匹配分组的。不幸的是,这么多组,你很容易迷失在翻译中。参见图 9-8 。

img/492721_1_En_9_Fig8_HTML.jpg

图 9-8。

正则表达式多编号组示例

现在,让我们做这个练习来学习这个正则表达式在真正的 Python 中是如何工作的。同样,您将遵循推荐的正则表达式流程,编译您的正则表达式并将其应用于字符串。如您所料,您可以将路由器名称 SYD-GW1 划分为组 1,通过打印组 2 来打印整个正常运行时间,然后在组 2 中嵌入更小的组,如组 3(年)、4(周)、5(天)、6(小时)和 7(分钟)。如果你还没有感到困惑,你真的已经步入正轨,并且在这个阶段做得很好。参见清单 9-62 。

>>> import re
>>> expr = "SYD-GW1 uptime is 1 year, 9 weeks, 2 days, 5 hours, 26 minutes"
>>> p = re.compile(r'(\w+[-]\w+)\s.+((\d+\sy\w+),\s(\d+\sw\w+),\s(\d+\sd\w+),\s(\d+\sh\w+),\s(\d+\sm\w+))')
>>> m = p.search(expr)
>>> print(m.group(0))
SYD-GW1 uptime is 1 year, 9 weeks, 2 days, 5 hours, 26 minutes
>>> print(m.group(1))
SYD-GW1
>>> print(m.group(2))
1 year, 9 weeks, 2 days, 5 hours, 26 minutes
>>> print(m.group(3))
1 year
[… omitted for brevity]
>>> print(m.group(7))
26 minutes

Listing 9-62.Multiple Numbered Groups exercise

为了在正则表达式中如此复杂地使用分组方法并增加灵活性,正则表达式有一个方便的命名组。您可以简单地在组的开头添加?P<group_name>,并以您指定的组名来引用该组。如果我们必须给前面的例子起一个组名,它看起来将类似于清单 9-63 。给一个组起一个名字会给它所匹配的数据赋予更多的含义,而且您不必花太多时间来解码正则表达式。

>>> import re
>>> expr = "SYD-GW1 uptime is 1 year, 9 weeks, 2 days, 5 hours, 26 minutes"
>>> p_named = re.compile(r'(?P<hostname>\w+[-]\w+)\s.+(?P<uptime>(?P<years>\d+\sy\w+),\s(?P<weeks>\d+\sw\w+),\s(?P<days>\d+\sd\w+),\s(?P<hours>\d+\sh\w+),\s(?P<minutes>\d+\sm\w+))')
>>> m = p_named.search(expr)
>>> print(m.group("minutes"))
26 minutes
[… omitted for brevity]
>>> print(m.group("uptime"))
1 year, 9 weeks, 2 days, 5 hours, 26 minutes
>>> print(m.group("hostname"))
SYD-GW1

Listing 9-63.Named Group exercise

当您根据字符串数据分析上述分组时,分组将如图 9-9 所示。

img/492721_1_En_9_Fig9_HTML.jpg

图 9-9。

正则表达式命名组示例

如您所见,这些名称与您试图匹配的信息相关,并且来自多个分组的混淆非常少。你应该把宝贵的工作时间花在更关键的问题上,或者提供良好的客户支持体验,而不是试图解码其他人神秘的正则表达式。

前瞻和后视断言

在进入这一部分之前,我必须先告诉你。如果您是第一次学习正则表达式,您可能无法立即理解 lookaheads 和 lookbehinds。为了减轻痛苦,我使用了练习和例子,但是在完全掌握这个概念之前,您可能需要多次回到这个主题。

向前看、向后看和非捕捉组

Lookaaheads 和 lookbehinds 被称为 lookarounds ,经常让 regex 新手感到困惑。乍一看,lookaheads 和 lookbehinds 看起来非常混乱,因为它们都以?开头,区别来自第二个和第三个元字符的使用。但是一旦掌握得好,它们就是缩短我们正则表达式的便利工具。你不需要马上理解这一部分;你可以慢慢学习周围的环境。此外,请将此部分作为将来参考的参考点。在环视中,有四个环视和一个非捕捉组,它们都以?标记开始。

在动手之前,作为一个快速提醒,表 9-7 展示了快速浏览和简单的例子。

表 9-7。

“向前看”、“向后看”和“非捕捉”组

|

环顾四周

|

名字

|

线

|

例子

|

说明

| | --- | --- | --- | --- | --- | | ?= | 展望 | abc | a(?=b) | 正则表达式必须匹配的资产 | | ?! | 消极前瞻 | abc | a(?!b) | 断言不可能匹配正则表达式 | | ?<= | 向后看 | abc | (?<=a)b | 正则表达式必须匹配的资产 | | ?<! | 消极回顾 | abc | (?<!a)b | 不可能匹配正则表达式的资产 | | ?: | 非捕获组 | abc | a(?:b) | 括号内的正则表达式必须匹配,但不会创建捕获组 |

现在,开始在 Python 解释器中输入粗体文本,看看返回的对象之间有什么不同。在下面的练习中,您将使用带有正则表达式和目标字符串'abc'的缩写打印函数。每个练习旁边都有解释。

|

练习#

|

练习

|

说明

| | --- | --- | --- | | one | >>> print(re.search('a(?=b)', 'abc'))``<re.Match object; span=(0, 1), match="a">``>>> print(re.search('a(?=b)', 'xbc'))``None | 匹配a,因为下一个字母是b。没有匹配,因为第一个字母不是字母b前面的a。 | | Two | >>> print(re.search('a(?!b)', 'abc'))``None``>>> print(re.search('a(?!b)', 'acd'))``<re.Match object; span=(0, 1), match="a"> | 没有匹配,因为下一个字母是b。匹配a,因为下一个字母不是b(它是c)。 | | three | >>> print(re.search('(?<=a)b', 'abc'))``<re.Match object; span=(1, 2), match="b">``>>> print(re.search('(?<=a)b', 'xbc'))``None | 匹配b,因为前一个字母是a。没有匹配,因为前一个字母不是a。 | | four | >>> print(re.search('(?<!a)b', 'abc'))``None``>>> print(re.search('(?<!a)b', 'xbc'))``<re.Match object; span=(1, 2), match="b"> | 不匹配,因为前一个字母是a。匹配b,因为前一个字母不是a。 | | five | >>> print(re.search('a(?:b)', 'abc'))``<re.Match object; span=(0, 2), match="ab">``>>> print(re.search('a(?=b)', 'abc'))``<re.Match object; span=(0, 1), match="a"> | 匹配ab,因为b后面跟着a。两个ab都匹配。匹配a,因为下一个字母是b。(这个和练习 1 一样。) | | six | >>> import re``>>> expr = "+1 408 526 1234"``>>> p = re.compile(r"((?:(\+1)[ -])?\(?(\d{3})\)?[ -]?\d{3}[ -]?\d{4})")``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(0, 15), match='+1 408 526 1234'>``>>> print(m.group(2))``+1 | ?:表示非捕获组,没有?:,+1 和尾随空格作为一组被捕获。使用?:,正则表达式不会像其他方式一样通过括号内的匹配来分组(通常,括号创建一个组)。换句话说,当您使用?:时,该组被匹配,但不会被捕获用于反向引用,这就是为什么它被称为非捕获组。 |

请访问 RexEgg regex 站点,获取更多的深度分析和正则表达式示例。这是我最喜欢的学习正则表达式的网站之一。

网址:www . rexeg . com/regex-look arounds . html

多练习环视

你需要大量的练习来理解这个话题。这一次,让我们使用字符串'abba'来使练习更加精彩,并使用完整的编译方法来构建我们的学习。

expr = 'abba'

请打开 Python 解释器,并在三个大于号旁边输入以粗体标记的文本。通读这一部分不会帮助你记住信息,所以你必须做所有的练习,从 1 到 10。

|

练习#

|

锻炼

|

正则表达式

|

说明

| | --- | --- | --- | --- | | 1 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('a(?=b)')``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(0, 1), match="a"> | a(?=b) | 找到后面有ba。 | | 2 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('b(?=b)')``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(1, 2), match="b"> | b(?=b) | 找到第一个b,它后面有b。 | | 3 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('a(?!b)')``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(3, 4), match="a"> | a(?!b) | 找到第二个a,它后面没有b。 | | 4 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('b(?!b)')``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(2, 3), match="b"> | b(?!b) | 找到第二个b,它后面没有b。 | | 5 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('(?<=a)b')``>>> print(m)``<re.Match object; span=(1, 2), match="b"> | (?<=a)b | 找到第一个b,它前面有a。 | | 6 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('(?<=b)b')``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(2, 3), match="b"> | (?<=b)b | 找到第二个b,它前面有b。 | | 7 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('(?<!a)b')``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(2, 3), match="b"> | (?<!a)b | 找到第二个b,它前面没有a。 | | 8 | >>> import re``>>> expr = 'abba'``>>> p = re.compile('(?<!b)b')``>>> m = p.search(expr)``>>> print(m)``<re.Match object; span=(1, 2), match="b"> | (?<!b)b | 找到第一个b,它前面没有b。 | | 9 | >>> import re``>>> expr = 'abc'``>>> p1 = re.compile('a(?:b)')``>>> m1 = p1.search(expr)``>>> print(m1)``<re.Match object; span=(0, 2), match="ab"> | a(?:b) |   | | 10 | >>> p2 = re.compile('a(?=b)')``>>> m2 = p2.search(expr)``>>> print(m2)``<re.Match object; span=(0, 1), match="a"> | a(?=b) |   |

环视应用示例

对于当前和未来学习 Python 编程语言的工程师来说,有很多内容需要涉及。直到你在工作中开始真正的项目,你可能看不到正则表达式的真正价值和力量。到目前为止,已经通过练习向您介绍了一些基本的正则表达式,现在您正在逐步提高正则表达式的技能。然而,除非你每天都在工作中生活和呼吸 regex,否则要达到顶级水平需要很长时间。但是,你仍然可以从这一章学习基础知识,并通过在互联网和其他书籍上找到的其他资料进行补充,以达到中级和高级水平。

让我们用思科 TAC 的网址 http://www.cisco.com/techsupport 来练习一下我们所学的内容。参见清单 9-64 。

>>> import re
>>> m = (re.search(r"\w{4,5}(?=:)", "http://www.cisco.com/techsupport"))
>>> print(m.group())
http

Listing 9-64.Only Use Lookahed Method to Print http

在清单 9-64 中,正则表达式\w{4,5}(?=:)匹配前面出现在冒号(:)前面的字母数字 4 到 5 次。

>>> import re
>>> m = (re.search(r"(?<=\/)\w.+[.]\w+[.]\w+(?=/)", "http://www.cisco.com/techsupport"))
>>> print(m.group())
www.cisco.com

Listing 9-65.Only Use Lookahead and Lookbehind Method to Print www.​cisco.​com

为了只匹配 www.cisco.com ,我们使用了一种后视方法(?<=),它以正斜杠/开始,然后匹配www格式。然后我们使用了一个以/结尾的前瞻(?=)。使用 lookrounds,您可以更快、更精确地匹配字符串。

以下表达式可以匹配任何具有filename.file_extension格式的文件名,例如vlan.datsw1.bakconfig.text,甚至可以匹配 Cisco 交换机 IOS 名称,例如2960x-universalk9-mz.152-2.E6.bin。在下面的例子中,我们将比较否定的前瞻方法和^的否定方法。

.*[.].*$

在 expr(表达式或字符串)中,要特别注意在文件名之间使用了\n来在单个字符串中创建换行符效果。这是为了简化我们的练习并节省页面。每个练习的后面都有解释。

在清单 9-66 中,您将使用正则表达式.*[.].*$来匹配 expr 中的所有文件。

>>> import re
>>> expr = "vlan.dat\nsw1.bak\nconfig.text\npynetauto.dat\nsw1_old.bak\n2960x-universalk9-mz.152-2.E6.bin"
>>> m = re.findall(".*[.].*$", expr, re.M)
>>> m
['vlan.dat', 'sw1.bak', 'config.text', 'pynetauto.dat', 'sw1_old.bak', '2960x-universalk9-mz.152-2.E6.bin']

Listing 9-66.Match All File Types Using Regular Expression - .*[.].*$

在清单 9-67 中,^(脱字符)方法用于否定任何以扩展名.dat结尾的文件,并返回列表中所有匹配的文件名。要特别注意的是,我们正在使用本章前面所学的re.findallre.M ( MULTILINE)方法。

在清单 9-68 中,否定前瞻示例返回与清单 9-67 相同的结果。提取相同数据的方法不止一种。

>>> import re
>>> expr = "vlan.dat\nsw1.bak\nconfig.text\npynetauto.dat\nsw1_old.bak\n2960x-universalk9-mz.152-2.E6.bin"
>>> m = re.findall(r".*..*$", expr, re.M)
>>> m
['sw1.bak', 'config.text', 'sw1_old.bak', '2960x-universalk9-mz.152-2.E6.bin']

Listing 9-68.Filter files with file extensions not starting with the letter d (using lookaround method)

>>> import re
>>> expr = "vlan.dat\nsw1.bak\nconfig.text\npynetauto.dat\nsw1_old.bak\n2960x-universalk9-mz.152-2.E6.bin"
>>> m = re.findall(r".*[.][^d].*$", expr, re.M)
>>> m
['sw1.bak', 'config.text', 'sw1_old.bak', '2960x-universalk9-mz.152-2.E6.bin']

Listing 9-67.Filter files with file extensions not starting with the letter d (Not using lookaround method)

在清单 9-69 和清单 9-6687 中,您已经使用两种否定方法否定了任何以.dat.bak结尾的文件名。尽管如此,如果文件扩展名以相同的字母开头或者扩展名的长度开始变化,后一种方法还是有优势的。

>>> import re
>>> expr = "vlan.dat\nsw1.bak\nconfig.text\npynetauto.dat\nsw1_old.bak\n2960x-universalk9-mz.152-2.E6.bin"

> m = re.findall(r".*..*$", expr, re.M)
>>> m
['config.text', '2960x-universalk9-mz.152-2.E6.bin']

Listing 9-70.Filter Any Files Ending with dat and bak Using Lookaround Method

>>> import re
>>> expr = "vlan.dat\nsw1.bak\nconfig.text\npynetauto.dat\nsw1_old.bak\n2960x-universalk9-mz.152-2.E6.bin"
>>> m = re.findall(r".*[.][^d|^b].*$", expr, re.M)
>>> m
['config.text', '2960x-universalk9-mz.152-2.E6.bin']

Listing 9-69.Filter Any Files Ending with .dat and .bak Without Using Lookaround Method

让我们在清单 9-71 中检查一下。

>>> import re
>>> expr = "file1.bak\nfile2.dat\nfile3.bakup\nfile4.data"
>>> m = re.findall(r".*[.][^d|^b].*$", expr, re.M)
>>> m
[]

Listing 9-71.Filter Any Files Ending with dat or bak Using ^ Negation Method

在清单 9-71 和清单 9-72 中,返回的结果是不同的,因为清单 9-71 中使用的正则表达式过滤或否定了所有文件,但在清单 9-72 中,只过滤了以.dat.bak结尾的文件。如果我们试图编写一个没有清单 9-72 中的负 lookahead 方法的工作正则表达式,那么这个正则表达式将会冗长而晦涩。在清单 9-72 中,使用负的 lookahead 方法,您可以简单地在正则表达式中添加带有|(或)符号的确切扩展名。

>>> import re
>>> expr = "file1.bak\nfile2.dat\nfile3.bakup\nfile4.data"
>>> m = re.findall(r".*..*$", expr, re.M)
>>> m
['file3.bakup', 'file4.data']

Listing 9-72.Filter Any Files Ending with dat or bak Using Negative Lookahead Method

子方法:替换字符串

在正则表达式中使用sub方法,可以改变或交换匹配的字符串。让我们做第一个练习,并检查结果。

使用 sub 替换字符串

像往常一样,让我们先从一个练习开始,然后回顾你从练习中学到了什么。参见清单 9-73 。

>>> import re
>>> p = re.compile('HP|Juniper|Arista')
>>> p.sub('Cisco', 'Juniper router, HP switch, Arista AP and Palo Alto firewall')
'Cisco router, Cisco switch, Cisco AP and Palo Alto firewall'

Listing 9-73.Use sub to substitute multiple matching words

在清单 9-73 中,使用sub方法,将“Juniper 路由器、HP 交换机、Arista AP 和 Palo Alto 防火墙”更改为“Cisco 路由器、Cisco 交换机、Cisco AP 和 Palo Alto 防火墙”因为 Palo Alto 不在编译的正则表达式中,所以只有 Juniper 和 HP 被替换为 Cisco。

按照清单 9-74 进行操作,如果我们只想控制更换的数量呢?

>>> p.sub('Cisco', 'Juniper router, HP switch, Arista AP and Palo Alto firewall', count=1)
'Cisco router, HP switch, Arista AP and Palo Alto firewall'

Listing 9-74.Use sub to substitute a matching word only once

您可以使用 count 参数控制替换。在前面的例子中,使用了 count 1,所以在清单 9-74 中,只有 Juniper 被替换为 Cisco,而不是 HP 或 Arista。

如果我们想找出一个长字符串中替换的个数呢?

>>> import re
>>> expr = '''Juniper router, HP switch, Palo Alto firewall, Juniper router, HP switch, Palo Alto firewall, Juniper router, HP switch, Palo Alto firewall, Juniper router, HP switch, Palo Alto firewall, Juniper router, HP switch, Palo Alto firewall, Juniper router, HP switch, Palo Alto firewall, and Arista router'''
>>> p = re.compile('HP|Juniper|Arista')
>>> p.subn('Cisco', expr)
('Cisco router, Cisco switch, Palo Alto firewall, Cisco router, Cisco switch, Palo Alto firewall, Cisco router, Cisco switch, Palo Alto firewall, Cisco router, Cisco switch, Palo Alto firewall, Cisco router, Cisco switch, Palo Alto firewall, Cisco router, Cisco switch, Palo Alto firewall and Cisco router', 13)

Listing 9-75.Use subn to count the number of replacements

你可以用subn的方法来替换字符串,找出被替换的字符串的个数。在清单 9-75 中,13 个字符串被替换为单词思科

使用 sub 和\g 交换位置

在正则表达式中使用sub方法时,可以将它与相对组引用(\g)结合使用。假设我们有一个字符串"Model Number : WS-C3650-48PD",我们想要交换'WS-C3650-48PD'Model Number'的位置。所以,看起来是这样的:

|

来自

|   |

| | --- | --- | --- | | 型号:WS-C3650-48PD | →中 | WS-C3650-48PD:型号 |

在 Python 解释器上,输入每个练习中的代码。您将很快了解这是如何工作的。

在清单 9-76 中,您已经将匹配正则表达式编译成三组,并使用sub方法,在匹配每组后简单地反转组序列。这个组仍然在同一个地方,但是组 1 和组 3 交换了位置。

>>> import re
>>> expr = "Model Number : WS-C3650-48PD"
>>> p = re.compile(r"(\w+\s\w+)(\s[:]\s)(\w+[-]\w+[-]\w+)") ← (r"(grp1)(grp2)(grp3)")
>>> m = p.sub("\g<3>\g<2>\g<1>", expr)
>>> print(m)
WS-C3650-48PD : Model Number
Listing 9-x >>> p = re.compile(r"(?P<Desc>\w+\s\w+)(\s[:]\s)(?P<Model>(\w+[-]\w+[-]\w+))")
>>> m = p.sub("\g<Model>\g<2>\g<Desc>", expr)
>>> print(m)
WS-C3650-48PD : Model Number

Listing 9-76.Use sub and grouping to swap positions

在清单 9-77 中,你也可以应用命名分组,给每个组取有意义的名字,实现我们的目标。请注意,该组没有像(?P<comma>\s[:]\s)这样的名称,以证明您可以混合搭配命名组和编号组。

>>> p = re.compile(r"(?P<Desc>\w+\s\w+)\s[:]\s(?P<Model>(\w+[-]\w+[-]\w+))")
>>> m = p.sub("\g<Model> : \g<Desc>", expr)
>>> print(m)
WS-C3650-48PD : Model Number

Listing 9-77.Use sub and named group method to swap positions

在清单 9-77 中,您只使用了两个命名组,并且省略了逗号组,但是当您使用sub方法进行匹配时,您在表达式中添加了(:)来返回期望的结果。

在子方法中插入函数

在网络操作中,我们使用二进制、十进制和十六进制值。我们使用十六进制值的两个例子是 MAC 地址和 IPv6 地址。这在 IPv6 寻址方案中变得非常有用;它有助于理解如何从十六进制计算到二进制和十进制,或者反过来。

在下面的练习中,我们将使用sub方法练习函数。

在列表 9-78 中,您使用了标准的 Python 方法将十进制随机 IP 地址转换为二进制数。使用列表 9-79 中的sub命令也可以得到相同的转换结果。

>>> ip = '172.168.123.245'
>>> def dec2bin(match):
...     value = int(match.group())
...     return bin(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(dec2bin, ip)
'0b10101100.0b10101000.0b1111011.0b11110101'

Listing 9-79.Decimal to Binary Using the sub Method

>>> ip = '172.168.123.245'
>>> print ('.'.join([bin(int(x)+256)[3:] for x in ip.split('.')]))
10101100.10101000.01111011.11110101

Listing 9-78.Decimal to Binary Using the join Method

使用sub方法时,编译后的返回结果以0b开头,表示这是一个二进制数。在清单 9-80 中,我们创建了一个名为dec2bin的函数,并在sub方法中使用它将十进制 IP 地址转换成二进制数。

>>> ip = "00001010.11010110.10001011.10111101"
>>> ip1 = ip.replace(".", "")
>>> ip1
'00001010110101101000101110111101'
>>> def bin2dec():
...     return ".".join(map(str, int(ip1, 2).to_bytes(4, "big")))
...
>>> bin2dec()
'10.214.139.189'

Listing 9-80.Binary to Decimal Using Join Method

在清单 9-81 中,我们选择了一个随机的二进制 IP 地址,并使用 Python join方法演示了二进制到十进制数的转换。将二进制数转换成小数后,我们知道我们要查找的 IP 地址是 10.214.139.189。

>>> mac = "84:3d:c6:f5:c9:ba"
>>> mac1 = mac.replace(":", "")
>>> mac1
'843dc6f5c9ba'
>>> i = int(mac1, 16)
>>> str(i)
'145400865868218'

Listing 9-81.Hexadecimal to Decimal Numbers

在清单 9-82 中,我们捕获了一个交换机的 MAC 地址,并想将这个十六进制数转换成十进制数。使用基本的 Python 方法,我们可以很容易地将十六进制数转换成十进制数。这是一个很好的例子,说明 Python 如何高效地将数字从一种形式转换成另一种形式。

>>> def hexrepl(match):
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r"\d+")
>>> p.sub(hexrepl, 'MAC address: 145400865868218')
'MAC address: 0x843dc6f5c9ba'

Listing 9-82.Decimal to Hexadecial

同样,出于演示的目的,您已经使用sub方法编写了一段 Python 代码,将十进制 MAC 地址转换回十六进制数。0x开头的数字表示这个数是十六进制数。

如果你是正则表达式的新手,这一章可能会很难。但是正如本章开头提到的,在一般编程中,掌握正则表达式是必须的,而不是可选的。

要获得关于这个主题的更多帮助,请访问下面的正则表达式备忘单。

网址: cheatography。com/Dave child/cheat-sheets/regular-expressions/

网址: web。麻省理工学院。edu/hackl/www/lab/Turk shop/slides/regex-cheat sheet。pdf

摘要

有些人会发现这一章是最难消化的一章。我不期望任何人一次阅读就能完全理解这一章。你将不得不一遍又一遍地回到这一章,慢慢地让正则表达式语法和概念渗透到你的大脑中。你刚刚完成了第九章的所有练习,理想情况下,你每周都能找到一些空闲时间来练习正则表达式。随着您编写 Python 代码,您将不得不存储和处理越来越多的数据。网络可编程性的真正力量也在于数据处理的力量。在下一章中,您将完成 Python 网络自动化的必要实验准备和集成。