Python-自动化秘籍-全-
Python 自动化秘籍(全)
原文:
zh.annas-archive.org/md5/de38d8b70825b858336fa5194110e245译者:飞龙
前言
我们都可能花费时间进行一些不太有价值的小手动任务。可能是在信息来源中搜索相关信息的小片段,使用电子表格一遍又一遍生成相同的图表,或者逐个搜索文件直到找到我们正在寻找的数据。其中一些——可能是大多数——任务实际上是可以自动化的。一开始需要投入一些时间,但对于那些一遍又一遍重复的任务,我们可以使用计算机来执行这些琐碎的任务,并将自己的努力集中在人类擅长的高级分析和基于结果的决策上。本书将解释如何使用 Python 语言来自动化可以大大加快计算机执行的常见业务任务。
鉴于 Python 的表现力和易用性,开始制作执行这些操作并将它们组合成更完整系统的小程序实际上非常简单。在整本书中,我们将展示一些小而易于遵循的配方,可以根据您的特定需求进行调整,并将它们组合起来执行更复杂的操作。我们将执行常见的操作,例如通过网络爬虫检测机会,分析信息以生成带有图表的自动电子表格报告,通过自动生成的电子邮件进行通信,通过短信获取通知,并学习如何在您专注于其他更重要的事情时运行任务。
尽管需要一些 Python 知识,但本书是针对非程序员编写的,提供清晰和有指导性的配方,可以提高读者的熟练程度,同时针对特定的日常目标。
这本书适合谁
这本书适合 Python 初学者,不一定是开发人员,他们希望利用和扩展他们的知识来自动化任务。本书中的大多数示例都针对营销、销售和其他非技术领域。读者需要了解一些 Python 语言,包括其基本概念。
本书涵盖的内容
第一章,“让我们开始自动化之旅”,介绍了整本书中将使用的一些基本内容。它描述了如何通过虚拟环境安装和管理第三方工具,如何进行有效的字符串操作,如何使用命令行参数,并向您介绍了正则表达式和其他文本处理方法。
第二章,“轻松自动化任务”,展示了如何准备并自动运行任务。它涵盖了如何编程任务以在应该执行时执行,而不是手动运行它们;如何在自动运行的任务的结果通知;以及如何在自动化过程中出现错误时得到通知。
第三章,“构建您的第一个网络爬虫应用程序”,探讨了发送网络请求以与外部网站以不同格式进行通信,如原始 HTML 内容;结构化的反馈;RESTful API;甚至自动执行浏览器步骤而无需手动干预。它还涵盖了如何处理结果以提取相关信息。
第四章,“搜索和阅读本地文件”,解释了如何搜索本地文件和目录并分析存储在那里的信息。您将学习如何在不同编码中过滤相关文件并阅读几种常见格式的文件,如 CSV、PDF、Word 文档,甚至图像。
第五章,“生成精彩的报告”,探讨了如何以多种格式显示文本格式中给出的信息。这包括创建模板以生成文本文件,以及创建格式丰富且样式良好的 Word 和 PDF 文档。
第六章,“电子表格的乐趣”,探讨了如何以 CSV 格式读取和写入电子表格;在功能丰富的 Microsoft Excel 中,包括格式和图表;以及在 LibreOffice 中,这是 Microsoft Excel 的免费替代品。
第七章,“开发令人惊叹的图表”,解释了如何生成美丽的图表,包括常见的示例,如饼图、折线图和条形图,以及其他高级情况,如堆叠条形图甚至地图。它还解释了如何组合和设计多个图表,以生成丰富的图形,并以易于理解的格式显示相关信息。
第八章,“处理通信渠道”,解释了如何在多个渠道发送消息,使用外部工具来完成大部分繁重的工作。本章涉及单独发送和接收电子邮件,以及通过短信进行通信,以及在 Telegram 中创建机器人。
第九章,“为什么不自动化您的营销活动?”,结合了本书中包含的不同配方,生成了一个完整的营销活动,包括机会检测、促销生成、向潜在客户的沟通,以及分析和报告促销产生的销售。本章展示了如何结合不同的元素,创建强大的系统。
第十章,“调试技术”,介绍了不同的方法和技巧,以帮助调试过程,并确保软件的质量。它利用了 Python 的强大内省能力和其开箱即用的调试工具,用于修复问题和生成可靠的自动化软件。
为了充分利用本书
在阅读本书之前,读者需要了解 Python 语言的基础知识。我们不假设读者是该语言的专家。
读者需要知道如何在命令行(终端、Bash 或等效工具)中输入命令。
要理解本书中的代码,您需要一个文本编辑器,它将使您能够阅读和编辑代码。您可以使用支持 Python 语言的集成开发环境,如 PyCharm 和 PyDev——您可以自行选择。请查看此链接以获取有关集成开发环境的想法:realpython.com/python-ides-code-editors-guide/。
下载示例代码文件
您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packt.com/support并注册,以便直接将文件发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
在www.packt.com上登录或注册。
-
选择“支持”选项卡。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的解压缩或提取文件夹:
-
Windows 上的 WinRAR/7-Zip
-
Mac 上的 Zipeg/iZip/UnRarX
-
Linux 上的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Python-Automation-Cookbook。如果代码有更新,将在现有的 GitHub 存储库上进行更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以从www.packtpub.com/sites/default/files/downloads/9781789133806_ColorImages.pdf下载它。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、对象名称、模块名称、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL 和用户输入。这里有一个例子:“对于这个食谱,我们需要导入requests模块。”
代码块设置如下:
# IMPORTS
from sale_log import SaleLog
def get_logs_from_file(shop, log_filename):
def main(log_dir, output_filename):
...
if __name__ == '__main__':
# PARSE COMMAND LINE ARGUMENTS AND CALL main()
请注意,代码可能会被编辑以简洁和清晰。必要时请参考完整的代码,可在 GitHub 上找到。
任何命令行输入或输出都是这样写的(注意$符号):
$ python execute_script.py parameters
Python 解释器中的任何输入都是这样写的(注意>>>符号):
>>> import delorean
>>> timestamp = delorean.utcnow().datetime.isoformat()
要进入 Python 解释器,请使用python3命令而不带任何参数:
$ python3
Python 3.7.0 (default, Aug 22 2018, 15:22:33)
[Clang 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
验证 Python 解释器是否为 Python 3.7 或更高版本。可能需要调用python或python3.7,具体取决于您的操作系统和安装选项。有关使用不同 Python 解释器的更多详细信息,请参见第一章,特别是创建虚拟环境食谱。
粗体:表示一个新术语、一个重要词或屏幕上看到的词。例如,菜单或对话框中的单词会以这样的形式出现在文本中。这里有一个例子:“转到账户|附加功能|API 密钥并创建一个新的:”
警告或重要说明会出现在这样的形式中。提示和技巧会出现在这样的形式中。
章节
在这本书中,你会发现一些经常出现的标题(准备工作、如何做、它是如何工作的、还有更多和另请参阅)。
准备工作
这一部分告诉你在食谱中可以期待什么,并描述如何设置食谱所需的任何软件或任何初步设置。
如何做...
这一部分包含了遵循食谱所需的步骤。
它是如何工作的...
这一部分通常包括对前一部分发生的事情的详细解释。
还有更多...
这一部分包含了有关食谱的额外信息,以使您对食谱更加了解。
另请参阅
这一部分提供了与食谱相关的其他有用信息的链接。
第一章:让我们开始我们的自动化之旅
在本章中,我们将介绍以下内容:
-
创建虚拟环境
-
安装第三方包
-
创建带有格式化值的字符串
-
操作字符串
-
从结构化字符串中提取数据
-
使用第三方工具—parse
-
介绍正则表达式
-
深入了解正则表达式
-
添加命令行参数
介绍
本章的目标是介绍一些基本技术,这些技术将在整本书中都很有用。主要思想是能够创建一个良好的 Python 环境来运行接下来的自动化任务,并能够将文本输入解析为结构化数据。
Python 默认安装了大量工具,但也很容易安装第三方工具,这些工具可以简化处理文本时的常见操作。在本章中,我们将看到如何从外部来源导入模块并使用它们来充分发挥 Python 的潜力。
在任何自动化任务中,结构化输入数据的能力至关重要。本书中大部分将处理的数据来自未格式化的来源,如网页或文本文件。正如古老的计算机格言所说,垃圾进,垃圾出,因此对输入进行消毒非常重要。
创建虚拟环境
在使用 Python 时的第一步是明确定义工作环境。这有助于脱离操作系统解释器和环境,并正确定义将要使用的依赖关系。不这样做往往会产生混乱的情况。记住,显式优于隐式!
这在两种情况下尤为重要:
-
在同一台计算机上处理多个项目时,它们可能具有在某些时候会发生冲突的不同依赖关系。例如,不能在同一环境中安装同一模块的两个版本。
-
在开发将最终在远程服务器上运行的个人笔记本电脑上开发一些代码等情况下,需要在不同计算机上使用的项目上工作。
开发人员之间的一个常见笑话是对错误的回应是它在我的机器上运行,意思是它似乎在他们的笔记本电脑上工作,但在生产服务器上却不工作。尽管有大量因素可能导致此错误,但一个好的做法是创建一个可以自动复制的环境,减少对实际使用的依赖关系的不确定性。
使用virtualenv模块很容易实现这一点,它可以设置一个虚拟环境,因此不会与计算机上安装的 Python 版本共享任何已安装的依赖项。
在 Python3 中,virtualenv工具会自动安装,而在以前的版本中并非如此。
准备就绪
要创建新的虚拟环境,请执行以下操作:
-
转到包含项目的主目录。
-
输入以下命令:
$ python3 -m venv .venv
这将创建一个名为.venv的子目录,其中包含虚拟环境。包含虚拟环境的目录可以位于任何位置。将其保留在相同的根目录下会很方便,并在其前面加上一个点可以避免在运行ls或其他命令时显示它。
- 在激活虚拟环境之前,检查
pip中安装的版本。这取决于您的操作系统,例如,MacOS High Sierra 10.13.4 的版本为 9.0.3。稍后将对其进行升级。还要检查引用的 Python 解释器,这将是主要操作系统的解释器:
$ pip --version
pip 9.0.3 from /usr/local/lib/python3.6/site-packages/pip (python 3.6)
$ which python3
/usr/local/bin/python3
现在,您的虚拟环境已准备就绪。
如何做...
- 通过运行以下命令激活虚拟环境:
$ source .venv/bin/activate
您会注意到提示会显示(.venv),表示虚拟环境已激活。
- 请注意,所使用的 Python 解释器是虚拟环境中的解释器,而不是准备就绪中第 3 步中的一般操作系统解释器。检查虚拟环境中的位置:
(.venv) $ which python
/root_dir/.venv/bin/python
(.venv) $ which pip
/root_dir/.venv/bin/pip
- 升级
pip的版本并检查版本:
(.venv) $ pip install --upgrade pip
...
Successfully installed pip-10.0.1
(.venv) $ pip --version
pip 10.0.1 from /root_dir/.venv/lib/python3.6/site-packages/pip (python 3.6)
- 退出环境并运行
pip来检查版本,这将返回之前的环境。检查pip版本和 Python 解释器以显示激活虚拟环境之前的版本,如准备就绪部分的第 3 步所示。请注意,它们是不同的 pip 版本!
(.venv) $ deactivate
$ which python3
/usr/local/bin/python3
$ pip --version
pip 9.0.3 from /usr/local/lib/python3.6/site-packages/pip (python 3.6)
它是如何工作的...
请注意,在虚拟环境中,您可以使用python而不是python3,尽管python3也可用。这将使用环境中定义的 Python 解释器。
在一些像 Linux 这样的系统中,可能需要使用python3.7而不是python3。验证您正在使用的 Python 解释器是否为 3.7 或更高版本。
在虚拟环境中,如何做...部分的第 3 步安装了最新版本的pip,而不会影响外部安装。
虚拟环境包含.venv目录中的所有 Python 数据,而activate脚本指向所有环境变量。最好的是,它可以很容易地被删除和重新创建,消除了在一个封闭的沙盒中进行实验的恐惧。
请记住,目录名称显示在提示符中。如果需要区分环境,请使用描述性目录名称,例如.my_automate_recipe,或使用--prompt选项。
还有更多...
要删除虚拟环境,请停用它并删除目录:
(.venv) $ deactivate
$ rm -rf .venv
venv模块有更多选项,可以使用-h标志显示:
$ python3 -m venv -h
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
[--upgrade] [--without-pip] [--prompt PROMPT]
ENV_DIR [ENV_DIR ...]
Creates virtual Python environments in one or more target directories.
positional arguments:
ENV_DIR A directory to create the environment in.
optional arguments:
-h, --help show this help message and exit
--system-site-packages
Give the virtual environment access to the system
site-packages dir.
--symlinks Try to use symlinks rather than copies, when symlinks
are not the default for the platform.
--copies Try to use copies rather than symlinks, even when
symlinks are the default for the platform.
--clear Delete the contents of the environment directory if it
already exists, before environment creation.
--upgrade Upgrade the environment directory to use this version
of Python, assuming Python has been upgraded in-place.
--without-pip Skips installing or upgrading pip in the virtual
environment (pip is bootstrapped by default)
--prompt PROMPT Provides an alternative prompt prefix for this
environment.
Once an environment has been created, you may wish to activate it, for example, by
sourcing an activate script in its bin directory.
处理虚拟环境的一种便捷方式,特别是如果您经常需要在它们之间切换,就是使用virtualenvwrapper模块:
- 要安装它,请运行以下命令:
$ pip install virtualenvwrapper
- 然后,将以下变量添加到您的启动脚本中,通常是
.bashrc或.bash_profile。虚拟环境将安装在WORKON_HOME目录下,而不是与项目相同的目录下,如前面所示:
export WORKON_HOME=~/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh
运行启动脚本或打开新的终端将允许您创建新的虚拟环境:
$ mkvirtualenv automation_cookbook
...
Installing setuptools, pip, wheel...done.
(automation_cookbook) $ deactivate
$ workon automation_cookbook
(automation_cookbook) $
有关更多信息,请查看virtualenvwrapper的文档:virtualenvwrapper.readthedocs.io/en/latest/index.html。
在workon后按下Tab键,将自动完成可用的环境。
另请参阅
-
安装第三方软件包的步骤
-
使用第三方工具—parse的步骤
安装第三方软件包
Python 最强大的功能之一是能够使用一个令人印象深刻的第三方软件包目录,涵盖了不同领域的大量内容,从专门执行数值操作、机器学习和网络通信的模块,到命令行便利工具、数据库访问、图像处理等等!
其中大多数都可以在官方 Python 软件包索引(pypi.org/)上找到,该索引拥有超过 130,000 个准备好使用的软件包。在本书中,我们将安装其中一些软件包,并且通常花一点时间研究外部工具来解决问题是值得的。很可能有人已经创建了一个解决问题的工具。
与找到并安装软件包一样重要的是跟踪使用了哪些软件包。这对于可复制性非常有帮助,意味着能够在任何情况下从头开始启动整个环境。
准备就绪
起点是找到一个在我们的项目中有用的软件包。
一个很棒的模块是requests,它处理 HTTP 请求并以其简单直观的界面以及出色的文档而闻名。查看文档,网址为:docs.python-requests.org/en/master/。
在本书中处理 HTTP 连接时,我们将使用requests。
下一步将是选择要使用的版本。在这种情况下,最新版本(在撰写时为 2.18.4)将是完美的。如果未指定模块的版本,默认情况下将安装最新版本,这可能会导致不同环境中的不一致性。
我们还将使用很棒的delorean模块来处理时间(版本 1.0.0 delorean.readthedocs.io/en/latest/)。
如何做...
- 在我们的主目录中创建一个
requirements.txt文件,其中将指定项目的所有要求。让我们从delorean和requests开始:
delorean==1.0.0
requests==2.18.4
- 使用
pip命令安装所有要求:
$ pip install -r requirements.txt
...
Successfully installed babel-2.5.3 certifi-2018.4.16 chardet-3.0.4 delorean-1.0.0 humanize-0.5.1 idna-2.6 python-dateutil-2.7.2 pytz-2018.4 requests-2.18.4 six-1.11.0 tzlocal-1.5.1 urllib3-1.22
- 现在在使用虚拟环境时可以同时使用这两个模块:
$ python
Python 3.6.5 (default, Mar 30 2018, 06:41:53)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import delorean
>>> import requests
它是如何工作的...
requirements.txt文件指定了模块和版本,pip在pypi.org上进行搜索。
请注意,从头开始创建一个新的虚拟环境并运行以下操作将完全重新创建您的环境,这使得可复制性非常简单:
$ pip install -r requirements.txt
请注意,如何做...部分的第 2 步会自动安装其他依赖模块,例如urllib3。
还有更多...
如果需要将任何模块更改为不同的版本,因为有新版本可用,可以使用要求进行更改,然后再次运行install命令:
$ pip install -r requirements.txt
当需要包含新模块时,这也适用。
在任何时候,都可以使用freeze命令来显示所有已安装的模块。freeze以与requirements.txt兼容的格式返回模块,从而可以生成一个包含当前环境的文件:
$ pip freeze > requirements.txt
这将包括依赖项,因此文件中会有更多的模块。
有时找到很棒的第三方模块并不容易。搜索特定功能可能效果很好,但有时会有一些出乎意料的很棒的模块,因为它们做了你从未想过的事情。一个很棒的策划列表是Awesome Python(awesome-python.com/),它涵盖了很多常见 Python 用例的很棒工具,如加密、数据库访问、日期和时间处理等。
在某些情况下,安装软件包可能需要额外的工具,例如编译器或支持某些功能的特定库(例如特定的数据库驱动程序)。如果是这种情况,文档通常会解释依赖关系。
另请参阅
-
创建虚拟环境的步骤
-
使用第三方工具—parse的步骤
创建带有格式化值的字符串
处理文本和文档时的基本能力之一是能够将值正确格式化为结构化字符串。Python 在提供良好的默认值方面非常聪明,比如正确呈现数字,但是有很多选项和可能性。
我们将通过一个表格的示例来讨论创建格式化文本时的一些常见选项。
准备工作
在 Python 中格式化字符串的主要工具是format方法。它使用一个定义的迷你语言以这种方式呈现变量:
result = template.format(*parameters)
template是一个基于迷你语言解释的字符串。在最简单的情况下,它会用参数替换大括号之间的值。以下是一些示例:
>>> 'Put the value of the string here: {}'.format('STRING')
"Put the value of the string here: STRING"
>>> 'It can be any type ({}) and more than one ({})'.format(1.23, str)
"It can be any type (1.23) and more than one (<class 'str'>)"
>> 'Specify the order: {1}, {0}'.format('first', 'second')
'Specify the order: second, first'
>>> 'Or name parameters: {first}, {second}'.format(second='SECOND', first='FIRST')
'Or name parameters: FIRST, SECOND'
在 95%的情况下,这种格式化就足够了;保持简单是很好的!但是对于复杂的情况,比如自动对齐字符串和创建漂亮的文本表格时,迷你语言format有更多的选项。
如何做...
- 编写以下脚本
recipe_format_strings_step1.py,以打印一个对齐的表格:
# INPUT DATA
data = [
(1000, 10),
(2000, 17),
(2500, 170),
(2500, -170),
]
# Print the header for reference
print('REVENUE | PROFIT | PERCENT')
# This template aligns and displays the data in the proper format
TEMPLATE = '{revenue:>7,} | {profit:>+7} | {percent:>7.2%}'
# Print the data rows
for revenue, profit in data:
row = TEMPLATE.format(revenue=revenue, profit=profit, percent=profit / revenue)
print(row)
- 运行它以显示以下对齐的表格。请注意,
PERCENT正确显示为百分比:
REVENUE | PROFIT | PERCENT
1,000 | +10 | 1.00%
2,000 | +17 | 0.85%
2,500 | +170 | 6.80%
2,500 | -170 | -6.80%
它是如何工作的...
TEMPLATE常量包含三列,每一列都有适当的名称(REVENUE,PROFIT,PERCENT)。这使得在格式调用上更加明确和简单。
在参数名称之后,有一个冒号,用于分隔格式定义。请注意,所有内容都在花括号内。在所有列中,格式规范将宽度设置为七个字符,并使用>符号将值对齐到右侧:
-
收入使用
,符号添加千位分隔符-[{revenue:>7,}]。 -
利润为正值添加
+符号。负值会自动添加--[{profit:>+7}]。 -
百分比显示百分比值,精确到两位小数-
[{percent:>7.2%}]。这是通过 0.2(精度)和添加%符号来完成的。
还有更多...
您可能也已经看到了使用%运算符的 Python 格式。虽然它适用于简单的格式,但它不如格式化的迷你语言灵活,不建议使用。
自 Python 3.6 以来的一个很棒的新功能是使用 f-strings,它使用定义的变量执行格式操作:
>>> param1 = 'first'
>>> param2 = 'second'
>>> f'Parameters {param1}:{param2}'
'Parameters first:second'
这简化了很多代码,使我们能够创建非常描述性和可读性的代码。
在使用 f-strings 时要小心,确保字符串在适当的时间被替换。一个常见问题是,定义为呈现的变量尚未定义。例如,先前定义的TEMPLATE不会作为 f-string 定义,因为revenue和其他参数在那时不可用。
如果需要写大括号,需要重复两次。请注意,每个复制将显示为单个大括号,再加上一个大括号用于值替换,总共三个大括号:
>> value = 'VALUE'
>>> f'This is the value, in curly brackets {{{value}}}'
'This is the value, in curly brackets {VALUE}'
这使我们能够创建元模板-生成模板的模板。在某些情况下,这将很有用,但请尽量限制它们的使用,因为它们会很快变得复杂,产生难以阅读的代码。
Python 格式规范迷你语言比这里显示的选项更多。
由于语言试图非常简洁,有时很难确定符号的位置。有时您可能会问自己问题,比如-+符号是在宽度参数之前还是之后。-请仔细阅读文档,并记住在格式规范之前始终包括一个冒号。
请在 Python 网站上查看完整的文档和示例(docs.python.org/3/library/string.html#formatspec)。
另请参阅
-
在第五章的生成精彩报告中的模板报告配方
-
操作字符串配方
操作字符串
处理文本时的基本能力是能够正确地操作该文本。这意味着能够将其连接,分割成常规块,或将其更改为大写或小写。我们将在以后讨论更高级的解析文本和分隔文本的方法,但在许多情况下,将段落分成行、句子甚至单词是有用的。有时,单词将必须删除一些字符或用规范版本替换以便与确定的值进行比较。
准备就绪
我们将定义一个基本文本,将其转换为其主要组件,然后重新构造它。例如,需要将报告转换为新格式以通过电子邮件发送。
我们将在此示例中使用的输入格式如下:
AFTER THE CLOSE OF THE SECOND QUARTER, OUR COMPANY, CASTAÑACORP
HAS ACHIEVED A GROWTH IN THE REVENUE OF 7.47%. THIS IS IN LINE
WITH THE OBJECTIVES FOR THE YEAR. THE MAIN DRIVER OF THE SALES HAS BEEN
THE NEW PACKAGE DESIGNED UNDER THE SUPERVISION OF OUR MARKETING DEPARTMENT.
OUR EXPENSES HAS BEEN CONTAINED, INCREASING ONLY BY 0.7%, THOUGH THE BOARD
CONSIDERS IT NEEDS TO BE FURTHER REDUCED. THE EVALUATION IS SATISFACTORY
AND THE FORECAST FOR THE NEXT QUARTER IS OPTIMISTIC. THE BOARD EXPECTS
AN INCREASE IN PROFIT OF AT LEAST 2 MILLION DOLLARS.
我们需要编辑文本以消除对数字的任何引用。需要通过在每个句号后添加一个新行来正确格式化它,使其对齐为 80 个字符,并将其转换为 ASCII 以确保兼容性。
文本将存储在解释器中的INPUT_TEXT变量中。
如何做...
- 输入文本后,将其拆分为单独的单词:
>>> INPUT_TEXT = '''
... AFTER THE CLOSE OF THE SECOND QUARTER, OUR COMPANY, CASTAÑACORP
... HAS ACHIEVED A GROWTH IN THE REVENUE OF 7.47%. THIS IS IN LINE
...
'''
>>> words = INPUT_TEXT.split()
- 用
'X'字符替换任何数字:
>>> redacted = [''.join('X' if w.isdigit() else w for w in word) for word in words]
- 将文本转换为纯 ASCII(请注意,公司名称包含一个不是 ASCII 的字母
ñ):
>>> ascii_text = [word.encode('ascii', errors='replace').decode('ascii')
... for word in redacted]
- 将单词分组为 80 个字符的行:
>>> newlines = [word + '\n' if word.endswith('.') else word for word in ascii_text]
>>> LINE_SIZE = 80
>>> lines = []
>>> line = ''
>>> for word in newlines:
... if line.endswith('\n') or len(line) + len(word) + 1 > LINE_SIZE:
... lines.append(line)
... line = ''
... line = line + ' ' + word
- 将所有行格式化为标题并将它们连接为单个文本片段:
>>> lines = [line.title() for line in lines]
>>> result = '\n'.join(lines)
- 打印结果:
>>> print(result)
After The Close Of The Second Quarter, Our Company, Casta?Acorp Has Achieved A
Growth In The Revenue Of X.Xx%.
This Is In Line With The Objectives For The Year.
The Main Driver Of The Sales Has Been The New Package Designed Under The
Supervision Of Our Marketing Department.
Our Expenses Has Been Contained, Increasing Only By X.X%, Though The Board
Considers It Needs To Be Further Reduced.
The Evaluation Is Satisfactory And The Forecast For The Next Quarter Is
Optimistic.
它是如何工作的...
每个步骤都对文本执行特定的转换:
-
第一个步骤在默认分隔符、空格和换行符上分割文本。这将它分割成没有行或多个空格用于分隔的单词。
-
为了替换数字,我们遍历每个单词的每个字符。对于每个字符,如果它是一个数字,就返回一个
'X'。这是通过两个列表推导式完成的,一个用于遍历列表,另一个用于每个单词,只有在有数字时才进行替换——['X' if w.isdigit() else w for w in word]。请注意,这些单词再次连接在一起。 -
每个单词都被编码为 ASCII 字节序列,然后再次解码为 Python 字符串类型。注意使用
errors参数来强制替换未知字符,如ñ。
字符串和字节之间的区别一开始并不直观,特别是如果你从来不用担心多种语言或编码转换。在 Python 3 中,字符串(内部 Python 表示)和字节之间有很强的分离,因此大多数适用于字符串的工具在字节对象中不可用。除非你很清楚为什么需要一个字节对象,总是使用 Python 字符串。如果你需要执行像这个任务中的转换,编码和解码在同一行中进行,这样你就可以保持对象在舒适的 Python 字符串领域。如果你有兴趣了解更多关于编码的信息,你可以查看这篇简短的文章(eli.thegreenplace.net/2012/01/30/the-bytesstr-dichotomy-in-python-3)和这篇更长更详细的文章(www.diveintopython3.net/strings.html)。
-
这一步首先为所有以句号结尾的单词添加一个额外的换行符(
\n字符)。这标记了不同的段落。之后,它创建一行并逐个添加单词。如果多一个单词会使它超过 80 个字符,它就结束该行并开始新的一行。如果该行已经以换行符结尾,它也结束并开始另一行。请注意,添加了额外的空格来分隔单词。 -
最后,每一行都被大写为标题(每个单词的第一个字母都是大写的),并且所有行都通过换行符连接在一起。
还有...
可以对字符串执行的一些其他有用操作如下:
-
字符串可以像任何其他列表一样切片。这意味着
'word'[0:2]将返回'wo'。 -
使用
.splitlines()通过换行符分隔行。 -
有
.upper()和.lower()方法,它们返回一个所有字符都设置为大写或小写的副本。它们的使用非常类似于.title():
>>> 'UPPERCASE'.lower()
'uppercase'
- 对于简单的替换(例如,将所有
A替换为B或将mine替换为ours),使用.replace()。这种方法对于非常简单的情况很有用,但替换很容易变得棘手。注意替换的顺序,以避免冲突和大小写敏感问题。请注意以下示例中错误的替换:
>>> 'One ring to rule them all, one ring to find them, One ring to bring them all and in the darkness bind them.'.replace('ring', 'necklace')
'One necklace to rule them all, one necklace to find them, One necklace to bnecklace them all and in the darkness bind them.'
这类似于我们将在正则表达式中看到的问题,匹配代码的意外部分。
还有更多示例将在后面介绍。有关更多信息,请参阅正则表达式示例。
如果您使用多种语言,或者任何非英语输入,学习 Unicode 和编码的基础知识非常有用。简而言之,鉴于世界上所有不同语言中的大量字符,包括与拉丁语无关的字母表,如中文或阿拉伯语,有一个标准来尝试覆盖所有这些字符,以便计算机可以正确理解它们。Python 3 极大地改善了这种情况,使字符串成为内部对象,以处理所有这些字符。Python 使用的编码,也是最常见和兼容的编码,目前是 UTF-8。
了解有关 UTF-8 基础知识的好文章是这篇博文:(www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/)。
处理编码在从可以使用不同编码的外部文件中读取时仍然很重要(例如 CP-1252 或 windows-1252,这是由传统 Microsoft 系统生成的常见编码,或 ISO 8859-15,这是行业标准)。
另请参阅
-
使用格式化值创建字符串食谱
-
介绍正则表达式食谱
-
深入研究正则表达式食谱
-
第四章中的处理编码食谱,搜索和读取本地文件
从结构化字符串中提取数据
在许多自动化任务中,我们需要处理特定格式的输入文本并提取相关信息。例如,电子表格可能以文本形式定义百分比(例如 37.4%),我们希望以后以数值格式检索它(0.374,作为浮点数)。
在这个食谱中,我们将看到如何处理包含有关产品的内联信息的销售日志,例如已售出、价格、利润和其他一些信息。
准备工作
想象一下,我们需要解析存储在销售日志中的信息。我们将使用以下结构的销售日志:
[<Timestamp in iso format>] - SALE - PRODUCT: <product id> - PRICE: $<price of the sale>
例如,特定的日志可能如下所示:
[2018-05-05T10:58:41.504054] - SALE - PRODUCT: 1345 - PRICE: $09.99
请注意,价格有一个前导零。所有价格都将有两位数字的美元,两位数字的美分。
我们需要在开始之前激活我们的虚拟环境:
$ source .venv/bin/activate
如何做...
- 在 Python 解释器中,进行以下导入。记得激活你的
virtualenv,就像创建虚拟环境食谱中描述的那样:
>>> import delorean
>>> from decimal import Decimal
- 输入要解析的日志:
>>> log = '[2018-05-05T11:07:12.267897] - SALE - PRODUCT: 1345 - PRICE: $09.99'
- 将日志分割为其部分,这些部分由
-(注意破折号前后的空格)分隔。我们忽略SALE部分,因为它没有添加任何相关信息:
>>> divide_it = log.split(' - ')
>>> timestamp_string, _, product_string, price_string = divide_it
- 将
timestamp解析为 datetime 对象:
>>> timestamp = delorean.parse(tmp_string.strip('[]'))
- 将
product_id解析为整数:
>>> product_id = int(product_string.split(':')[-1])
- 将价格解析为
Decimal类型:
>>> price = Decimal(price_string.split('$')[-1])
- 现在,您已经拥有了所有本机 Python 格式的值:
>> timestamp, product_id, price
(Delorean(datetime=datetime.datetime(2018, 5, 5, 11, 7, 12, 267897), timezone='UTC'), 1345, Decimal('9.99'))
它是如何工作的...
这个基本的工作是隔离每个元素,然后将它们解析为适当的类型。第一步是将完整的日志分割成较小的部分。-字符串是一个很好的分隔符,因为它将其分成四个部分——一个时间戳部分,一个只有SALE一词的部分,产品和价格。
在时间戳的情况下,我们需要隔离日志中的 ISO 格式。这就是为什么它被剥离括号。我们使用delorean模块(之前介绍过)将其解析为datetime对象。
单词SALE被忽略。那里没有相关信息。
为了隔离产品 ID,我们将产品部分分割为冒号。然后,我们将最后一个元素解析为整数:
>>> product_string.split(':')
['PRODUCT', ' 1345']
>>> int(' 1345')
1345
为了分割价格,我们使用美元符号作为分隔符,并将其解析为Decimal字符:
>>> price_string.split('$')
['PRICE: ', '09.99']
>>> Decimal('09.99')
Decimal('9.99')
如下一节所述,不要将此值解析为浮点类型。
还有更多...
这些日志元素可以组合成一个单一对象,有助于解析和聚合它们。例如,我们可以在 Python 代码中以以下方式定义一个类:
class PriceLog(object):
def __init__(self, timestamp, product_id, price):
self.timestamp = timestamp
self.product_id = product_id
self.price = price
def __repr__(self):
return '<PriceLog ({}, {}, {})>'.format(self.timestamp,
self.product_id,
self.price)
@classmethod
def parse(cls, text_log):
'''
Parse from a text log with the format
[<Timestamp>] - SALE - PRODUCT: <product id> - PRICE: $<price>
to a PriceLog object
'''
divide_it = text_log.split(' - ')
tmp_string, _, product_string, price_string = divide_it
timestamp = delorean.parse(tmp_string.strip('[]'))
product_id = int(product_string.split(':')[-1])
price = Decimal(price_string.split('$')[-1])
return cls(timestamp=timestamp, product_id=product_id, price=price)
因此,解析可以按以下方式进行:
>>> log = '[2018-05-05T12:58:59.998903] - SALE - PRODUCT: 897 - PRICE: $17.99'
>>> PriceLog.parse(log)
<PriceLog (Delorean(datetime=datetime.datetime(2018, 5, 5, 12, 58, 59, 998903), timezone='UTC'), 897, 17.99)>
避免使用浮点数类型来表示价格。浮点数存在精度问题,可能在聚合多个价格时产生奇怪的错误,例如:
>>> 0.1 + 0.1 + 0.1 0.30000000000000004
尝试这两个选项以避免问题:
-
使用整数分为基本单位:这意味着将货币输入乘以 100,并将其转换为整数(或者正确的分数单位,根据所使用的货币而定)。在显示它们时,您可能仍然希望更改基数。
-
解析为十进制类型:
Decimal类型保持固定精度,并且按预期工作。您可以在 Python 文档中找到有关Decimal类型的更多信息,网址为docs.python.org/3.6/library/decimal.html。
如果使用Decimal类型,请直接从字符串解析结果为Decimal。如果首先将其转换为浮点数,则可能会将精度错误传递给新类型。
另请参阅
-
创建虚拟环境食谱
-
使用第三方工具—解析食谱
-
介绍正则表达式食谱
-
深入了解正则表达式食谱
使用第三方工具—解析
手动解析数据,如前一篇文章中所示,对于小字符串非常有效,但是要调整确切的公式以适应各种输入可能非常费力。如果输入有时有额外的破折号呢?或者根据某个字段的大小而变化的变长标题呢?
更高级的选项是使用正则表达式,我们将在下一篇文章中看到。但是 Python 中有一个名为parse的出色模块(github.com/r1chardj0n3s/parse),它允许我们反转格式字符串。这是一个强大、易于使用的工具,极大地提高了代码的可读性。
准备工作
将parse模块添加到虚拟环境中的requirements.txt文件中,并重新安装依赖项,如创建虚拟环境食谱中所示。
requirements.txt文件应如下所示:
delorean==1.0.0
requests==2.18.3
parse==1.8.2
然后,在虚拟环境中重新安装模块:
$ pip install -r requirements.txt
...
Collecting parse==1.8.2 (from -r requirements.txt (line 3))
Using cached https://files.pythonhosted.org/packages/13/71/e0b5c968c552f75a938db18e88a4e64d97dc212907b4aca0ff71293b4c80/parse-1.8.2.tar.gz
...
Installing collected packages: parse
Running setup.py install for parse ... done
Successfully installed parse-1.8.2
如何做...
- 导入
parse函数:
>>> from parse import parse
- 定义要解析的日志,格式与从结构化字符串中提取数据食谱中的格式相同:
>>> LOG = '[2018-05-06T12:58:00.714611] - SALE - PRODUCT: 1345 - PRICE: $09.99'
- 分析它并描述它,就像打印时所做的那样,如下所示:
>>> FORMAT = '[{date}] - SALE - PRODUCT: {product} - PRICE: ${price}'
- 运行
parse并检查结果:
>>> result = parse(FORMAT, LOG)
>>> result
<Result () {'date': '2018-05-06T12:58:00.714611', 'product': '1345', 'price': '09.99'}>
>>> result['date']
'2018-05-06T12:58:00.714611'
>>> result['product']
'1345'
>>> result['price']
'09.99'
- 请注意,结果都是字符串。定义要解析的类型:
>>> FORMAT = '[{date:ti}] - SALE - PRODUCT: {product:d} - PRICE: ${price:05.2f}'
- 再次解析:
>>> result = parse(FORMAT, LOG)
>>> result
<Result () {'date': datetime.datetime(2018, 5, 6, 12, 58, 0, 714611), 'product': 1345, 'price': 9.99}>
>>> result['date']
datetime.datetime(2018, 5, 6, 12, 58, 0, 714611)
>>> result['product']
1345
>>> result['price']
9.99
- 定义自定义类型以避免浮点类型的问题:
>>> from decimal import Decimal
>>> def price(string):
... return Decimal(string)
...
>>> FORMAT = '[{date:ti}] - SALE - PRODUCT: {product:d} - PRICE: ${price:price}'
>>> parse(FORMAT, LOG, {'price': price})
<Result () {'date': datetime.datetime(2018, 5, 6, 12, 58, 0, 714611), 'product': 1345, 'price': Decimal('9.99')}>
工作原理...
parse模块允许我们定义一个格式,例如字符串,以便在解析值时反转格式方法。我们在创建字符串时讨论的许多概念也适用于此处—将值放在括号中,在冒号后定义类型等。
默认情况下,如第 4 步所示,值被解析为字符串。这是分析文本的一个很好的起点。值可以被解析为更有用的本机类型,如如何做...部分的第 5 和第 6 步所示。请注意,虽然大多数解析类型与 Python 格式规范迷你语言中的类型相同,但还有其他一些可用,例如用于 ISO 格式时间戳的ti。
如果本机类型不够用,我们可以定义自己的解析,如如何做...部分的第 7 步所示。请注意,价格函数的定义接收一个字符串并返回正确的格式,本例中为Decimal类型。
从结构化字符串中提取数据食谱的还有更多部分中描述的有关浮点数和价格信息的所有问题在这里同样适用。
还有更多...
时间戳也可以转换为delorean对象以保持一致性。此外,delorean对象携带时区信息。添加与上一个示例相同的结构,得到以下对象,可以解析日志:
class PriceLog(object):
def __init__(self, timestamp, product_id, price):
self.timestamp = timestamp
self.product_id = product_id
self.price = price
def __repr__(self):
return '<PriceLog ({}, {}, {})>'.format(self.timestamp,
self.product_id,
self.price)
@classmethod
def parse(cls, text_log):
'''
Parse from a text log with the format
[<Timestamp>] - SALE - PRODUCT: <product id> - PRICE: $<price>
to a PriceLog object
'''
def price(string):
return Decimal(string)
def isodate(string):
return delorean.parse(string)
FORMAT = ('[{timestamp:isodate}] - SALE - PRODUCT: {product:d} - '
'PRICE: ${price:price}')
formats = {'price': price, 'isodate': isodate}
result = parse.parse(FORMAT, text_log, formats)
return cls(timestamp=result['timestamp'],
product_id=result['product'],
price=result['price'])
因此,解析它会返回类似的结果:
>>> log = '[2018-05-06T14:58:59.051545] - SALE - PRODUCT: 827 - PRICE: $22.25'
>>> PriceLog.parse(log)
<PriceLog (Delorean(datetime=datetime.datetime(2018, 6, 5, 14, 58, 59, 51545), timezone='UTC'), 827, 22.25)>
此代码包含在 GitHub 文件Chapter01/price_log.py中。
所有parse支持的类型都可以在github.com/r1chardj0n3s/parse#format-specification的文档中找到。
另请参阅
-
从结构化字符串中提取数据示例
-
介绍正则表达式示例
-
深入了解正则表达式示例
介绍正则表达式
正则表达式,或regex,是一种用于匹配文本的模式。换句话说,它允许我们定义一个抽象字符串(通常是结构化文本的定义)来检查其他字符串是否匹配。
最好用示例来描述它们。想象一下,定义一个文本模式为“以大写 A 开头,之后只包含小写 N 和 A 的单词”。单词Anna符合此模式,但Bob、Alice和James不符合。单词Aaan、Ana、Annnn和Aaaan也符合,但ANNA不符合。
如果这听起来很复杂,那是因为它确实很复杂。正则表达式可能非常复杂,因为它们可能非常复杂且难以理解。但它们非常有用,因为它们允许我们执行非常强大的模式匹配。
正则表达式的一些常见用途如下:
-
验证输入数据:例如,电话号码只包含数字、破折号和括号。
-
字符串解析:从结构化字符串(如日志或 URL)中检索数据。这与前一个示例中描述的内容类似。
-
抓取:在长文本中查找某些内容的出现。例如,在网页中查找所有电子邮件。
-
替换:查找并用其他单词替换一个单词或多个单词。例如,将the owner替换为John Smith。
“有些人遇到问题时会想到“我知道了,我会使用正则表达式。”现在他们有了两个问题。”
- Jamie Zawinski
正则表达式在保持非常简单时效果最好。一般来说,如果有特定的工具可以做到,最好使用它而不是正则表达式。HTML 解析就是一个非常明显的例子;查看第三章,构建您的第一个网络抓取应用程序,以了解更好的工具来实现这一点。
一些文本编辑器也允许我们使用正则表达式进行搜索。虽然大多数是针对编写代码的编辑器,如 Vim、BBEdit 或 Notepad++,但它们也存在于更通用的工具中,如 MS Office、Open Office 或 Google 文档。但要小心,因为特定的语法可能略有不同。
准备工作
处理正则表达式的python模块称为re。我们将介绍的主要函数是re.search(),它返回一个关于匹配模式的match对象的信息。
由于正则表达式模式也是字符串,我们将它们区分开来,通过在前面加上r来区分它们,例如r'pattern'。这是 Python 标记文本为原始字符串文字的方式,这意味着其中的字符串会被直接接受,不会进行任何转义。这意味着\被用作反斜杠,而不是一个序列。例如,没有 r 前缀,\n表示换行符。
有些字符是特殊的,表示诸如字符串结尾、任何数字、任何字符、任何空白字符等概念。
最简单的形式只是一个字面字符串。例如,正则表达式模式r'LOG'匹配字符串'LOGS',但不匹配字符串'NOT A MATCH'。如果没有匹配,搜索返回None:
>>> import re
>>> re.search(r'LOG', 'LOGS')
<_sre.SRE_Match object; span=(0, 3), match='LOG'>
>>> re.search(r'LOG', 'NOT A MATCH')
>>>
如何做...
- 导入
re模块:
>>> import re
- 然后,匹配不位于字符串开头的模式:
>>> re.search(r'LOG', 'SOME LOGS')
<_sre.SRE_Match object; span=(5, 8), match='LOG'>
- 匹配仅位于字符串开头的模式。注意
^字符:
>>> re.search(r'^LOG', 'LOGS')
<_sre.SRE_Match object; span=(0, 3), match='LOG'>
>>> re.search(r'^LOG', 'SOME LOGS')
>>>
- 仅在字符串末尾匹配模式。请注意
$字符:
>>> re.search(r'LOG$', 'SOME LOG')
<_sre.SRE_Match object; span=(5, 8), match='LOG'>
>>> re.search(r'LOG$', 'SOME LOGS')
>>>
- 匹配单词
'thing'(不包括things),但不匹配something或anything。请注意第二个模式的开头处的\b:
>>> STRING = 'something in the things she shows me'
>>> match = re.search(r'thing', STRING)
>>> STRING[:match.start()], STRING[match.start():match.end()], STRING[match.end():]
('some', 'thing', ' in the things she shows me')
>>> match = re.search(r'\bthing', STRING)
>>> STRING[:match.start()], STRING[match.start():match.end()], STRING[match.end():]
('something in the ', 'thing', 's she shows me')
- 匹配仅为数字和破折号(例如电话号码)的模式。检索匹配的字符串:
>>> re.search(r'[0123456789-]+', 'the phone number is 1234-567-890')
<_sre.SRE_Match object; span=(20, 32), match='1234-567-890'>
>>> re.search(r'[0123456789-]+', 'the phone number is 1234-567-890').group()
'1234-567-890'
- 天真地匹配电子邮件地址:
>>> re.search(r'\S+@\S+', 'my email is email.123@test.com').group()
'email.123@test.com'
它是如何工作的...
re.search函数匹配模式,无论其在字符串中的位置如何。如前所述,如果未找到模式,将返回None,或者匹配对象。
使用以下特殊字符:
-
^:标记字符串的开头 -
$:标记字符串的结尾 -
\b:标记单词的开头或结尾 -
\S:标记任何非空白字符,包括特殊字符
更多特殊字符将在下一个配方中显示。
在如何做...部分的第 6 步中,r'[0123456789-]+'模式由两部分组成。第一部分在方括号之间,匹配0到9之间的任何单个字符(任何数字)和破折号(-)字符。之后的+表示该字符可以出现一次或多次。这在正则表达式中称为量词。这使得可以匹配任何数字和破折号的组合,无论长度如何。
步骤 7 再次使用+号匹配尽可能多的字符,然后再次使用@。在这种情况下,字符匹配是\S,它匹配任何非空白字符。
请注意,此处描述的电子邮件的天真模式非常天真,因为它将匹配无效的电子邮件,例如john@smith@test.com。对于大多数用途,更好的正则表达式是r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"。您可以访问emailregex.com/查找它并链接到更多信息。
请注意,解析包括边缘情况在内的有效电子邮件实际上是一个困难且具有挑战性的问题。前面的正则表达式对于本书涵盖的大多数用途应该都可以,但在诸如 Django 之类的通用框架项目中,电子邮件验证是一个非常冗长且非常难以阅读的正则表达式。
生成的匹配对象返回匹配模式开始和结束的位置(使用start和end方法),如步骤 5 所示,该步骤将字符串拆分为匹配部分,显示两个匹配模式之间的区别。
步骤 5 中显示的差异非常常见。尝试捕获 GP 可能最终捕获 eggplant 和 bagpipe!同样,things\b不会捕获 things。请务必测试并进行适当的调整,例如捕获\bGP\b以获取单词 GP。
可以通过调用group()来检索特定匹配的模式,如步骤 6 所示。请注意,结果始终是一个字符串。可以进一步使用我们之前看到的任何方法进行处理,例如通过破折号将电话号码拆分成组:
>>> match = re.search(r'[0123456789-]+', 'the phone number is 1234-567-890')
>>> [int(n) for n in match.group().split('-')]
[1234, 567, 890]
还有更多...
处理正则表达式可能会很困难和复杂。请花时间测试您的匹配,并确保它们按照您的期望工作,以避免不愉快的惊喜。
您可以使用一些工具进行交互式地检查您的正则表达式。一个很好的免费在线工具是regex101.com/,它显示每个元素并解释正则表达式。请仔细检查您是否使用了 Python 风格:

请注意,解释描述了\b匹配单词边界(单词的开头或结尾),以及thing字面上匹配这些字符。
在某些情况下,正则表达式可能非常缓慢,甚至会产生所谓的正则表达式拒绝服务,即创建一个字符串以混淆特定的正则表达式,使其花费大量时间,甚至在最坏的情况下阻塞计算机。虽然自动化任务可能不会让您陷入这些问题,但请注意,如果正则表达式花费的时间太长,请留意。
另请参阅
-
从结构化字符串中提取数据配方
-
使用第三方工具—解析配方
-
深入了解正则表达式配方
深入了解正则表达式
在这个配方中,我们将更多地了解如何处理正则表达式。在介绍基础知识之后,我们将深入了解模式元素,引入组作为检索和解析字符串的更好方法,看看如何搜索相同字符串的多个出现,并处理更长的文本。
如何做...
- 导入
re:
>>> import re
- 将电话模式作为组的一部分进行匹配(在括号中)。注意使用
\d作为任何数字的特殊字符:
>>> match = re.search(r'the phone number is ([\d-]+)', '37: the phone number is 1234-567-890')
>>> match.group()
'the phone number is 1234-567-890'
>>> match.group(1)
'1234-567-890'
- 编译一个模式并捕获一个不区分大小写的模式,使用
yes|no选项:
>>> pattern = re.compile(r'The answer to question (\w+) is (yes|no)', re.IGNORECASE)
>>> pattern.search('Naturaly, the answer to question 3b is YES')
<_sre.SRE_Match object; span=(10, 42), match='the answer to question 3b is YES'>
>>> _.groups()
('3b', 'YES')
- 在文本中匹配所有城市和州的缩写的出现。请注意,它们由一个单个字符分隔,城市的名称始终以大写字母开头。为简单起见,只匹配了四个州:
>>> PATTERN = re.compile(r'([A-Z][\w\s]+).(TX|OR|OH|MI)')
>>> TEXT ='the jackalopes are the team of Odessa,TX while the knights are native of Corvallis OR and the mud hens come from Toledo.OH; the whitecaps have their base in Grand Rapids,MI'
>>> list(PATTERN.finditer(TEXT))
[<_sre.SRE_Match object; span=(31, 40), match='Odessa,TX'>, <_sre.SRE_Match object; span=(73, 85), match='Corvallis OR'>, <_sre.SRE_Match object; span=(113, 122), match='Toledo.OH'>, <_sre.SRE_Match object; span=(157, 172), match='Grand Rapids,MI'>]
>>> _[0].groups()
('Odessa', 'TX')
它是如何工作的...
引入的新特殊字符如下。请注意,大写或小写的相同字母表示相反的匹配,例如\d匹配数字,而\D匹配非数字。:
-
\d:标记任何数字(0 到 9)。 -
\s:标记任何空白字符,包括制表符和其他空白特殊字符。请注意,这与上一个配方中引入的\S相反。 -
\w:标记任何字母(包括数字,但不包括句号等字符)。 -
.:标记任何字符。
要定义组,请将定义的组放在括号中。可以单独检索组,使它们非常适合匹配包含稍后将处理的可变部分的更大模式,如步骤 2 中所示。请注意与上一个配方中步骤 6 模式的区别。在这种情况下,模式不仅是数字,而且包括前缀,即使我们随后提取数字。请查看这种差异,其中有一个不是我们想要捕获的数字:
>>> re.search(r'the phone number is ([\d-]+)', '37: the phone number is 1234-567-890')
<_sre.SRE_Match object; span=(4, 36), match='the phone number is 1234-567-890'>
>>> _.group(1)
'1234-567-890'
>>> re.search(r'[0123456789-]+', '37: the phone number is 1234-567-890')
<_sre.SRE_Match object; span=(0, 2), match='37'>
>>> _.group()
'37'
记住,第 0 组(.group()或.group(0))始终是整个匹配。其余的组按它们出现的顺序排列。
模式也可以编译。如果模式需要一遍又一遍地匹配,这样可以节省一些时间。要以这种方式使用它,编译模式,然后使用该对象执行搜索,如步骤 3 和 4 所示。可以添加一些额外的标志,例如使模式不区分大小写。
第 4 步的模式需要一点信息。它由两个组成,由一个单个字符分隔。特殊字符.表示它匹配一切,例如一个句号、一个空格和一个逗号。第二组是一组明确定义的选项,例如美国州的缩写。
第一组以大写字母([A-Z])开头,并接受任何字母或空格的组合([\w\s]+),但不接受句号或逗号等标点符号。这匹配城市,包括由多个单词组成的城市。
请注意,这个模式从任何大写字母开始匹配,直到找到一个州,除非被标点符号分隔,这可能不是预期的结果,例如:
>>> re.search(r'([A-Z][\w\s]+).(TX|OR|OH|MI)', 'This is a test, Escanaba MI')
<_sre.SRE_Match object; span=(16, 27), match='Escanaba MI'>
>>> re.search(r'([A-Z][\w\s]+).(TX|OR|OH|MI)', 'This is a test with Escanaba MI')
<_sre.SRE_Match object; span=(0, 31), match='This is a test with Escanaba MI'>
第 4 步还展示了如何在长文本中查找多个出现。虽然.findall()方法存在,但它不返回完整的匹配对象,而.findalliter()则返回。现在在 Python 3 中很常见,.findalliter()返回一个迭代器,可以在 for 循环或列表推导中使用。请注意,.search()仅返回模式的第一个匹配,即使出现更多匹配:
>>> PATTERN.search(TEXT)
<_sre.SRE_Match object; span=(31, 40), match='Odessa,TX'>
>>> PATTERN.findall(TEXT)
[('Odessa', 'TX'), ('Corvallis', 'OR'), ('Toledo', 'OH')]
还有更多...
特殊字符可以反转,如果它们被大小写交换。例如,我们使用的特殊字符的反向如下:
-
\D:标记任何非数字 -
\W:标记任何非字母 -
\B:标记任何不在单词开头或结尾的字符
最常用的特殊字符通常是\d(数字)和\w(字母和数字),因为它们标记了常见的搜索模式,加号表示一个或多个。
组也可以分配名称。这样可以使它们更加明确,但会使组变得更冗长,形式如下—(?P<groupname>PATTERN)。可以通过名称引用组,使用.group(groupname)或通过调用.groupdict()来保持其数字位置。
例如,步骤 4 的模式可以描述如下:
>>> PATTERN = re.compile(r'(?P<city>[A-Z][\w\s]+?).(?P<state>TX|OR|OH|MN)')
>>> match = PATTERN.search(TEXT)
>>> match.groupdict()
{'city': 'Odessa', 'state': 'TX'}
>>> match.group('city')
'Odessa'
>>> match.group('state')
'TX'
>>> match.group(1), match.group(2)
('Odessa', 'TX')
正则表达式是一个非常广泛的主题。有整本专门讨论它们的技术书籍,它们可能非常深奥。Python 文档是一个很好的参考(docs.python.org/3/library/re.html)并且可以学到更多。
如果一开始感到有点害怕,这是完全正常的感觉。仔细分析每个模式,将其分成不同的部分,它们将开始变得有意义。不要害怕运行正则表达式交互式分析器!
正则表达式可能非常强大和通用,但它们可能不是您尝试实现的目标的合适工具。我们已经看到了一些细微差别和模式。作为一个经验法则,如果一个模式开始感觉复杂,那么是时候寻找另一个工具了。还记得之前的配方以及它们提供的选项,比如parse。
另请参阅
-
介绍正则表达式配方
-
使用第三方工具—parse配方
添加命令行参数
许多任务最好被构造为接受不同参数以改变工作方式的命令行接口,例如,抓取一个网页或另一个网页。Python 在标准库中包含了一个强大的argparse模块,可以轻松创建丰富的命令行参数解析。
准备工作
脚本中argparse的基本用法可以分为三个步骤:
-
定义脚本将接受的参数,生成一个新的解析器。
-
调用定义的解析器,返回一个包含所有结果参数的对象。
-
使用参数调用脚本的入口点,这将应用定义的行为。
尝试使用以下通用结构编写脚本:
IMPORTS
def main(main parameters):
DO THINGS
if __name__ == '__main__':
DEFINE ARGUMENT PARSER
PARSE ARGS
VALIDATE OR MANIPULATE ARGS, IF NEEDED
main(arguments)
main函数使得很容易知道代码的入口点。if语句下的部分只有在文件直接调用时才会执行,而不是在导入时执行。我们将对所有步骤都遵循这一点。
如何做...
- 创建一个脚本,它将接受一个单个整数作为位置参数,并打印出相应次数的哈希符号。
recipe_cli_step1.py脚本如下,但请注意我们正在遵循之前介绍的结构,并且main函数只是打印参数:
import argparse
def main(number):
print('#' * number)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('number', type=int, help='A number')
args = parser.parse_args()
main(args.number)
- 调用脚本并查看参数的呈现方式。使用无参数调用脚本会显示自动帮助信息。使用自动参数
-h显示扩展帮助信息:
$ python3 recipe_cli_step1.py
usage: recipe_cli_step1.py [-h] number
recipe_cli_step1.py: error: the following arguments are required: number
$ python3 recipe_cli_step1.py -h
usage: recipe_cli_step1.py [-h] number
positional arguments:
number A number
optional arguments:
-h, --help show this help message and exit
- 使用额外参数调用脚本会按预期工作:
$ python3 recipe_cli_step1.py 4
####
$ python3 recipe_cli_step1.py not_a_number
usage: recipe_cli_step1.py [-h] number
recipe_cli_step1.py: error: argument number: invalid int value: 'not_a_number'
- 更改脚本以接受一个可选参数用于打印的字符。默认值将是
'#'。recipe_cli_step2.py脚本将如下所示:
import argparse
def main(character, number):
print(character * number)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('number', type=int, help='A number')
parser.add_argument('-c', type=str, help='Character to print',
default='#')
args = parser.parse_args()
main(args.c, args.number)
- 帮助信息已更新,使用
-c标志允许我们打印不同的字符:
$ python3 recipe_cli_step2.py -h
usage: recipe_cli_step2.py [-h] [-c C] number
positional arguments:
number A number
optional arguments:
-h, --help show this help message and exit
-c C Character to print
$ python3 recipe_cli_step2.py 4
####
$ python3 recipe_cli_step2.py 5 -c m
mmmmm
- 添加一个标志,当存在时改变行为。
recipe_cli_step3.py脚本如下:
import argparse
def main(character, number):
print(character * number)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('number', type=int, help='A number')
parser.add_argument('-c', type=str, help='Character to print',
default='#')
parser.add_argument('-U', action='store_true', default=False,
dest='uppercase',
help='Uppercase the character')
args = parser.parse_args()
if args.uppercase:
args.c = args.c.upper()
main(args.c, args.number)
- 如果添加了
-U标志,则调用它会将字符转换为大写:
$ python3 recipe_cli_step3.py 4 -c f
ffff
$ python3 recipe_cli_step3.py 4 -c f -U
FFFF
工作原理...
如如何做...部分中的步骤 1 所述,通过.add_arguments将参数添加到解析器中。一旦定义了所有参数,调用parse_args()将返回一个包含结果的对象(或者如果有错误则退出)。
每个参数都应该添加一个帮助描述,但它们的行为可能会有很大变化:
- 如果参数以
-开头,则被视为可选参数,就像步骤 4 中的-c参数一样。如果不是,则是位置参数,就像步骤 1 中的number参数一样。
为了清晰起见,始终为可选参数定义默认值。如果不这样做,它将是None,但这可能会令人困惑。
-
记得始终添加一个带有参数描述的帮助参数;帮助将自动生成,如步骤 2 所示。
-
如果存在类型,将进行验证,例如,在步骤 3 中的
number。默认情况下,类型将为字符串。 -
store_true和store_false操作可用于生成标志,不需要任何额外参数的参数。将相应的默认值设置为相反的布尔值。这在步骤 6 和 7 中的U参数中有所示。 -
args对象中属性的名称默认情况下将是参数的名称(如果存在破折号,则不包括)。您可以使用dest更改它。例如,在步骤 6 中,命令行参数-U被描述为uppercase。
在使用短参数(如单个字母)时,更改参数的名称以供内部使用非常有用。一个良好的命令行界面将使用-c,但在内部使用更详细的标签,如configuration_file可能是一个好主意。显式胜于隐式!
- 一些参数可以与其他参数协同工作,如步骤 3 所示。执行所有必需的操作,以清晰简洁的参数传递主要函数。例如,在步骤 3 中,只传递了两个参数,但可能已经修改了一个参数。
还有更多...
您也可以使用双破折号创建长参数,例如:
parser.add_argument('-v', '--verbose', action='store_true', default=False,
help='Enable verbose output')
这将接受-v和--verbose,并将存储名称verbose。
添加长名称是使界面更直观和易于记忆的好方法。几次之后很容易记住有一个冗长的选项,并且以v开头。
处理命令行参数时的主要不便之处可能是最终拥有太多参数。这会造成混乱。尽量使参数尽可能独立,不要在它们之间建立太多依赖关系,否则处理组合可能会很棘手。
特别是,尽量不要创建超过一对位置参数,因为它们没有助记符。位置参数也接受默认值,但大多数情况下这不是预期的行为。
有关详细信息,请查看 Python 的argparse文档(docs.python.org/3/library/argparse.html)。
另请参阅
-
创建虚拟环境食谱
-
安装第三方软件包食谱
第二章:简化任务自动化
在本章中,我们将涵盖以下内容:
-
准备一个任务
-
设置一个定时任务
-
捕获错误和问题
-
发送电子邮件通知
介绍
要正确自动化任务,我们需要一个平台,让它们在适当的时间自动运行。需要手动运行的任务并不真正实现了自动化。
但是,为了能够让它们在后台运行而不用担心更紧急的问题,任务需要适合以 fire-and-forget 模式运行。我们应该能够监控它是否正确运行,确保我们能够捕获未来的动作(比如在出现有趣的情况时接收通知),并知道在运行过程中是否出现了任何错误。
确保软件始终以高可靠性一致运行实际上是一件大事,这是一个需要专业知识和人员的领域,通常被称为系统管理员、运维或 SRE(站点可靠性工程)。像亚马逊和谷歌这样的网站需要巨大的投资来确保一切都能 24/7 正常运行。
这本书的目标要比那更加谦虚。你可能不需要每年低于几秒的停机时间。以合理的可靠性运行任务要容易得多。但是,要意识到还有维护工作要做,所以要有所准备。
准备一个任务
一切都始于准确定义需要运行的任务,并设计成不需要人工干预就能运行的方式。
一些理想的特点如下:
-
单一、明确的入口点:不会对要运行的任务产生混淆。
-
清晰的参数:如果有任何参数,它们应该非常明确。
-
无交互:停止执行以请求用户信息是不可能的。
-
结果应该被存储:可以在运行时以外的时间进行检查。
-
清晰的结果:如果我们在交互中工作,我们会接受更详细的结果或进度报告。但是,对于自动化任务,最终结果应尽可能简洁明了。
-
错误应该被记录下来:以便分析出错的原因。
命令行程序已经具备了许多这些特点。它有明确的运行方式,有定义的参数,并且结果可以被存储,即使只是以文本格式。但是,通过配置文件来澄清参数,并且输出到一个文件,可以进一步改进。
注意,第 6 点是 捕获错误和问题 配方的目标,并将在那里进行介绍。
为了避免交互,不要使用任何需要用户输入的命令,比如 input。记得删除调试时的断点!
准备工作
我们将按照一个结构开始,其中一个主函数作为入口点,并将所有参数提供给它。
这与第一章中 添加命令行参数 配方中呈现的基本结构相同,让我们开始自动化之旅。
定义一个主函数,包含所有明确的参数,涵盖了第 1 和第 2 点。第 3 点并不难实现。
为了改进第 2 和第 5 点,我们将研究如何从文件中检索配置并将结果存储在另一个文件中。另一个选项是发送通知,比如电子邮件,这将在本章后面介绍。
如何做...
- 准备以下任务,并将其保存为
prepare_task_step1.py:
import argparse
def main(number, other_number):
result = number * other_number
print(f'The result is {result}')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-n1', type=int, help='A number', default=1)
parser.add_argument('-n2', type=int, help='Another number', default=1)
args = parser.parse_args()
main(args.n1, args.n2)
- 更新文件以定义包含两个参数的配置文件,并将其保存为
prepare_task_step2.py。注意,定义配置文件会覆盖任何命令行参数:
import argparse
import configparser
def main(number, other_number):
result = number * other_number
print(f'The result is {result}')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-n1', type=int, help='A number', default=1)
parser.add_argument('-n2', type=int, help='Another number', default=1)
parser.add_argument('--config', '-c', type=argparse.FileType('r'),
help='config file')
args = parser.parse_args()
if args.config:
config = configparser.ConfigParser()
config.read_file(args.config)
# Transforming values into integers
args.n1 = int(config['DEFAULT']['n1'])
args.n2 = int(config['DEFAULT']['n2'])
main(args.n1, args.n2)
- 创建配置文件
config.ini:
[ARGUMENTS]
n1=5
n2=7
- 使用配置文件运行命令。注意,配置文件会覆盖命令行参数,就像第 2 步中描述的那样:
$ python3 prepare_task_step2.py -c config.ini
The result is 35
$ python3 prepare_task_step2.py -c config.ini -n1 2 -n2 3
The result is 35
- 添加一个参数来将结果存储在文件中,并将其保存为
prepare_task_step5.py:
import argparse
import sys
import configparser
def main(number, other_number, output):
result = number * other_number
print(f'The result is {result}', file=output)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-n1', type=int, help='A number', default=1)
parser.add_argument('-n2', type=int, help='Another number', default=1)
parser.add_argument('--config', '-c', type=argparse.FileType('r'),
help='config file')
parser.add_argument('-o', dest='output', type=argparse.FileType('w'),
help='output file',
default=sys.stdout)
args = parser.parse_args()
if args.config:
config = configparser.ConfigParser()
config.read_file(args.config)
# Transforming values into integers
args.n1 = int(config['DEFAULT']['n1'])
args.n2 = int(config['DEFAULT']['n2'])
main(args.n1, args.n2, args.output)
- 运行结果以检查是否将输出发送到定义的文件。请注意,结果文件之外没有输出:
$ python3 prepare_task_step5.py -n1 3 -n2 5 -o result.txt
$ cat result.txt
The result is 15
$ python3 prepare_task_step5.py -c config.ini -o result2.txt
$ cat result2.txt
The result is 35
工作原理...
请注意,argparse模块允许我们将文件定义为参数,使用argparse.FileType类型,并自动打开它们。这非常方便,如果文件无效,将会引发错误。
记得以正确的模式打开文件。在步骤 5 中,配置文件以读模式(r)打开,输出文件以写模式(w)打开,如果文件存在,将覆盖该文件。您可能会发现追加模式(a),它将在现有文件的末尾添加下一段数据。
configparser模块允许我们轻松使用配置文件。如步骤 2 所示,文件的解析就像下面这样简单:
config = configparser.ConfigParser()
config.read_file(file)
然后,配置将作为由部分和值分隔的字典访问。请注意,值始终以字符串格式存储,需要转换为其他类型,如整数:
如果需要获取布尔值,请不要执行value = bool(config[raw_value]),因为无论如何都会转换为True;例如,字符串False是一个真字符串,因为它不是空的。相反,使用.getboolean方法,例如,value = config.getboolean(raw_value)。
Python3 允许我们向print函数传递一个file参数,它将写入该文件。步骤 5 展示了将所有打印信息重定向到文件的用法。
请注意,默认参数是sys.stdout,它将值打印到终端(标准输出)。这样做会使得在没有-o参数的情况下调用脚本将在屏幕上显示信息,这在调试时很有帮助:
$ python3 prepare_task_step5.py -c config.ini
The result is 35
$ python3 prepare_task_step5.py -c config.ini -o result.txt
$ cat result.txt
The result is 35
还有更多...
请查看官方 Python 文档中configparse的完整文档:docs.python.org/3/library/configparser.html.
在大多数情况下,这个配置解析器应该足够好用,但如果需要更多的功能,可以使用 YAML 文件作为配置文件。YAML 文件(learn.getgrav.org/advanced/yaml)作为配置文件非常常见,结构更好,可以直接解析,考虑到数据类型。
- 将 PyYAML 添加到
requirements.txt文件并安装它:
PyYAML==3.12
- 创建
prepare_task_yaml.py文件:
import yaml
import argparse
import sys
def main(number, other_number, output):
result = number * other_number
print(f'The result is {result}', file=output)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-n1', type=int, help='A number', default=1)
parser.add_argument('-n2', type=int, help='Another number', default=1)
parser.add_argument('-c', dest='config', type=argparse.FileType('r'),
help='config file in YAML format',
default=None)
parser.add_argument('-o', dest='output', type=argparse.FileType('w'),
help='output file',
default=sys.stdout)
args = parser.parse_args()
if args.config:
config = yaml.load(args.config)
# No need to transform values
args.n1 = config['ARGUMENTS']['n1']
args.n2 = config['ARGUMENTS']['n2']
main(args.n1, args.n2, args.output)
- 定义配置文件
config.yaml,可在 GitHubgithub.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter02/config.yaml中找到:
ARGUMENTS:
n1: 7
n2: 4
- 然后运行以下命令:
$ python3 prepare_task_yaml.py -c config.yaml
The result is 28
还有设置默认配置文件和默认输出文件的可能性。这对于创建一个不需要输入参数的纯任务非常方便。
一般规则是,如果任务有一个非常具体的目标,请尽量避免创建太多的输入和配置参数。尝试将输入参数限制为任务的不同执行。一个永远不会改变的参数可能很好地被定义为常量。大量的参数将使配置文件或命令行参数变得复杂,并将在长期内增加更多的维护。另一方面,如果您的目标是创建一个非常灵活的工具,可以在非常不同的情况下使用,那么创建更多的参数可能是一个好主意。尝试找到适合自己的平衡!
另请参阅
-
第一章中的命令行参数配方,让我们开始自动化之旅
-
发送电子邮件通知配方
-
第十章中的使用断点进行调试配方,调试技术
设置 cron 作业
Cron 是一种老式但可靠的执行命令的方式。它自 Unix 的 70 年代以来就存在,并且是系统管理中常用的维护方式,比如释放空间、旋转日志、制作备份和其他常见操作。
这个配方是特定于 Unix 的,因此它将在 Linux 和 MacOS 中工作。虽然在 Windows 中安排任务是可能的,但非常不同,并且使用任务计划程序,这里不会描述。如果你有 Linux 服务器的访问权限,这可能是安排周期性任务的好方法。其主要优点如下:
-
它几乎存在于所有的 Unix 或 Linux 系统中,并配置为自动运行。
-
它很容易使用,尽管有点欺骗性。
-
这是众所周知的。几乎所有涉及管理任务的人都对如何使用它有一个大致的概念。
-
它允许轻松地周期性命令,精度很高。
但它也有一些缺点,如下:
-
默认情况下,它可能不会提供太多反馈。检索输出、记录执行和错误是至关重要的。
-
任务应尽可能自包含,以避免环境变量的问题,比如使用错误的 Python 解释器,或者应该执行的路径。
-
它是特定于 Unix 的。
-
只有固定的周期时间可用。
-
它不控制同时运行的任务数量。每次倒计时结束时,它都会创建一个新任务。例如,一个需要一个小时才能完成的任务,计划每 45 分钟运行一次,将有 15 分钟的重叠时间,两个任务将同时运行。
不要低估最新效果。同时运行多个昂贵的任务可能会对性能产生不良影响。昂贵的任务重叠可能导致竞争条件,使每个任务都无法完成!充分时间让你的任务完成并密切关注它们。
准备就绪
我们将生成一个名为cron.py的脚本:
import argparse
import sys
from datetime import datetime
import configparser
def main(number, other_number, output):
result = number * other_number
print(f'[{datetime.utcnow().isoformat()}] The result is {result}',
file=output)
if __name__ == '__main__':
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--config', '-c', type=argparse.FileType('r'),
help='config file',
default='/etc/automate.ini')
parser.add_argument('-o', dest='output', type=argparse.FileType('a'),
help='output file',
default=sys.stdout)
args = parser.parse_args()
if args.config:
config = configparser.ConfigParser()
config.read_file(args.config)
# Transforming values into integers
args.n1 = int(config['DEFAULT']['n1'])
args.n2 = int(config['DEFAULT']['n2'])
main(args.n1, args.n2, args.output)
注意以下细节:
-
配置文件默认为
/etc/automate.ini。重用上一个配方中的config.ini。 -
时间戳已添加到输出中。这将明确显示任务运行的时间。
-
结果将被添加到文件中,如使用
'a'模式打开文件所示。 -
ArgumentDefaultsHelpFormatter参数在使用-h参数打印帮助时会自动添加有关默认值的信息。
检查任务是否产生了预期的结果,并且你可以记录到一个已知的文件中:
$ python3 cron.py
[2018-05-15 22:22:31.436912] The result is 35
$ python3 cron.py -o /path/automate.log
$ cat /path/automate.log
[2018-05-15 22:28:08.833272] The result is 35
如何做...
- 获取 Python 解释器的完整路径。这是你的虚拟环境中的解释器:
$ which python
/your/path/.venv/bin/python
- 准备执行 cron。获取完整路径并检查是否可以无问题执行。执行几次:
$ /your/path/.venv/bin/python /your/path/cron.py -o /path/automate.log
$ /your/path/.venv/bin/python /your/path/cron.py -o /path/automate.log
- 检查结果是否正确地添加到结果文件中:
$ cat /path/automate.log
[2018-05-15 22:28:08.833272] The result is 35
[2018-05-15 22:28:10.510743] The result is 35
- 编辑 crontab 文件,以便每五分钟运行一次任务:
$ crontab -e
*/5 * * * * /your/path/.venv/bin/python /your/path/cron.py -o /path/automate.log
请注意,这将使用默认的命令行编辑器打开一个编辑终端。
如果你还没有设置默认的命令行编辑器,默认情况下可能是 Vim。如果你对 Vim 没有经验,这可能会让你感到困惑。按I开始插入文本,Esc完成后退出。然后,在保存文件后退出,使用:wq。有关 Vim 的更多信息,请参阅此介绍:null-byte.wonderhowto.com/how-to/intro-vim-unix-text-editor-every-hacker-should-be-familiar-with-0174674。
有关如何更改默认命令行编辑器的信息,请参阅以下链接:www.a2hosting.com/kb/developer-corner/linux/setting-the-default-text-editor-in-linux.
- 检查 crontab 内容。请注意,这会显示 crontab 内容,但不会设置为编辑:
$ contab -l
*/5 * * * * /your/path/.venv/bin/python /your/path/cron.py -o /path/automate.log
- 等待并检查结果文件,看任务是如何执行的:
$ tail -F /path/automate.log
[2018-05-17 21:20:00.611540] The result is 35
[2018-05-17 21:25:01.174835] The result is 35
[2018-05-17 21:30:00.886452] The result is 35
它的工作原理...
crontab 行由描述任务运行频率的行(前六个元素)和任务组成。初始的六个元素中的每一个代表不同的执行时间单位。它们大多数是星号,表示任何:
* * * * * *
| | | | | |
| | | | | +-- Year (range: 1900-3000)
| | | | +---- Day of the Week (range: 1-7, 1 standing for Monday)
| | | +------ Month of the Year (range: 1-12)
| | +-------- Day of the Month (range: 1-31)
| +---------- Hour (range: 0-23)
+------------ Minute (range: 0-59)
因此,我们的行,*/5 * * * * *,意味着每当分钟可被 5 整除时,在所有小时、所有天...所有年。
以下是一些例子:
30 15 * * * * means "every day at 15:30"
30 * * * * * means "every hour, at 30 minutes"
0,30 * * * * * means "every hour, at 0 minutes and 30 minutes"
*/30 * * * * * means "every half hour"
0 0 * * 1 * means "every Monday at 00:00"
不要试图猜测太多。使用像crontab.guru/这样的备忘单来获取示例和调整。大多数常见用法将直接在那里描述。您还可以编辑一个公式并获得有关其运行方式的描述性文本。
在描述如何运行 cron 作业之后,包括执行任务的行,如如何操作…部分的第 2 步中准备的那样。
请注意,任务的描述中包含了每个相关文件的完整路径——解释器、脚本和输出文件。这消除了与路径相关的所有歧义,并减少了可能出现错误的机会。一个非常常见的错误是无法确定其中一个(或多个)元素。
还有更多...
如果 crontab 执行时出现任何问题,您应该收到系统邮件。这将显示为终端中的消息,如下所示:
You have mail.
$
这可以通过mail来阅读:
$ mail
Mail version 8.1 6/6/93\. Type ? for help.
"/var/mail/jaime": 1 message 1 new
>N 1 jaime@Jaimes-iMac-5K Thu May 17 21:15 19/914 "Cron <jaime@Jaimes-iM"
? 1
Message 1:
...
/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/Resources/Python.app/Contents/MacOS/Python: can't open file 'cron.py': [Errno 2] No such file or directory
在下一个食谱中,我们将看到独立捕获错误的方法,以便任务可以顺利运行。
另请参阅
-
第一章《让我们开始自动化之旅》中的添加命令行选项食谱
-
捕获错误和问题食谱
捕获错误和问题
自动化任务的主要特点是其fire-and-forget质量。我们不会积极地查看结果,而是让它在后台运行。
此外,由于本书中大多数食谱涉及外部信息,如网页或其他报告,因此在运行时发现意外问题的可能性很高。这个食谱将呈现一个自动化任务,它将安全地将意外行为存储在一个日志文件中,以便以后检查。
准备工作
作为起点,我们将使用一个任务,该任务将按照命令行中的描述来除两个数字。
这个任务与如何操作…部分中的第 5 步中介绍的任务非常相似,但是我们将除法代替乘法。
如何操作...
- 创建
task_with_error_handling_step1.py文件,如下所示:
import argparse
import sys
def main(number, other_number, output):
result = number / other_number
print(f'The result is {result}', file=output)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-n1', type=int, help='A number', default=1)
parser.add_argument('-n2', type=int, help='Another number', default=1)
parser.add_argument('-o', dest='output', type=argparse.FileType('w'),
help='output file', default=sys.stdout)
args = parser.parse_args()
main(args.n1, args.n2, args.output)
- 多次执行它,看看它是如何除以两个数字的:
$ python3 task_with_error_handling_step1.py -n1 3 -n2 2
The result is 1.5
$ python3 task_with_error_handling_step1.py -n1 25 -n2 5
The result is 5.0
- 检查除以
0是否会产生错误,并且该错误是否未记录在结果文件中:
$ python task_with_error_handling_step1.py -n1 5 -n2 1 -o result.txt
$ cat result.txt
The result is 5.0
$ python task_with_error_handling_step1.py -n1 5 -n2 0 -o result.txt
Traceback (most recent call last):
File "task_with_error_handling_step1.py", line 20, in <module>
main(args.n1, args.n2, args.output)
File "task_with_error_handling_step1.py", line 6, in main
result = number / other_number
ZeroDivisionError: division by zero
$ cat result.txt
- 创建
task_with_error_handling_step4.py文件:
import logging
import sys
import logging
LOG_FORMAT = '%(asctime)s %(name)s %(levelname)s %(message)s'
LOG_LEVEL = logging.DEBUG
def main(number, other_number, output):
logging.info(f'Dividing {number} between {other_number}')
result = number / other_number
print(f'The result is {result}', file=output)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-n1', type=int, help='A number', default=1)
parser.add_argument('-n2', type=int, help='Another number', default=1)
parser.add_argument('-o', dest='output', type=argparse.FileType('w'),
help='output file', default=sys.stdout)
parser.add_argument('-l', dest='log', type=str, help='log file',
default=None)
args = parser.parse_args()
if args.log:
logging.basicConfig(format=LOG_FORMAT, filename=args.log,
level=LOG_LEVEL)
else:
logging.basicConfig(format=LOG_FORMAT, level=LOG_LEVEL)
try:
main(args.n1, args.n2, args.output)
except Exception as exc:
logging.exception("Error running task")
exit(1)
- 运行它以检查它是否显示正确的
INFO和ERROR日志,并且是否将其存储在日志文件中:
$ python3 task_with_error_handling_step4.py -n1 5 -n2 0
2018-05-19 14:25:28,849 root INFO Dividing 5 between 0
2018-05-19 14:25:28,849 root ERROR division by zero
Traceback (most recent call last):
File "task_with_error_handling_step4.py", line 31, in <module>
main(args.n1, args.n2, args.output)
File "task_with_error_handling_step4.py", line 10, in main
result = number / other_number
ZeroDivisionError: division by zero
$ python3 task_with_error_handling_step4.py -n1 5 -n2 0 -l error.log
$ python3 task_with_error_handling_step4.py -n1 5 -n2 0 -l error.log
$ cat error.log
2018-05-19 14:26:15,376 root INFO Dividing 5 between 0
2018-05-19 14:26:15,376 root ERROR division by zero
Traceback (most recent call last):
File "task_with_error_handling_step4.py", line 33, in <module>
main(args.n1, args.n2, args.output)
File "task_with_error_handling_step4.py", line 11, in main
result = number / other_number
ZeroDivisionError: division by zero
2018-05-19 14:26:19,960 root INFO Dividing 5 between 0
2018-05-19 14:26:19,961 root ERROR division by zero
Traceback (most recent call last):
File "task_with_error_handling_step4.py", line 33, in <module>
main(args.n1, args.n2, args.output)
File "task_with_error_handling_step4.py", line 11, in main
result = number / other_number
ZeroDivisionError: division by zero
它是如何工作的...
为了正确捕获任何意外异常,主函数应该被包装到一个try-except块中,就像如何操作…部分中的第 4 步中所做的那样。将此与第 1 步中未包装代码的方式进行比较:
try:
main(...)
except Exception as exc:
# Something went wrong
logging.exception("Error running task")
exit(1)
请注意,记录异常对于获取出了什么问题很重要。
这种异常被昵称为宝可梦,因为它可以捕获所有,因此它将在最高级别捕获任何意外错误。不要在代码的其他区域使用它,因为捕获所有可能会隐藏意外错误。至少,任何意外异常都应该被记录下来以便进行进一步分析。
使用exit(1)调用额外的步骤来以状态 1 退出通知操作系统我们的脚本出了问题。
logging模块允许我们记录。请注意基本配置,其中包括一个可选的文件来存储日志、格式和要显示的日志级别。
日志的可用级别从不太关键到更关键——DEBUG、INFO、WARNING、ERROR和CRITICAL。日志级别将设置记录消息所需的最小严重性。例如,如果将严重性设置为WARNING,则不会存储INFO日志。
创建日志很容易。您可以通过调用logging.<logging level>方法来实现(其中logging level是debug、info等)。例如:
>>> import logging
>>> logging.basicConfig(level=logging.INFO)
>>> logging.warning('a warning message')
WARNING:root:a warning message
>>> logging.info('an info message')
INFO:root:an info message
>>> logging.debug('a debug message')
>>>
注意,低于INFO的严重性的日志不会显示。使用级别定义来调整要显示的信息量。例如,这可能会改变DEBUG日志仅在开发任务时使用,但在运行时不显示。请注意,task_with_error_handling_step4.py默认将日志级别定义为DEBUG。
良好的日志级别定义是显示相关信息的关键,同时减少垃圾邮件。有时设置起来并不容易,但特别是如果有多个人参与,尝试就WARNING与ERROR的确切含义达成一致,以避免误解。
logging.exception()是一个特殊情况,它将创建一个ERROR日志,但也将包括有关异常的信息,例如堆栈跟踪。
记得检查日志以发现错误。一个有用的提醒是在结果文件中添加一个注释,如下所示:
try:
main(args.n1, args.n2, args.output)
except Exception as exc:
logging.exception(exc)
print('There has been an error. Check the logs', file=args.output)
还有更多...
Python logging模块具有许多功能,例如以下内容:
-
进一步调整日志的格式,例如,包括生成日志的文件和行号。
-
定义不同的记录器对象,每个对象都有自己的配置,如日志级别和格式。这允许以不同的方式将日志发送到不同的系统,尽管通常不会出于简单起见而使用。
-
将日志发送到多个位置,例如标准输出和文件,甚至远程记录器。
-
自动旋转日志,创建新的日志文件,一段时间或大小后。这对于按天保持日志组织和允许压缩或删除旧日志非常方便。
-
从文件中读取标准日志配置。
与创建复杂规则相比,尝试进行广泛的日志记录,但使用适当的级别,然后进行过滤。
有关详细信息,请查看模块的 Python 文档docs.python.org/3.7/library/logging.html,或者查看教程docs.python.org/3.7/howto/logging.html。
另请参阅
-
在第一章的添加命令行选项中,让我们开始自动化之旅中的添加命令行选项。
-
准备任务配方
发送电子邮件通知
电子邮件已成为每个人每天都使用的不可避免的工具。如果自动化任务检测到某些情况,它可能是发送通知的最佳位置。另一方面,电子邮件收件箱已经充斥着垃圾邮件,所以要小心。
垃圾邮件过滤器也是现实。小心选择发送电子邮件的对象和发送的电子邮件数量。电子邮件服务器或地址可能被标记为垃圾邮件,所有电子邮件都将被互联网悄悄丢弃。本示例将展示如何使用已有的电子邮件帐户发送单个电子邮件。
这种方法适用于发送给几个人的备用电子邮件,作为自动化任务的结果,但不要超过这个数量。
准备就绪
对于本示例,我们需要设置一个有效的电子邮件帐户,其中包括以下内容:
-
有效的电子邮件服务器
-
连接的端口
-
一个地址
-
密码
这四个元素应该足以发送电子邮件。
例如,Gmail 等一些电子邮件服务将鼓励您设置 2FA,这意味着仅密码不足以发送电子邮件。通常,它们允许您为应用程序创建一个特定的密码来使用,绕过 2FA 请求。查看您的电子邮件提供商的信息以获取选项。
要使用的电子邮件提供商应指示 SMTP 服务器和端口在其文档中使用。它们也可以从电子邮件客户端中检索,因为它们是相同的参数。查看您的提供商文档。在以下示例中,我们将使用 Gmail 帐户。
如何做...
- 创建
email_task.py文件,如下所示:
import argparse
import configparser
import smtplib
from email.message import EmailMessage
def main(to_email, server, port, from_email, password):
print(f'With love, from {from_email} to {to_email}')
# Create the message
subject = 'With love, from ME to YOU'
text = '''This is an example test'''
msg = EmailMessage()
msg.set_content(text)
msg['Subject'] = subject
msg['From'] = from_email
msg['To'] = to_email
# Open communication and send
server = smtplib.SMTP_SSL(server, port)
server.login(from_email, password)
server.send_message(msg)
server.quit()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('email', type=str, help='destination email')
parser.add_argument('-c', dest='config', type=argparse.FileType('r'),
help='config file', default=None)
args = parser.parse_args()
if not args.config:
print('Error, a config file is required')
parser.print_help()
exit(1)
config = configparser.ConfigParser()
config.read_file(args.config)
main(args.email,
server=config['DEFAULT']['server'],
port=config['DEFAULT']['port'],
from_email=config['DEFAULT']['email'],
password=config['DEFAULT']['password'])
- 创建一个名为
email_conf.ini的配置文件,其中包含您的电子邮件账户的具体信息。例如,对于 Gmail 账户,请填写以下模板。该模板可在 GitHubgithub.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter02/email_conf.ini中找到,但请确保用您的数据填写它:
[DEFAULT]
email = EMAIL@gmail.com
server = smtp.gmail.com
port = 465
password = PASSWORD
- 确保文件不能被系统上的其他用户读取或写入,设置文件的权限只允许我们的用户。
600权限意味着我们的用户有读写权限,其他人没有访问权限:
$ chmod 600 email_config.ini
- 运行脚本发送测试邮件:
$ python3 email_task.py -c email_config.ini destination_email@server.com
- 检查目标电子邮件的收件箱;应该收到一封主题为
With love, from ME to YOU的电子邮件。
它是如何工作的...
脚本中有两个关键步骤——消息的生成和发送。
消息主要需要包含To和From电子邮件地址,以及Subject。如果内容是纯文本,就像在这种情况下一样,调用.set_content()就足够了。然后可以发送整个消息。
从一个与发送邮件的账户不同的邮箱发送邮件在技术上是可能的。尽管如此,这是不被鼓励的,因为你的电子邮件提供商可能会认为你试图冒充另一个邮箱。您可以使用reply-to头部来允许回复到不同的账户。
发送邮件需要连接到指定的服务器并启动 SMPT 连接。SMPT 是电子邮件通信的标准。
步骤非常简单——配置服务器,登录,发送准备好的消息,然后退出。
如果您需要发送多条消息,可以登录,发送多封电子邮件,然后退出,而不是每次都连接。
还有更多...
如果目标是更大规模的操作,比如营销活动,或者生产邮件,比如确认用户的电子邮件,请查看第八章,处理通信渠道
本步骤中使用的电子邮件消息内容非常简单,但电子邮件可能比这更复杂。
To字段可以包含多个收件人。用逗号分隔它们,就像这样:
message['To'] = ','.join(recipients)
电子邮件可以以 HTML 格式定义,并附有纯文本和附件。基本操作是设置一个MIMEMultipart,然后附加组成邮件的每个 MIME 部分:
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage message = MIMEMultipart()
part1 = MIMEText('some text', 'plain')
message.attach(part1)
with open('path/image', 'rb') as image:
part2 = MIMEImage(image.read()) message.attach(part2)
最常见的 SMPT 连接是SMPT_SSL,它更安全,需要登录和密码,但也存在普通的未经身份验证的 SMPT;请查看您的电子邮件提供商的文档。
请记住,这个步骤是为简单的通知而设计的。如果附加不同的信息,电子邮件可能会变得非常复杂。如果您的目标是为客户或任何一般群体发送电子邮件,请尝试使用第八章,处理通信渠道中的想法。
另请参阅
-
在第一章,让我们开始自动化之旅中的添加命令行选项步骤
-
准备任务的步骤
第三章:构建您的第一个 Web 抓取应用程序
在本章中,我们将涵盖以下内容:
-
下载网页
-
解析 HTML
-
爬取网络
-
订阅源
-
访问 Web API
-
与表单交互
-
使用 Selenium 进行高级交互
-
访问受密码保护的页面
-
加速网络抓取
介绍
互联网和WWW(万维网)可能是当今最重要的信息来源。大部分信息可以通过 HTTP 协议检索。HTTP最初是为了共享超文本页面而发明的(因此称为超文本传输协议),这开创了 WWW。
这个操作非常熟悉,因为它是任何网络浏览器中发生的事情。但我们也可以以编程方式执行这些操作,自动检索和处理信息。Python 在标准库中包含了一个 HTTP 客户端,但是 fantastic requests模块使它变得非常容易。在本章中,我们将看到如何做到这一点。
下载网页
下载网页的基本能力涉及对 URL 发出 HTTP GET请求。这是任何网络浏览器的基本操作。让我们快速回顾一下这个操作的不同部分:
-
使用 HTTP 协议。
-
使用最常见的 HTTP 方法
GET。我们将在访问 Web API配方中看到更多。 -
URL 描述页面的完整地址,包括服务器和路径。
该请求将由服务器处理,并发送回一个响应。这个响应将包含一个状态码,通常是 200,如果一切顺利的话,以及一个包含结果的 body,通常是一个包含 HTML 页面的文本。
大部分由用于执行请求的 HTTP 客户端自动处理。在这个配方中,我们将看到如何发出简单的请求以获取网页。
HTTP 请求和响应也可以包含头部。头部包含额外的信息,如请求的总大小,内容的格式,请求的日期以及使用的浏览器或服务器。
准备工作
使用 fantastic requests模块,获取网页非常简单。安装模块:
$ echo "requests==2.18.3" >> requirements.txt
$ source .venv/bin/activate
(.venv) $ pip install -r requirements.txt
我们将下载页面在www.columbia.edu/~fdc/sample.html,因为它是一个简单的 HTML 页面,很容易在文本模式下阅读。
如何做...
- 导入
requests模块:
>>> import requests
- 对 URL 发出请求,这将花费一两秒钟:
>>> url = 'http://www.columbia.edu/~fdc/sample.html'
>>> response = requests.get(url)
- 检查返回的对象状态码:
>>> response.status_code
200
- 检查结果的内容:
>>> response.text
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">\n<html>\n<head>\n
...
FULL BODY
...
<!-- close the <html> begun above -->\n'
- 检查进行中和返回的头部:
>>> response.request.headers
{'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
>>> response.headers
{'Date': 'Fri, 25 May 2018 21:51:47 GMT', 'Server': 'Apache', 'Last-Modified': 'Thu, 22 Apr 2004 15:52:25 GMT', 'Accept-Ranges': 'bytes', 'Vary': 'Accept-Encoding,User-Agent', 'Content-Encoding': 'gzip', 'Content-Length': '8664', 'Keep-Alive': 'timeout=15, max=85', 'Connection': 'Keep-Alive', 'Content-Type': 'text/html', 'Set-Cookie': 'BIGipServer~CUIT~www.columbia.edu-80-pool=1764244352.20480.0000; expires=Sat, 26-May-2018 03:51:47 GMT; path=/; Httponly'}
它是如何工作的...
requests的操作非常简单;在 URL 上执行操作,这种情况下是GET,返回一个可以分析的result对象。主要元素是status_code和 body 内容,可以呈现为text。
可以在request字段中检查完整的请求:
>>> response.request
<PreparedRequest [GET]>
>>> response.request.url
'http://www.columbia.edu/~fdc/sample.html'
完整的请求文档可以在这里找到:docs.python-requests.org/en/master/。在本章中,我们将展示更多功能。
还有更多...
所有 HTTP 状态码可以在这个网页上检查:httpstatuses.com/。它们也在httplib模块中以方便的常量名称进行描述,如OK,NOT_FOUND或FORBIDDEN。
最著名的错误状态码可能是 404,当 URL 未找到时会发生。通过执行requests.get('http://www.columbia.edu/invalid')来尝试。
请求可以使用HTTPS协议(安全 HTTP)。它是等效的,但确保请求和响应的内容是私有的。requests会自动处理它。
任何处理任何私人信息的网站都将使用 HTTPS 来确保信息没有泄漏。HTTP 容易受到窃听。尽可能使用 HTTPS。
另请参阅
-
在第一章的让我们开始自动化之旅中的安装第三方包配方中
-
解析 HTML配方
解析 HTML
下载原始文本或二进制文件是一个很好的起点,但是网页的主要语言是 HTML。
HTML 是一种结构化语言,定义文档的不同部分,如标题和段落。HTML 也是分层的,定义了子元素。将原始文本解析为结构化文档的能力基本上是能够从网页中自动提取信息的能力。例如,如果在特定的class div中或在标题h3标签后面包含一些文本,则该文本可能是相关的。
准备就绪
我们将使用优秀的 Beautiful Soup 模块将 HTML 文本解析为可以分析的内存对象。我们需要使用beautifulsoup4包来使用可用的最新 Python 3 版本。将包添加到您的requirements.txt并在虚拟环境中安装依赖项:
$ echo "beautifulsoup4==4.6.0" >> requirements.txt
$ pip install -r requirements.txt
如何做...
- 导入
BeautifulSoup和requests:
>>> import requests >>> from bs4 import BeautifulSoup
- 设置要下载并检索的页面的 URL:
>>> URL = 'http://www.columbia.edu/~fdc/sample.html'
>>> response = requests.get(URL)
>>> response
<Response [200]>
- 解析下载的页面:
>>> page = BeautifulSoup(response.text, 'html.parser')
- 获取页面的标题。注意它与浏览器中显示的内容相同:
>>> page.title
<title>Sample Web Page</title>
>>> page.title.string
'Sample Web Page'
- 在页面中查找所有的
h3元素,以确定现有的部分:
>>> page.find_all('h3')
[<h3><a name="contents">CONTENTS</a></h3>, <h3><a name="basics">1\. Creating a Web Page</a></h3>, <h3><a name="syntax">2\. HTML Syntax</a></h3>, <h3><a name="chars">3\. Special Characters</a></h3>, <h3><a name="convert">4\. Converting Plain Text to HTML</a></h3>, <h3><a name="effects">5\. Effects</a></h3>, <h3><a name="lists">6\. Lists</a></h3>, <h3><a name="links">7\. Links</a></h3>, <h3><a name="tables">8\. Tables</a></h3>, <h3><a name="install">9\. Installing Your Web Page on the Internet</a></h3>, <h3><a name="more">10\. Where to go from here</a></h3>]
- 提取部分链接上的文本。当达到下一个
<h3>标签时停止:
>>> link_section = page.find('a', attrs={'name': 'links'})
>>> section = []
>>> for element in link_section.next_elements:
... if element.name == 'h3':
... break
... section.append(element.string or '')
...
>>> result = ''.join(section)
>>> result
'7\. Links\n\nLinks can be internal within a Web page (like to\nthe Table of ContentsTable of Contents at the top), or they\ncan be to external web pages or pictures on the same website, or they\ncan be to websites, pages, or pictures anywhere else in the world.\n\n\n\nHere is a link to the Kermit\nProject home pageKermit\nProject home page.\n\n\n\nHere is a link to Section 5Section 5 of this document.\n\n\n\nHere is a link to\nSection 4.0Section 4.0\nof the C-Kermit\nfor Unix Installation InstructionsC-Kermit\nfor Unix Installation Instructions.\n\n\n\nHere is a link to a picture:\nCLICK HERECLICK HERE to see it.\n\n\n'
注意没有 HTML 标记;这都是原始文本。
它是如何工作的...
第一步是下载页面。然后,可以像第 3 步那样解析原始文本。生成的page对象包含解析的信息。
html.parser解析器是默认的,但是对于特定操作可能会出现问题。例如,对于大页面,它可能会很慢,或者在渲染高度动态的网页时可能会出现问题。您可以使用其他解析器,例如lxml,它速度更快,或者html5lib,它将更接近浏览器的操作,包括 HTML5 产生的动态更改。它们是外部模块,需要添加到requirements.txt文件中。
BeautifulSoup允许我们搜索 HTML 元素。它可以使用.find()搜索第一个元素,或者使用.find_all()返回一个列表。在第 5 步中,它搜索了一个具有特定属性name=link的特定标签<a>。之后,它继续在.next_elements上迭代,直到找到下一个h3标签,标志着该部分的结束。
提取每个元素的文本,最后组合成单个文本。注意or,它避免存储None,当元素没有文本时返回。
HTML 非常灵活,可以有多种结构。本配方中介绍的情况是典型的,但是在划分部分方面的其他选项可能是将相关部分组合在一个大的<div>标签或其他元素内,甚至是原始文本。需要进行一些实验,直到找到从网页中提取重要部分的特定过程。不要害怕尝试!
还有更多...
正则表达式也可以用作.find()和.find_all()方法的输入。例如,此搜索使用h2和h3标签:
>>> page.find_all(re.compile('^h(2|3)'))
[<h2>Sample Web Page</h2>, <h3><a name="contents">CONTENTS</a></h3>, <h3><a name="basics">1\. Creating a Web Page</a></h3>, <h3><a name="syntax">2\. HTML Syntax</a></h3>, <h3><a name="chars">3\. Special Characters</a></h3>, <h3><a name="convert">4\. Converting Plain Text to HTML</a></h3>, <h3><a name="effects">5\. Effects</a></h3>, <h3><a name="lists">6\. Lists</a></h3>, <h3><a name="links">7\. Links</a></h3>,
<h3><a name="tables">8\. Tables</a></h3>, <h3><a name="install">9\. Installing Your Web Page on the Internet</a></h3>, <h3><a name="more">10\. Where to go from here</a></h3>]
另一个有用的 find 参数是包含class_参数的 CSS 类。这将在本书的后面显示。
完整的 Beautiful Soup 文档可以在这里找到:www.crummy.com/software/BeautifulSoup/bs4/doc/。
另请参阅
-
在第一章的让我们开始自动化之旅中的安装第三方包配方
-
在第一章的让我们开始自动化之旅中的介绍正则表达式配方
-
下载网页配方
爬取网页
考虑到超链接页面的性质,从已知位置开始并跟随链接到其他页面是在抓取网页时的重要工具。
为此,我们爬取页面寻找一个小短语,并打印包含它的任何段落。我们只会在属于同一网站的页面中搜索。即只有以 www.somesite.com 开头的 URL。我们不会跟踪外部网站的链接。
准备工作
这个食谱是基于介绍的概念构建的,因此它将下载和解析页面以搜索链接并继续下载。
在爬取网页时,记得在下载时设置限制。很容易爬取太多页面。任何查看维基百科的人都可以证实,互联网是潜在无限的。
我们将使用一个准备好的示例,该示例可在 GitHub 存储库中找到:github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter03/test_site。下载整个站点并运行包含的脚本。
$ python simple_delay_server.py
它在 URLhttp://localhost:8000中提供站点。您可以在浏览器上查看它。这是一个简单的博客,有三篇文章。大部分内容都不那么有趣,但我们添加了一些包含关键字python的段落。

如何做到这一点...
- 完整的脚本
crawling_web_step1.py可以在 GitHub 的以下链接找到:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter03/crawling_web_step1.py。最相关的部分显示在这里:
...
def process_link(source_link, text):
logging.info(f'Extracting links from {source_link}')
parsed_source = urlparse(source_link)
result = requests.get(source_link)
# Error handling. See GitHub for details
...
page = BeautifulSoup(result.text, 'html.parser')
search_text(source_link, page, text)
return get_links(parsed_source, page)
def get_links(parsed_source, page):
'''Retrieve the links on the page'''
links = []
for element in page.find_all('a'):
link = element.get('href')
# Validate is a valid link. See GitHub for details
...
links.append(link)
return links
- 搜索对
python的引用,返回包含它和段落的 URL 列表。请注意,由于损坏的链接,有一些错误:
$ python crawling_web_step1.py https://localhost:8000/ -p python
Link http://localhost:8000/: --> A smaller article , that contains a reference to Python
Link http://localhost:8000/files/5eabef23f63024c20389c34b94dee593-1.html: --> A smaller article , that contains a reference to Python
Link http://localhost:8000/files/33714fc865e02aeda2dabb9a42a787b2-0.html: --> This is the actual bit with a python reference that we are interested in.
Link http://localhost:8000/files/archive-september-2018.html: --> A smaller article , that contains a reference to Python
Link http://localhost:8000/index.html: --> A smaller article , that contains a reference to Python
- 另一个很好的搜索词是
crocodile。试一下:
$ python crawling_web_step1.py http://localhost:8000/ -p crocodile
它是如何工作的...
让我们看看脚本的每个组件:
- 一个循环,遍历
main函数中找到的所有链接:
请注意,有 10 页的检索限制,并且正在检查是否已经添加了要添加的任何新链接。
请注意这两件事是有限制的。我们不会下载相同的链接两次,我们会在某个时候停止。
- 在
process_link函数中下载和解析链接:
它下载文件,并检查状态是否正确,以跳过诸如损坏链接之类的错误。它还检查类型(如Content-Type中描述的)是否为 HTML 页面,以跳过 PDF 和其他格式。最后,它将原始 HTML 解析为BeautifulSoup对象。
它还使用urlparse解析源链接,以便在步骤 4 中跳过所有对外部来源的引用。urlparse将 URL 分解为其组成元素:
>>> from urllib.parse import urlparse
>>> >>> urlparse('http://localhost:8000/files/b93bec5d9681df87e6e8d5703ed7cd81-2.html')
ParseResult(scheme='http', netloc='localhost:8000', path='/files/b93bec5d9681df87e6e8d5703ed7cd81-2.html', params='', query='', fragment='')
- 在
search_text函数中找到要搜索的文本:
它在解析的对象中搜索指定的文本。请注意,搜索是作为regex进行的,仅在文本中进行。它打印出结果的匹配项,包括source_link,引用找到匹配项的 URL:
for element in page.find_all(text=re.compile(text)):
print(f'Link {source_link}: --> {element}')
get_links函数检索页面上的所有链接:
它在解析页面中搜索所有<a>元素,并检索href元素,但只有具有这些href元素并且是完全合格的 URL(以http开头)的元素。这将删除不是 URL 的链接,例如'#'链接,或者是页面内部的链接。
还进行了额外的检查,以检查它们是否与原始链接具有相同的来源,然后将它们注册为有效链接。netloc属性允许检测链接是否来自与步骤 2 中生成的解析 URL 相同的 URL 域。
我们不会跟踪指向不同地址的链接(例如www.google.com)。
最后,链接被返回,它们将被添加到步骤 1 中描述的循环中。
还有更多...
还可以进一步强制执行其他过滤器,例如丢弃所有以.pdf结尾的链接,这意味着它们是 PDF 文件:
# In get_links
if link.endswith('pdf'):
continue
还可以使用Content-Type来确定以不同方式解析返回的对象。例如,PDF 结果(Content-Type: application/pdf)将没有有效的response.text对象进行解析,但可以用其他方式解析。其他类型也是如此,例如 CSV 文件(Content-Type: text/csv)或可能需要解压缩的 ZIP 文件(Content-Type: application/zip)。我们将在后面看到如何处理这些。
另请参阅
-
下载网页食谱
-
解析 HTML食谱
订阅 Feed
RSS 可能是互联网上最大的“秘密”。虽然它的辉煌时刻似乎是在 2000 年代,现在它不再处于聚光灯下,但它可以轻松订阅网站。它存在于许多地方,非常有用。
在其核心,RSS 是一种呈现有序引用(通常是文章,但也包括其他元素,如播客剧集或 YouTube 出版物)和发布时间的方式。这使得很自然地知道自上次检查以来有哪些新文章,以及呈现一些关于它们的结构化数据,如标题和摘要。
在这个食谱中,我们将介绍feedparser模块,并确定如何从 RSS Feed 中获取数据。
RSS不是唯一可用的 Feed 格式。还有一种称为Atom的格式,但两者几乎是等效的。feedparser也能够解析它,因此两者可以不加区分地使用。
准备工作
我们需要将feedparser依赖项添加到我们的requirements.txt文件中并重新安装它:
$ echo "feedparser==5.2.1" >> requirements.txt
$ pip install -r requirements.txt
几乎所有涉及出版物的页面上都可以找到 Feed URL,包括博客、新闻、播客等。有时很容易找到它们,但有时它们会隐藏得有点深。可以通过feed或RSS进行搜索。
大多数报纸和新闻机构都将它们的 RSS Feed 按主题划分。我们将使用纽约时报主页 Feed 作为示例,rss.nytimes.com/services/xml/rss/nyt/HomePage.xml。主要 Feed 页面上还有更多可用的 Feed:archive.nytimes.com/www.nytimes.com/services/xml/rss/index.html。
请注意,Feed 可能受到使用条款和条件的约束。在纽约时报的情况下,它们在主要 Feed 页面的末尾有描述。
请注意,此 Feed 经常更改,这意味着链接的条目将与本书中的示例不同。
如何做...
- 导入
feedparser模块,以及datetime、delorean和requests:
import feedparser
import datetime
import delorean
import requests
- 解析 Feed(它将自动下载)并检查其上次更新时间。Feed 信息,如 Feed 的标题,可以在
feed属性中获取:
>>> rss = feedparser.parse('http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml')
>>> rss.updated
'Sat, 02 Jun 2018 19:50:35 GMT'
- 获取新于六小时的条目:
>>> time_limit = delorean.parse(rss.updated) - datetime.timedelta(hours=6)
>>> entries = [entry for entry in rss.entries if delorean.parse(entry.published) > time_limit]
- 条目将比总条目少,因为返回的条目中有些条目的时间已经超过六个小时:
>>> len(entries)
10
>>> len(rss.entries)
44
- 检索条目的信息,如
title。完整的条目 URL 可作为link获取。探索此特定 Feed 中的可用信息:
>>> entries[5]['title']
'Loose Ends: How to Live to 108'
>>> entries[5]['link']
'https://www.nytimes.com/2018/06/02/opinion/sunday/how-to-live-to-108.html?partner=rss&emc=rss'
>>> requests.get(entries[5].link)
<Response [200]>
工作原理...
解析的feed对象包含条目的信息,以及有关 Feed 本身的一般信息,例如更新时间。feed信息可以在feed属性中找到:
>>> rss.feed.title
'NYT > Home Page'
每个条目都像一个字典,因此很容易检索字段。它们也可以作为属性访问,但将它们视为键可以获取所有可用的字段:
>>> entries[5].keys()
dict_keys(['title', 'title_detail', 'links', 'link', 'id', 'guidislink', 'media_content', 'summary', 'summary_detail', 'media_credit', 'credit', 'content', 'authors', 'author', 'author_detail', 'published', 'published_parsed', 'tags'])
处理 Feed 的基本策略是解析它们并浏览条目,快速检查它们是否有趣,例如通过检查描述或摘要。如果它们有趣,就使用“链接”字段下载整个页面。然后,为了避免重新检查条目,存储最新的发布日期,下次只检查更新的条目。
还有更多...
完整的feedparser文档可以在这里找到:pythonhosted.org/feedparser/。
可用的信息可能因源而异。在纽约时报的例子中,有一个带有标签信息的tag字段,但这不是标准的。至少,条目将有一个标题,一个描述和一个链接。
RSS 订阅也是筛选自己的新闻来源的好方法。有很好的订阅阅读器。
另请参阅
-
第一章中的安装第三方软件包配方,让我们开始我们的自动化之旅
-
下载网页的配方
访问 Web API
通过 Web 可以创建丰富的接口,通过 HTTP 进行强大的交互。最常见的接口是使用 JSON 的 RESTful API。这些基于文本的接口易于理解和编程,并使用通用技术,与语言无关,这意味着它们可以在任何具有 HTTPclient模块的编程语言中访问,当然包括 Python。
除了 JSON 之外,还使用了其他格式,例如 XML,但 JSON 是一种非常简单和可读的格式,非常适合转换为 Python 字典(以及其他语言的等价物)。JSON 目前是 RESTful API 中最常见的格式。在这里了解更多关于 JSON 的信息:www.json.org/。
RESTful 的严格定义需要一些特征,但更非正式的定义可能是通过 URL 访问资源。这意味着 URL 代表特定的资源,例如报纸上的文章或房地产网站上的属性。然后可以通过 HTTP 方法(GET查看,POST创建,PUT/PATCH编辑和DELETE删除)来操作资源。
适当的 RESTful 接口需要具有某些特征,并且是创建接口的一种方式,不严格限于 HTTP 接口。您可以在这里阅读更多信息:codewords.recurse.com/issues/five/what-restful-actually-means。
使用requests与它们非常容易,因为它包含本机 JSON 支持。
准备就绪
为了演示如何操作 RESTful API,我们将使用示例站点jsonplaceholder.typicode.com/。它模拟了帖子,评论和其他常见资源的常见情况。我们将使用帖子和评论。要使用的 URL 如下:
# The collection of all posts
/posts
# A single post. X is the ID of the post
/posts/X
# The comments of post X
/posts/X/comments
网站为它们中的每一个返回了适当的结果。非常方便!
因为这是一个测试站点,数据不会被创建,但站点将返回所有正确的响应。
如何做...
- 导入
requests:
>>> import requests
- 获取所有帖子的列表并显示最新帖子:
>>> result = requests.get('https://jsonplaceholder.typicode.com/posts')
>>> result
<Response [200]>
>>> result.json()
# List of 100 posts NOT DISPLAYED HERE
>>> result.json()[-1]
{'userId': 10, 'id': 100, 'title': 'at nam consequatur ea labore ea harum', 'body': 'cupiditate quo est a modi nesciunt soluta\nipsa voluptas error itaque dicta in\nautem qui minus magnam et distinctio eum\naccusamus ratione error aut'}
- 创建一个新帖子。查看新创建资源的 URL。调用还返回资源:
>>> new_post = {'userId': 10, 'title': 'a title', 'body': 'something something'}
>>> result = requests.post('https://jsonplaceholder.typicode.com/posts',
json=new_post)
>>> result
<Response [201]>
>>> result.json()
{'userId': 10, 'title': 'a title', 'body': 'something something', 'id': 101}
>>> result.headers['Location']
'http://jsonplaceholder.typicode.com/posts/101'
注意,创建资源的POST请求返回 201,这是创建的适当状态。
- 使用
GET获取现有帖子:
>>> result = requests.get('https://jsonplaceholder.typicode.com/posts/2')
>>> result
<Response [200]>
>>> result.json()
{'userId': 1, 'id': 2, 'title': 'qui est esse', 'body': 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla'}
- 使用
PATCH更新其值。检查返回的资源:
>>> update = {'body': 'new body'}
>>> result = requests.patch('https://jsonplaceholder.typicode.com/posts/2', json=update)
>>> result
<Response [200]>
>>> result.json()
{'userId': 1, 'id': 2, 'title': 'qui est esse', 'body': 'new body'}
它是如何工作的...
通常访问两种资源。单个资源(https://jsonplaceholder.typicode.com/posts/X)和集合(https://jsonplaceholder.typicode.com/posts):
-
集合接受
GET以检索它们所有,并接受POST以创建新资源 -
单个元素接受
GET以获取元素,PUT和PATCH以编辑,DELETE以删除它们
所有可用的 HTTP 方法都可以在requests中调用。在以前的配方中,我们使用了.get(),但.post(),.patch(),.put()和.delete()也可用。
返回的响应对象具有.json()方法,用于解码 JSON 结果。
同样,发送信息时,有一个json参数可用。这将字典编码为 JSON 并将其发送到服务器。数据需要遵循资源的格式,否则可能会引发错误。
GET和DELETE不需要数据,而PATCH、PUT和POST需要数据。
将返回所引用的资源,其 URL 在标头位置可用。这在创建新资源时非常有用,因为其 URL 事先是未知的。
PATCH和PUT之间的区别在于后者替换整个资源,而前者进行部分更新。
还有更多...
RESTful API 非常强大,但也具有巨大的可变性。请查看特定 API 的文档,了解其详细信息。
另请参阅
-
下载网页配方
-
第一章中的安装第三方软件包配方,让我们开始我们的自动化之旅
与表单交互
网页中常见的一个元素是表单。表单是将值发送到网页的一种方式,例如,在博客文章上创建新评论或提交购买。
浏览器呈现表单,以便您可以输入值并在按下提交或等效按钮后以单个操作发送它们。我们将在此配方中看到如何以编程方式创建此操作。
请注意,向网站发送数据通常比从网站接收数据更明智。例如,向网站发送自动评论非常符合垃圾邮件的定义。这意味着自动化和包含安全措施可能更加困难。请仔细检查您尝试实现的是否是有效的、符合道德的用例。
准备就绪
我们将针对测试服务器httpbin.org/forms/post进行操作,该服务器允许我们发送测试表单并返回提交的信息。
以下是一个订购比萨的示例表单:

您可以手动填写表单并查看它以 JSON 格式返回信息,包括浏览器使用等额外信息。
以下是生成的 Web 表单的前端:

以下图像是生成的 Web 表单的后端:

我们需要分析 HTML 以查看表单的接受数据。检查源代码,显示如下:
源代码
检查输入的名称,custname、custtel、custemail、size(单选按钮选项)、topping(多选复选框)、delivery(时间)和comments。
如何做...
- 导入
requests、BeautifulSoup和re模块:
>>> import requests
>>> from bs4 import BeautifulSoup
>>> import re
- 检索表单页面,解析它,并打印输入字段。检查发布 URL 是否为
/post(而不是/forms/post):
>>> response = requests.get('https://httpbin.org/forms/post')
>>> page = BeautifulSoup(response.text)
>>> form = soup.find('form')
>>> {field.get('name') for field in form.find_all(re.compile('input|textarea'))}
{'delivery', 'topping', 'size', 'custemail', 'comments', 'custtel', 'custname'}
请注意,textarea是有效输入,也是在 HTML 格式中定义的。
- 准备要发布的数据作为字典。检查值是否与表单中定义的相同:
>>> data = {'custname': "Sean O'Connell", 'custtel': '123-456-789', 'custemail': 'sean@oconnell.ie', 'size': 'small', 'topping': ['bacon', 'onion'], 'delivery': '20:30', 'comments': ''}
- 发布值并检查响应是否与浏览器中返回的相同:
>>> response = requests.post('https://httpbin.org/post', data)
>>> response
<Response [200]>
>>> response.json()
{'args': {}, 'data': '', 'files': {}, 'form': {'comments': '', 'custemail': 'sean@oconnell.ie', 'custname': "Sean O'Connell", 'custtel': '123-456-789', 'delivery': '20:30', 'size': 'small', 'topping': ['bacon', 'onion']}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'Content-Length':
'140', 'Content-Type': 'application/x-www-form-urlencoded', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.18.3'}, 'json': None, 'origin': '89.100.17.159', 'url': 'https://httpbin.org/post'}
它是如何工作的...
requests直接接受以正确方式发送数据。默认情况下,它以application/x-www-form-urlencoded格式发送POST数据。
将其与访问 Web API配方进行比较,其中数据是使用参数json以 JSON 格式明确发送的。这使得Content-Type为application/json而不是application/x-www-form-urlencoded。
这里的关键是尊重表单的格式和可能返回错误的可能值,通常是 400 错误。
还有更多...
除了遵循表单的格式和输入有效值之外,在处理表单时的主要问题是防止垃圾邮件和滥用行为的多种方式。
非常常见的限制是确保在提交表单之前下载了表单,以避免提交多个表单或跨站点请求伪造(CSRF)。
CSRF,意味着从一个页面对另一个页面发出恶意调用,利用您的浏览器已经经过身份验证,这是一个严重的问题。例如,进入一个利用您已经登录到银行页面执行操作的小狗网站。这是一个很好的描述:stackoverflow.com/a/33829607。浏览器中的新技术默认情况下有助于解决这些问题。
要获取特定令牌,您需要首先下载表单,如配方中所示,获取 CSRF 令牌的值,并重新提交。请注意,令牌可以有不同的名称;这只是一个例子:
>>> form.find(attrs={'name': 'token'}).get('value')
'ABCEDF12345'
另请参阅
-
下载网页的配方
-
解析 HTML的配方
使用 Selenium 进行高级交互
有时,除了真实的东西外,什么都行不通。 Selenium 是一个实现 Web 浏览器自动化的项目。它被构想为一种自动测试的方式,但也可以用于自动化与网站的交互。
Selenium 可以控制 Safari、Chrome、Firefox、Internet Explorer 或 Microsoft Edge,尽管它需要为每种情况安装特定的驱动程序。我们将使用 Chrome。
准备工作
我们需要为 Chrome 安装正确的驱动程序,称为chromedriver。它在这里可用:sites.google.com/a/chromium.org/chromedriver/。它适用于大多数平台。它还要求您已安装 Chrome:www.google.com/chrome/。
将selenium模块添加到requirements.txt并安装它:
$ echo "selenium==3.12.0" >> requirements.txt
$ pip install -r requirements.txt
如何做...
- 导入 Selenium,启动浏览器,并加载表单页面。将打开一个反映操作的页面:
>>> from selenium import webdriver
>>> browser = webdriver.Chrome()
>>> browser.get('https://httpbin.org/forms/post')
请注意,Chrome 中的横幅由自动化测试软件控制。
- 在“客户名称”字段中添加一个值。请记住它被称为
custname:
>>> custname = browser.find_element_by_name("custname")
>>> custname.clear()
>>> custname.send_keys("Sean O'Connell")
表单将更新:

- 选择披萨大小为
medium:
>>> for size_element in browser.find_elements_by_name("size"):
... if size_element.get_attribute('value') == 'medium':
... size_element.click()
...
>>>
这将改变披萨大小比例框。
- 添加
bacon和cheese:
>>> for topping in browser.find_elements_by_name('topping'):
... if topping.get_attribute('value') in ['bacon', 'cheese']:
... topping.click()
...
>>>
最后,复选框将显示为已标记:

- 提交表单。页面将提交,结果将显示:
>>> browser.find_element_by_tag_name('form').submit()
表单将被提交,服务器的结果将显示:

- 关闭浏览器:
>>> browser.quit()
它是如何工作的...
如何做...部分的第 1 步显示了如何创建一个 Selenium 页面并转到特定的 URL。
Selenium 的工作方式与 Beautiful Soup 类似。选择适当的元素,然后操纵它。 Selenium 中的选择器的工作方式与 Beautiful Soup 中的选择器的工作方式类似,最常见的选择器是find_element_by_id、find_element_by_class_name、find_element_by_name、find_element_by_tag_name和find_element_by_css_selector。还有等效的find_elements_by_X,它们返回一个列表而不是第一个找到的元素(find_elements_by_tag_name、find_elements_by_name等)。当检查元素是否存在时,这也很有用。如果没有元素,find_element将引发错误,而find_elements将返回一个空列表。
可以通过.get_attribute()获取元素上的数据,用于 HTML 属性(例如表单元素上的值)或.text。
可以通过模拟发送按键输入文本来操作元素,方法是.send_keys(),点击是.click(),如果它们接受,可以使用.submit()进行提交。.submit()将在表单上搜索适当的提交,.click()将以与鼠标点击相同的方式选择/取消选择。
最后,第 6 步关闭浏览器。
还有更多...
这是完整的 Selenium 文档:selenium-python.readthedocs.io/。
对于每个元素,都可以提取额外的信息,例如.is_displayed()或.is_selected()。可以使用.find_element_by_link_text()和.find_element_by_partial_link_text()来搜索文本。
有时,打开浏览器可能会不方便。另一种选择是以无头模式启动浏览器,并从那里操纵它,就像这样:
>>> from selenium.webdriver.chrome.options import Options
>>> chrome_options = Options()
>>> chrome_options.add_argument("--headless")
>>> browser = webdriver.Chrome(chrome_options=chrome_options)
>>> browser.get('https://httpbin.org/forms/post')
页面不会显示。但是可以使用以下命令保存截图:
>>> browser.save_screenshot('screenshot.png')
另请参阅
-
解析 HTML配方
-
与表单交互配方
访问受密码保护的页面
有时,网页对公众不开放,而是以某种方式受到保护。最基本的方面是使用基本的 HTTP 身份验证,它集成到几乎每个 Web 服务器中,并且是用户/密码模式。
准备就绪
我们可以在httpbin.org中测试这种身份验证。
它有一个路径,/basic-auth/{user}/{password},强制进行身份验证,用户和密码已声明。这对于理解身份验证的工作原理非常方便。
如何做...
- 导入
requests:
>>> import requests
- 使用错误的凭据对 URL 进行
GET请求。注意,我们在 URL 上设置了凭据为user和psswd:
>>> requests.get('https://httpbin.org/basic-auth/user/psswd',
auth=('user', 'psswd'))
<Response [200]>
- 使用错误的凭据返回 401 状态码(未经授权):
>>> requests.get('https://httpbin.org/basic-auth/user/psswd',
auth=('user', 'wrong'))
<Response [401]>
- 凭据也可以直接通过 URL 传递,在服务器之前用冒号和
@符号分隔,就像这样:
>>> requests.get('https://user:psswd@httpbin.org/basic-auth/user/psswd')
<Response [200]>
>>> requests.get('https://user:wrong@httpbin.org/basic-auth/user/psswd')
<Response [401]>
它的工作原理...
由于 HTTP 基本身份验证在各处都受支持,因此从“请求”获得支持非常容易。
如何做...部分的第 2 步和第 4 步显示了如何提供正确的密码。第 3 步显示了密码错误时会发生什么。
请记住始终使用 HTTPS,以确保密码的发送保密。如果使用 HTTP,密码将在网络上以明文发送。
还有更多...
将用户和密码添加到 URL 中也适用于浏览器。尝试直接访问页面,看到一个框显示要求输入用户名和密码:
用户凭据页面
在使用包含用户和密码的 URL 时,https://user:psswd@httpbin.org/basic-auth/user/psswd,对话框不会出现,它会自动进行身份验证。
如果您需要访问多个页面,可以在“请求”中创建一个会话,并将身份验证参数设置为避免在各处输入它们:
>>> s = requests.Session()
>>> s.auth = ('user', 'psswd')
>>> s.get('https://httpbin.org/basic-auth/user/psswd')
<Response [200]>
另请参阅
-
下载网页配方
-
访问 Web API配方
加快网页抓取速度
从网页下载信息所花费的大部分时间通常是在等待。请求从我们的计算机发送到将处理它的任何服务器,直到响应被组成并返回到我们的计算机,我们无法做太多事情。
在书中执行配方时,您会注意到requests调用通常需要等待大约一两秒。但是计算机可以在等待时做其他事情,包括同时发出更多请求。在这个配方中,我们将看到如何并行下载页面列表并等待它们全部准备就绪。我们将使用一个故意缓慢的服务器来说明这一点。
准备就绪
我们将获得代码来爬取和搜索关键字,利用 Python 3 的futures功能同时下载多个页面。
future是表示值承诺的对象。这意味着在代码在后台执行时,您立即收到一个对象。只有在明确请求其.result()时,代码才会阻塞,直到获取它。
要生成一个future,你需要一个后台引擎,称为executor。一旦创建,submit一个函数和参数给它以检索一个future。结果的检索可以被延迟,直到需要,允许连续生成多个futures,并等待直到所有都完成,以并行执行它们,而不是创建一个,等待它完成,再创建另一个,依此类推。
有几种方法可以创建一个 executor;在这个示例中,我们将使用ThreadPoolExecutor,它将使用线程。
我们将以一个准备好的示例为例,该示例可在 GitHub 存储库中找到:github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter03/test_site。下载整个站点并运行包含的脚本。
$ python simple_delay_server.py -d 2
这是 URL 为http://localhost:8000的站点。你可以在浏览器上查看它。这是一个简单的博客,有三篇文章。大部分内容都不那么有趣,但我们添加了几段包含关键字python的段落。参数-d 2使服务器故意变慢,模拟一个糟糕的连接。
如何做...
- 编写以下脚本
speed_up_step1.py。完整的代码可以在 GitHub 的Chapter03目录下找到:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter03/speed_up_step1.py。这里只列出了最相关的部分。它基于crawling_web_step1.py。
...
def process_link(source_link, text):
...
return source_link, get_links(parsed_source, page)
...
def main(base_url, to_search, workers):
checked_links = set()
to_check = [base_url]
max_checks = 10
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
while to_check:
futures = [executor.submit(process_link, url, to_search)
for url in to_check]
to_check = []
for data in concurrent.futures.as_completed(futures):
link, new_links = data.result()
checked_links.add(link)
for link in new_links:
if link not in checked_links and link not in to_check:
to_check.append(link)
max_checks -= 1
if not max_checks:
return
if __name__ == '__main__':
parser = argparse.ArgumentParser()
...
parser.add_argument('-w', type=int, help='Number of workers',
default=4)
args = parser.parse_args()
main(args.u, args.p, args.w)
-
请注意
main函数中的差异。还添加了一个额外的参数(并发工作人员的数量),并且process_link函数现在返回源链接。 -
运行
crawling_web_step1.py脚本以获取时间基准。注意这里已经删除了输出以保持清晰:
$ time python crawling_web_step1.py http://localhost:8000/
... REMOVED OUTPUT
real 0m12.221s
user 0m0.160s
sys 0m0.034s
- 使用比原始版本慢的一个工作人员运行新脚本:
$ time python speed_up_step1.py -w 1
... REMOVED OUTPUT
real 0m16.403s
user 0m0.181s
sys 0m0.068s
- 增加工作人员的数量:
$ time python speed_up_step1.py -w 2
... REMOVED OUTPUT
real 0m10.353s
user 0m0.199s
sys 0m0.068s
- 增加更多的工作人员会减少时间。
$ time python speed_up_step1.py -w 5
... REMOVED OUTPUT
real 0m6.234s
user 0m0.171s
sys 0m0.040s
它是如何工作的...
创建并发请求的主要引擎是主函数。请注意,代码的其余部分基本上没有改动(除了在process_link函数中返回源链接)。
这种变化在适应并发时实际上是相当常见的。并发任务需要返回所有相关的数据,因为它们不能依赖于有序的上下文。
这是处理并发引擎的代码的相关部分:
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
while to_check:
futures = [executor.submit(process_link, url, to_search)
for url in to_check]
to_check = []
for data in concurrent.futures.as_completed(futures):
link, new_links = data.result()
checked_links.add(link)
for link in new_links:
if link not in checked_links and link not in to_check:
to_check.append(link)
max_checks -= 1
if not max_checks:
return
with上下文创建了一个指定数量的工作人员池。在内部,创建了一个包含所有要检索的 URL 的futures列表。.as_completed()函数返回已完成的futures,然后进行一些工作,处理获取到的新链接,并检查它们是否需要被添加到检索中。这个过程类似于Crawling the web示例中呈现的过程。
该过程会再次开始,直到检索到足够的链接或没有链接可检索为止。请注意,链接是批量检索的;第一次,处理基本链接并检索所有链接。在第二次迭代中,将请求所有这些链接。一旦它们都被下载,将处理一个新的批次。
处理并发请求时,请记住它们可以在两次执行之间改变顺序。如果一个请求花费的时间稍微多一点或少一点,那可能会影响检索信息的顺序。因为我们在下载 10 页后停止,这也意味着这 10 页可能是不同的。
还有更多...
Python 的完整futures文档可以在这里找到:docs.python.org/3/library/concurrent.futures.html。
正如您在如何做…部分的第 4 和第 5 步中所看到的,正确确定工作人员的数量可能需要一些测试。一些数字可能会使过程变慢,因为管理增加了。不要害怕尝试!
在 Python 世界中,还有其他方法可以进行并发的 HTTP 请求。有一个原生请求模块,允许我们使用futures,名为requests-futures。它可以在这里找到:github.com/ross/requests-futures。
另一种选择是使用异步编程。最近,这种工作方式引起了很多关注,因为在处理许多并发调用时可以非常高效,但编码的方式与传统方式不同,需要一些时间来适应。Python 包括asyncio模块来进行这种工作,还有一个名为aiohttp的好模块来处理 HTTP 请求。您可以在这里找到有关aiohttp的更多信息:aiohttp.readthedocs.io/en/stable/client_quickstart.html。
关于异步编程的良好介绍可以在这篇文章中找到:djangostars.com/blog/asynchronous-programming-in-python-asyncio/。
另请参阅
-
爬取网页配方
-
下载网页配方
第四章:搜索和阅读本地文件
在本章中,我们将涵盖以下食谱:
-
爬取和搜索目录
-
阅读文本文件
-
处理编码
-
阅读 CSV 文件
-
阅读日志文件
-
阅读文件元数据
-
阅读图像
-
阅读 PDF 文件
-
阅读 Word 文档
-
扫描文档以查找关键字
介绍
在本章中,我们将处理读取文件的基本操作,从搜索和打开目录和子目录中的文件开始。然后,我们将描述一些最常见的文件类型以及如何读取它们,包括原始文本文件、PDF 和 Word 文档等格式。
最后一个食谱将把它们全部结合起来,展示如何在目录中递归搜索不同类型的文件中的单词。
爬取和搜索目录
在本食谱中,我们将学习如何递归扫描目录以获取其中包含的所有文件。文件可以是特定类型的,也可以是所有类型的。
准备工作
让我们从创建一个带有一些文件信息的测试目录开始:
$ mkdir dir
$ touch dir/file1.txt
$ touch dir/file2.txt
$ mkdir dir/subdir
$ touch dir/subdir/file3.txt
$ touch dir/subdir/file4.txt
$ touch dir/subdir/file5.pdf
$ touch dir/file6.pdf
所有文件都将是空的;我们只会在本食谱中使用它们来发现它们。请注意,有四个文件的扩展名是.txt,另外两个文件的扩展名是.pdf。
这些文件也可以在 GitHub 存储库中找到:github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter04/documents/dir。
进入创建的dir目录:
$ cd dir
操作步骤...
- 打印
dir目录和子目录中的所有文件名:
>>> import os
>>> for root, dirs, files in os.walk('.'):
... for file in files:
... print(file)
...
file1.txt
file2.txt
file6.pdf
file3.txt
file4.txt
file5.pdf
- 打印文件的完整路径,与
root连接:
>>> for root, dirs, files in os.walk('.'):
... for file in files:
... full_file_path = os.path.join(root, file)
... print(full_file_path)
...
./dir/file1.txt
./dir/file2.txt
./dir/file6.pdf
./dir/subdir/file3.txt
./dir/subdir/file4.txt
./dir/subdir/file5.pdf
- 仅打印
.pdf文件:
>>> for root, dirs, files in os.walk('.'):
... for file in files:
... if file.endswith('.pdf'):
... full_file_path = os.path.join(root, file)
... print(full_file_path)
...
./dir/file6.pdf
./dir/subdir/file5.pdf
- 仅打印包含偶数的文件:
>>> import re
>>> for root, dirs, files in os.walk('.'):
... for file in files:
... if re.search(r'[13579]', file):
... full_file_path = os.path.join(root, file)
... print(full_file_path)
...
./dir/file1.txt
./dir/subdir/file3.txt
./dir/subdir/file5.pdf
工作原理...
os.walk()遍历整个目录和所有子目录,返回所有文件。它返回一个元组,其中包含特定目录、直接依赖的子目录和所有文件:
>>> for root, dirs, files in os.walk('.'):
... print(root, dirs, files)
...
. ['dir'] []
./dir ['subdir'] ['file1.txt', 'file2.txt', 'file6.pdf']
./dir/subdir [] ['file3.txt', 'file4.txt', 'file5.pdf']
os.path.join()函数允许我们清晰地连接两个路径,比如基本路径和文件。
由于文件以纯字符串形式返回,因此可以进行任何类型的过滤,就像第 3 步那样。在第 4 步中,可以使用正则表达式的全部功能进行过滤。
在下一个食谱中,我们将处理文件的内容,而不仅仅是文件名。
还有更多...
返回的文件不会以任何方式打开或修改。此操作是只读的。文件可以像平常一样打开,并且可以像下面的食谱中描述的那样进行操作。
请注意,在遍历目录时更改目录的结构可能会影响结果。如果需要在工作时存储任何文件,例如复制或移动文件时,通常最好将其存储在不同的目录中。
os.path模块还有其他有趣的函数。除了join()之外,最有用的可能是:
-
os.path.abspath(),返回文件的绝对路径 -
os.path.split(),用于在目录和文件之间拆分路径:
>>> os.path.split('/a/very/long/path/file.txt')
('/a/very/long/path', 'file.txt')
os.path.exists(),用于返回文件在文件系统上是否存在
有关os.path的完整文档可以在这里找到:docs.python.org/3/library/os.path.html。另一个模块pathlib可以用于以面向对象的方式进行更高级别的访问:docs.python.org/3/library/pathlib.html。
如第 4 步所示,可以使用多种过滤方式。在第一章中展示的所有字符串操作,让我们开始自动化之旅都可以使用。
另请参阅
-
第一章中的介绍正则表达式食谱,让我们开始自动化之旅
-
阅读文本文件食谱
阅读文本文件
在搜索特定文件之后,我们可能会打开并阅读它。文本文件非常简单,但非常强大。它们以纯文本形式存储数据,而不是复杂的二进制格式。
Python 本身提供了对文本文件的支持,并且很容易将其视为一系列行。
准备工作
我们将阅读包含 Tim Peters 的Python 之禅的zen_of_python.txt文件,这是一系列很好地描述了 Python 设计原则的格言。它在 GitHub 存储库中可用:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/documents/zen_of_python.txt:
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Python 之禅在 PEP-20 中描述:www.python.org/dev/peps/pep-0020/。
Python 之禅可以通过调用import this在任何 Python 解释器中显示。
如何做...
- 打开并打印整个文件,逐行(结果不显示):
>>> with open('zen_of_python.txt') as file:
... for line in file:
... print(line)
...
[RESULT NOT DISPLAYED]
- 打开文件并打印包含字符串
should的任何行:
>>> with open('zen_of_python.txt', 'r') as file:
... for line in file:
... if 'should' in line.lower():
... print(line)
...
Errors should never pass silently.
There should be one-- and preferably only one --obvious way to do it.
- 打开文件并打印包含单词
better的第一行:
>>> with open('zen_of_python.txt', 'rt') as file:
... for line in file:
... if 'better' in line.lower():
... print(line)
... break
...
Beautiful is better than ugly.
它是如何工作的...
要以文本模式打开文件,请使用open()函数。这将返回一个file对象,然后可以迭代返回每一行,如如何做...部分的步骤 1 所示。
with上下文管理器是处理文件的非常方便的方式,因为它在完成使用后会关闭它们(离开块)。即使出现异常,它也会这样做。
步骤 2 显示了如何迭代和过滤基于哪些行适用于我们的任务。这些行作为字符串返回,可以以多种方式进行过滤,如前面所述。
可能不需要读取整个文件,如步骤 3 所示。因为逐行迭代文件将在读取文件时进行,您可以随时停止,避免读取文件的其余部分。对于像我们的示例这样的小文件来说,这并不是很重要,但对于长文件来说,这可以减少内存使用和时间。
还有更多...
with上下文管理器是处理文件的首选方式,但不是唯一的方式。您也可以像这样手动打开和关闭它们:
>>> file = open('zen_of_python')
>>> content = file.read()
>>> file.close()
请注意.close()方法,以确保文件已关闭并释放与打开文件相关的资源。.read()方法一次读取整个文件,而不是逐行读取。
.read()方法还接受以字节为单位的大小参数,限制读取的数据大小。例如,file.read(1024)将返回最多 1KB 的信息。下一次调用.read()将从那一点继续。
文件以特定模式打开。模式定义了读/写以及文本或二进制数据的组合。默认情况下,文件以只读和文本模式打开,描述为'r'(步骤 2)或'rt'(步骤 3)。
其他配方将探讨更多模式。
另请参阅
-
爬取和搜索目录配方
-
处理编码配方
处理编码
文本文件可以以不同的编码形式存在。近年来,情况有了很大改善,但在处理不同系统时仍然存在兼容性问题。
文件中的原始数据和 Python 中的字符串对象之间存在差异。字符串对象已从文件包含的任何编码转换为本机字符串。一旦以这种格式存在,可能需要以不同的编码进行存储。默认情况下,Python 使用操作系统定义的编码,在现代操作系统中为 UTF-8。这是一种高度兼容的编码,但您可能需要以不同的编码保存文件。
准备工作
我们在 GitHub 存储库中准备了两个文件,这两个文件以两种不同的编码存储字符串20£。一个是通常的 UTF8,另一个是 ISO 8859-1,另一种常见的编码。这些文件可以在 GitHub 的Chapter04/documents目录下找到,文件名分别是example_iso.txt和example_utf8.txt:
github.com/PacktPublishing/Python-Automation-Cookbook
我们将使用 Beautiful Soup 模块,该模块在第三章中的解析 HTML食谱中介绍,构建您的第一个网络爬虫应用程序。
操作步骤...
- 打开
example_utf8.txt文件并显示其内容:
>>> with open('example_utf8.txt') as file:
... print(file.read())
...
20£
- 尝试打开
example_iso.txt文件,这将引发异常:
>>> with open('example_iso.txt') as file:
... print(file.read())
...
Traceback (most recent call last):
...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa3 in position 2: invalid start byte
- 以正确的编码打开
example_iso.txt文件:
>>> with open('example_iso.txt',
encoding='iso-8859-1') as file:
... print(file.read())
...
20£
- 打开
utf8文件并将其内容保存在iso-8859-1文件中:
>>> with open('example_utf8.txt') as file:
... content = file.read()
>>> with open('example_output_iso.txt', 'w',
encoding='iso-8859-1') as file:
... file.write(content)
...
4
- 最后,从新文件中以正确的格式读取,以确保它已正确保存:
>>> with open('example_output_iso.txt',
encoding='iso-8859-1') as file:
... print(file.read())
...
20£
工作原理...
操作步骤...部分的步骤 1 和 2 非常简单。在第 3 步中,我们添加了一个额外的参数encoding,以指定文件需要以与 UTF-8 不同的方式打开。
Python 可以直接接受很多标准编码。在这里检查所有标准编码及其别名:docs.python.org/3/library/codecs.html#standard-encodings。
在第 4 步中,我们创建一个新的 ISO-8859-1 文件,并像往常一样写入。注意'w'参数,它指定以文本模式打开文件进行写入。
步骤 5 是确认文件已正确保存。
还有更多...
这个食谱假设我们知道文件的编码。但有时我们不确定。Beautiful Soup,一个用于解析 HTML 的模块,可以尝试检测特定文件的编码。
自动检测文件的编码可能是不可能的,因为潜在的编码可能有无限多种。但我们将检查通常的编码,应该可以覆盖 90%的真实情况。只需记住,确切知道的最简单方法是询问创建文件的人。
为此,我们需要以'rb'参数以二进制格式打开文件进行读取,然后将二进制内容传递给 Beautiful Soup 的UnicodeDammit模块,如下所示:
>>> from bs4 import UnicodeDammit
>>> with open('example_output_iso.txt', 'rb') as file:
... content = file.read()
...
>>> suggestion = UnicodeDammit(content)
>>> suggestion.original_encoding
'iso-8859-1'
>>> suggestion.unicode_markup
'20£\n'
然后可以推断出编码。虽然.unicode_markup返回解码后的字符串,但最好只使用这个建议一次,然后以正确的编码打开文件进行自动化任务。
另请参阅
-
第一章中的操作字符串食谱,让我们开始我们的自动化之旅
-
第三章中的解析 HTML食谱,构建您的第一个网络爬虫应用程序
读取 CSV 文件
一些文本文件包含用逗号分隔的表格数据。这是一种方便的创建结构化数据的方式,而不是使用专有的、更复杂的格式,如 Excel 或其他格式。这些文件称为逗号分隔值,或CSV文件,大多数电子表格软件也可以导出到它。
准备工作
我们使用了这个页面描述的 10 部票房电影的数据制作了一个 CSV 文件:www.mrob.com/pub/film-video/topadj.html。
我们将表格的前十个元素复制到电子表格程序(Numbers)中,并将文件导出为 CSV。该文件可以在 GitHub 存储库的Chapter04/documents目录中找到,文件名为top_films.csv:

操作步骤...
- 导入
csv模块:
>>> import csv
- 打开文件,创建读取器,并迭代显示所有行的表格数据(仅显示了三行):
>>> with open('top_films.csv') as file:
... data = csv.reader(file)
... for row in data:
... print(row)
...
['Rank', 'Admissions\n(millions)', 'Title (year) (studio)', 'Director(s)']
['1', '225.7', 'Gone With the Wind (1939)\xa0(MGM)', 'Victor Fleming, George Cukor, Sam Wood']
['2', '194.4', 'Star Wars (Ep. IV: A New Hope) (1977)\xa0(Fox)', 'George Lucas']
...
['10', '118.9', 'The Lion King (1994)\xa0(BV)', 'Roger Allers, Rob Minkoff']
- 打开文件并使用
DictReader来构造数据,包括标题:
>>> with open('top_films.csv') as file:
... data = csv.DictReader(file)
... structured_data = [row for row in data]
...
>>> structured_data[0]
OrderedDict([('Rank', '1'), ('Admissions\n(millions)', '225.7'), ('Title (year) (studio)', 'Gone With the Wind (1939)\xa0(MGM)'), ('Director(s)', 'Victor Fleming, George Cukor, Sam Wood')])
structured_data中的每个项目都是一个包含所有值的完整字典:
>>> structured_data[0].keys()
odict_keys(['Rank', 'Admissions\n(millions)', 'Title (year) (studio)', 'Director(s)'])
>>> structured_data[0]['Rank']
'1'
>>> structured_data[0]['Director(s)']
'Victor Fleming, George Cukor, Sam Wood'
工作原理...
请注意,需要读取文件,并且我们使用with上下文管理器。这确保了文件在块结束时关闭。
如如何做部分的第 2 步所示,csv.reader类允许我们通过将它们细分为列表来结构化返回的代码行,遵循表格数据的格式。请注意,所有值都被描述为字符串。csv.reader无法理解第一行是否是标题。
为了更结构化地读取文件,在第 3 步中我们使用csv.DictReader,它默认将第一行读取为一个标题,定义后面描述的字段,然后将每一行转换为包含这些字段的字典。
有时,就像在这种情况下一样,文件中描述的字段名称可能有点冗长。不要害怕将字典翻译成更易管理的字段名称。
还有更多...
由于 CSV 是一个非常宽泛的解释,数据可以以几种方式存储。这在csv模块中表示为方言。例如,值可以由逗号、分号或制表符分隔。可以通过调用csv.list_dialect来显示默认接受的方言列表。
默认情况下,方言将是 Excel,这是最常见的方言。即使其他电子表格也通常会使用它。
但是,方言也可以通过Sniffer类从文件本身推断出来。Sniffer类分析文件的样本(或整个文件)并返回一个dialect对象,以允许以正确的方式进行读取。
请注意,文件是没有换行符打开的,因此不要对其进行任何假设:
>>> with open('top_films.csv', newline='') as file:
... dialect = csv.Sniffer().sniff(file.read())
然后可以在打开读取器时使用方言。再次注意newline,因为方言将正确地拆分行:
>>> with open('top_films.csv', newline='') as file:
... reader = csv.reader(file, dialect)
... for row in reader:
... print(row)
完整的csv模块文档可以在这里找到:docs.python.org/3.6/library/csv.html。
另请参阅
-
处理编码配方
-
读取文本文件配方
读取日志文件
另一种常见的结构化文本文件格式是日志文件。日志文件由一行行的日志组成,每行都有特定格式的文本。通常,每个日志都会有发生时间,因此文件是事件的有序集合。
准备工作
可以从 GitHub 存储库获取包含五个销售日志的example_log.log文件:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/documents/example_logs.log。
格式如下:
[<Timestamp in iso format>] - SALE - PRODUCT: <product id> - PRICE: $<price of the sale>
我们将使用Chapter01/price_log.py文件来将每个日志处理为一个对象。
如何做...
- 导入
PriceLog:
>>> from price_log import PriceLog
- 打开日志文件并解析所有日志:
>>> with open('example_logs.log') as file:
... logs = [PriceLog.parse(log) for log in file]
...
>>> len(logs)
5
>>> logs[0]
<PriceLog (Delorean(datetime=datetime.datetime(2018, 6, 17, 22, 11, 50, 268396), timezone='UTC'), 1489, 9.99)>
- 确定所有销售的总收入:
>>> total = sum(log.price for log in logs)
>>> total
Decimal('47.82')
- 确定每个
product_id已售出多少个单位:
>>> from collections import Counter
>>> counter = Counter(log.product_id for log in logs)
>>> counter
Counter({1489: 2, 4508: 1, 8597: 1, 3086: 1})
- 过滤日志,找到所有销售产品 ID 为
1489的事件:
>>> logs = []
>>> with open('example_logs.log') as file:
... for log in file:
... plog = PriceLog.parse(log)
... if plog.product_id == 1489:
... logs.append(plog)
...
>> len(logs)
2
>>> logs[0].product_id, logs[0].timestamp
(1489, Delorean(datetime=datetime.datetime(2018, 6, 17, 22, 11, 50, 268396), timezone='UTC'))
>>> logs[1].product_id, logs[1].timestamp
(1489, Delorean(datetime=datetime.datetime(2018, 6, 17, 22, 11, 50, 268468), timezone='UTC'))
工作原理...
由于每个日志都是单独的一行,我们逐个打开文件并解析每个日志。解析代码在price_log.py中可用。查看它以获取更多细节。
在如何做部分的第 2 步中,我们打开文件并处理每一行,以创建包含所有已处理日志的日志列表。然后,我们可以进行聚合操作,就像下一步一样。
第 3 步显示了如何聚合所有值,例如对文件日志中出售的所有商品的价格进行求和,以获得总收入。
第 4 步使用 Counter 来确定文件日志中每个项目的数量。这将返回一个类似字典的对象,其中包含要计数的值以及它们出现的次数。
过滤也可以逐行进行,就像第 5 步中所示的那样。这类似于本章中其他配方中的过滤。
还有更多...
请记住,一旦获得所需的所有数据,就可以立即停止处理文件。如果文件非常大,通常情况下是日志文件的情况,这可能是一个很好的策略。
Counter 是一个快速计算列表的好工具。有关更多详细信息,请参阅 Python 文档:docs.python.org/2/library/collections.html#counter-objects。您可以通过调用以下方式获取有序项目:
>>> counter.most_common()
[(1489, 2), (4508, 1), (8597, 1), (3086, 1)]
另请参阅
-
第一章中的使用第三方工具—parse食谱,让我们开始自动化之旅
-
读取文本文件食谱
读取文件元数据
文件元数据是与特定文件相关的除数据本身之外的所有内容。这意味着参数,如文件的大小、创建日期或其权限。
浏览这些数据很重要,例如,要筛选早于某个日期的文件,或查找所有大于某个 KB 值的文件。在本食谱中,我们将看到如何在 Python 中访问文件元数据。
准备工作
我们将使用 GitHub 存储库中的zen_of_python.txt文件(github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/documents/zen_of_python.txt)。通过使用ls命令,您可以看到该文件有856字节,并且在此示例中,它是在 6 月 14 日创建的:
$ ls -lrt zen_of_python.txt
-rw-r--r--@ 1 jaime staff 856 14 Jun 21:22 zen_of_python.txt
在您的计算机上,日期可能会有所不同,这取决于您下载代码的时间。
如何做…
- 导入
os和datetime:
>>> import os
>>> from datetime import datetime
- 检索
zen_of_python.txt文件的统计信息:
>>> stats = os.stat(('zen_of_python.txt')
>>> stats
os.stat_result(st_mode=33188, st_ino=15822537, st_dev=16777224, st_nlink=1, st_uid=501, st_gid=20, st_size=856, st_atime=1529461935, st_mtime=1529007749, st_ctime=1529007757)
- 获取文件的大小,以字节为单位:
>>> stats.st_size
856
- 获取文件上次修改的时间:
>>> datetime.fromtimestamp(stats.st_mtime)
datetime.datetime(2018, 6, 14, 21, 22, 29)
- 获取文件上次访问的时间:
>>> datetime.fromtimestamp(stats.st_atime)
datetime.datetime(2018, 6, 20, 3, 32, 15)
它是如何工作的…
os.stats返回一个表示文件系统中存储的元数据的 stats 对象。元数据包括:
-
文件的大小,以字节为单位,如如何做…部分中的步骤 3 所示,使用
st_size -
文件内容上次修改的时间,如步骤 4 所示,使用
st_mtime -
文件上次读取(访问)的时间,如步骤 5 所示,使用
st_atime
时间以时间戳形式返回,因此在步骤 4 和 5 中,我们从时间戳创建一个datetime对象,以更好地访问数据。
所有这些值都可以用来过滤文件。
请注意,您无需使用open()打开文件以读取其元数据。检测文件是否在已知值之后已更改将比比较其内容更快,因此您可以利用这一点进行比较。
还有更多…
要逐个获取统计信息,还有os.path中可用的便利函数,其遵循模式get<value>:
>>> os.path.getsize('zen_of_python.txt')
856
>>> os.path.getmtime('zen_of_python.txt')
1529531584.0
>>> os.path.getatime('zen_of_python.txt')
1529531669.0
该值以 UNIX 时间戳格式指定(自 1970 年 1 月 1 日以来的秒数)。
请注意,调用这三个函数的速度将比调用os.stats和处理结果要慢。此外,返回的stats可以被检查以检测可用的值。
该食谱中描述的数值适用于所有文件系统,但还有更多可以使用的数值。
例如,要获取文件的创建日期,可以在 MacOS 中使用st_birthtime参数,或在 Windows 中使用st_mtime。
st_mtime始终可用,但其含义在不同系统之间会有所不同。在 Unix 系统中,当内容被修改时,它会发生变化,因此它不是一个可靠的创建时间。
os.stat将遵循符号链接。如果要获取符号链接的统计信息,请使用os.lstat()。
查看所有可用统计信息的完整文档:docs.python.org/3.6/library/os.html#os.stat_result。
另请参阅
-
读取文本文件食谱
-
读取图像食谱
读取图像
可能最常见的非文本数据是图像数据。图像有自己一套特定的元数据,可以读取以筛选值或执行其他操作。
主要挑战是处理多种格式和不同的元数据定义。在本示例中,我们将展示如何从 JPEG 和 PNG 中获取信息,以及相同的信息如何以不同的方式编码。
准备工作
处理 Python 中图像的最佳通用工具可能是 Pillow。该模块允许您轻松读取最常见格式的文件,并对其进行操作。Pillow 最初是PIL(Python Imaging Library)的一个分支,几年前成为停滞不前的模块。
我们还将使用xmltodict模块将一些 XML 数据转换为更方便的字典。将这两个模块添加到requirements.txt中,并重新安装到虚拟环境中:
$ echo "Pillow==5.1.0" >> requirements.txt
$ echo "xmltodict==0.11.0" >> requirements.txt
$ pip install -r requirements.txt
照片文件中的元数据信息是以EXIF(Exchangeable Image File)格式定义的。EXIF 是一种存储有关照片信息的标准,包括拍摄照片的相机、拍摄时间、GPS 位置、曝光、焦距、颜色信息等。
您可以在此处获取更多信息:www.slrphotographyguide.com/what-is-exif-metadata/。所有信息都是可选的,但几乎所有数字相机和处理软件都会存储一些数据。由于隐私问题,其中的部分信息,如确切位置,可以被禁用。
以下图像将用于此示例,并可在 GiHub 存储库中下载(github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter04/images):
-
photo-dublin-a1.jpg -
photo-dublin-a2.png -
photo-dublin-b.png
其中两张照片,photo-dublin-a1.jpg和photo-dublin-a2.png,是同一张照片,但第一张是原始照片,而第二张经过了轻微的颜色变化和裁剪。请注意,一张是 JPEG 格式,另一张是 PNG 格式。另一张照片,photo-dublin-b.png,是一张不同的照片。这两张照片是在都柏林用同一部手机相机拍摄的,分别在两天拍摄。
虽然 Pillow 可以直接理解 JPG 文件存储的 EXIF 信息,但 PNG 文件存储 XMP 信息,这是一种更通用的标准,可以包含 EXIF 信息。
可以在此处获取有关 XMP 的更多信息:www.adobe.com/devnet/xmp.html。在很大程度上,它定义了一个相对易于阅读的 XML 树结构。
更进一步复杂化的是,XMP 是 RDF 的一个子集,RDF 是一种描述信息编码方式的标准。
如果 EFIX、XMP 和 RDF 听起来令人困惑,那是因为它们确实如此。最终,它们只是用来存储我们感兴趣的值的名称。我们可以使用 Python 内省工具检查名称的具体信息,确切地查看数据的结构以及我们要查找的参数名称。
由于 GPS 信息以不同的格式存储,我们在 GitHub 存储库中包含了一个名为gps_conversion.py的文件,位于此处:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/gps_conversion.py。其中包括exif_to_decimal和rdf_to_decimal函数,它们将两种格式转换为十进制以进行比较。
如何做...
- 导入要在此示例中使用的模块和函数:
>>> from PIL import Image
>>> from PIL.ExifTags import TAGS, GPSTAGS
>>> import xmltodict
>>> from gps_conversion import exif_to_decimal, rdf_to_decimal
- 打开第一张照片:
>>> image1 = Image.open('photo-dublin-a1.jpg')
- 获取文件的宽度、高度和格式:
>>> image1.height
3024
>>> image1.width
4032
>>> image1.format
'JPEG'
- 检索图像的 EXIF 信息,并处理为方便的字典。显示相机、使用的镜头以及拍摄时间:
>>> exif_info_1 = {TAGS.get(tag, tag): value
for tag, value in image1._getexif().items()}
>>> exif_info_1['Model']
'iPhone X'
>>> exif_info_1['LensModel']
'iPhone X back dual camera 4mm f/1.8'
>>> exif_info_1['DateTimeOriginal']
'2018:04:21 12:07:55'
- 打开第二张图像并获取 XMP 信息:
>>> image2 = Image.open('photo-dublin-a2.png')
>>> image2.height
2630
>>> image2.width
3943
>>> image2.format
'PNG'
>>> xmp_info = xmltodict.parse(image2.info['XML:com.adobe.xmp'])
- 获取包含我们正在寻找的所有值的 RDF 描述字段。检索模型(TIFF 值)、镜头模型(EXIF 值)和创建日期(XMP 值)。检查这些值是否与第 4 步中的相同,即使文件不同:
>>> rdf_info_2 = xmp_info['x:xmpmeta']['rdf:RDF']['rdf:Description']
>>> rdf_info_2['tiff:Model']
'iPhone X'
>>> rdf_info_2['exifEX:LensModel']
'iPhone X back dual camera 4mm f/1.8'
>>> rdf_info_2['xmp:CreateDate']
'2018-04-21T12:07:55'
- 获取两张图片中的 GPS 信息,转换为等效格式,并检查它们是否相同。请注意,分辨率不同,但它们匹配到第四位小数:
>>> gps_info_1 = {GPSTAGS.get(tag, tag): value
for tag, value in exif_info_1['GPSInfo'].items()}
>>> exif_to_decimal(gps_info_1)
('N53.34690555555556', 'W6.247797222222222')
>>> rdf_to_decimal(rdf_info_2)
('N53.346905', 'W6.247796666666667')
- 打开第三张图片,获取创建日期和 GPS 信息,并检查它与另一张照片不匹配,尽管它很接近(第二和第三位小数不相同):
>>> image3 = Image.open('photo-dublin-b.png')
>>> xmp_info = xmltodict.parse(image3.info['XML:com.adobe.xmp'])
>>> rdf_info_3 = xmp_info['x:xmpmeta']['rdf:RDF']['rdf:Description']
>>> rdf_info_3['xmp:CreateDate']
'2018-03-08T18:16:57'
>>> rdf_to_decimal(rdf_info_3)
('N53.34984166666667', 'W6.260388333333333')
工作原理...
Pillow 能够解释大多数常见语言的文件,并将它们以 JPG 格式的图像打开,就像在如何做…部分的第 2 步中所示。
Image对象包含有关文件大小和格式的基本信息,并在第 3 步中显示。 info属性包含取决于格式的信息。
JPG 文件的 EXIF 元数据可以使用._getexif()方法进行解析,但随后需要正确翻译,因为它使用原始二进制定义。例如,数字 42,036 对应于LensModel属性。幸运的是,PIL.ExifTags模块中有所有标签的定义。我们在第 4 步中将字典翻译为可读标签,以获得更可读的字典。
第 5 步打开了 PNG 格式,其与大小相关的属性相同,但元数据存储在 XML/RDF 格式中,并且需要借助“xmltodict”进行解析。第 6 步展示了如何导航此元数据以提取与 JPG 格式中相同的信息。数据是相同的,因为这两个文件来自同一原始图片,即使图片不同。
xmltodict在尝试解析非 XML 格式的数据时会出现一些问题。请检查输入是否为有效的 XML。
第 7 步提取了两张图片的 GPS 信息,这些信息以不同的方式存储,并显示它们是相同的(尽管由于编码方式不同,精度也不同)。
第 8 步显示了不同照片的信息。
还有更多...
Pillow 还具有许多围绕修改图片的功能。很容易调整大小或对文件进行简单修改,例如旋转。您可以在这里找到完整的 Pillow 文档:pillow.readthedocs.io。
Pillow 允许对图像进行许多操作。不仅可以进行简单的操作,如调整大小或将一个格式转换为另一个格式,还可以进行诸如裁剪图像、应用颜色滤镜或生成动画 GIF 等操作。如果您对使用 Python 进行图像处理感兴趣,那么 Pillow 绝对值得一看。
食谱中的 GPS 坐标以 DMS(度,分,秒),DDM(度,十进制分钟)表示,并转换为 DD(十进制度)。您可以在这里找到有关不同 GPS 格式的更多信息:www.ubergizmo.com/how-to/read-gps-coordinates/。如果您感兴趣,还可以在那里找到如何搜索图片的确切位置。
阅读图像文件的更高级用法是尝试对其进行 OCR(光学字符识别)处理。这意味着自动检测图像中的文本并提取和处理它。开源模块tesseract允许您执行此操作,并且可以与 Python 和 Pillow 一起使用。
您需要在系统中安装tesseract(github.com/tesseract-ocr/tesseract/wiki),以及pytesseract Python 模块(使用pip install pytesseract)。您可以从 GitHub 存储库中下载一个带有清晰文本的文件,称为photo-text.jpg,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/images/photo-text.jpg。
>>> from PIL import Image
>>> import pytesseract
>>> pytesseract.image_to_string(Image.open('photo-text.jpg'))
'Automate!'
如果图像中的文本不太清晰,或者与图像混合在一起,或者使用了独特的字体,OCR 可能会很困难。在 GitHub 存储库中提供了photo-dublin-a-text.jpg文件的示例(可在github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/images/photo-dublin-a-text.jpg找到),其中包含图片上的文本:
>>> >>> pytesseract.image_to_string(Image.open('photo-dublin-a-text.jpg'))
'fl\n\nAutomat'
有关 Tesseract 的更多信息,请访问以下链接:github.com/tesseract-ocr/tesseract
github.com/madmaze/pytesseract
将文件正确导入 OCR 可能需要进行初始图像处理以获得更好的结果。图像处理超出了本书的目标范围,但您可以使用比 Pillow 更强大的 OpenCV。您可以处理一个文件,然后使用 Pillow 打开它:opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_tutorials.html。
另请参阅
-
阅读文本文件食谱
-
阅读文件元数据食谱
-
爬行和搜索目录食谱
阅读 PDF 文件
文档的常见格式是 PDF(便携式文档格式)。它起初是一种描述任何打印机文档的格式,因此 PDF 是一种确保文档将被打印为其显示的格式的格式,因此是保证一致性的绝佳方式。它已成为共享文档的强大标准,特别是只读文档。
做好准备
对于这个食谱,我们将使用PyPDF2模块。我们需要将其添加到我们的虚拟环境中:
>>> echo "PyPDF2==1.26.0" >> requirements.txt
>>> pip install -r requirements.txt
在 GitHub 目录Chapter03/documents中,我们准备了两个文档,document-1.pdf和document-2.pdf,供本食谱使用。请注意,它们主要包含 Lorem Ipsum 文本,这只是占位文本。
Lorem Ipsum 文本通常用于设计,以显示文本而无需在设计之前创建内容。在这里了解更多:loremipsum.io/。
它们都是相同的测试文档,但第二个只能使用密码打开。密码是automate。
如何做...
- 导入模块:
>>> from PyPDF2 import PdfFileReader
- 打开
document-1.pdf文件并创建一个 PDF 文档对象。请注意,文件需要一直处于打开状态以进行阅读:
>>> file = open('document-1.pdf', 'rb')
>>> document = PdfFileReader(file)
- 获取文档的页数,并检查它是否已加密:
>>> document.numPages
3
>>> document.isEncrypted
False
- 从文档信息中获取创建日期(2018 年 6 月 24 日 11:15:18),并发现它是使用 Mac 的
Quartz PDFContext创建的:
>>> document.documentInfo['/CreationDate']
"D:20180624111518Z00'00'"
>>> document.documentInfo['/Producer']
'Mac OS X 10.13.5 Quartz PDFContext'
- 获取第一页,并阅读其上的文本:
>>> document.pages[0].extractText()
'!A VERY IMPORTANT DOCUMENT \nBy James McCormac CEO Loose Seal Inc '
- 对第二页执行相同的操作(此处已编辑):
>>> document.pages[1].extractText()
'"!This is an example of a test document that is stored in PDF format. It contains some \nsentences to describe what it is and the it has lore ipsum text.\n!"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. ...$'
- 关闭文件并打开
document-2.pdf:
>>> file.close()
>>> file = open('document-2.pdf', 'rb')
>>> document = PdfFileReader(file)
- 检查文档是否已加密(需要密码),并在尝试访问其内容时引发错误:
>>> document.isEncrypted
True
>>> document.numPages
...
PyPDF2.utils.PdfReadError: File has not been decrypted
- 解密文件并访问其内容:
>>> document.decrypt('automate')
1
>>> document.numPages
3
>>> document.pages[0].extractText()
'!A VERY IMPORTANT DOCUMENT \nBy James McCormac CEO Loose Seal Inc '
- 关闭文件以进行清理:
>>> file.close()
它是如何工作的...
一旦文档打开,如如何做...部分的步骤 1 和 2 所示,document对象将提供对文档的访问。
最有趣的属性是页面数量,可在 .numPages 中找到,以及每个页面,可在 .pages 中找到,可以像列表一样访问。
其他可访问的数据存储在 .documentInfo 中,其中存储了有关创建者和创建时间的元数据。
.documentInfo 中的信息是可选的,有时不是最新的。这在很大程度上取决于用于生成 PDF 的工具。
每个 page 对象都可以通过调用 .extractText() 来获取其文本,这将返回页面中包含的所有文本,就像步骤 5 和 6 中所做的那样。这种方法尝试提取所有文本,但它也有一些限制。对于结构良好的文本,例如我们的示例,它运行得相当好,生成的文本可以被干净地处理。处理多列文本或位于奇怪位置的文本可能会使处理变得复杂。
请注意,PDF 文件需要在整个操作期间保持打开状态,而不是使用 with 上下文运算符。离开 with 块后,文件将被关闭。
步骤 8 和 9 展示了如何处理加密文件。您可以使用 .isEncrypted 检测文件是否已加密,然后使用 .decrypt 方法解密文件,提供密码。
还有更多...
PDF 是一种非常灵活的格式,因此它非常标准,但这也意味着它可能很难解析和处理。
虽然大多数 PDF 文件包含文本信息,但并不罕见它们包含图像。例如,扫描文档经常会出现这种情况。这意味着信息被存储为图像的集合,而不是文本。这使得提取数据变得困难;我们最终不得不采用诸如 OCR 这样的方法来将图像解析为文本。
PyPDF2 没有提供处理图像的良好接口。您可能需要将 PDF 转换为一组图像,然后对其进行处理。大多数 PDF 阅读器都可以做到这一点,或者您可以使用命令行工具,如 pdftooppm(linux.die.net/man/1/pdftoppm)或 QPDF(参见下文)。有关 OCR 的想法,请参阅 读取图像 配方。
某些加密文件的加密方式可能无法被 PyPDF2 理解。它会生成 NotImplementedError: only algorithm code 1 and 2 are supported。如果发生这种情况,您需要在外部解密 PDF 并在解密后打开它。您可以使用 QPDF 创建一个无需密码的副本,方法如下:
$ qpdf --decrypt --password=PASSWORD encrypted.pdf output-decrypted.pdf
完整的 QPDF 可在此处找到:qpdf.sourceforge.net/files/qpdf-manual.html。QPDF 也可以在大多数软件包管理器中找到。
QPDF 能够进行大量的转换和深入分析 PDF。还有一个名为 pikepdf 的 Python 模块的绑定(pikepdf.readthedocs.io/en/stable/)。这个模块比 PyPDF2 更难使用,对于文本提取来说也不那么直接,但如果需要其他操作,比如从 PDF 中提取图像,它可能会很有用。
另请参阅
-
读取文本文件 配方
-
爬取和搜索目录 配方
阅读 Word 文档
Word 文档(.docx)是另一种常见的存储文本的文档类型。它们通常是使用 Microsoft Office 生成的,但其他工具也会生成兼容的文件。它们可能是最常见的用于共享需要可编辑的文件的格式,但也常用于分发文档。
在本配方中,我们将看到如何从 Word 文档中提取文本信息。
准备工作
我们将使用 python-docx 模块来读取和处理 Word 文档:
>>> echo "python-docx==0.8.6" >> requirements.txt
>>> pip install -r requirements.txt
我们已经准备了一个测试文件,位于 GitHub 的 Chapter04/documents 目录中,名为 document-1.docx,我们将在本配方中使用它。请注意,该文档遵循了与配方 读取 PDF 文件 配方中的测试文档中描述的 Lorem Ipsun 模式相同。
如何做...
- 导入
python-docx:
>> import docx
- 打开
document-1.docx文件:
>>> doc = docx.Document('document-1.docx')
- 检查存储在
core_properties中的一些元数据属性:
>> doc.core_properties.title
'A very important document'
>>> doc.core_properties.keywords
'lorem ipsum'
>>> doc.core_properties.modified
datetime.datetime(2018, 6, 24, 15, 1, 7)
- 检查段落的数量:
>>> len(doc.paragraphs)
58
- 遍历段落以检测包含文本的段落。请注意,并非所有文本都在此处显示:
>>> for index, paragraph in enumerate(doc.paragraphs):
... if paragraph.text:
... print(index, paragraph.text)
...
30 A VERY IMPORTANT DOCUMENT
31 By James McCormac
32 CEO Loose Seal Inc
34
...
56 TITLE 2
57 ...
- 获取段落
30和31的文本,这对应于第一页的标题和副标题:
>>> doc.paragraphs[30].text
'A VERY IMPORTANT DOCUMENT'
>>> doc.paragraphs[31].text
'By James McCormac'
- 每个段落都有
runs,这些是具有不同属性的文本部分。检查第一个文本段落和run是否为粗体,第二个是否为斜体:
>>> doc.paragraphs[30].runs[0].italic
>>> doc.paragraphs[30].runs[0].bold
True
>>> doc.paragraphs[31].runs[0].bold
>>> doc.paragraphs[31].runs[0].italic
True
- 在这个文档中,大多数段落只有一个
run,但我们在第48段有一个不错的例子,其中包含不同的运行。显示其文本和不同的样式。例如,单词Word是粗体,ipsum是斜体:
>>> [run.text for run in doc.paragraphs[48].runs]
['This is an example of a test document that is stored in ', 'Word', ' format', '. It contains some ', 'sentences', ' to describe what it is and it has ', 'lore', 'm', ' ipsum', ' text.']
>>> run1 = doc.paragraphs[48].runs[1]
>>> run1.text
'Word'
>>> run1.bold
True
>>> run2 = doc.paragraphs[48].runs[8]
>>> run2.text
' ipsum'
>>> run2.italic
True
它是如何工作的…
Word 文档最重要的特点是数据是以段落而不是页面结构化的。字体大小、行大小和其他考虑因素可能导致页面数量发生变化。
大多数段落通常也是空的,或者只包含换行符、制表符或其他空白字符。检查段落是否为空并跳过它是一个好主意。
在如何做…部分,第 2 步打开文件,第 3 步显示如何访问核心属性。这些属性在 Word 中被定义为文档元数据,例如作者或创建日期。
这些信息需要谨慎对待,因为许多生成 Word 文档的工具(但不包括 Microsoft Office)不一定会填充它。在使用该信息之前,请再次检查。
文档的段落可以被迭代,并以原始格式提取其文本,如第 6 步所示。这是不包括样式信息的信息,通常对于自动处理数据来说是最有用的。
如果需要样式信息,可以使用运行,如第 7 和第 8 步。每个段落可以包含一个或多个运行,这些运行是共享相同样式的较小单位。例如,如果一个句子是Word1 word2 word3,将有三个运行,一个是斜体文本(Word1),另一个是下划线(word2),另一个是粗体(word3)。更甚者,可能会有包含空格的常规文本的中间运行,总共有 5 个运行。
样式可以通过属性进行单独检测,例如粗体、斜体或下划线。
运行的划分可能相当复杂。由于编辑器的工作方式,半词是很常见的,一个单词分成两个运行,有时具有相同的属性。不要依赖于运行的数量并分析内容。特别是在试图确保具有特定样式的部分是否分成两个或更多个运行时,请再次检查。一个很好的例子是第 8 步中的单词lore m(应该是lorem)。
请注意,由于 Word 文档由许多来源生成,许多属性可能未设置,因此需要工具决定使用哪些具体属性。例如,保留默认字体非常常见,这可能意味着字体信息为空。
还有更多...
可以在字体属性下找到更多样式信息,例如small_caps或大小:
>>> run2.font.cs_italic
True
>>> run2.font.size
152400
>>> run2.font.small_caps
通常专注于原始文本,而不关注样式信息是正确的解析。但有时段落中的粗体单词会有特殊意义。它可能是标题或您正在寻找的结果。因为它被突出显示,很可能就是您要找的!在分析文档时请记住这一点。
你可以在这里找到整个python-docx文档:python-docx.readthedocs.io/en/latest/。
另请参阅
-
阅读文本文件配方
-
阅读 PDF 文件配方
扫描文档以查找关键字
在这个配方中,我们将汇总前几个配方的所有课程,并在目录中搜索特定关键字的文件。这是本章其余配方的总结,包括一个搜索不同类型文件的脚本。
准备就绪
确保在requirements.txt文件中包含以下所有模块,并将它们安装到您的虚拟环境中:
beautifulsoup4==4.6.0
Pillow==5.1.0
PyPDF2==1.26.0
python-docx==0.8.6
检查要搜索的目录是否有以下文件(所有文件都在 GitHub 的Chapter04/documents目录中可用)。请注意,file5.pdf和file6.pdf是document-1.pdf的副本,以简化。file1.txt到file4.txt是空文件:
├── dir
│ ├── file1.txt
│ ├── file2.txt
│ ├── file6.pdf
│ └── subdir
│ ├── file3.txt
│ ├── file4.txt
│ └── file5.pdf
├── document-1.docx
├── document-1.pdf
├── document-2-1.pdf
├── document-2.pdf
├── example_iso.txt
├── example_output_iso.txt
├── example_utf8.txt
├── top_films.csv
└── zen_of_python.txt
我们准备了一个名为scan.py的脚本,它将在所有.txt、.csv、.pdf和.docx文件中搜索一个单词。该脚本可在 GitHub 存储库的Chapter04目录中找到。
如何做...
- 有关如何使用
scan.py脚本,请参考帮助-h:
$ python scan.py -h
usage: scan.py [-h] [-w W]
optional arguments:
-h, --help show this help message and exit
-w W Word to search
- 搜索单词
the,它出现在大多数文件中:
$ python scan.py -w the
>>> Word found in ./document-1.pdf
>>> Word found in ./top_films.csv
>>> Word found in ./zen_of_python.txt
>>> Word found in ./dir/file6.pdf
>>> Word found in ./dir/subdir/file5.pdf
- 搜索单词
lorem,只出现在 PDF 和 docx 文件中:
$ python scan.py -w lorem
>>> Word found in ./document-1.docx
>>> Word found in ./document-1.pdf
>>> Word found in ./dir/file6.pdf
>>> Word found in ./dir/subdir/file5.pdf
- 搜索单词
20£,只出现在两个 ISO 文件中,使用不同的编码:
$ python scan.py -w 20£
>>> Word found in ./example_iso.txt
>>> Word found in ./example_output_iso.txt
- 搜索是不区分大小写的。搜索单词
BETTER,只出现在zen_of_python.txt文件中:
$ python scan.py -w BETTER
>>> Word found in ./zen_of_python.txt
它是如何工作的...
文件scan.py包含以下元素:
-
解析输入参数并为命令行创建帮助的入口点。
-
一个主要函数遍历目录并分析找到的每个文件。根据它们的扩展名,它决定是否有可用的函数来处理和搜索它。
-
一个
EXTENSION字典,将扩展名与搜索它们的函数配对。 -
search_txt,search_csv,search_pdf和search_docx函数,用于处理和搜索每种文件所需的单词。
比较不区分大小写,因此搜索词转换为小写,在所有比较中,文本都转换为小写。
每个搜索函数都有自己的特点:
-
search_txt首先打开文件以确定其编码,使用UnicodeDammit,然后逐行打开文件并读取。如果找到该单词,它会立即停止并返回成功。 -
search_csv以 CSV 格式打开文件,并不仅逐行迭代,还逐列迭代。一旦找到该单词,它就会返回。 -
search_pdf打开文件,如果文件被加密,则退出。如果没有加密,它会逐页提取文本并与单词进行比较。一旦找到匹配项,它就会立即返回。 -
search_docx打开文件并遍历其所有段落以进行匹配。一旦找到匹配项,函数就会返回。
还有更多...
还有一些额外的想法可以实现:
-
可以添加更多的搜索函数。在本章中,我们浏览了日志文件和图像。
-
类似的结构也可以用于搜索文件并仅返回最后 10 个。
-
search_csv没有嗅探以检测方言。这也可以添加。 -
阅读是相当顺序的。应该可以并行读取文件,分析它们以获得更快的返回,但要注意,并行读取文件可能会导致排序问题,因为文件不总是以相同的顺序处理。
另请参阅
-
爬行和搜索目录的配方
-
阅读文本文件的配方
-
处理编码的配方
-
阅读 CSV 文件的配方
-
阅读 PDF 文件的配方
-
阅读 Word 文档的配方
第五章:生成精彩的报告
在本章中,我们将涵盖以下配方:
-
在纯文本中创建简单报告
-
使用模板生成报告
-
在 Markdown 中格式化文本
-
编写基本的 Word 文档
-
为 Word 文档设置样式
-
在 Word 文档中生成结构
-
向 Word 文档添加图片
-
编写简单的 PDF 文档
-
构建 PDF
-
聚合 PDF 报告
-
给 PDF 加水印和加密
介绍
在本章中,我们将看到如何编写文档并执行基本操作,如处理不同格式的模板,如纯文本和 Markdown。我们将花费大部分时间处理常见且有用的格式,如 Word 和 PDF。
在纯文本中创建简单报告
最简单的报告是生成一些文本并将其存储在文件中。
准备工作
对于这个配方,我们将以文本格式生成简要报告。要存储的数据将在一个字典中。
如何操作...
- 导入
datetime:
>>> from datetime import datetime
- 使用文本格式创建报告模板:
>>> TEMPLATE = '''
Movies report
-------------
Date: {date}
Movies seen in the last 30 days: {num_movies}
Total minutes: {total_minutes}
'''
- 创建一个包含要存储的值的字典。请注意,这是将在报告中呈现的数据:
>>> data = {
'date': datetime.utcnow(),
'num_movies': 3,
'total_minutes': 376,
}
- 撰写报告,将数据添加到模板中:
>>> report = TEMPLATE.format(**data)
- 创建一个带有当前日期的新文件,并存储报告:
>>> FILENAME_TMPL = "{date}_report.txt"
>>> filename = FILENAME_TMPL.format(date=data['date'].strftime('%Y-%m-%d'))
>>> filename
2018-06-26_report.txt
>>> with open(filename, 'w') as file:
... file.write(report)
- 检查新创建的报告:
$ cat 2018-06-26_report.txt
Movies report
-------------
Date: 2018-06-26 23:40:08.737671
Movies seen in the last 30 days: 3
Total minutes: 376
工作原理...
如何操作...部分的第 2 步和第 3 步设置了一个简单的模板,并添加了包含报告中所有数据的字典。然后,在第 4 步,这两者被合并成一个特定的报告。
在第 4 步中,将字典与模板结合。请注意,字典中的键对应模板中的参数。诀窍是在format调用中使用双星号来解压字典,将每个键作为参数传递给format()。
在第 5 步中,生成的报告(一个字符串)存储在一个新创建的文件中,使用with上下文管理器。open()函数根据打开模式w创建一个新文件,并在块期间保持打开状态,该块将数据写入文件。退出块时,文件将被正确关闭。
打开模式确定如何打开文件,无论是读取还是写入,以及文件是文本还是二进制。w模式打开文件以进行写入,如果文件已存在,则覆盖它。小心不要错误删除现有文件!
第 6 步检查文件是否已使用正确的数据创建。
还有更多...
文件名使用今天的日期创建,以最小化覆盖值的可能性。日期的格式从年份开始,以天结束,已选择文件可以按正确顺序自然排序。
即使出现异常,with上下文管理器也会关闭文件。如果出现异常,它将引发IOError异常。
在写作中一些常见的异常可能是权限问题,硬盘已满,或路径问题(例如,尝试在不存在的目录中写入)。
请注意,文件可能在关闭或显式刷新之前未完全提交到磁盘。一般来说,处理文件时这不是问题,但如果尝试打开一个文件两次(一次用于读取,一次用于写入),则需要牢记这一点。
另请参阅
-
使用模板生成报告配方
-
在 Markdown 中格式化文本配方
-
聚合 PDF 报告配方
使用模板生成报告
HTML 是一种非常灵活的格式,可用于呈现丰富的报告。虽然可以将 HTML 模板视为纯文本创建,但也有工具可以让您更好地处理结构化文本。这也将模板与代码分离,将数据的生成与数据的表示分开。
准备工作
此配方中使用的工具 Jinja2 读取包含模板的文件,并将上下文应用于它。上下文包含要显示的数据。
我们应该从安装模块开始:
$ echo "jinja2==2.20" >> requirements.txt
$ pip install -r requirements.txt
Jinja2 使用自己的语法,这是 HTML 和 Python 的混合体。它旨在 HTML 文档,因此可以轻松执行操作,例如正确转义特殊字符。
在 GitHub 存储库中,我们已经包含了一个名为jinja_template.html的模板文件。
如何做...
- 导入 Jinja2
Template和datetime:
>>> from jinja2 import Template
>>> from datetime import datetime
- 从文件中读取模板到内存中:
>>> with open('jinja_template.html') as file:
... template = Template(file.read())
- 创建一个包含要显示数据的上下文:
>>> context = {
'date': datetime.now(),
'movies': ['Casablanca', 'The Sound of Music', 'Vertigo'],
'total_minutes': 404,
}
- 渲染模板并写入一个新文件
report.html,结果如下:
>>> with open('report.html', 'w') as file:
... file.write(template.render(context))
- 在浏览器中打开
report.html文件:

它是如何工作的...
如何做...部分中的步骤 2 和 4 非常简单:它们读取模板并保存生成的报告。
如步骤 3 和 4 所示,主要任务是创建一个包含要显示信息的上下文字典。然后模板呈现该信息,如步骤 5 所示。让我们来看看jinja_template.html:
<!DOCTYPE html>
<html lang="en">
<head>
<title> Movies Report</title>
</head>
<body>
<h1>Movies Report</h1>
<p>Date {{date}}</p>
<p>Movies seen in the last 30 days: {{movies|length}}</p>
<ol>
{% for movie in movies %}
<li>{{movie}}</li>
{% endfor %}
</ol>
<p>Total minutes: {{total_minutes}} </p>
</body>
</html>
大部分是替换上下文值,如{{total_minutes}}在花括号之间定义。
注意标签{% for ... %} / {% endfor %},它定义了一个循环。这允许基于 Python 的赋值生成多行或元素。
可以对变量应用过滤器进行修改。在这种情况下,将length过滤器应用于movies列表,以使用管道符号获得大小,如{{movies|length}}所示。
还有更多...
除了{% for %}标签之外,还有一个{% if %}标签,允许它有条件地显示:
{% if movies|length > 5 %}
Wow, so many movies this month!
{% else %}
Regular number of movies
{% endif %}
已经定义了许多过滤器(在此处查看完整列表:jinja.pocoo.org/docs/2.10/templates/#list-of-builtin-filters)。但也可以定义自定义过滤器。
请注意,您可以使用过滤器向模板添加大量处理和逻辑。虽然少量是可以的,但请尝试限制模板中的逻辑量。大部分用于显示数据的计算应该在之前完成,使上下文非常简单,并简化模板,从而允许进行更改。
处理 HTML 文件时,最好自动转义变量。这意味着具有特殊含义的字符,例如<字符,将被替换为等效的 HTML 代码,以便在 HTML 页面上正确显示。为此,使用autoescape参数创建模板。在这里检查差异:
>>> Template('{{variable}}', autoescape=False).render({'variable': '<'})
'<'
>>> Template('{{variable}}', autoescape=True).render({'variable': '<'})
'<'
可以对每个变量应用转义,使用e过滤器(表示转义),并使用safe过滤器取消应用(表示可以安全地渲染)。
Jinja2 模板是可扩展的,这意味着可以创建一个base_template.html,然后扩展它,更改一些元素。还可以包含其他文件,对不同部分进行分区和分离。有关更多详细信息,请参阅完整文档。
Jinja2 非常强大,可以让我们创建复杂的 HTML 模板,还可以在其他格式(如 LaTeX 或 JavaScript)中使用,尽管这需要配置。我鼓励您阅读整个文档,并查看其所有功能!
完整的 Jinja2 文档可以在这里找到:jinja.pocoo.org/docs/2.10/.
另请参阅
-
在纯文本中创建简单报告配方
-
在 Markdown 中格式化文本配方
在 Markdown 中格式化文本
Markdown是一种非常流行的标记语言,用于创建可以转换为样式化 HTML 的原始文本。这是一种良好的方式,可以以原始文本格式对文档进行结构化,同时能够在 HTML 中正确地对其进行样式设置。
在这个配方中,我们将看到如何使用 Python 将 Markdown 文档转换为样式化的 HTML。
准备工作
我们应该首先安装mistune模块,它将 Markdown 文档编译为 HTML:
$ echo "mistune==0.8.3" >> requirements.txt
$ pip install -r requirements.txt
在 GitHub 存储库中,有一个名为markdown_template.md的模板文件,其中包含要生成的报告的模板。
如何做到这一点...
- 导入
mistune和datetime:
>>> import mistune
- 从文件中读取模板:
>>> with open('markdown_template.md') as file:
... template = file.read()
- 设置要包含在报告中的数据的上下文:
context = {
'date': datetime.now(),
'pmovies': ['Casablanca', 'The Sound of Music', 'Vertigo'],
'total_minutes': 404,
}
- 由于电影需要显示为项目符号,我们将列表转换为适当的 Markdown 项目符号列表。同时,我们存储了电影的数量:
>>> context['num_movies'] = len(context['pmovies'])
>>> context['movies'] = '\n'.join('* {}'.format(movie) for movie in context['pmovies'])
- 渲染模板并将生成的 Markdown 编译为 HTML:
>>> md_report = template.format(**context)
>>> report = mistune.markdown(md_report)
- 最后,将生成的报告存储在
report.html文件中:
>>> with open('report.html', 'w') as file:
... file.write(report)
- 在浏览器中打开
report.html文件以检查结果:

它是如何工作的...
如何做...部分的第 2 步和第 3 步准备模板和要显示的数据。在第 4 步中,产生了额外的信息——电影的数量,这是从movies元素派生出来的。然后,将movies元素从 Python 列表转换为有效的 Markdown 元素。注意新行和初始的*,它将被呈现为一个项目符号:
>>> '\n'.join('* {}'.format(movie) for movie in context['pmovies'])
'* Casablanca\n* The Sound of Music\n* Vertigo'
在第 5 步中,模板以 Markdown 格式生成。这种原始形式非常易读,这是 Markdown 的优点:
Movies Report
=======
Date: 2018-06-29 20:47:18.930655
Movies seen in the last 30 days: 3
* Casablanca
* The Sound of Music
* Vertigo
Total minutes: 404
然后,使用mistune,报告被转换为 HTML 并在第 6 步中存储在文件中。
还有更多...
学习 Markdown 非常有用,因为它被许多常见的网页支持,可以作为一种启用文本输入并能够呈现为样式化格式的方式。一些例子是 GitHub,Stack Overflow 和大多数博客平台。
实际上,Markdown 不止一种。这是因为官方定义有限或模糊,并且没有兴趣澄清或标准化它。这导致了几种略有不同的实现,如 GitHub Flavoured Markdown,MultiMarkdown 和 CommonMark。
Markdown 中的文本非常易读,但如果您需要交互式地查看它的外观,可以使用 Dillinger 在线编辑器在dillinger.io/上使用。
Mistune的完整文档在这里可用:mistune.readthedocs.io/en/latest/.
完整的 Markdown 语法可以在daringfireball.net/projects/markdown/syntax找到,并且有一个包含最常用元素的好的速查表在beegit.com/markdown-cheat-sheet.上。
另请参阅
-
在疼痛文本中创建简单报告食谱
-
使用报告模板食谱
撰写基本 Word 文档
Microsoft Office 是最常见的软件之一,尤其是 MS Word 几乎成为了文档的事实标准。使用自动化脚本可以生成docx文档,这将有助于以一种易于阅读的格式分发报告。
在这个食谱中,我们将学习如何生成一个完整的 Word 文档。
准备工作
我们将使用python-docx模块处理 Word 文档:
>>> echo "python-docx==0.8.6" >> requirements.txt
>>> pip install -r requirements.txt
如何做到这一点...
- 导入
python-docx和datetime:
>>> import docx
>>> from datetime import datetime
- 定义要存储在报告中的数据的
context:
context = {
'date': datetime.now(),
'movies': ['Casablanca', 'The Sound of Music', 'Vertigo'],
'total_minutes': 404,
}
- 创建一个新的
docx文档,并包括一个标题,电影报告:
>>> document = docx.Document()
>>> document.add_heading('Movies Report', 0)
- 添加一个描述日期的段落,并在其中使用斜体显示日期:
>>> paragraph = document.add_paragraph('Date: ')
>>> paragraph.add_run(str(context['date'])).italic = True
- 添加有关已观看电影数量的信息到不同的段落中:
>>> paragraph = document.add_paragraph('Movies see in the last 30 days: ')
>>> paragraph.add_run(str(len(context['movies']))).italic = True
- 将每部电影添加为一个项目符号:
>>> for movie in context['movies']:
... document.add_paragraph(movie, style='List Bullet')
- 添加总分钟数并将文件保存如下:
>>> paragraph = document.add_paragraph('Total minutes: ')
>>> paragraph.add_run(str(context['total_minutes'])).italic = True
>>> document.save('word-report.docx')
- 打开
word-report.docx文件进行检查:

它是如何工作的...
Word 文档的基础是它被分成段落,每个段落又被分成运行。运行是一个段落的一部分,它共享相同的样式。
如何做...部分的第 1 步和第 2 步是导入和定义要存储在报告中的数据的准备工作。
在第 3 步中,创建了文档并添加了一个具有适当标题的标题。这会自动为文本设置样式。
处理段落是在第 4 步中介绍的。基于引入的文本创建了一个新段落,默认样式,但可以添加新的运行来更改它。在这里,我们添加了第一个带有文本“日期:”的运行,然后添加了另一个带有特定时间并标记为斜体的运行。
在第 5 步和第 6 步中,我们看到了有关电影的信息。第一部分以与第 4 步类似的方式存储了电影的数量。之后,电影逐个添加到报告中,并设置为项目符号的样式。
最后,第 7 步以与第 4 步类似的方式存储了所有电影的总运行时间,并将文档存储在文件中。
还有更多...
如果需要在文档中引入额外的行以进行格式设置,请添加空段落。
由于 MS Word 格式的工作方式,很难确定将有多少页。您可能需要对大小进行一些测试,特别是如果您正在动态生成文本。
即使生成了docx文件,也不需要安装 MS Office。还有其他应用程序可以打开和处理这些文件,包括免费的替代品,如 LibreOffice。
整个python-docx文档可以在这里找到:python-docx.readthedocs.io/en/latest/.
另请参阅
-
为 Word 文档设置样式的方法
-
在 Word 文档中生成结构的方法
为 Word 文档设置样式
Word 文档可能非常简单,但我们也可以添加样式以帮助正确理解显示的数据。Word 具有一组预定义的样式,可用于变化文档并突出显示其中的重要部分。
准备工作
我们将使用python-docx模块处理 Word 文档:
>>> echo "python-docx==0.8.6" >> requirements.txt
>>> pip install -r requirements.txt
如何操作...
- 导入
python-docx模块:
>>> import docx
- 创建一个新文档:
>>> document = docx.Document()
- 添加一个突出显示某些单词的段落,斜体,粗体和下划线:
>>> p = document.add_paragraph('This shows different kinds of emphasis: ')
>>> p.add_run('bold').bold = True
>>> p.add_run(', ')
<docx.text.run.Run object at ...>
>>> p.add_run('italics').italic = True
>>> p.add_run(' and ')
<docx.text.run.Run object at ...>
>>> p.add_run('underline').underline = True
>>> p.add_run('.')
<docx.text.run.Run object at ...>
- 创建一些段落,使用默认样式进行样式设置,如
List Bullet、List Number或Quote:
>>> document.add_paragraph('a few', style='List Bullet')
<docx.text.paragraph.Paragraph object at ...>
>>> document.add_paragraph('bullet', style='List Bullet')
<docx.text.paragraph.Paragraph object at ...>
>>> document.add_paragraph('points', style='List Bullet')
<docx.text.paragraph.Paragraph object at ...>
>>>
>>> document.add_paragraph('Or numbered', style='List Number')
<docx.text.paragraph.Paragraph object at ...>
>>> document.add_paragraph('that will', style='List Number')
<docx.text.paragraph.Paragraph object at ...>
>>> document.add_paragraph('that keep', style='List Number')
<docx.text.paragraph.Paragraph object at ...>
>>> document.add_paragraph('count', style='List Number')
<docx.text.paragraph.Paragraph object at ...>
>>>
>>> document.add_paragraph('And finish with a quote', style='Quote')
<docx.text.paragraph.Paragraph object at 0x10d2336d8>
- 创建一个不同字体和大小的段落。我们将使用
Arial字体和25号字体大小。段落将右对齐:
>>> from docx.shared import Pt
>>> from docx.enum.text import WD_ALIGN_PARAGRAPH
>>> p = document.add_paragraph('This paragraph will have a manual styling and right alignment')
>>> p.runs[0].font.name = 'Arial'
>>> p.runs[0].font.size = Pt(25)
>>> p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
- 保存文档:
>>> document.save('word-report-style.docx')
- 打开
word-report-style.docx文档以验证其内容:

它是如何工作的...
在第 1 步创建文档后,如何操作...部分的第 2 步添加了一个具有多个运行的段落。在 Word 中,一个段落可以包含多个运行,这些运行是可以具有不同样式的部分。一般来说,任何与单词相关的格式更改都将应用于运行,而影响段落的更改将应用于段落。
默认情况下,每个运行都使用Normal样式创建。任何.bold、.italic或.underline的属性都可以更改为True,以设置运行是否应以适当的样式或组合显示。值为False将停用它,而None值将保留为默认值。
请注意,此协议中的正确单词是italic,而不是italics。将属性设置为 italics 不会产生任何效果,但也不会显示错误。
第 4 步显示了如何应用一些默认样式以显示项目符号、编号列表和引用。还有更多样式,可以在文档的此页面中进行检查:python-docx.readthedocs.io/en/latest/user/styles-understanding.html?highlight=List%20Bullet#paragraph-styles-in-default-template。尝试找出哪些样式最适合您的文档。
运行的.font属性显示在第 5 步中。这允许您手动设置特定的字体和大小。请注意,需要使用适当的Pt(点)对象来指定大小。
段落的对齐是在paragraph对象中设置的,并使用常量来定义它是左对齐、右对齐、居中还是两端对齐。所有对齐选项都可以在这里找到:python-docx.readthedocs.io/en/latest/api/enum/WdAlignParagraph.html.
最后,第 7 步保存文件,使其存储在文件系统中。
还有更多...
font属性也可以用来设置文本的更多属性,比如小型大写字母、阴影、浮雕或删除线。所有可能性的范围都在这里显示:python-docx.readthedocs.io/en/latest/api/text.html#docx.text.run.Font.
另一个可用的选项是更改文本的颜色。注意,运行可以是先前生成的运行之一:
>>> from docx.shared import RGBColor
>>> DARK_BLUE = RGBColor.from_string('1b3866')
>>> run.font.color.rbg = DARK_BLUE
颜色可以用字符串的常规十六进制格式描述。尝试定义要使用的所有颜色,以确保它们都是一致的,并且在报告中最多使用三种颜色,以免过多。
您可以使用在线颜色选择器,比如这个:www.w3schools.com/colors/colors_picker.asp。记住不要在开头使用#。如果需要生成调色板,最好使用工具,比如coolors.co/来生成好的组合。
整个python-docx文档在这里可用:python-docx.readthedocs.io/en/latest/.
另请参阅
-
编写基本的 Word 文档配方
-
在 Word 文档中生成结构配方
在 Word 文档中生成结构
为了创建适当的专业报告,它们需要有适当的结构。MS Word 文档没有“页面”的概念,因为它是按段落工作的,但我们可以引入分页和部分来正确地划分文档。
在本配方中,我们将看到如何创建结构化的 Word 文档。
准备工作
我们将使用python-docx模块来处理 Word 文档:
>>> echo "python-docx==0.8.6" >> requirements.txt
>>> pip install -r requirements.txt
如何做...
- 导入
python-docx模块:
>>> import docx
- 创建一个新文档:
>>> document = docx.Document()
- 创建一个有换行的段落:
>>> p = document.add_paragraph('This is the start of the paragraph')
>>> run = p.add_run()
>>> run.add_break(docx.text.run.WD_BREAK.LINE)
>>> p.add_run('And now this in a different line')
>>> p.add_run(". Even if it's on the same paragraph.")
- 创建一个分页并写一个段落:
>>> document.add_page_break()
>>> document.add_paragraph('This appears in a new page')
- 创建一个新的部分,将位于横向页面上:
>>> section = document.add_section( docx.enum.section.WD_SECTION.NEW_PAGE)
>>> section.orientation = docx.enum.section.WD_ORIENT.LANDSCAPE
>>> section.page_height, section.page_width = section.page_width, section.page_height
>>> document.add_paragraph('This is part of a new landscape section')
- 创建另一个部分,恢复为纵向方向:
>>> section = document.add_section( docx.enum.section.WD_SECTION.NEW_PAGE)
>>> section.orientation = docx.enum.section.WD_ORIENT.PORTRAIT
>>> section.page_height, section.page_width = section.page_width, section.page_height
>>> document.add_paragraph('In this section, recover the portrait orientation')
- 保存文档:
>>> document.save('word-report-structure.docx')
- 检查结果,打开文档并检查生成的部分:

检查新页面:

检查横向部分:

然后,返回到纵向方向:

它是如何工作的...
在如何做...部分的第 2 步中创建文档后,我们为第一部分添加了一个段落。请注意,文档以一个部分开始。段落在段落中间引入了一个换行。
段落中的换行和新段落之间有一点差异,尽管对于大多数用途来说它们是相似的。尝试对它们进行实验。
第 3 步引入了分页符,但未更改部分。
第 4 步在新页面上创建一个新的部分。第 5 步还将页面方向更改为横向。在第 6 步,引入了一个新的部分,并且方向恢复为纵向。
请注意,当更改方向时,我们还需要交换宽度和高度。每个新部分都继承自上一个部分的属性,因此这种交换也需要在第 6 步中发生。
最后,在第 6 步保存文档。
还有更多...
一个部分规定了页面构成,包括页面的方向和大小。可以使用长度选项(如Inches或Cm)来更改页面的大小:
>>> from docx.shared import Inches, Cm
>>> section.page_height = Inches(10)
>>> section.page_width = Cm(20)
页面边距也可以用同样的方式定义:
>>> section.left_margin = Inches(1.5) >>> section.right_margin = Cm(2.81) >>> section.top_margin = Inches(1) >>> section.bottom_margin = Cm(2.54)
还可以强制节在下一页开始,而不仅仅是在下一页开始,这在双面打印时看起来更好:
>>> document.add_section( docx.enum.section.WD_SECTION.ODD_PAGE)
整个python-docx文档在这里可用:python-docx.readthedocs.io/en/latest/.
另请参阅
-
编写基本 Word 文档配方
-
对 Word 文档进行样式设置配方
向 Word 文档添加图片
Word 文档能够添加图像以显示图表或任何其他类型的额外信息。能够添加图像是创建丰富报告的好方法。
在这个配方中,我们将看到如何在 Word 文档中包含现有文件。
准备工作
我们将使用python-docx模块来处理 Word 文档:
$ echo "python-docx==0.8.6" >> requirements.txt
$ pip install -r requirements.txt
我们需要准备一个要包含在文档中的图像。我们将使用 GitHub 上的文件github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/images/photo-dublin-a1.jpg,显示了都柏林的景色。您可以通过命令行下载它,就像这样:
$ wget https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/images/photo-dublin-a1.jpg
如何做...
- 导入
python-docx模块:
>>> import docx
- 创建一个新文档:
>>> document = docx.Document()
- 创建一个带有一些文本的段落:
>>> document.add_paragraph('This is a document that includes a picture taken in Dublin')
- 添加图像:
>>> image = document.add_picture('photo-dublin-a1.jpg')
- 适当地缩放图像以适合页面(14 x 10):
>>> from docx.shared import Cm
>>> image.width = Cm(14)
>>> image.height = Cm(10)
- 图像已添加到新段落。将其居中并添加描述性文本:
>>> paragraph = document.paragraphs[-1]
>>> from docx.enum.text import WD_ALIGN_PARAGRAPH
>>> paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
>>> paragraph.add_run().add_break()
>>> paragraph.add_run('A picture of Dublin')
- 添加一个带有额外文本的新段落,并保存文档:
>>> document.add_paragraph('Keep adding text after the image')
<docx.text.paragraph.Paragraph object at XXX>
>>> document.save('report.docx')
- 检查结果:

它是如何工作的...
前几个步骤(如何做...部分的第 1 步到第 3 步)创建文档并添加一些文本。
第 4 步从文件中添加图像,第 5 步将其调整为可管理的大小。默认情况下,图像太大了。
调整图像大小时请注意图像的比例。请注意,您还可以使用其他度量单位,如Inch,也在shared中定义。
插入图像也会创建一个新段落,因此可以对段落进行样式设置,以使图像对齐或添加更多文本,例如参考或描述。通过document.paragraph属性在第 6 步获得段落。最后一个段落被获得并适当地样式化,使其居中。添加了一个新行和一个带有描述性文本的run。
第 7 步在图像后添加额外文本并保存文档。
还有更多...
图像的大小可以更改,但是如前所述,如果更改了图像的比例,需要计算图像的比例。如果通过近似值进行调整,调整大小可能不会完美,就像如何做...部分的第 5 步一样。
请注意,图像的比例不是完美的 10:14。它应该是 10:13.33。对于图像来说,这可能足够好,但对于更敏感于比例变化的数据,如图表,可能需要额外的注意。
为了获得适当的比例,将高度除以宽度,然后进行适当的缩放:
>>> image = document.add_picture('photo-dublin-a1.jpg')
>>> image.height / image.width
0.75
>>> RELATION = image.height / image.width
>>> image.width = Cm(12)
>>> image.height = Cm(12 * RELATION)
如果需要将值转换为特定大小,可以使用cm、inches、mm或pt属性:
>>> image.width.cm
12.0
>>> image.width.mm
120.0
>>> image.width.inches
4.724409448818897
>>> image.width.pt
340.15748031496065
整个python-docx文档在这里可用:python-docx.readthedocs.io/en/latest/.
另请参阅
-
编写基本 Word 文档配方
-
对 Word 文档进行样式设置配方
-
在 Word 文档中生成结构配方
编写简单的 PDF 文档
PDF 文件是共享报告的常用方式。PDF 文档的主要特点是它们确切地定义了文档的外观,并且在生成后是只读的,这使得它们非常容易共享。
在这个配方中,我们将看到如何使用 Python 编写一个简单的 PDF 报告。
准备工作
我们将使用fpdf模块来创建 PDF 文档:
>>> echo "fpdf==1.7.2" >> requirements.txt
>>> pip install -r requirements.txt
如何做...
- 导入
fpdf模块:
>>> import fpdf
- 创建文档:
>>> document = fpdf.FPDF()
- 为标题定义字体和颜色,并添加第一页:
>>> document.set_font('Times', 'B', 14)
>>> document.set_text_color(19, 83, 173)
>>> document.add_page()
- 写文档的标题:
>>> document.cell(0, 5, 'PDF test document')
>>> document.ln()
- 写一个长段落:
>>> document.set_font('Times', '', 12)
>>> document.set_text_color(0)
>>> document.multi_cell(0, 5, 'This is an example of a long paragraph. ' * 10)
[]
>>> document.ln()
- 写另一个长段落:
>>> document.multi_cell(0, 5, 'Another long paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit.' * 20)
- 保存文档:
>>> document.output('report.pdf')
- 检查
report.pdf文档:

它是如何工作的...
fpdf模块创建 PDF 文档并允许我们在其中写入。
由于 PDF 的特殊性,最好的思考方式是想象一个光标在文档中写字并移动到下一个位置,类似于打字机。
首先要做的操作是指定要使用的字体和大小,然后添加第一页。这是在步骤 3 中完成的。第一个字体是粗体(第二个参数为'B'),比文档的其余部分大,用作标题。颜色也使用.set_text_color设置为 RGB 组件。
文本也可以使用I斜体和U下划线。您可以将它们组合,因此BI将产生粗体和斜体的文本。
.cell调用创建具有指定文本的文本框。前面的几个参数是宽度和高度。宽度0使用整个空间直到右边距。高度5(mm)适用于大小12字体。对.ln的调用引入了一个新行。
要写多行段落,我们使用.multi_cell方法。它的参数与.cell相同。在步骤 5 和 6 中写入两个段落。请注意在报告的标题和正文之间的字体变化。.set_text_color使用单个参数调用以设置灰度颜色。在这种情况下,它是黑色。
对于长文本使用.cell会超出边距并超出页面。仅用于适合单行的文本。您可以使用.get_string_width找到字符串的大小。
在步骤 7 中将文档保存到磁盘。
还有更多...
如果multi_cell操作占据页面上的所有可用空间,则页面将自动添加。调用.add_page将移动到新页面。
您可以使用任何默认字体(Courier、Helvetica和Times),或使用.add_font添加额外的字体。查看更多详细信息,请参阅文档:pyfpdf.readthedocs.io/en/latest/reference/add_font/index.html.
字体Symbol和ZapfDingbats也可用,但用于符号。如果您需要一些额外的符号,这可能很有用,但在使用之前进行测试。其余默认字体应包括您对衬线、无衬线和等宽情况的需求。在 PDF 中,使用的字体将嵌入文档中,因此它们将正确显示。
保持整个文档中的高度一致,至少在相同大小的文本之间。定义一个您满意的常数,并在整个文本中使用它:
>>> BODY_TEXT_HEIGHT = 5
>>> document.multi_cell(0, BODY_TEXT_HEIGHT, text)
默认情况下,文本将被调整对齐,但可以更改。使用J(调整对齐)、C(居中)、R(右对齐)或L(左对齐)的对齐参数。例如,这将产生左对齐的文本:
>>> document.multi_cell(0, BODY_TEXT_HEIGHT, text, align='L')
完整的 FPDF 文档可以在这里找到:pyfpdf.readthedocs.io/en/latest/index.html.
另请参阅
-
构建 PDF
-
汇总 PDF 报告
-
给 PDF 加水印和加密
构建 PDF
在创建 PDF 时,某些元素可以自动生成,以使您的元素看起来更好并具有更好的结构。在本教程中,我们将看到如何添加页眉和页脚,以及如何创建到其他元素的链接。
准备工作
我们将使用fpdf模块创建 PDF 文档:
>>> echo "fpdf==1.7.2" >> requirements.txt
>>> pip install -r requirements.txt
操作步骤...
structuring_pdf.py脚本在 GitHub 上可用:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter05/structuring_pdf.py。最相关的部分显示如下:
import fpdf
from random import randint
class StructuredPDF(fpdf.FPDF):
LINE_HEIGHT = 5
def footer(self):
self.set_y(-15)
self.set_font('Times', 'I', 8)
page_number = 'Page {number}/{{nb}}'.format(number=self.page_no())
self.cell(0, self.LINE_HEIGHT, page_number, 0, 0, 'R')
def chapter(self, title, paragraphs):
self.add_page()
link = self.title_text(title)
page = self.page_no()
for paragraph in paragraphs:
self.multi_cell(0, self.LINE_HEIGHT, paragraph)
self.ln()
return link, page
def title_text(self, title):
self.set_font('Times', 'B', 15)
self.cell(0, self.LINE_HEIGHT, title)
self.set_font('Times', '', 12)
self.line(10, 17, 110, 17)
link = self.add_link()
self.set_link(link)
self.ln()
self.ln()
return link
def get_full_line(self, head, tail, fill):
...
def toc(self, links):
self.add_page()
self.title_text('Table of contents')
self.set_font('Times', 'I', 12)
for title, page, link in links:
line = self.get_full_line(title, page, '.')
self.cell(0, self.LINE_HEIGHT, line, link=link)
self.ln()
LOREM_IPSUM = ...
def main():
document = StructuredPDF()
document.alias_nb_pages()
links = []
num_chapters = randint(5, 40)
for index in range(1, num_chapters):
chapter_title = 'Chapter {}'.format(index)
num_paragraphs = randint(10, 15)
link, page = document.chapter(chapter_title,
[LOREM_IPSUM] * num_paragraphs)
links.append((chapter_title, page, link))
document.toc(links)
document.output('report.pdf')
- 运行脚本,它将生成
report.pdf文件,其中包含一些章节和目录。请注意,它会生成一些随机性,因此每次运行时具体数字会有所变化。
$ python3 structuring_pdf.py
- 检查结果。这是一个示例:

在结尾处检查目录:

它是如何工作的...
让我们来看看脚本的每个元素。
StructuredPDF定义了一个从FPDF继承的类。这对于覆盖footer方法很有用,它在创建页面时每次创建一个页脚。它还有助于简化main中的代码。
main函数创建文档。它启动文档,并添加每个章节,收集它们的链接信息。最后,它调用toc方法使用链接信息生成目录。
要存储的文本是通过乘以 LOREM_IPSUM 文本生成的,这是一个占位符。
chapter方法首先打印标题部分,然后添加每个定义的段落。它收集章节开始的页码和title_text方法返回的链接以返回它们。
title_text方法以更大、更粗的文本编写文本。然后,它添加一行来分隔标题和章节的正文。它生成并设置一个指向以下行中当前页面的link对象:
link = self.add_link()
self.set_link(link)
此链接将用于目录,以添加指向本章的可点击元素。
footer方法会自动向每个页面添加页脚。它设置一个较小的字体,并添加当前页面的文本(通过page_no获得),并使用{nb},它将被替换为总页数。
在main中调用alias_nb_pages确保在生成文档时替换{nb}。
最后,在toc方法中生成目录。它写入标题,并添加所有已收集的引用链接作为链接、页码和章节名称,这是所有所需的信息。
还有更多...
注意使用randint为文档添加一些随机性。这个调用在 Python 的标准库中可用,返回一个在定义的最大值和最小值之间的数字。两者都包括在内。
get_full_line方法为目录生成适当大小的行。它需要一个开始(章节的名称)和结束(页码),并添加填充字符(点)的数量,直到行具有适当的宽度(120 毫米)。
为了计算文本的大小,脚本调用get_string_width,它考虑了字体和大小。
链接对象可用于指向特定页面,而不是当前页面,并且也不是页面的开头;使用set_link(link, y=place, page=num_page)。在pyfpdf.readthedocs.io/en/latest/reference/set_link/index.html上查看文档。
调整一些元素可能需要一定程度的试错,例如,调整线的位置。稍微长一点或短一点的线可能是品味的问题。不要害怕尝试和检查,直到产生期望的效果。
完整的 FPDF 文档可以在这里找到:pyfpdf.readthedocs.io/en/latest/index.html.
另请参阅
-
编写简单的 PDF 文档食谱
-
聚合 PDF 报告食谱
-
给 PDF 加水印和加密食谱
聚合 PDF 报告
在这个食谱中,我们将看到如何将两个 PDF 合并成一个。这将允许我们将报告合并成一个更大的报告。
准备工作
我们将使用PyPDF2模块。Pillow和pdf2image也是脚本使用的依赖项:
$ echo "PyPDF2==1.26.0" >> requirements.txt
$ echo "pdf2image==0.1.14" >> requirements.txt
$ echo "Pillow==5.1.0" >> requirements.txt
$ pip install -r requirements.txt
为了使pdf2image正常工作,需要安装pdftoppm,因此请在此处查看如何在不同平台上安装它的说明:github.com/Belval/pdf2image#first-you-need-pdftoppm.
我们需要两个 PDF 文件来合并它们。对于这个示例,我们将使用两个 PDF 文件:一个是structuring_pdf.py脚本生成的report.pdf文件,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter05/structuring_pdf.py,另一个是经过水印处理后的(report2.pdf),命令如下:
$ python watermarking_pdf.py report.pdf -u automate_user -o report2.pdf
使用加水印脚本watermarking_pdf.py,在 GitHub 上可用,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter05/watermarking_pdf.py。
如何操作...
- 导入
PyPDF2并创建输出 PDF:
>>> import PyPDF2
>>> output_pdf = PyPDF2.PdfFileWriter()
- 读取第一个文件并创建一个阅读器:
>>> file1 = open('report.pdf', 'rb')
>>> pdf1 = PyPDF2.PdfFileReader(file1)
- 将所有页面附加到输出 PDF:
>>> output_pdf.appendPagesFromReader(pdf1)
- 打开第二个文件,创建一个阅读器,并将页面附加到输出 PDF:
>>> file2 = open('report2.pdf', 'rb')
>>> pdf2 = PyPDF2.PdfFileReader(file2)
>>> output_pdf.appendPagesFromReader(pdf2)
- 创建输出文件并保存:
>>> with open('result.pdf', 'wb') as out_file:
... output_pdf.write(out_file)
- 关闭打开的文件:
>>> file1.close()
>>> file2.close()
- 检查输出文件,并确认它包含两个 PDF 页面。
工作原理...
PyPDF2允许我们为每个输入文件创建一个阅读器,并将其所有页面添加到新创建的 PDF 写入器中。请注意,文件以二进制模式(rb)打开。
输入文件需要保持打开状态,直到保存结果。这是由于页面复制的方式。如果文件是打开的,则生成的文件可以存储为空文件。
PDF 写入器最终保存到一个新文件中。请注意,文件需要以二进制模式(wb)打开以进行写入。
还有更多...
.appendPagesFromReader非常方便,可以添加所有页面,但也可以使用.addPage逐个添加页面。例如,要添加第三页,代码如下:
>>> page = pdf1.getPage(3)
>>> output_pdf.addPage(page)
PyPDF2的完整文档在这里:pythonhosted.org/PyPDF2/.
另请参阅
-
编写简单的 PDF 文档示例
-
结构化 PDF示例
-
加水印和加密 PDF示例
加水印和加密 PDF
PDF 文件有一些有趣的安全措施,限制了文档的分发。我们可以加密内容,使其必须知道密码才能阅读。我们还将看到如何添加水印,以清楚地标记文档为不适合公开分发,并且如果泄漏,可以知道其来源。
准备工作
我们将使用pdf2image模块将 PDF 文档转换为 PIL 图像。Pillow是先决条件。我们还将使用PyPDF2:
$ echo "pdf2image==0.1.14" >> requirements.txt
$ echo "Pillow==5.1.0" >> requirements.txt
$ echo "PyPDF2==1.26.0" >> requirements.txt
$ pip install -r requirements.txt
为了使pdf2image正常工作,需要安装pdftoppm,因此请在此处查看如何在不同平台上安装它的说明:github.com/Belval/pdf2image#first-you-need-pdftoppm.
我们还需要一个 PDF 文件来加水印和加密。我们将使用 GitHub 上的structuring_pdf.py脚本生成的report.pdf文件,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/chapter5/structuring_pdf.py。
如何操作...
watermarking_pdf.py脚本在 GitHub 上可用,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter05/watermarking_pdf.py。这里显示了最相关的部分:
def encrypt(out_pdf, password):
output_pdf = PyPDF2.PdfFileWriter()
in_file = open(out_pdf, "rb")
input_pdf = PyPDF2.PdfFileReader(in_file)
output_pdf.appendPagesFromReader(input_pdf)
output_pdf.encrypt(password)
# Intermediate file
with open(INTERMEDIATE_ENCRYPT_FILE, "wb") as out_file:
output_pdf.write(out_file)
in_file.close()
# Rename the intermediate file
os.rename(INTERMEDIATE_ENCRYPT_FILE, out_pdf)
def create_watermark(watermarked_by):
mask = Image.new('L', WATERMARK_SIZE, 0)
draw = ImageDraw.Draw(mask)
font = ImageFont.load_default()
text = 'WATERMARKED BY {}\n{}'.format(watermarked_by, datetime.now())
draw.multiline_text((0, 100), text, 55, font=font)
watermark = Image.new('RGB', WATERMARK_SIZE)
watermark.putalpha(mask)
watermark = watermark.resize((1950, 1950))
watermark = watermark.rotate(45)
# Crop to only the watermark
bbox = watermark.getbbox()
watermark = watermark.crop(bbox)
return watermark
def apply_watermark(watermark, in_pdf, out_pdf):
# Transform from PDF to images
images = convert_from_path(in_pdf)
...
# Paste the watermark in each page
for image in images:
image.paste(watermark, position, watermark)
# Save the resulting PDF
images[0].save(out_pdf, save_all=True, append_images=images[1:])
- 使用以下命令给 PDF 文件加水印:
$ python watermarking_pdf.py report.pdf -u automate_user -o out.pdf
Creating a watermark
Watermarking the document
$
- 检查文档是否添加了
automate_user水印和时间戳到out.pdf的所有页面:

- 使用以下命令加水印和加密。请注意,加密可能需要一些时间:
$ python watermarking_pdf.py report.pdf -u automate_user -o out.pdf -p secretpassword
Creating a watermark
Watermarking the document
Encrypting the document
$
- 打开生成的
out.pdf文件,并检查是否需要输入secretpassword密码。时间戳也将是新的。
工作原理...
watermarking_pdf.py脚本首先使用argparse从命令行获取参数,然后将其传递给调用其他三个函数的main函数,create_watermark,apply_watermark和(如果使用密码)encrypt。
create_watermark生成带有水印的图像。它使用 Pillow 的Image类创建灰色图像(模式L)并绘制文本。然后,将此图像应用为新图像上的 Alpha 通道,使图像半透明,因此它将显示水印文本。
Alpha 通道使白色(颜色 0)完全透明,黑色(颜色 255)完全不透明。在这种情况下,背景是白色,文本的颜色是 55,使其半透明。
然后将图像旋转 45 度并裁剪以减少可能出现的透明背景。这将使图像居中并允许更好的定位。
在下一步中,apply_watermark使用pdf2image模块将 PDF 转换为 PILImages序列。它计算应用水印的位置,然后粘贴水印。
图像需要通过其左上角定位。这位于文档的一半,减去水印的一半,高度和宽度都是如此。请注意,脚本假定文档的所有页面都是相等的。
最后,结果保存为 PDF;请注意save_all参数,它允许我们保存多页 PDF。
如果传递了密码,则调用encrypt函数。它使用PdfFileReader打开输出 PDF,并使用PdfFileWriter创建一个新的中间 PDF。将输出 PDF 的所有页面添加到新 PDF 中,对 PDF 进行加密,然后使用os.rename将中间 PDF 重命名为输出 PDF。
还有更多...
作为水印的一部分,请注意页面是从文本转换为图像的。这增加了额外的保护,因为文本不会直接可提取,因为它存储为图像。在保护文件时,这是一个好主意,因为它将阻止直接复制/粘贴。
这不是一个巨大的安全措施,因为文本可能可以通过 OCR 工具提取。但是,它可以防止对文本的轻松提取。
PIL 的默认字体可能有点粗糙。如果有TrueType或OpenType文件可用,可以通过调用以下内容添加并使用另一种字体:
font = ImageFont.truetype('my_font.ttf', SIZE)
请注意,这可能需要安装FreeType库,通常作为libfreetype软件包的一部分提供。更多文档可在www.freetype.org/找到。根据字体和大小,您可能需要调整大小。
完整的pdf2image文档可以在github.com/Belval/pdf2image找到,PyPDF2的完整文档在pythonhosted.org/PyPDF2/,Pillow的完整文档可以在pillow.readthedocs.io/en/5.2.x/.找到。
另请参阅
-
编写简单的 PDF 文档配方
-
构建 PDF配方
-
聚合 PDF 报告配方
第六章:与电子表格一起玩
在本章中,我们将涵盖以下食谱:
-
编写 CSV 电子表格
-
更新 CSV 电子表格
-
读取 Excel 电子表格
-
更新 Excel 电子表格
-
在 Excel 电子表格中创建新工作表
-
在 Excel 中创建图表
-
在 Excel 中处理格式
-
在 LibreOffice 中读写
-
在 LibreOffice 中创建宏
介绍
电子表格是计算机世界中最通用和无处不在的工具之一。它们直观的表格和单元格的方法被几乎每个使用计算机作为日常操作的人所使用。甚至有一个笑话说整个复杂的业务都是在一个电子表格中管理和描述的。它们是一种非常强大的工具。
这使得自动从电子表格中读取和写入变得非常强大。在本章中,我们将看到如何处理电子表格,主要是在最常见的格式 Excel 中。最后一个食谱将涵盖一个免费的替代方案,Libre Office,特别是如何在其中使用 Python 作为脚本语言。
编写 CSV 电子表格
CSV 文件是简单的电子表格,易于共享。它们基本上是一个文本文件,其中包含用逗号分隔的表格数据(因此称为逗号分隔值),以简单的表格格式。CSV 文件可以使用 Python 的标准库创建,并且可以被大多数电子表格软件读取。
准备工作
对于这个食谱,只需要 Python 的标准库。一切都已经准备就绪!
如何做到这一点...
- 导入
csv模块:
>>> import csv
- 定义标题以及数据的存储方式:
>>> HEADER = ('Admissions', 'Name', 'Year')
>>> DATA = [
... (225.7, 'Gone With the Wind', 1939),
... (194.4, 'Star Wars', 1977),
... (161.0, 'ET: The Extra-Terrestrial', 1982)
... ]
- 将数据写入 CSV 文件:
>>> with open('movies.csv', 'w', newline='') as csvfile:
... movies = csv.writer(csvfile)
... movies.writerow(HEADER)
... for row in DATA:
... movies.writerow(row)
- 在电子表格中检查生成的 CSV 文件。在下面的屏幕截图中,使用 LibreOffice 软件显示文件:

工作原理...
在如何做部分的步骤 1 和 2 中进行准备工作后,步骤 3 是执行工作的部分。
它以写(w)模式打开一个名为movies.csv的新文件。然后在csvfile中创建一个原始文件对象。所有这些都发生在with块中,因此在结束时关闭文件。
注意newline=''参数。这是为了让writer直接存储换行,并避免兼容性问题。
写入器使用.writerow逐行写入元素。第一个是HEADER,然后是每行数据。
还有更多...
所呈现的代码将数据存储在默认方言中。方言定义了每行数据之间的分隔符(逗号或其他字符),如何转义,换行等。如果需要调整方言,可以在writer调用中定义这些参数。请参见以下链接,了解可以定义的所有参数列表:
docs.python.org/3/library/csv.html#dialects-and-formatting-parameters。
CSV 文件在简单时更好。如果要存储的数据很复杂,也许最好的选择不是 CSV 文件。但是在处理表格数据时,CSV 文件非常有用。它们几乎可以被所有程序理解,甚至在低级别处理它们也很容易。
完整的csv模块文档可以在这里找到:
docs.python.org/3/library/csv.html。
另请参阅
-
在第四章中的读取和搜索本地文件中的读取 CSV 文件食谱
-
更新 CSV 文件食谱
更新 CSV 文件
鉴于 CSV 文件是简单的文本文件,更新其内容的最佳解决方案是读取它们,将它们更改为内部 Python 对象,然后以相同的格式写入结果。在这个食谱中,我们将看到如何做到这一点。
准备工作
在这个配方中,我们将使用 GitHub 上的movies.csv文件。它包含以下数据:
| 招生 | 姓名 | 年份 |
|---|---|---|
| 225.7 | 乱世佳人 | 1939 年 |
| 194.4 | 星球大战 | 1968 年 |
| 161.0 | 外星人 | 1982 年 |
注意星球大战的年份是错误的(应为 1977 年)。我们将在配方中更改它。
如何做...
- 导入
csv模块并定义文件名:
>>> import csv
>>> FILENAME = 'movies.csv'
- 使用
DictReader读取文件的内容,并将其转换为有序行的列表:
>>> with open(FILENAME, newline='') as file:
... data = [row for row in csv.DictReader(file)]
- 检查获取的数据。将 1968 年的正确值更改为 1977 年:
>>> data
[OrderedDict([('Admissions', '225.7'), ('Name', 'Gone With the Wind'), ('Year', '1939')]), OrderedDict([('Admissions', '194.4'), ('Name', 'Star Wars'), ('Year', '1968')]), OrderedDict([('Admissions', '161.0'), ('Name', 'ET: The Extra-Terrestrial'), ('Year', '1982')])]
>>> data[1]['Year']
'1968'
>>> data[1]['Year'] = '1977'
- 再次打开文件,并存储值:
>>> HEADER = data[0].keys()
>>> with open(FILENAME, 'w', newline='') as file:
... writer = csv.DictWriter(file, fieldnames=HEADER)
... writer.writeheader()
... writer.writerows(data)
- 在电子表格软件中检查结果。结果与编写 CSV 电子表格配方中的第 4 步中显示的结果类似。
工作原理...
在如何做...部分的第 2 步中导入csv模块后,我们从文件中提取所有数据。文件在with块中打开。DictReader方便地将其转换为字典列表,其中键是标题值。
然后可以操纵和更改方便格式化的数据。我们在第 3 步中将数据更改为适当的值。
在这个配方中,我们直接更改值,但在更一般的情况下可能需要搜索。
第 4 步将覆盖文件,并使用DictWriter存储数据。DictWriter要求我们通过fieldnames在列上定义字段。为了获得它,我们检索一行的键并将它们存储在HEADER中。
文件再次以w模式打开以覆盖它。DictWriter首先使用.writeheader存储标题,然后使用单个调用.writerows存储所有行。
也可以通过调用.writerow逐个添加行
关闭with块后,文件将被存储并可以进行检查。
还有更多...
CSV 文件的方言通常是已知的,但也可能不是这种情况。在这种情况下,Sniffer类可以帮助。它分析文件的样本(或整个文件)并返回一个dialect对象,以允许以正确的方式进行读取:
>>> with open(FILENAME, newline='') as file:
... dialect = csv.Sniffer().sniff(file.read())
然后可以在打开文件时将方言传递给DictReader类。需要两次打开文件进行读取。
记得在DictWriter类上也使用方言以相同的格式保存文件。
csv模块的完整文档可以在这里找到:
docs.python.org/3.6/library/csv.html。
另请参阅
-
在第四章的读取 CSV 文件配方中
-
编写 CSV 电子表格配方
读取 Excel 电子表格
MS Office 可以说是最常见的办公套件软件,使其格式几乎成为标准。在电子表格方面,Excel 可能是最常用的格式,也是最容易交换的格式。
在这个配方中,我们将看到如何使用openpyxl模块从 Python 中以编程方式获取 Excel 电子表格中的信息。
准备工作
我们将使用openpyxl模块。我们应该安装该模块,并将其添加到我们的requirements.txt文件中,如下所示:
$ echo "openpyxl==2.5.4" >> requirements.txt
$ pip install -r requirements.txt
在 GitHub 存储库中,有一个名为movies.xlsx的 Excel 电子表格,其中包含前十部电影的出席信息。文件可以在此处找到:
github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter06/movies.xlsx。
信息来源是这个网页:
www.mrob.com/pub/film-video/topadj.html。
如何做...
- 导入
openpyxl模块:
>>> import openpyxl
- 将文件加载到内存中:
>>> xlsfile = openpyxl.load_workbook('movies.xlsx')
- 列出所有工作表并获取第一个工作表,这是唯一包含数据的工作表:
>>> xlsfile.sheetnames
['Sheet1']
>>> sheet = xlsfile['Sheet1']
- 获取单元格
B4和D4的值(入场和 E.T.的导演):
>>> sheet['B4'].value
161
>>> sheet['D4'].value
'Steven Spielberg'
- 获取行和列的大小。超出该范围的任何单元格将返回
None作为值:
>>> sheet.max_row
11
>>> sheet.max_column
4
>>> sheet['A12'].value
>>> sheet['E1'].value
它是如何工作的...
在第 1 步中导入模块后,如何做…部分的第 2 步将文件加载到Workbook对象的内存中。每个工作簿可以包含一个或多个包含单元格的工作表。
要确定可用的工作表,在第 3 步中,我们获取所有工作表(在此示例中只有一个),然后像字典一样访问工作表,以检索Worksheet对象。
然后,Worksheet可以通过它们的名称直接访问所有单元格,例如A4或C3。它们中的每一个都将返回一个Cell对象。.value属性存储单元格中的值。
在本章的其余配方中,我们将看到Cell对象的更多属性。继续阅读!
可以使用max_columns和max_rows获取存储数据的区域。这允许我们在数据的限制范围内进行搜索。
Excel 将列定义为字母(A、B、C 等),行定义为数字(1、2、3 等)。记住始终先设置列,然后设置行(D1,而不是1D),否则将引发错误。
可以访问区域外的单元格,但不会返回数据。它们可以用于写入新信息。
还有更多...
也可以使用sheet.cell(column, row)检索单元格。这两个元素都从 1 开始。
从工作表中迭代数据区域内的所有单元格,例如:
>>> for row in sheet:
... for cell in row:
... # Do stuff with cell
这将返回一个包含所有单元格的列表的列表,逐行:A1、A2、A3... B1、B2、B3 等。
您可以通过sheet.columns迭代来检索单元格的列:A1、B1、C1 等,A2、B2、C2 等。
在检索单元格时,可以使用.coordinate、.row和.column找到它们的位置:
>>> cell.coordinate
'D4'
>>> cell.column
'D'
>>> cell.row
4
完整的openpyxl文档可以在此处找到:
openpyxl.readthedocs.io/en/stable/index.html。
另请参阅
-
更新 Excel 电子表格配方
-
在 Excel 电子表格中创建新工作表配方
-
在 Excel 中创建图表配方
-
在 Excel 中处理格式配方
更新 Excel 电子表格
在这个配方中,我们将看到如何更新现有的 Excel 电子表格。这将包括更改单元格中的原始值,还将设置在打开电子表格时将被评估的公式。我们还将看到如何向单元格添加注释。
准备就绪
我们将使用模块openpyxl。我们应该安装该模块,并将其添加到我们的requirements.txt文件中,如下所示:
$ echo "openpyxl==2.5.4" >> requirements.txt
$ pip install -r requirements.txt
在 GitHub 存储库中,有一个名为movies.xlsx的 Excel 电子表格,其中包含前十部电影的观众人数信息。
文件可以在此处找到:
github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter06/movies.xlsx.
如何做…
- 导入模块
openpyxl和Comment类:
>>> import openpyxl
>>> from openpyxl.comments import Comment
- 将文件加载到内存中并获取工作表:
>>> xlsfile = openpyxl.load_workbook('movies.xlsx')
>>> sheet = xlsfile['Sheet1']
- 获取单元格
D4的值(E.T.的导演):
>>> sheet['D4'].value
'Steven Spielberg'
- 将值更改为
Spielberg:
>>> sheet['D4'].value = 'Spielberg'
- 向该单元格添加注释:
>>> sheet['D4'].comment = Comment('Changed text automatically', 'User')
- 添加一个新元素,获取
Admission列中所有值的总和:
>>> sheet['B12'] = '=SUM(B2:B11)'
- 将电子表格保存到
movies_comment.xlsx文件中:
>>> xlsfile.save('movies_comment.xlsx')
- 检查包含注释和在
A12中计算B列总和的结果文件:

它是如何工作的...
在如何做…部分,第 1 步中的导入和第 2 步中的读取电子表格,我们在第 3 步中选择要更改的单元格。
在第 4 步中进行值的更新。在单元格中添加注释,覆盖.coment属性并添加新的Comment。请注意,还需要添加进行注释的用户。
值也可以包括公式的描述。在第 6 步,我们向单元格B12添加一个新的公式。在第 8 步打开文件时,该值将被计算并显示。
公式的值不会在 Python 对象中计算。这意味着公式可能包含错误,或者通过错误显示意外结果。请务必仔细检查公式是否正确。
最后,在第 9 步,通过调用文件的.save方法将电子表格保存到磁盘。
生成的文件名可以与输入文件相同,以覆盖该文件。
可以通过外部访问文件来检查注释和值。
还有更多...
您可以将数据存储在多个值中,并且它将被转换为 Excel 的适当类型。例如,存储datetime将以适当的日期格式存储。对于float或其他数字格式也是如此。
如果需要推断类型,可以在加载文件时使用guess_type参数来启用此功能,例如:
>>> xlsfile = openpyxl.load_workbook('movies.xlsx', guess_types=True)
>>> xlsfile['Sheet1']['A1'].value = '37%'
>>> xlsfile['Sheet1']['A1'].value
0.37
>>> xlsfile['Sheet1']['A1'].value = '2.75'
>>> xlsfile['Sheet1']['A1'].value
2.75
向自动生成的单元格添加注释可以帮助审查结果文件,清楚地说明它们是如何生成的。
虽然可以添加公式来自动生成 Excel 文件,但调试结果可能会很棘手。在生成结果时,通常最好在 Python 中进行计算并将结果存储为原始数据。
完整的openpyxl文档可以在这里找到:
openpyxl.readthedocs.io/en/stable/index.html。
另请参阅
-
读取 Excel 电子表格教程
-
在 Excel 电子表格上创建新工作表教程
-
在 Excel 中创建图表教程
-
在 Excel 中处理格式教程
在 Excel 电子表格上创建新工作表
在这个教程中,我们将演示如何从头开始创建一个新的 Excel 电子表格,并添加和处理多个工作表。
准备工作
我们将使用openpyxl模块。我们应该安装该模块,并将其添加到我们的requirements.txt文件中,如下所示:
$ echo "openpyxl==2.5.4" >> requirements.txt
$ pip install -r requirements.txt
我们将在新文件中存储有关参与人数最多的电影的信息。数据从这里提取:
www.mrob.com/pub/film-video/topadj.html。
如何做...
- 导入
openpyxl模块:
>>> import openpyxl
- 创建一个新的 Excel 文件。它创建了一个名为
Sheet的默认工作表:
>>> xlsfile = openpyxl.Workbook()
>>> xlsfile.sheetnames
['Sheet']
>>> sheet = xlsfile['Sheet']
- 从源中向该工作表添加有关参与者人数的数据。为简单起见,只添加了前三个:
>>> data = [
... (225.7, 'Gone With the Wind', 'Victor Fleming'),
... (194.4, 'Star Wars', 'George Lucas'),
... (161.0, 'ET: The Extraterrestrial', 'Steven Spielberg'),
... ]
>>> for row, (admissions, name, director) in enumerate(data, 1):
... sheet['A{}'.format(row)].value = admissions
... sheet['B{}'.format(row)].value = name
- 创建一个新的工作表:
>>> sheet = xlsfile.create_sheet("Directors")
>>> sheet
<Worksheet "Directors">
>>> xlsfile.sheetnames
['Sheet', 'Directors']
- 为每部电影添加导演的名称:
>>> for row, (admissions, name, director) in enumerate(data, 1):
... sheet['A{}'.format(row)].value = director
... sheet['B{}'.format(row)].value = name
- 将文件保存为
movie_sheets.xlsx:
>>> xlsfile.save('movie_sheets.xlsx')
- 打开
movie_sheets.xlsx文件,检查它是否有两个工作表,并且包含正确的信息,如下截图所示:

它是如何工作的...
在如何做…部分,在第 1 步导入模块后,在第 2 步创建一个新的电子表格。这是一个只包含默认工作表的新电子表格。
要存储的数据在第 3 步中定义。请注意,它包含将放在两个工作表中的信息(两个工作表中都有名称,第一个工作表中有入场人数,第二个工作表中有导演的名称)。在这一步中,填充了第一个工作表。
请注意值是如何存储的。正确的单元格定义为列A或B和正确的行(行从 1 开始)。enumerate函数返回一个元组,第一个元素是索引,第二个元素是枚举参数(迭代器)。
之后,在第 4 步创建了新的工作表,使用名称Directors。.create_sheet返回新的工作表。
在第 5 步中存储了Directors工作表中的信息,并在第 6 步保存了文件。
还有更多...
可以通过.title属性更改现有工作表的名称:
>>> sheet = xlsfile['Sheet']
>>> sheet.title = 'Admissions'
>>> xlsfile.sheetnames
['Admissions', 'Directors']
要小心,因为无法访问xlsfile['Sheet']工作表。那个名称不存在!
活动工作表,文件打开时将显示的工作表,可以通过.active属性获得,并且可以使用._active_sheet_index进行更改。索引从第一个工作表开始为0:
>> xlsfile.active
<Worksheet "Admissions">
>>> xlsfile._active_sheet_index
0
>>> xlsfile._active_sheet_index = 1
>>> xlsfile.active
<Worksheet "Directors">
工作表也可以使用.copy_worksheet进行复制。请注意,某些数据,例如图表,不会被复制。大多数重复的信息将是单元格数据:
new_copied_sheet = xlsfile.copy_worksheet(source_sheet)
完整的openpyxl文档可以在这里找到:
openpyxl.readthedocs.io/en/stable/index.html。
另请参阅
-
读取 Excel 电子表格的方法
-
更新 Excel 电子表格并添加注释的方法
-
在 Excel 中创建图表
-
在 Excel 中使用格式的方法
在 Excel 中创建图表
电子表格包括许多处理数据的工具,包括以丰富多彩的图表呈现数据。让我们看看如何以编程方式将图表附加到 Excel 电子表格。
准备工作
我们将使用openpyxl模块。我们应该安装该模块,将其添加到我们的requirements.txt文件中,如下所示:
$ echo "openpyxl==2.5.4" >> requirements.txt
$ pip install -r requirements.txt
我们将在新文件中存储有关观众人数最多的电影的信息。数据从这里提取:
www.mrob.com/pub/film-video/topadj.html。
如何做...
- 导入
openpyxl模块并创建一个新的 Excel 文件:
>>> import openpyxl
>>> from openpyxl.chart import BarChart, Reference
>>> xlsfile = openpyxl.Workbook()
- 从源中在该工作表中添加有关观众人数的数据。为简单起见,只添加前三个:
>>> data = [
... ('Name', 'Admissions'),
... ('Gone With the Wind', 225.7),
... ('Star Wars', 194.4),
... ('ET: The Extraterrestrial', 161.0),
... ]
>>> sheet = xlsfile['Sheet']
>>> for row in data:
... sheet.append(row)
- 创建一个
BarChart对象并填充基本信息:
>>> chart = BarChart()
>>> chart.title = "Admissions per movie"
>>> chart.y_axis.title = 'Millions'
- 创建对
data的引用,并将data附加到图表:
>>> data = Reference(sheet, min_row=2, max_row=4, min_col=1, max_col=2)
>>> chart.add_data(data, from_rows=True, titles_from_data=True)
- 将图表添加到工作表并保存文件:
>>> sheet.add_chart(chart, "A6")
>>> xlsfile.save('movie_chart.xlsx')
- 在电子表格中检查生成的图表,如下截图所示:

工作原理...
在如何做...部分,在步骤 1 和 2 中准备数据后,数据已准备在范围A1:B4中。请注意,A1和B1都包含不应在图表中使用的标题。
在步骤 3 中,我们设置了新图表并包括基本数据,如标题和Y轴的单位。
标题更改为Millions;虽然更正确的方式应该是Admissions(millions),但这将与图表的完整标题重复。
步骤 4 通过Reference对象创建一个引用框,从第 2 行第 1 列到第 4 行第 2 列,这是我们的数据所在的区域,不包括标题。使用.add_data将数据添加到图表中。from_rows使每一行成为不同的数据系列。titles_from_data使第一列被视为系列的名称。
在步骤 5 中,将图表添加到单元格A6并保存到磁盘中。
还有更多...
可以创建各种不同的图表,包括柱状图、折线图、面积图(填充线和轴之间的区域的折线图)、饼图或散点图(其中一个值相对于另一个值绘制的 XY 图)。每种类型的图表都有一个等效的类,例如PieChart或LineChart。
同时,每个都可以具有不同的类型。例如,BarChart的默认类型是列,将柱形图垂直打印,但也可以选择不同的类型将其垂直打印:
>>> chart.type = 'bar'
检查openpyxl文档以查看所有可用的组合。
可以使用set_categories来明确设置数据的x轴标签,而不是从数据中提取。例如,将步骤 4 与以下代码进行比较:
data = Reference(sheet, min_row=2, max_row=4, min_col=2, max_col=2)
labels = Reference(sheet, min_row=2, max_row=4, min_col=1, max_col=1)
chart.add_data(data, from_rows=False, titles_from_data=False)
chart.set_categories(labels)
可以使用描述区域的文本标签来代替Reference对象的范围:
chart.add_data('Sheet!B2:B4', from_rows=False, titles_from_data=False)
chart.set_categories('Sheet!A2:A4')
如果数据范围需要以编程方式创建,这种描述方式可能更难处理。
正确地在 Excel 中定义图表有时可能很困难。Excel 从特定范围提取数据的方式可能令人困惑。记住要留出时间进行试验和错误,并处理差异。例如,在第 4 步中,我们定义了三个数据点的三个系列,而在前面的代码中,我们定义了一个具有三个数据点的单个系列。这些差异大多是微妙的。最后,最重要的是最终图表的外观。尝试不同的图表类型并了解差异。
完整的openpyxl文档可以在这里找到:
openpyxl.readthedocs.io/en/stable/index.html。
另请参阅
-
读取 Excel 电子表格食谱
-
更新 Excel 电子表格并添加注释食谱
-
在 Excel 电子表格上创建新工作表食谱
-
在 Excel 中处理格式食谱
在 Excel 中处理格式
在电子表格中呈现信息不仅仅是将其组织到单元格中或以图表形式显示,还涉及更改格式以突出显示有关它的重要要点。在这个食谱中,我们将看到如何操纵单元格的格式以增强数据并以最佳方式呈现它。
准备工作
我们将使用openpyxl模块。我们应该安装该模块,并将其添加到我们的requirements.txt文件中,如下所示:
$ echo "openpyxl==2.5.4" >> requirements.txt
$ pip install -r requirements.txt
我们将在新文件中存储有关出席人数最多的电影的信息。数据从这里提取:
www.mrob.com/pub/film-video/topadj.html。
如何做...
- 导入
openpyxl模块并创建一个新的 Excel 文件:
>>> import openpyxl
>>> from openpyxl.styles import Font, PatternFill, Border, Side
>>> xlsfile = openpyxl.Workbook()
- 从来源中在此工作表中添加有关出席人数的数据。为简单起见,只添加前四个:
>>> data = [
... ('Name', 'Admissions'),
... ('Gone With the Wind', 225.7),
... ('Star Wars', 194.4),
... ('ET: The Extraterrestrial', 161.0),
... ('The Sound of Music', 156.4),
]
>>> sheet = xlsfile['Sheet']
>>> for row in data:
... sheet.append(row)
- 定义要用于样式化电子表格的颜色:
>>> BLUE = "0033CC"
>>> LIGHT_BLUE = 'E6ECFF'
>>> WHITE = "FFFFFF"
- 在蓝色背景和白色字体中定义标题:
>>> header_font = Font(name='Tahoma', size=14, color=WHITE)
>>> header_fill = PatternFill("solid", fgColor=BLUE)
>>> for row in sheet['A1:B1']:
... for cell in row:
... cell.font = header_font
... cell.fill = header_fill
- 在标题后为列定义一个替代模式和每行一个边框:
>>> white_side = Side(border_style='thin', color=WHITE)
>>> blue_side = Side(border_style='thin', color=BLUE)
>>> alternate_fill = PatternFill("solid", fgColor=LIGHT_BLUE)
>>> border = Border(bottom=blue_side, left=white_side, right=white_side)
>>> for row_index, row in enumerate(sheet['A2:B5']):
... for cell in row:
... cell.border = border
... if row_index % 2:
... cell.fill = alternate_fill
- 将文件保存为
movies_format.xlsx:
>>> xlsfile.save('movies_format.xlsx')
- 检查生成的文件:

它是如何工作的...
在如何做...部分,第 1 步中我们导入openpyxl模块并创建一个新的 Excel 文件。在第 2 步中,我们向第一个工作表添加数据。第 3 步也是一个准备步骤,用于定义要使用的颜色。颜色以十六进制格式定义,这在网页设计世界中很常见。
要找到颜色的定义,有很多在线颜色选择器,甚至嵌入在操作系统中。像coolors.co/这样的工具可以帮助定义要使用的调色板。
在第 4 步中,我们准备格式以定义标题。标题将具有不同的字体(Tahoma)、更大的大小(14pt),并且将以蓝色背景上的白色显示。为此,我们准备了一个具有字体、大小和前景颜色的Font对象,以及具有背景颜色的PatternFill。
在创建header_font和header_fill后的循环将字体和填充应用到适当的单元格。
请注意,迭代范围始终返回行,然后是单元格,即使只涉及一行。
在第 5 步中,为行添加边框和交替背景。边框定义为蓝色顶部和底部,白色左侧和右侧。填充的创建方式与第 4 步类似,但是颜色是浅蓝色。背景只应用于偶数行。
请注意,单元格的顶部边框是上面一个单元格的底部,反之亦然。这意味着可能在循环中覆盖边框。
文件最终在第 6 步中保存。
还有更多...
要定义字体,还有其他可用的选项,如粗体、斜体、删除线或下划线。定义字体并重新分配它,如果需要更改任何元素。记得检查字体是否可用。
还有各种创建填充的方法。PatternFill接受几种模式,但最有用的是solid。GradientFill也可以用于应用双色渐变。
最好限制自己使用PatternFill进行实体填充。您可以调整颜色以最好地表示您想要的内容。记得包括style='solid',否则颜色可能不会出现。
也可以定义条件格式,但最好尝试在 Python 中定义条件,然后应用适当的格式。
可以正确设置数字格式,例如:
cell.style = 'Percent'
这将显示值0.37为37%。
完整的openpyxl文档可以在这里找到:
openpyxl.readthedocs.io/en/stable/index.html。
另请参见
-
读取 Excel 电子表格配方
-
更新 Excel 电子表格并添加注释配方
-
在 Excel 电子表格中创建新工作表配方
-
在 Excel 中创建图表配方
在 LibreOffice 中创建宏
LibreOffice 是一个免费的办公套件,是 MS Office 和其他办公套件的替代品。它包括一个文本编辑器和一个名为Calc的电子表格程序。Calc 可以理解常规的 Excel 格式,并且也可以通过其 UNO API 在内部进行完全脚本化。UNO 接口允许以编程方式访问套件,并且可以用不同的语言(如 Java)进行访问。
其中一种可用的语言是 Python,这使得在套件格式中生成非常复杂的应用程序非常容易,因为这样可以使用完整的 Python 标准库。
使用完整的 Python 标准库可以访问诸如加密、打开外部文件(包括 ZIP 文件)或连接到远程数据库等元素。此外,利用 Python 语法,避免使用 LibreOffice BASIC。
在本配方中,我们将看到如何将外部 Python 文件作为宏添加到电子表格中,从而改变其内容。
准备工作
需要安装 LibreOffice。它可以在www.libreoffice.org/上找到。
下载并安装后,需要配置以允许执行宏:
- 转到设置|安全以查找宏安全详细信息:

- 打开宏安全并选择中等以允许执行我们的宏。这将在允许运行宏之前显示警告:

要将宏插入文件中,我们将使用一个名为include_macro.py的脚本,该脚本可在github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter06/include_macro.py上找到。带有宏的脚本也可以在此处作为libreoffice_script.py找到:
github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter06/libreoffice_script.py。
要将脚本放入的文件名为movies.ods的文件也可以在此处找到:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter06/movies.ods。它以.ods格式(LibreOffice 格式)包含了 10 部入场人数最高的电影的表格。数据是从这里提取的:
www.mrob.com/pub/film-video/topadj.html。
如何做...
- 使用
include_macro.py脚本将libreoffice_script.py附加到文件movies.ods的宏文件中:
$ python include_macro.py -h
usage: It inserts the macro file "script" into the file "spreadsheet" in .ods format. The resulting file is located in the macro_file directory, that will be created
[-h] spreadsheet script
positional arguments:
spreadsheet File to insert the script
script Script to insert in the file
optional arguments:
-h, --help show this help message and exit
$ python include_macro.py movies.ods libreoffice_script.py
- 在 LibreOffice 中打开生成的文件
macro_file/movies.ods。请注意,它会显示一个警告以启用宏(单击启用)。转到工具|宏|运行宏:

- 在
movies.ods|libreoffice_script宏下选择ObtainAggregated并单击运行。它计算聚合入场人数并将其存储在单元格B12中。它在A15中添加了一个Total标签:

- 重复步骤 2 和 3 以再次运行。现在它运行所有的聚合,但是将
B12相加,并在B13中得到结果:

工作原理...
步骤 1 中的主要工作在include_macro.py脚本中完成。它将文件复制到macro_file子目录中,以避免修改输入。
在内部,.ods文件是一个具有特定结构的 ZIP 文件。脚本利用 ZIP 文件 Python 模块,将脚本添加到内部的适当子目录中。它还修改manifest.xml文件,以便 LibreOffice 知道文件中有一个脚本。
在步骤 3 中执行的宏在libreoffice_script.py中定义,并包含一个函数:
def ObtainAggregated(*args):
"""Prints the Python version into the current document"""
# get the doc from the scripting context
# which is made available to all scripts
desktop = XSCRIPTCONTEXT.getDesktop()
model = desktop.getCurrentComponent()
# get the first sheet
sheet = model.Sheets.getByIndex(0)
# Find the admissions column
MAX_ELEMENT = 20
for column in range(0, MAX_ELEMENT):
cell = sheet.getCellByPosition(column, 0)
if 'Admissions' in cell.String:
break
else:
raise Exception('Admissions not found')
accumulator = 0.0
for row in range(1, MAX_ELEMENT):
cell = sheet.getCellByPosition(column, row)
value = cell.getValue()
if value:
accumulator += cell.getValue()
else:
break
cell = sheet.getCellByPosition(column, row)
cell.setValue(accumulator)
cell = sheet.getCellRangeByName("A15")
cell.String = 'Total'
return None
变量XSCRIPTCONTEXT会自动创建并允许获取当前组件,然后获取第一个Sheet。之后,通过.getCellByPosition迭代表找到Admissions列,并通过.String属性获取字符串值。使用相同的方法,聚合列中的所有值,通过.getValue提取它们的数值。
当循环遍历列直到找到空单元格时,第二次执行时,它将聚合B12中的值,这是上一次执行中的聚合值。这是故意为了显示宏可以多次执行,产生不同的结果。
还可以通过.getCellRangeByName按其字符串位置引用单元格,将Total存储在单元格A15中。
还有更多...
Python 解释器嵌入到 LibreOffice 中,这意味着如果 LibreOffice 发生变化,特定版本也会发生变化。在撰写本书时的最新版本的 LibreOffice(6.0.5)中,包含的版本是 Python 3.5.1。
UNO 接口非常完整,可以访问许多高级元素。不幸的是,文档不是很好,获取起来可能会很复杂和耗时。文档是用 Java 或 C++定义的,LibreOffice BASIC 或其他语言中有示例,但 Python 的示例很少。完整的文档可以在这里找到:api.libreoffice.org/,参考在这里:
api.libreoffice.org/docs/idl/ref/index.html。
例如,可以创建复杂的图表,甚至是要求用户提供并处理响应的交互式对话框。在论坛和旧答案中有很多信息。基本代码大多数时候也可以适应 Python。
LibreOffice 是以前的项目 OpenOffice 的一个分支。UNO 已经可用,这意味着在搜索互联网时会找到一些涉及 OpenOffice 的引用。
请记住,LibreOffice 能够读取和写入 Excel 文件。一些功能可能不是 100%兼容;例如,可能会出现格式问题。
出于同样的原因,完全可以使用本章其他食谱中描述的工具生成 Excel 格式的文件,并在 LibreOffice 中打开。这可能是一个不错的方法,因为openpyxl的文档更好。
调试有时也可能会很棘手。记住确保在用新代码重新打开文件之前,文件已完全关闭。
UNO 还能够与 LibreOffice 套件的其他部分一起工作,比如创建文档。
另请参阅
-
编写 CSV 电子表格食谱
-
更新 Excel 电子表格并添加注释和公式食谱
第七章:开发令人惊叹的图表
本章将涵盖以下示例:
-
绘制简单的销售图表
-
绘制堆叠条形图
-
绘制饼图
-
显示多条线。
-
绘制散点图
-
可视化地图
-
添加图例和注释
-
组合图表
-
保存图表
介绍
图表和图像是呈现复杂数据的绝妙方式,易于理解。在本章中,我们将利用强大的matplotlib库来学习如何创建各种图表。matplotlib是一个旨在以多种方式显示数据的库,它可以创建绝对令人惊叹的图表,有助于以最佳方式传输和显示信息。
我们将涵盖的图表将从简单的条形图到线图或饼图,并结合多个图表在同一图表中,注释它们,甚至绘制地理地图。
绘制简单的销售图表
在这个示例中,我们将看到如何通过绘制与不同时期销售成比例的条形来绘制销售图表。
准备工作
我们可以使用以下命令在我们的虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
在某些操作系统中,这可能需要我们安装额外的软件包;例如,在 Ubuntu 中可能需要我们运行apt-get install python3-tk。查看matplolib文档以获取详细信息。
如果您使用的是 macOS,可能会出现这样的错误—RuntimeError: Python is not installed as a framework。请参阅matplolib文档以了解如何解决:matplotlib.org/faq/osx_framework.html。
如何做...
- 导入
matplotlib:
>>> import matplotlib.pyplot as plt
- 准备要在图表上显示的数据:
>>> DATA = (
... ('Q1 2017', 100),
... ('Q2 2017', 150),
... ('Q3 2017', 125),
... ('Q4 2017', 175),
... )
- 将数据拆分为图表可用的格式。这是一个准备步骤:
>>> POS = list(range(len(DATA)))
>>> VALUES = [value for label, value in DATA]
>>> LABELS = [label for label, value in DATA]
- 创建一个带有数据的条形图:
>>> plt.bar(POS, VALUES)
>>> plt.xticks(POS, LABELS)
>>> plt.ylabel('Sales')
- 显示图表:
>>> plt.show()
- 结果将在新窗口中显示如下:

它是如何工作的...
导入模块后,数据将以方便的方式呈现在第 2 步的如何做部分中,这很可能类似于数据最初的存储方式。
由于matplotlib的工作方式,它需要X组件以及Y组件。在这种情况下,我们的X组件只是一系列整数,与数据点一样多。我们在POS中创建了这个。在VALUES中,我们将销售的数值存储为一个序列,在LABELS中存储了每个数据点的相关标签。所有这些准备工作都在第 3 步完成。
第 4 步创建了条形图,使用了X(POS)和Y(VALUES)的序列。这些定义了我们的条形。为了指定它所指的时期,我们使用.xticks在x轴上为每个值放置标签。为了澄清含义,我们使用.ylabel添加标签。
要显示结果图表,第 5 步调用.show,它会打开一个新窗口显示结果。
调用.show会阻止程序的执行。当窗口关闭时,程序将恢复。
还有更多...
您可能希望更改值的呈现格式。在我们的示例中,也许数字代表数百万美元。为此,您可以向y轴添加格式化程序,以便在那里表示的值将应用于它们:
>>> from matplotlib.ticker import FuncFormatter
>>> def value_format(value, position):
... return '$ {}M'.format(int(value))
>>> axes = plt.gca()
>>> axes.yaxis.set_major_formatter(FuncFormatter(value_format))
value_format是一个根据数据的值和位置返回值的函数。在这里,它将返回值 100 作为$ 100 M。
值将以浮点数形式检索,需要将它们转换为整数进行显示。
要应用格式化程序,我们需要使用.gca(获取当前轴)检索axis对象。然后,.yaxis获取格式化程序。
条的颜色也可以使用color参数确定。颜色可以以多种格式指定,如matplotlib.org/api/colors_api.html中所述,但我最喜欢的是遵循 XKCD 颜色调查,使用xkcd:前缀(冒号后没有空格):
>>> plt.bar(POS, VALUES, color='xkcd:moss green')
完整的调查可以在这里找到:xkcd.com/color/rgb/。
大多数常见的颜色,如蓝色或红色,也可以用于快速测试。但它们往往有点亮,不能用于漂亮的报告。
将颜色与格式化轴结合起来,得到以下结果:

条形图不一定需要以时间顺序显示信息。正如我们所见,matplotlib要求我们指定每个条的X参数。这是一个生成各种图表的强大工具。
例如,可以安排条形以显示直方图,比如显示特定身高的人。条形将从较低的高度开始增加到平均大小,然后再降低。不要局限于电子表格图表!
完整的matplotlib文档可以在这里找到:matplotlib.org/。
另请参阅
-
绘制堆叠条形图的方法
-
添加图例和注释的方法
-
组合图表的方法
绘制堆叠条形图
一种强大的显示不同类别的方法是将它们呈现为堆叠条形图,因此每个类别和总数都会显示出来。我们将在这个方法中看到如何做到这一点。
准备就绪
我们需要在虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
如果您使用的是 macOS,可能会出现这样的错误:RuntimeError: Python is not installed as a framework。请参阅matplolib文档以了解如何解决:matplotlib.org/faq/osx_framework.html。
如何做...
- 导入
matplotlib:
>>> import matplotlib.pyplot as plt
- 准备数据。这代表了两种产品的销售,一个是已建立的,另一个是新产品:
>>> DATA = (
... ('Q1 2017', 100, 0),
... ('Q2 2017', 105, 15),
... ('Q3 2017', 125, 40),
... ('Q4 2017', 115, 80),
... )
- 处理数据以准备期望的格式:
>>> POS = list(range(len(DATA)))
>>> VALUESA = [valueA for label, valueA, valueB in DATA]
>>> VALUESB = [valueB for label, valueA, valueB in DATA]
>>> LABELS = [label for label, value1, value2 in DATA]
- 创建条形图。需要两个图:
>>> plt.bar(POS, VALUESB)
>>> plt.bar(POS, VALUESA, bottom=VALUESB)
>>> plt.ylabel('Sales')
>>> plt.xticks(POS, LABELS)
- 显示图表:
>>> plt.show()
- 结果将显示在一个新窗口中,如下所示:

它是如何工作的...
导入模块后,在第 2 步以一种方便的方式呈现数据,这可能与数据最初存储的方式类似。
在第 3 步中,数据准备为三个序列,VALUESA,VALUEB和LABELS。添加了一个POS序列以正确定位条形。
第 4 步创建了条形图,使用了序列X(POS)和Y(VALUESB)。第二个条形序列VALUESA添加到前一个上面,使用bottom参数。这样就堆叠了条形。
请注意,我们首先堆叠第二个值VALUESB。第二个值代表市场上推出的新产品,而VALUESA更加稳定。这更好地显示了新产品的增长。
每个期间都在X轴上用.xticks标记。为了澄清含义,我们使用.ylabel添加标签。
要显示生成的图表,第 5 步调用.show,这将打开一个新窗口显示结果。
调用.show会阻止程序的执行。当窗口关闭时,程序将恢复。
还有更多...
呈现堆叠条形的另一种方法是将它们添加为百分比,这样总数不会改变,只是相对大小相互比较。
为了做到这一点,需要根据百分比计算VALUESA和VALUEB:
>>> VALUESA = [100 * valueA / (valueA + valueB) for label, valueA, valueB in DATA]
>>> VALUESB = [100 * valueB / (valueA + valueB) for label, valueA, valueB in DATA]
这使得每个值都等于总数的百分比,总数始终加起来为100。这产生了以下图形:

条形不一定需要堆叠。有时,将条形相互对比呈现可能会更有趣。
为了做到这一点,我们需要移动第二个条形序列的位置。我们还需要设置更细的条形以留出空间:
>>> WIDTH = 0.3
>>> plt.bar([p - WIDTH / 2 for p in POS], VALUESA, width=WIDTH)
>>> plt.bar([p + WIDTH / 2 for p in POS], VALUESB, width=WIDTH)
注意条的宽度设置为空间的三分之一,因为我们的参考空间在条之间是1。第一根条移到左边,第二根移到右边以使它们居中。已删除bottom参数,以不堆叠条形:

完整的matplotlib文档可以在这里找到:matplotlib.org/。
另请参阅
-
绘制简单销售图表食谱
-
添加图例和注释食谱
-
组合图表食谱
绘制饼图
饼图!商业 101 最喜欢的图表,也是呈现百分比的常见方式。在这个食谱中,我们将看到如何绘制一个饼图,不同的切片代表不同的比例。
准备工作
我们需要使用以下命令在虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
如果您使用的是 macOS,可能会出现这样的错误——RuntimeError: Python is not installed as a framework。请参阅matplotlib文档以了解如何解决此问题:matplotlib.org/faq/osx_framework.html。
如何做...
- 导入
matplotlib:
>>> import matplotlib.pyplot as plt
- 准备数据。这代表了几条产品线:
>>> DATA = (
... ('Common', 100),
... ('Premium', 75),
... ('Luxurious', 50),
... ('Extravagant', 20),
... )
- 处理数据以准备预期格式:
>>> VALUES = [value for label, value in DATA]
>>> LABELS = [label for label, value in DATA]
- 创建饼图:
>>> plt.pie(VALUES, labels=LABELS, autopct='%1.1f%%')
>>> plt.gca().axis('equal')
- 显示图表:
>>> plt.show()
- 结果将显示在新窗口中,如下所示:

工作原理...
在如何做...部分的第 1 步中导入了该模块,并在第 2 步中导入了要呈现的数据。在第 3 步中,数据被分成两个部分,一个是VALUES的列表,另一个是LABELS的列表。
图表的创建发生在第 4 步。饼图是通过添加VALUES和LABELS来创建的。autopct参数格式化值,以便将其显示为百分比到小数点后一位。
对axis的调用确保饼图看起来是圆形的,而不是有一点透视并呈现为椭圆。
要显示生成的图表,第 5 步调用.show,它会打开一个新窗口显示结果。
调用.show会阻塞程序的执行。当窗口关闭时,程序将恢复。
还有更多...
饼图在商业图表中有点过度使用。大多数情况下,使用带百分比或值的条形图会更好地可视化数据,特别是当显示两个或三个以上的选项时。尽量限制在报告和数据演示中使用饼图。
通过startangle参数可以旋转楔形的起始位置,使用counterclock来设置楔形的方向(默认为True):
>>> plt.pie(VALUES, labels=LABELS, startangle=90, counterclock=False)
标签内的格式可以通过函数设置。由于饼图内的值被定义为百分比,找到原始值可能有点棘手。以下代码片段创建了一个按整数百分比索引的字典,因此我们可以检索引用的值。请注意,这假设没有重复的百分比。如果有这种情况,标签可能会略有不正确。在这种情况下,我们可能需要使用更好的精度,最多使用小数点后一位:
>>> from matplotlib.ticker import FuncFormatter
>>> total = sum(value for label, value in DATA)
>>> BY_VALUE = {int(100 * value / total): value for label, value in DATA}
>>> def value_format(percent, **kwargs):
... value = BY_VALUE[int(percent)]
... return '{}'.format(value)
一个或多个楔形也可以通过使用 explode 参数分开。这指定了楔形与中心的分离程度:
>>> explode = (0, 0, 0.1, 0)
>>> plt.pie(VALUES, labels=LABELS, explode=explode, autopct=value_format,
startangle=90, counterclock=False)
结合所有这些选项,我们得到以下结果:

完整的matplotlib文档可以在这里找到:matplotlib.org/。
另请参阅
-
绘制简单销售图表食谱
-
绘制堆叠条形图食谱
显示多条线
这个食谱将展示如何在图表中显示多条线。
准备工作
我们需要在虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
如果您使用的是 macOS,可能会出现这样的错误——RuntimeError: Python is not installed as a framework。请参阅matplolib文档以了解如何解决此问题:matplotlib.org/faq/osx_framework.html。
如何做...
- 导入
matplotlib:
>>> import matplotlib.pyplot as plt
- 准备数据。这代表了两种产品的销售:
>>> DATA = (
... ('Q1 2017', 100, 5),
... ('Q2 2017', 105, 15),
... ('Q3 2017', 125, 40),
... ('Q4 2017', 115, 80),
... )
- 处理数据以准备预期格式:
>>> POS = list(range(len(DATA)))
>>> VALUESA = [valueA for label, valueA, valueB in DATA]
>>> VALUESB = [valueB for label, valueA, valueB in DATA]
>>> LABELS = [label for label, value1, value2 in DATA]
- 创建线图。需要两条线:
>>> plt.plot(POS, VALUESA, 'o-')
>>> plt.plot(POS, VALUESB, 'o-')
>>> plt.ylabel('Sales')
>>> plt.xticks(POS, LABELS)
- 显示图表:
>>> plt.show()
- 结果将显示在一个新窗口中:

工作原理…
在如何做…部分,第 1 步导入模块,第 2 步以格式化的方式显示要绘制的数据。
在第 3 步中,数据准备好了三个序列VALUESA,VALUEB和LABELS。添加了一个POS序列来正确定位每个点。
第 4 步创建了图表,使用了序列X(POS)和Y(VALUESA),然后是POS和VALUESB。添加了值为'o-',以在每个数据点上绘制一个圆圈,并在它们之间绘制一条实线。
默认情况下,图表将显示一条实线,每个点上没有标记。如果只使用标记(即'o'),就不会有线。
X轴上的每个周期都带有.xticks标签。为了澄清含义,我们使用.ylabel添加了一个标签。
要显示结果图表,第 5 步调用.show,它会打开一个新窗口显示结果。
调用.show会阻塞程序的执行。当窗口关闭时,程序将恢复。
还有更多…
带有线条的图表看起来简单,能够创建许多有趣的表示。在显示数学图表时,这可能是最方便的。例如,我们可以用几行代码显示 Moore 定律的图表。
摩尔定律是戈登·摩尔观察到的一个现象,即集成电路中的元件数量每两年翻一番。它首次在 1965 年被描述,然后在 1975 年得到修正。它似乎与过去 40 年的技术进步历史速度非常接近。
我们首先创建了一条描述理论线的线,数据点从 1970 年到 2013 年。从 1000 个晶体管开始,每两年翻一番,直到 2013 年:
>>> POS = [year for year in range(1970, 2013)]
>>> MOORES = [1000 * (2 ** (i * 0.5)) for i in range(len(POS))]
>>> plt.plot(POS, MOORES)
根据一些文档,我们从这里提取了一些商用 CPU 的例子,它们的发布年份以及集成元件的数量:www.wagnercg.com/Portals/0/FunStuff/AHistoryofMicroprocessorTransistorCount.pdf。由于数字很大,我们将使用 Python 3 中的1_000_000表示一百万:
>>> DATA = (
... ('Intel 4004', 2_300, 1971),
... ('Motorola 68000', 68_000, 1979),
... ('Pentium', 3_100_000, 1993),
... ('Core i7', 731_000_000, 2008),
... )
绘制一条带有标记的线,以在正确的位置显示这些点。'v'标记将显示一个三角形:
>>> data_x = [x for label, y, x in DATA]
>>> data_y = [y for label, y, x in DATA]
>>> plt.plot(data_x, data_y, 'v')
对于每个数据点,将一个标签附加在正确的位置,标有 CPU 的名称:
>>> for label, y, x in DATA:
>>> plt.text(x, y, label)
最后,成长在线性图表中没有意义,因此我们将比例改为对数,这样指数增长看起来像一条直线。但为了保持尺度的意义,添加一个网格。调用.show显示图表:
>>> plt.gca().grid()
>>> plt.yscale('log')
结果图将显示:

完整的matplotlib文档可以在这里找到:matplotlib.org/。特别是,可以在这里检查线条(实线、虚线、点线等)和标记(点、圆圈、三角形、星形等)的可用格式:matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html。
另请参阅
-
添加图例和注释配方
-
组合图表配方
绘制散点图
散点图是一种只显示为点的信息,具有X和Y值。当呈现样本并查看两个变量之间是否存在关系时,它们非常有用。在这个配方中,我们将显示一个图表,绘制在网站上花费的时间与花费的金钱,以查看是否可以看到一个模式。
准备就绪
我们需要在虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
如果您使用的是 macOS,可能会出现这样的错误——RuntimeError: Python is not installed as a framework。请参阅matplolib文档,了解如何解决此问题:matplotlib.org/faq/osx_framework.html。
作为数据点,我们将使用scatter.csv文件来读取数据。此文件可在 GitHub 上找到:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter07/scatter.csv。
如何做...
- 导入
matplotlib和csv。还导入FuncFormatter以稍后格式化轴:
>>> import csv
>>> import matplotlib.pyplot as plt
>>> from matplotlib.ticker import FuncFormatter
- 准备数据,使用
csv模块从文件中读取:
>>> with open('scatter.csv') as fp:
... reader = csv.reader(fp)
... data = list(reader)
- 准备绘图数据,然后绘制:
>>> data_x = [float(x) for x, y in data]
>>> data_y = [float(y) for x, y in data]
>>> plt.scatter(data_x, data_y)
- 通过格式化轴来改善上下文:
>>> def format_minutes(value, pos):
... return '{}m'.format(int(value))
>>> def format_dollars(value, pos):
... return '${}'.format(value)
>>> plt.gca().xaxis.set_major_formatter(FuncFormatter(format_minutes))
>>> plt.xlabel('Time in website')
>>> plt.gca().yaxis.set_major_formatter(FuncFormatter(format_dollars))
>>> plt.ylabel('Spending')
- 显示图表:
>>> plt.show()
- 结果将显示在新窗口中:

工作原理...
如何做…部分的步骤 1 和 2 导入了我们稍后将使用的模块并从 CSV 文件中读取数据。数据被转换为列表,以允许我们多次迭代,这在第 3 步中是必要的。
第 3 步将数据准备为两个数组,然后使用.scatter来绘制它们。与matplotlib的其他方法一样,.scatter的参数需要X和Y值的数组。它们都需要具有相同的大小。数据从文件格式转换为float,以确保数字格式。
第 4 步改进了数据在每个轴上的呈现方式。相同的操作被呈现两次——创建一个函数来定义该轴上的值应该如何显示(以美元或分钟)。该函数接受要显示的值和位置作为输入。通常,位置将被忽略。轴格式化程序将被覆盖为.set_major_formatter。请注意,两个轴都将使用.gca(获取当前轴)返回。
使用.xlabel和.ylabel向轴添加标签。
最后,第 5 步在新窗口中显示图表。分析结果,我们可以说似乎有两种用户,一些用户花费不到 10 分钟,从不花费超过 10 美元,还有一些用户花费更多时间,也更有可能花费高达 100 美元。
请注意,所呈现的数据是合成的,并且已经根据结果生成。现实生活中的数据可能看起来更分散。
还有更多...
散点图不仅可以显示二维空间中的点,还可以添加第三个(面积)甚至第四个维度(颜色)。
要添加这些元素,使用参数s表示大小,c表示颜色。
大小被定义为点的直径的平方。因此,对于直径为 10 的球,将使用 100。颜色可以使用matplotlib中颜色的任何常规定义,例如十六进制颜色、RGB 等。有关更多详细信息,请参阅文档:matplotlib.org/users/colors.html。例如,我们可以使用以下方式生成一个随机图表的四个维度:
>>> import matplotlib.pyplot as plt
>>> import random
>>> NUM_POINTS = 100
>>> COLOR_SCALE = ['#FF0000', '#FFFF00', '#FFFF00', '#7FFF00', '#00FF00']
>>> data_x = [random.random() for _ in range(NUM_POINTS)]
>>> data_y = [random.random() for _ in range(NUM_POINTS)]
>>> size = [(50 * random.random()) ** 2 for _ in range(NUM_POINTS)]
>>> color = [random.choice(COLOR_SCALE) for _ in range(NUM_POINTS)]
>>> plt.scatter(data_x, data_y, s=size, c=color, alpha=0.5)
>>> plt.show()
COLOR_SCALE从绿色到红色,每个点的大小将在0到50之间。结果应该是这样的:

请注意,这是随机的,因此每次都会生成不同的图表。
alpha值使每个点半透明,使我们能够看到它们重叠的位置。该值越高,点的透明度越低。此参数将影响显示的颜色,因为它将点与背景混合。
尽管可以在大小和颜色中显示两个独立的值,但它们也可以与任何其他值相关联。例如,使颜色依赖于大小将使所有相同大小的点具有相同的颜色,这可能有助于区分数据。请记住,图表的最终目标是使数据易于理解。尝试不同的方法来改进这一点。
完整的matplotlib文档可以在这里找到:matplotlib.org/。
另请参阅
-
显示多行的方法
-
添加图例和注释的方法
可视化地图
要显示从区域到区域变化的信息,最好的方法是显示一张呈现信息的地图,同时为数据提供区域位置和位置的感觉。
在此示例中,我们将利用fiona模块导入 GIS 信息,以及matplotlib来显示信息。 我们将显示西欧的地图,并显示每个国家的人口与颜色等级。 颜色越深,人口越多。
准备工作
我们需要在虚拟环境中安装matplotlib和fiona:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ echo "Fiona==1.7.13" >> requirements.txt
$ pip install -r requirements.txt
如果您使用的是 macOS,可能会出现这样的错误-RuntimeError: Python is not installed as a framework。 请参阅matplolib文档以了解如何解决此问题:matplotlib.org/faq/osx_framework.html。
需要下载地图数据。 幸运的是,有很多免费提供的地理信息数据。 在 Google 上搜索应该很快返回几乎您需要的所有内容,包括有关地区,县,河流或任何其他类型数据的详细信息。
来自许多公共组织的 GIS 信息以不同格式可用。 fiona能够理解大多数常见格式并以等效方式处理它们,但存在细微差异。 请阅读fiona文档以获取更多详细信息。
我们将在此示例中使用的数据,涵盖所有欧洲国家,可在 GitHub 的以下网址找到:github.com/leakyMirror/map-of-europe/blob/master/GeoJSON/europe.geojson。 请注意,它是 GeoJSON 格式,这是一种易于使用的标准。
如何操作...
- 导入稍后要使用的模块:
>>> import matplotlib.pyplot as plt
>>> import matplotlib.cm as cm
>>> import fiona
- 加载要显示的国家的人口。 人口已经是:
>>> COUNTRIES_POPULATION = {
... 'Spain': 47.2,
... 'Portugal': 10.6,
... 'United Kingdom': 63.8,
... 'Ireland': 4.7,
... 'France': 64.9,
... 'Italy': 61.1,
... 'Germany': 82.6,
... 'Netherlands': 16.8,
... 'Belgium': 11.1,
... 'Denmark': 5.6,
... 'Slovenia': 2,
... 'Austria': 8.5,
... 'Luxembourg': 0.5,
... 'Andorra': 0.077,
... 'Switzerland': 8.2,
... 'Liechtenstein': 0.038,
... }
>>> MAX_POPULATION = max(COUNTRIES_POPULATION.values())
>>> MIN_POPULATION = min(COUNTRIES_POPULATION.values())
- 准备
colormap,它将确定每个国家显示在绿色阴影中的颜色。 计算每个国家对应的颜色:
>>> colormap = cm.get_cmap('Greens')
>>> COUNTRY_COLOUR = {
... country_name: colormap(
... (population - MIN_POPULATION) / (MAX_POPULATION - MIN_POPULATION)
... )
... for country_name, population in COUNTRIES_POPULATION.items()
... }
- 打开文件并读取数据,按照我们在第 1 步中定义的国家进行过滤:
>>> with fiona.open('europe.geojson') as fd:
>>> full_data = [data for data in full_data
... if data['properties']['NAME'] in COUNTRIES_POPULATION]
- 以正确的颜色绘制每个国家:
>>> for data in full_data:
... country_name = data['properties']['NAME']
... color = COUNTRY_COLOUR[country_name]
... geo_type = data['geometry']['type']
... if geo_type == 'Polygon':
... data_x = [x for x, y in data['geometry']['coordinates'][0]]
... data_y = [y for x, y in data['geometry']['coordinates'][0]]
... plt.fill(data_x, data_y, c=color)
... elif geo_type == 'MultiPolygon':
... for coordinates in data['geometry']['coordinates']:
... data_x = [x for x, y in coordinates[0]]
... data_y = [y for x, y in coordinates[0]]
... plt.fill(data_x, data_y, c=color)
- 显示结果:
>>> plt.show()
- 结果将显示在新窗口中:

它是如何工作的...
在如何操作...部分的第 1 步中导入模块后,将在第 2 步中定义要显示的数据。 请注意,名称需要与 GEO 文件中的格式相同。 最小和最大人口将被计算以正确平衡范围。
人口已经舍入到一个显著数字,并以百万定义。 仅为此示例的目的定义了一些国家,但在 GIS 文件中还有更多可用的国家,并且地图可以向东扩展。
在第 3 步中描述了定义绿色阴影(Greens)范围的colormap。 这是matplotlib中的一个标准colormap,但可以使用文档中描述的其他colormap(matplotlib.org/examples/color/colormaps_reference.html),例如橙色,红色或等离子体,以获得更冷到热的方法。
COUNTRY_COLOUR字典存储了由colormap为每个国家定义的颜色。 人口减少到从 0.0(最少人口)到 1.0(最多)的数字,并传递给colormap以检索其对应的比例的颜色。
然后在第 4 步中检索 GIS 信息。 使用fiona读取europe.geojson文件,并复制数据,以便在接下来的步骤中使用。 它还会过滤,只处理我们定义了人口的国家,因此不会绘制额外的国家。
步骤 5 中的循环逐个国家进行,然后我们使用.fill来绘制它,它绘制一个多边形。每个不同国家的几何形状都是一个单一的多边形(Polygon)或多个多边形(MultiPolygon)。在每种情况下,适当的多边形都以相同的颜色绘制。这意味着MultiPolygon会被绘制多次。
GIS 信息以描述纬度和经度的坐标点的形式存储。区域,如国家,有一系列坐标来描述其中的区域。一些地图更精确,有更多的点来定义区域。可能需要多个多边形来定义一个国家,因为一些部分可能相互分离,岛屿是最明显的情况,但也有飞地。
最后,通过调用.show来显示数据。
还有更多...
利用 GIS 文件中包含的信息,我们可以向地图添加额外的信息。properties对象包含有关国家名称的信息,还有 ISO 名称、FID 代码和中心位置的LON和LAT。我们可以使用这些信息来使用.text显示国家的名称:
long, lat = data['properties']['LON'], data['properties']['LAT']
iso3 = data['properties']['ISO3']
plt.text(long, lat, iso3, horizontalalignment='center')
这段代码将存在于如何做部分的步骤 6 中的循环中。
如果你分析这个文件,你会发现properties对象包含有关人口的信息,存储为 POP2005,所以你可以直接从地图上绘制人口信息。这留作练习。不同的地图文件将包含不同的信息,所以一定要尝试一下,释放所有可能性。
此外,你可能会注意到在某些情况下地图可能会变形。matplotlib会尝试将其呈现为一个正方形的框,如果地图不是大致正方形,这将是明显的。例如,尝试只显示西班牙、葡萄牙、爱尔兰和英国。我们可以强制图表以 1 点纬度与 1 点经度相同的空间来呈现,这是一个很好的方法,如果我们不是在靠近极地的地方绘制东西。这是通过在轴上调用.set_aspect来实现的。当前轴可以通过.gca(获取当前轴)获得。
>>> axes = plt.gca()
>>> axes.set_aspect('equal', adjustable='box')
此外,为了改善地图的外观,我们可以设置一个背景颜色,以帮助区分背景和前景,并删除轴上的标签,因为打印纬度和经度可能会分散注意力。通过使用.xticks和.yticks设置空标签来实现在轴上删除标签。背景颜色由轴的前景颜色规定:
>>> plt.xticks([])
>>> plt.yticks([])
>>> axes = plt.gca()
>>> axes.set_facecolor('xkcd:light blue')
最后,为了更好地区分不同的区域,可以添加一个包围每个区域的线。这可以通过在.fill之后用相同的数据绘制一条细线来实现。请注意,这段代码在步骤 2 中重复了两次。
plt.fill(data_x, data_y, c=color)
plt.plot(data_x, data_y, c='black', linewidth=0.2)
将所有这些元素应用到地图上,现在看起来是这样的:

生成的代码可以在 GitHub 上找到:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter07/visualising_maps.py。
正如我们所见,地图是以一般的多边形绘制的。不要害怕包括其他几何形状。你可以定义自己的多边形,并用.fill或一些额外的标签打印它们。例如,远离的地区可能需要被运输,以避免地图太大。或者,可以使用矩形在地图的部分上打印额外的信息。
完整的fiona文档可以在这里找到:toblerity.org/fiona/。完整的matplotlib文档可以在这里找到:matplotlib.org/。
另请参阅
-
添加图例和注释配方
-
组合图表配方
添加图例和注释
在绘制具有密集信息的图表时,可能需要图例来确定特定颜色或更好地理解所呈现的数据。在matplotlib中,图例可以非常丰富,并且有多种呈现方式。注释也是吸引观众注意力的好方法,以便更好地传达信息。
在本示例中,我们将创建一个具有三个不同组件的图表,并显示一个包含信息的图例,以更好地理解它,并在图表上注释最有趣的点。
准备工作
我们需要在虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
如果您正在使用 macOS,可能会出现这样的错误——RuntimeError: Python is not installed as a framework。请参阅matplolib文档以了解如何解决:matplotlib.org/faq/osx_framework.html。
操作步骤...
- 导入
matplotlib:
>>> import matplotlib.pyplot as plt
- 准备要在图表上显示的数据,以及应该显示的图例。每行由时间标签、
ProductA的销售额、ProductB的销售额和ProductC的销售额组成:
>>> LEGEND = ('ProductA', 'ProductB', 'ProductC')
>>> DATA = (
... ('Q1 2017', 100, 30, 3),
... ('Q2 2017', 105, 32, 15),
... ('Q3 2017', 125, 29, 40),
... ('Q4 2017', 115, 31, 80),
... )
- 将数据拆分为图表可用的格式。这是一个准备步骤:
>>> POS = list(range(len(DATA)))
>>> VALUESA = [valueA for label, valueA, valueB, valueC in DATA]
>>> VALUESB = [valueB for label, valueA, valueB, valueC in DATA]
>>> VALUESC = [valueC for label, valueA, valueB, valueC in DATA]
>>> LABELS = [label for label, valueA, valueB, valueC in DATA]
- 创建带有数据的条形图:
>>> WIDTH = 0.2
>>> plt.bar([p - WIDTH for p in POS], VALUESA, width=WIDTH)
>>> plt.bar([p for p in POS], VALUESB, width=WIDTH)
>>> plt.bar([p + WIDTH for p in POS], VALUESC, width=WIDTH)
>>> plt.ylabel('Sales')
>>> plt.xticks(POS, LABELS)
- 添加一个注释,显示图表中的最大增长:
>>> plt.annotate('400% growth', xy=(1.2, 18), xytext=(1.3, 40),
horizontalalignment='center',
arrowprops=dict(facecolor='black', shrink=0.05))
- 添加
legend:
>>> plt.legend(LEGEND)
- 显示图表:
>>> plt.show()
- 结果将显示在新窗口中:

它是如何工作的...
操作步骤的第 1 步和第 2 步准备了导入和将在条形图中显示的数据,格式类似于良好结构化的输入数据。在第 3 步中,数据被拆分成不同的数组,以准备在matplotlib中输入。基本上,每个数据序列都存储在不同的数组中。
第 4 步绘制数据。每个数据序列都会调用.bar,指定其位置和值。标签与.xticks相同。为了在标签周围分隔每个条形图,第一个条形图向左偏移,第三个向右偏移。
在第二季度的ProductC条形图上方添加了一个注释。请注意,注释包括xy中的点和xytext中的文本位置。
在第 6 步中,添加了图例。请注意,标签需要按照输入数据的顺序添加。图例会自动放置在不覆盖任何数据的区域。arroprops详细说明了指向数据的箭头。
最后,在第 7 步通过调用.show绘制图表。
调用.show会阻止程序的执行。当窗口关闭时,程序将恢复执行。
还有更多...
图例通常会自动显示,只需调用.legend即可。如果需要自定义它们的显示顺序,可以将每个标签指定给特定元素。例如,这种方式(注意它将ProductA称为valueC系列)
>>> valueA = plt.bar([p - WIDTH for p in POS], VALUESA, width=WIDTH)
>>> valueB = plt.bar([p for p in POS], VALUESB, width=WIDTH)
>>> valueC = plt.bar([p + WIDTH for p in POS], VALUESC, width=WIDTH)
>>> plt.legend((valueC, valueB, valueA), LEGEND)
图例的位置也可以通过loc参数手动更改。默认情况下,它是best,它会在数据最少重叠的区域绘制图例(理想情况下没有)。但是可以使用诸如right、upper left等值,或者特定的(X, Y)元组。
另一种选择是在图表之外绘制图例,使用bbox_to_anchor选项。在这种情况下,图例附加到边界框的(X,Y)位置,其中0是图表的左下角,1是右上角。这可能导致图例被外部边框剪切,因此您可能需要通过.subplots_adjust调整图表的起始和结束位置:
>>> plt.legend(LEGEND, title='Products', bbox_to_anchor=(1, 0.8))
>>> plt.subplots_adjust(right=0.80)
调整bbox_to_anchor参数和.subplots_adjust将需要一些试错,直到产生预期的结果。
.subplots_adjust引用了位置,作为将显示的轴的位置。这意味着right=0.80将在绘图的右侧留下 20%的屏幕空间,而左侧的默认值为 0.125,这意味着在绘图的左侧留下 12.5%的空间。有关更多详细信息,请参阅文档:matplotlib.org/api/_as_gen/matplotlib.pyplot.subplots_adjust.html。
注释可以以不同的样式进行,并可以通过不同的选项进行调整,例如连接方式等。例如,这段代码将创建一个箭头,使用fancy样式连接一个曲线。结果显示在这里:
plt.annotate('400% growth', xy=(1.2, 18), xytext=(1.3, 40),
horizontalalignment='center',
arrowprops={'facecolor': 'black',
'arrowstyle': "fancy",
'connectionstyle': "angle3",
})
在我们的方法中,我们没有精确地注释到条的末端(点(1.2,15)),而是略高于它,以留出一点空间。
调整注释的确切位置和文本的位置将需要进行一些测试。文本的位置也是通过寻找最佳位置来避免与条形图重叠而定位的。字体大小和颜色可以使用.legend和.annotate调用中的fontsize和color参数进行更改。
应用所有这些元素,图表可能看起来类似于这样。可以通过调用 GitHub 上的legend_and_annotation.py脚本来复制此图表:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter07/adding_legend_and_annotations.py:

完整的matplotlib文档可以在这里找到:matplotlib.org/。特别是图例的指南在这里:matplotlib.org/users/legend_guide.html#plotting-guide-legend,注释的指南在这里:matplotlib.org/users/annotations.html。
另请参阅
-
绘制堆叠条形图的方法
-
组合图表的方法
组合图表
可以在同一图表中组合多个图表。在这个方法中,我们将看到如何在同一图表上以两个不同的轴呈现数据,并如何在同一图表上添加更多的图表。
准备工作
我们需要在虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
如果您使用的是 macOS,可能会出现这样的错误——RuntimeError: Python is not installed as a framework。请参阅matplolib文档,了解如何解决此问题:matplotlib.org/faq/osx_framework.html。
如何做…
- 导入
matplotlib:
>>> import matplotlib.pyplot as plt
- 准备数据以在图表上显示,并显示应该显示的图例。每条线由时间标签、
ProductA的销售额和ProductB的销售额组成。请注意,ProductB的值远高于A:
>>> DATA = (
... ('Q1 2017', 100, 3000, 3),
... ('Q2 2017', 105, 3200, 5),
... ('Q3 2017', 125, 2900, 7),
... ('Q4 2017', 115, 3100, 3),
... )
- 准备独立数组中的数据:
>>> POS = list(range(len(DATA)))
>>> VALUESA = [valueA for label, valueA, valueB, valueC in DATA]
>>> VALUESB = [valueB for label, valueA, valueB, valueC in DATA]
>>> VALUESC = [valueC for label, valueA, valueB, valueC in DATA]
>>> LABELS = [label for label, valueA, valueB, valueC in DATA]
请注意,这将扩展并为每个值创建一个列表。
这些值也可以通过LABELS、VALUESA、VALUESB、VALUESC = ZIP(*DATA)进行扩展。
- 创建第一个子图:
>>> plt.subplot(2, 1, 1)
- 创建一个关于
VALUESA的条形图:
>>> valueA = plt.bar(POS, VALUESA)
>>> plt.ylabel('Sales A')
- 创建一个不同的Y轴,并将
VALUESB的信息添加为线图:
>>> plt.twinx()
>>> valueB = plt.plot(POS, VALUESB, 'o-', color='red')
>>> plt.ylabel('Sales B')
>>> plt.xticks(POS, LABELS)
- 创建另一个子图,并用
VALUESC填充它:
>>> plt.subplot(2, 1, 2)
>>> plt.plot(POS, VALUESC)
>>> plt.gca().set_ylim(ymin=0)
>>> plt.xticks(POS, LABELS)
- 显示图表:
>>> plt.show()
- 结果将显示在一个新窗口中:

它是如何工作的…
导入模块后,数据以一种方便的方式呈现在“如何做…”部分的第 2 步中,这很可能类似于数据最初的存储方式。第 3 步是一个准备步骤,将数据分割成不同的数组,以便进行下一步。
第 4 步创建一个新的.subplot。这将把整个图形分成两个元素。参数是行数、列数和所选的子图。因此,我们在一列中创建了两个子图,并在第一个子图中绘制。
第 5 步使用VALUESA数据在此子图中打印了一个.bar图,并使用.ylabel标记了Y轴为Sales A。
第 6 步使用.twinx创建一个新的Y轴,通过.plot绘制VALUESB为线图。标签使用.ylabel标记为Sales B。使用.xticks标记X轴。
VALUESB图形设置为红色,以避免两个图形具有相同的颜色。默认情况下,两种情况下的第一种颜色是相同的,这将导致混淆。数据点使用'o'选项标记。
在第 7 步中,我们使用.subplot切换到第二个子图。图形以线条形式打印VALUESC,然后使用.xticker在X轴上放置标签,并将Y轴的最小值设置为0。然后在第 8 步显示图形。
还有更多...
通常情况下,具有多个轴的图形很难阅读。只有在有充分理由这样做并且数据高度相关时才使用它们。
默认情况下,线图中的Y轴将尝试呈现Y值的最小值和最大值之间的信息。通常截断轴不是呈现信息的最佳方式,因为它可能扭曲感知的差异。例如,如果图形从 10 到 11,那么在 10 到 11 之间的值的变化可能看起来很重要,但这不到 10%。将Y轴最小值设置为0,使用plt.gca().set_ylim(ymin=0)是一个好主意,特别是在有两个不同的轴时。
选择子图的调用将首先按行,然后按列进行,因此.subplot(2, 2, 3)将选择第一列,第二行的子图。
分割的子图网格可以更改。首先调用.subplot(2, 2, 1)和.subplot(2, 2, 2),然后调用.subplot(2, 1, 2),将在第一行创建两个小图和第二行一个较宽的图的结构。返回将覆盖先前绘制的子图。
完整的matplotlib文档可以在这里找到:matplotlib.org/。特别是,图例指南在这里:matplotlib.org/users/legend_guide.html#plotting-guide-legend。有关注释的信息在这里:matplotlib.org/users/annotations.html。
另请参阅
-
绘制多条线教程
-
可视化地图教程
保存图表
一旦图表准备好,我们可以将其存储在硬盘上,以便在其他文档中引用。在本教程中,我们将看到如何以不同的格式保存图表。
准备工作
我们需要在虚拟环境中安装matplotlib:
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
如果您使用的是 macOS,可能会出现这样的错误——RuntimeError: Python is not installed as a framework。请参阅matplolib文档以了解如何解决此问题:matplotlib.org/faq/osx_framework.html。
如何做…
- 导入
matplotlib:
>>> import matplotlib.pyplot as plt
- 准备要显示在图表上的数据,并将其拆分为不同的数组:
>>> DATA = (
... ('Q1 2017', 100),
... ('Q2 2017', 150),
... ('Q3 2017', 125),
... ('Q4 2017', 175),
... )
>>> POS = list(range(len(DATA)))
>>> VALUES = [value for label, value in DATA]
>>> LABELS = [label for label, value in DATA]
- 使用数据创建条形图:
>>> plt.bar(POS, VALUES)
>>> plt.xticks(POS, LABELS)
>>> plt.ylabel('Sales')
- 将图表保存到硬盘:
>>> plt.savefig('data.png')
工作原理...
在如何做…部分的第 1 和第 2 步中导入和准备数据后,通过调用.bar在第 3 步生成图表。添加了一个.ylabel,并通过.xticks标记了X轴的适当时间描述。
第 4 步将文件保存到硬盘上,文件名为data.png。
还有更多...
图像的分辨率可以通过dpi参数确定。这将影响文件的大小。使用72到300之间的分辨率。较低的分辨率将难以阅读,较高的分辨率除非图形的大小巨大,否则没有意义:
>>> plt.savefig('data.png', dpi=72)
matplotlib了解如何存储最常见的文件格式,如 JPEG、PDF 和 PNG。当文件名具有适当的扩展名时,它将自动使用。
除非您有特定要求,否则请使用 PNG。与其他格式相比,它在存储具有有限颜色的图形时非常高效。如果您需要找到所有支持的文件,可以调用plt.gcf().canvas.get_supported_filetypes()。
完整的matplotlib文档可以在这里找到:matplotlib.org/。特别是图例指南在这里:matplotlib.org/users/legend_guide.html#plotting-guide-legend。有关注释的信息在这里:matplotlib.org/users/annotations.html。
另请参阅
-
绘制简单销售图配方
-
添加图例和注释配方
第八章:处理通信渠道
在本章中,我们将涵盖以下配方:
-
使用电子邮件模板
-
发送单个电子邮件
-
阅读电子邮件
-
将订阅者添加到电子邮件通讯中
-
通过电子邮件发送通知
-
生成短信
-
接收短信
-
创建一个 Telegram 机器人
介绍
处理通信渠道是自动化事务可以产生巨大收益的地方。在本配方中,我们将看到如何处理两种最常见的通信渠道——电子邮件,包括新闻通讯,以及通过电话发送和接收短信。
多年来,交付方法中存在相当多的滥用,如垃圾邮件或未经请求的营销信息,这使得与外部工具合作以避免消息被自动过滤器自动拒绝成为必要。我们将在适用的情况下提出适当的注意事项。所有工具都有很好的文档,所以不要害怕阅读它。它们还有很多功能,它们可能能够做一些正是你所寻找的东西。
使用电子邮件模板
要发送电子邮件,我们首先需要生成其内容。在本配方中,我们将看到如何生成适当的模板,既以纯文本样式又以 HTML 样式。
准备就绪
我们应该首先安装mistune模块,它将 Markdown 文档编译为 HTML。我们还将使用jinja2模块将 HTML 与我们的文本组合在一起。
$ echo "mistune==0.8.3" >> requirements.txt
$ echo "jinja2==2.20" >> requirements.txt
$ pip install -r requirements.txt
在 GitHub 存储库中,有一些我们将使用的模板——email_template.md在github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter08/email_template.md和一个用于样式的模板,email_styling.html在github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter08/email_styling.html。
如何做...
- 导入模块:
>>> import mistune
>>> import jinja2
- 从磁盘读取两个模板:
>>> with open('email_template.md') as md_file:
... markdown = md_file.read()
>>> with open('email_styling.html') as styling_file:
... styling = styling_file.read()
- 定义要包含在模板中的
data。模板非常简单,只接受一个参数:
>>> data = {'name': 'Seamus'}
- 呈现 Markdown 模板。这会产生
data的纯文本版本:
>>> text = markdown.format(**data)
- 呈现 Markdown 并添加样式:
>>> html_content = mistune.markdown(text)
>>> html = jinja2.Template(styling).render(content=html_content)
- 将文本和 HTML 版本保存到磁盘以进行检查:
>>> with open('text_version.txt', 'w') as fp:
... fp.write(text)
>>> with open('html_version.html', 'w') as fp:
... fp.write(html)
- 检查文本版本:
$ cat text_version.txt
Hi Seamus:
This is an email talking about **things**
### Very important info
1\. One thing
2\. Other thing
3\. Some extra detail
Best regards,
*The email team*
- 在浏览器中检查 HTML 版本:

它是如何工作的...
第 1 步获取稍后将使用的模块,第 2 步读取将呈现的两个模板。email_template.md是内容的基础,它是一个 Markdown 模板。email_styling.html是一个包含基本 HTML 环绕和 CSS 样式信息的 HTML 模板。
基本结构是以 Markdown 格式创建内容。这是一个可读的纯文本文件,可以作为电子邮件的一部分发送。然后可以将该内容转换为 HTML,并添加一些样式来创建 HTML 函数。email_styling.html有一个内容区域,用于放置从 Markdown 呈现的 HTML。
第 3 步定义了将在email_template.md中呈现的数据。这是一个非常简单的模板,只需要一个名为name的参数。
在第 4 步,Markdown 模板与data一起呈现。这会产生电子邮件的纯文本版本。
第 5 步呈现了HTML版本。使用mistune将纯文本版本呈现为HTML,然后使用jinja2模板将其包装在email_styling.html中。最终版本是一个独立的 HTML 文档。
最后,我们将两个版本,纯文本(作为text)和 HTML(作为html),保存到文件中的第 6 步。第 7 步和第 8 步检查存储的值。信息是相同的,但在HTML版本中,它是有样式的。
还有更多...
使用 Markdown 可以轻松生成包含文本和 HTML 的双重电子邮件。Markdown 在文本格式中非常易读,并且可以自然地呈现为 HTML。也就是说,可以生成完全不同的 HTML 版本,这将允许更多的自定义和利用 HTML 的特性。
完整的 Markdown 语法可以在daringfireball.net/projects/markdown/syntax找到,最常用元素的好的速查表在beegit.com/markdown-cheat-sheet。
虽然制作电子邮件的纯文本版本并不是绝对必要的,但这是一个很好的做法,表明您关心谁阅读了电子邮件。大多数电子邮件客户端接受 HTML,但并非完全通用。
对于 HTML 电子邮件,请注意整个样式应该包含在电子邮件中。这意味着 CSS 需要嵌入到 HTML 中。避免进行可能导致电子邮件在某些电子邮件客户端中无法正确呈现,甚至被视为垃圾邮件的外部调用。
email_styling.html中的样式基于可以在markdowncss.github.io/找到的modest样式。还有更多可以使用的 CSS 样式,可以在 Google 中搜索找到更多。请记住删除任何外部引用,如前面所讨论的。
可以通过以base64格式对图像进行编码,以便直接嵌入 HTMLimg标签中,而不是添加引用,将图像包含在 HTML 中。
>>> import base64
>>> with open("image.png",'rb') as file:
... encoded_data = base64.b64encode(file) >>> print "<img src='data:image/png;base64,{data}'/>".format(data=encoded_data)
您可以在css-tricks.com/data-uris/的文章中找到有关此技术的更多信息。
mistune完整文档可在mistune.readthedocs.io/en/latest/找到,jinja2文档可在jinja.pocoo.org/docs/2.10/找到。
另请参阅
-
第五章中的在 Markdown 中格式化文本食谱,生成精彩的报告
-
第五章中的使用模板生成报告食谱,生成精彩的报告
-
第五章中的发送事务性电子邮件食谱,生成精彩的报告
发送单个电子邮件
发送电子邮件的最基本方法是从电子邮件帐户发送单个电子邮件。这个选项只建议用于非常零星的使用,但对于简单的目的,比如每天向受控地址发送几封电子邮件,这可能足够了。
不要使用此方法向分发列表或具有未知电子邮件地址的客户批量发送电子邮件。您可能因反垃圾邮件规则而被服务提供商禁止。有关更多选项,请参阅本章中的其他食谱。
准备工作
对于这个示例,我们将需要一个带有服务提供商的电子邮件帐户。根据要使用的提供商有一些小的差异,但我们将使用 Gmail 帐户,因为它非常常见且免费访问。
由于 Gmail 的安全性,我们需要创建一个特定的应用程序密码,用于发送电子邮件。请按照这里的说明操作:support.google.com/accounts/answer/185833。这将有助于为此示例生成一个密码。记得为邮件访问创建它。您可以随后删除密码以将其删除。
我们将使用 Python 标准库中的smtplib模块。
如何做...
- 导入
smtplib和email模块:
>>> import smtplib
>>> from email.mime.multipart import MIMEMultipart
>>> from email.mime.text import MIMEText
- 设置凭据,用您自己的凭据替换这些。出于测试目的,我们将发送到相同的电子邮件,但请随意使用不同的地址:
>>> USER = 'your.account@gmail.com'
>>> PASSWORD = 'YourPassword'
>>> sent_from = USER
>>> send_to = [USER]
- 定义要发送的数据。注意两种选择,纯文本和 HTML:
>>> text = "Hi!\nThis is the text version linking to https://www.packtpub.com/\nCheers!"
>>> html = """<html><head></head><body>
... <p>Hi!<br>
... This is the HTML version linking to <a href="https://www.packtpub.com/">Packt</a><br>
... </p>
... </body></html>
"""
- 将消息组成为
MIME多部分,包括主题,收件人和发件人:
>>> msg = MIMEMultipart('alternative')
>>> msg['Subject'] = 'An interesting email'
>>> msg['From'] = sent_from
>>> msg['To'] = ', '.join(send_to)
- 填写电子邮件的数据内容部分:
>>> part_plain = MIMEText(text, 'plain')
>>> part_html = MIMEText(html, 'html')
>>> msg.attach(part_plain)
>>> msg.attach(part_html)
- 使用
SMTP SSL协议发送电子邮件:
>>> with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
... server.login(USER, PASSWORD)
... server.sendmail(sent_from, send_to, msg.as_string())
- 邮件已发送。检查您的电子邮件帐户是否收到了消息。检查原始电子邮件,您可以看到完整的原始电子邮件,其中包含 HTML 和纯文本元素。电子邮件已被编辑:

工作原理...
在第 1 步之后,从stmplib和email进行相关导入,第 2 步定义了从 Gmail 获取的凭据。
第 3 步显示了将要发送的 HTML 和文本。它们是替代方案,因此它们应该呈现相同的信息,但以不同的格式呈现。
基本的消息信息在第 4 步中设置。它指定了电子邮件的主题,以及from和to。第 5 步添加了多个部分,每个部分都有适当的MIMEText类型。
最后添加的部分是首选的替代方案,根据MIME格式,因此我们最后添加了HTML部分。
第 6 步建立与服务器的连接,使用凭据登录并发送消息。它使用with上下文来获取连接。
如果凭据出现错误,它将引发一个异常,显示用户名和密码不被接受。
还有更多...
请注意,sent_to是一个地址列表。您可以将电子邮件发送给多个地址。唯一的注意事项在第 4 步,需要将其指定为所有地址的逗号分隔值列表。
尽管可以将sent_from标记为与发送电子邮件时使用的地址不同,但并不建议这样做。这可能被解释为试图伪造电子邮件的来源,并引发检测为垃圾邮件来源的迹象。
此处使用的服务器smtp.gmail.com*是 Gmail 指定的服务器,并且SMTPS(安全SMTP)的定义端口为465。Gmail 还接受端口587,这是标准端口,但需要您通过调用.starttls指定会话的类型,如下面的代码所示:
with smtplib.SMTP('smtp.gmail.com', 587) as server:
server.starttls()
server.login(USER, PASSWORD)
server.sendmail(sent_from, send_to, msg.as_string())
如果您对这些差异和两种协议的更多细节感兴趣,可以在这篇文章中找到更多信息:www.fastmail.com/help/technical/ssltlsstarttls.html。
完整的smtplib文档可以在docs.python.org/3/library/smtplib.html找到,email模块中包含有关电子邮件不同格式的信息,包括MIME类型的示例,可以在这里找到:docs.python.org/3/library/email.html。
另请参阅
-
使用电子邮件模板示例
-
发送单个电子邮件示例
阅读电子邮件
在本示例中,我们将看到如何从帐户中读取电子邮件。我们将使用IMAP4标准,这是最常用的用于阅读电子邮件的标准。
一旦读取,电子邮件可以被自动处理和分析,以生成智能自动响应、将电子邮件转发到不同的目标、聚合结果进行监控等操作。选项是无限的!
准备就绪
对于此示例,我们将需要一个带有服务提供商的电子邮件帐户。基于要使用的提供商的小差异,但我们将使用 Gmail 帐户,因为它非常常见且免费访问。
由于 Gmail 的安全性,我们需要创建一个特定的应用程序密码来发送电子邮件。请按照这里的说明操作:support.google.com/accounts/answer/185833。这将为此示例生成一个密码。记得为邮件创建它。您可以在之后删除密码以将其删除。
我们将使用 Python 标准库中的imaplib模块。
该示例将读取最后收到的电子邮件,因此您可以使用它更好地控制将要读取的内容。我们将发送一封看起来像是发送给支持的简短电子邮件。
如何做...
- 导入
imaplib和email模块:
>>> import imaplib
>>> import email
>>> from email.parser import BytesParser, Parser
>>> from email.policy import default
- 设置凭据,用您自己的凭据替换这些:
>>> USER = 'your.account@gmail.com'
>>> PASSWORD = 'YourPassword'
- 连接到电子邮件服务器:
>>> mail = imaplib.IMAP4_SSL('imap.gmail.com')
>>> mail.login(USER, PASSWORD)
- 选择收件箱文件夹:
>>> mail.select('inbox')
- 读取所有电子邮件 UID 并检索最新收到的电子邮件:
>>> result, data = mail.uid('search', None, 'ALL')
>>> latest_email_uid = data[0].split()[-1]
>>> result, data = mail.uid('fetch', latest_email_uid, '(RFC822)')
>>> raw_email = data[0][1]
- 将电子邮件解析为 Python 对象:
>>> email_message = BytesParser(policy=default).parsebytes(raw_email)
- 显示电子邮件的主题和发件人:
>>> email_message['subject']
'[Ref ABCDEF] Subject: Product A'
>>> email.utils.parseaddr(email_message['From'])
('Sender name', 'sender@gmail.com')
- 检索文本的有效载荷:
>>> email_type = email_message.get_content_maintype()
>>> if email_type == 'multipart':
... for part in email_message.get_payload():
... if part.get_content_type() == 'text/plain':
... payload = part.get_payload()
... elif email_type == 'text':
... payload = email_message.get_payload()
>>> print(payload)
Hi:
I'm having difficulties getting into my account. What was the URL, again?
Thanks!
A confuser customer
工作原理...
导入将要使用的模块并定义凭据后,我们在第 3 步连接到服务器。
第 4 步连接到inbox。这是 Gmail 中包含收件箱的默认文件夹,其中包含收到的电子邮件。
当然,您可能需要阅读不同的文件夹。您可以通过调用mail.list()来获取所有文件夹的列表。
在第 5 步,首先通过调用.uid('search', None, "ALL")检索收件箱中所有电子邮件的 UID 列表。然后通过fetch操作和.uid('fetch', latest_email_uid, '(RFC822)')再次从服务器检索最新收到的电子邮件。这将以 RFC822 格式检索电子邮件,这是标准格式。请注意,检索电子邮件会将其标记为已读。
.uid命令允许我们调用 IMAP4 命令,返回一个带有结果(OK或NO)和数据的元组。如果出现错误,它将引发适当的异常。
BytesParser模块用于将原始的RFC822电子邮件转换为 Python 对象。这是在第 6 步完成的。
元数据,包括主题、发件人和时间戳等详细信息,可以像字典一样访问,如第 7 步所示。地址可以从原始文本格式解析为带有email.utils.parseaddr的部分。
最后,内容可以展开和提取。如果电子邮件的类型是多部分的,可以通过迭代.get_payload()来提取每个部分。最容易处理的是plain/text,因此假设它存在,第 8 步中的代码将提取它。
电子邮件正文存储在payload变量中。
还有更多...
在第 5 步,我们正在检索收件箱中的所有电子邮件,但这并非必要。搜索可以进行过滤,例如只检索最近一天的电子邮件:
import datetime
since = (datetime.date.today() - datetime.timedelta(days=1)).strftime("%d-%b-%Y")
result, data = mail.uid('search', None, f'(SENTSINCE {since})')
这将根据电子邮件的日期进行搜索。请注意,分辨率以天为单位。
还有更多可以通过IMAP4完成的操作。查看 RFC 3501 tools.ietf.org/html/rfc3501和 RFC 6851 tools.ietf.org/html/rfc6851以获取更多详细信息。
RFC 描述了 IMAP4 协议,可能有点枯燥。检查可能的操作将让您了解详细调查的可能性,可能通过 Google 搜索示例。
可以解析和处理电子邮件的主题和正文,以及日期、收件人、发件人等其他元数据。例如,本食谱中检索的主题可以按以下方式处理:
>>> import re
>>> re.search(r'\[Ref (\w+)] Subject: (\w+)', '[Ref ABCDEF] Subject: Product A').groups()
('ABCDEF', 'Product')
有关正则表达式和其他解析信息的更多信息,请参见第一章,让我们开始自动化之旅。
另请参阅
- 第一章中的介绍正则表达式食谱,让我们开始自动化之旅
向电子邮件通讯订阅者添加订阅者
常见的营销工具是电子邮件通讯。它们是向多个目标发送信息的便捷方式。一个好的通讯系统很难实现,推荐的方法是使用市场上可用的。一个著名的是 MailChimp (mailchimp.com/)。
MailChimp 有很多可能性,但与本书相关的有趣之一是其 API,可以编写脚本来自动化工具。这个 RESTful API 可以通过 Python 访问。在这个食谱中,我们将看到如何向现有列表添加更多的订阅者。
准备就绪
由于我们将使用 MailChimp,因此需要有一个可用的帐户。您可以在login.mailchimp.com/signup/上创建一个免费帐户。
创建帐户后,请确保至少有一个我们将向其添加订阅者的列表。作为注册的一部分,可能已经创建了。它将显示在列表下:

列表将包含已订阅的用户。
对于 API,我们将需要一个 API 密钥。转到帐户|额外|API 密钥并创建一个新的:

我们将使用requests模块来访问 API。将其添加到您的虚拟环境中:
$ echo "requests==2.18.3" >> requirements.txt
$ pip install -r requirements.txt
MailChimp API 使用DC(数据中心)的概念,您的帐户使用它。这可以从您的 API 的最后几位数字中获得,或者从 MailChimp 管理站点的 URL 开头获得。例如,在所有先前的截图中,它是us19。
如何做...
- 导入
requests模块:
>>> import requests
- 定义身份验证和基本 URL。基本 URL 需要在开头加上您的
dc(例如us19):
>>> API = 'your secret key'
>>> BASE = 'https://<dc>.api.mailchimp.com/3.0'
>>> auth = ('user', API)
- 获取所有列表:
>>> url = f'{BASE}/lists'
>>> response = requests.get(url, auth=auth)
>>> result = response.json()
- 过滤列表以获取所需列表的
href:
>>> LIST_NAME = 'Your list name'
>>> this_list = [l for l in result['lists'] if l['name'] == LIST_NAME][0]
>>> list_url = [l['href'] for l in this_list['_links'] if l['rel'] == 'self'][0]
- 使用列表 URL,您可以获取列表成员的 URL:
>>> response = requests.get(list_url, auth=auth)
>>> result = response.json()
>>> result['stats']
{'member_count': 1, 'unsubscribe_count': 0, 'cleaned_count': 0, ...}
>>> members_url = [l['href'] for l in result['_links'] if l['rel'] == 'members'][0]
- 可以通过向
members_url发出GET请求来检索成员列表:
>>> response = requests.get(members_url, json=new_member, auth=auth)
>>> result = response.json()
>>> len(result['members'])
1
- 向列表添加新成员:
>>> new_member = {
'email_address': 'test@test.com',
'status': 'subscribed',
}
>>> response = requests.post(members_url, json=new_member, auth=auth)
- 使用
GET获取用户列表会获取到所有用户:
>>> response = requests.post(members_url, json=new_member, auth=auth)
>>> result = response.json()
>>> len(result['members'])
2
工作原理...
在第 1 步导入 requests 模块后,在第 2 步定义连接的基本值,即基本 URL 和凭据。请注意,对于身份验证,我们只需要 API 密钥作为密码,以及任何用户(如 MailChimp 文档所述:developer.mailchimp.com/documentation/mailchimp/guides/get-started-with-mailchimp-api-3/)。
第 3 步检索所有列表,调用适当的 URL。结果以 JSON 格式返回。调用包括具有定义凭据的auth参数。所有后续调用都将使用该auth参数进行身份验证。
第 4 步显示了如何过滤返回的列表以获取感兴趣的特定列表的 URL。每个返回的调用都包括一系列与相关信息的_links列表,使得可以通过 API 进行遍历。
在第 5 步调用列表的 URL。这将返回列表的信息,包括基本统计信息。类似于第 4 步的过滤,我们检索成员的 URL。
由于尺寸限制和显示相关数据,未显示所有检索到的元素。请随时进行交互式分析并了解它们。数据构造良好,遵循 RESTful 的可发现性原则;再加上 Python 的内省能力,使其非常易读和易懂。
第 6 步检索成员列表,向members_url发出GET请求,可以将其视为单个用户。这可以在网页界面的Getting Ready部分中看到。
第 7 步创建一个新用户,并在members_url上发布json参数中传递的信息,以便将其转换为 JSON 格式。第 7 步检索更新后的数据,显示列表中有一个新用户。
还有更多...
完整的 MailChimp API 非常强大,可以执行大量任务。请查看完整的 MailChimp 文档以发现所有可能性:developer.mailchimp.com/。
简要说明一下,超出了本书的范围,请注意向自动列表添加订阅者的法律影响。垃圾邮件是一个严重的问题,有新的法规来保护客户的权利,如 GPDR。确保您有用户的许可才能给他们发送电子邮件。好消息是,MailChimp 自动实现了帮助解决这个问题的工具,如自动退订按钮。
一般的 MailChimp 文档也非常有趣,展示了许多可能性。MailChimp 能够管理通讯和一般的分发列表,但也可以定制生成流程,安排发送电子邮件,并根据参数(如生日)自动向您的受众发送消息。
另请参阅
-
发送单个电子邮件配方
-
发送交易电子邮件配方
通过电子邮件发送通知
在这个配方中,我们将介绍如何发送将发送给客户的电子邮件。作为对用户操作的响应发送的电子邮件,例如确认电子邮件或警报电子邮件,称为交易电子邮件。由于垃圾邮件保护和其他限制,最好使用外部工具来实现这种类型的电子邮件。
在这个配方中,我们将使用 Mailgun (www.mailgun.com),它能够发送这种类型的电子邮件,并与之通信。
准备工作
我们需要在 Mailgun 中创建一个帐户。转到signup.mailgun.com创建一个。请注意,信用卡信息是可选的。
注册后,转到域以查看是否有沙箱环境。我们可以使用它来测试功能,尽管它只会向注册的测试电子邮件帐户发送电子邮件。API 凭据将显示在那里:

我们需要注册帐户,以便我们将作为授权收件人收到电子邮件。您可以在此处添加:

要验证帐户,请检查授权收件人的电子邮件并确认。电子邮件地址现在已准备好接收测试邮件:

我们将使用requests模块来连接 Mailgun API。在虚拟环境中安装它:
$ echo "requests==2.18.3" >> requirements.txt
$ pip install -r requirements.txt
一切准备就绪,可以发送电子邮件,但请注意只发送给授权收件人。要能够在任何地方发送电子邮件,我们需要设置域。请参阅 Mailgun 文档:documentation.mailgun.com/en/latest/quickstart-sending.html#verify-your-domain。
如何做...
- 导入
requests模块:
>>> import requests
- 准备凭据,以及要发送和接收的电子邮件。请注意,我们正在使用模拟发件人:
>>> KEY = 'YOUR-SECRET-KEY'
>>> DOMAIN = 'YOUR-DOMAIN.mailgun.org'
>>> TO = 'YOUR-AUTHORISED-RECEIVER'
>>> FROM = f'sender@{DOMAIN}'
>>> auth = ('api', KEY)
- 准备要发送的电子邮件。这里有 HTML 版本和备用纯文本版本:
>>> text = "Hi!\nThis is the text version linking to https://www.packtpub.com/\nCheers!"
>>> html = '''<html><head></head><body>
... <p>Hi!<br>
... This is the HTML version linking to <a href="https://www.packtpub.com/">Packt</a><br>
... </p>
... </body></html>'''
- 设置要发送到 Mailgun 的数据:
>>> data = {
... 'from': f'Sender <{FROM}>',
... 'to': f'Jaime Buelta <{TO}>',
... 'subject': 'An interesting email!',
... 'text': text,
... 'html': html,
... }
- 调用 API:
>>> response = requests.post(f"https://api.mailgun.net/v3/{DOMAIN}/messages", auth=auth, data=data)
>>> response.json()
{'id': '<YOUR-ID.mailgun.org>', 'message': 'Queued. Thank you.'}
- 检索事件并检查电子邮件是否已发送:
>>> response_events = requests.get(f'https://api.mailgun.net/v3/{DOMAIN}/events', auth=auth)
>>> response_events.json()['items'][0]['recipient'] == TO
True
>>> response_events.json()['items'][0]['event']
'delivered'
- 邮件应该出现在您的收件箱中。由于它是通过沙箱环境发送的,请确保在直接显示时检查您的垃圾邮件文件夹。
它是如何工作的...
第 1 步导入requests模块以供以后使用。第 2 步定义了凭据和消息中的基本信息,并应从 Mailgun Web 界面中提取,如前所示。
第 3 步定义将要发送的电子邮件。第 4 步将信息结构化为 Mailgun 所期望的方式。请注意html和text字段。默认情况下,它将设置 HTML 为首选项,并将纯文本选项作为备选项。TO和FROM的格式应为Name <address>格式。您可以使用逗号将多个收件人分隔在TO中。
在第 5 步进行 API 调用。这是对消息端点的POST调用。数据以标准方式传输,并使用auth参数进行基本身份验证。请注意第 2 步中的定义。所有对 Mailgun 的调用都应包括此参数。它返回一条消息,通知您它已成功排队了消息。
在第 6 步,通过GET请求调用检索事件。这将显示最新执行的操作,其中最后一个将是最近的发送。还可以找到有关交付的信息。
还有更多...
要发送电子邮件,您需要设置用于发送电子邮件的域,而不是使用沙箱环境。您可以在这里找到说明:documentation.mailgun.com/en/latest/quickstart-sending.html#verify-your-domain。这需要您更改 DNS 记录以验证您是其合法所有者,并提高电子邮件的可交付性。
电子邮件可以以以下方式包含附件:
attachments = [("attachment", ("attachment1.jpg", open("image.jpg","rb").read())),
("attachment", ("attachment2.txt", open("text.txt","rb").read()))]
response = requests.post(f"https://api.mailgun.net/v3/{DOMAIN}/messages",
auth=auth, files=attachments, data=data)
数据可以包括常规信息,如cc或bcc,但您还可以使用o:deliverytime参数将交付延迟最多三天:
import datetime
import email.utils
delivery_time = datetime.datetime.now() + datetime.timedelta(days=1)
data = {
...
'o:deliverytime': email.utils.format_datetime(delivery_time),
}
Mailgun 还可以用于接收电子邮件并在其到达时触发流程,例如,根据规则转发它们。查看 Mailgun 文档以获取更多信息。
完整的 Mailgun 文档可以在这里找到,documentation.mailgun.com/en/latest/quickstart.html。一定要检查他们的最佳实践部分(documentation.mailgun.com/en/latest/best_practices.html#email-best-practices),以了解发送电子邮件的世界以及如何避免被标记为垃圾邮件。
另请参阅
-
使用电子邮件模板配方
-
发送单个电子邮件配方
生成短信
最广泛使用的通信渠道之一是短信。短信非常方便用于分发信息。
短信可以用于营销目的,也可以用作警报或发送通知的方式,或者最近非常常见的是作为实施双因素身份验证系统的一种方式。
我们将使用 Twilio,这是一个提供 API 以轻松发送短信的服务。
准备就绪
我们需要在www.twilio.com/为 Twilio 创建一个帐户。转到该页面并注册一个新帐户。
您需要按照说明设置一个电话号码来接收消息。您需要输入发送到此电话的代码或接听电话以验证此线路。
创建一个新项目并检查仪表板。从那里,您将能够创建第一个电话号码,能够接收和发送短信:

一旦号码配置完成,它将出现在所有产品和服务 | 电话号码.的活动号码部分。
在主仪表板上,检查ACCOUNT SID和AUTH TOKEN。稍后将使用它们。请注意,您需要显示身份验证令牌。
我们还需要安装twilio模块。将其添加到您的虚拟环境中:
$ echo "twilio==6.16.1" >> requirements.txt
$ pip install -r requirements.txt
请注意,接收者电话号码只能是经过试用账户验证的号码。您可以验证多个号码;请参阅support.twilio.com/hc/en-us/articles/223180048-Adding-a-Verified-Phone-Number-or-Caller-ID-with-Twilio上的文档。
如何做...
- 从
twilio模块导入Client:
>>> from twilio.rest import Client
- 在之前从仪表板获取的身份验证凭据。还要设置您的 Twilio 电话号码;例如,这里我们设置了
+353 12 345 6789,一个虚假的爱尔兰号码。它将是您国家的本地号码:
>>> ACCOUNT_SID = 'Your account SID'
>>> AUTH_TOKEN = 'Your secret token'
>>> FROM = '+353 12 345 6789'
- 启动
client以访问 API:
>>> client = Client(ACCOUNT_SID, AUTH_TOKEN)
- 向您授权的电话号码发送一条消息。请注意
from_末尾的下划线:
>>> message = client.messages.create(body='This is a test message from Python!',
from_=FROM,
to='+your authorised number')
- 您将收到一条短信到您的手机:

它是如何工作的...
使用 Twilio 客户端发送消息非常简单。
在第 1 步,我们导入Client,并准备在第 2 步配置的凭据和电话号码。
第 3 步使用适当的身份验证创建客户端,并在第 4 步发送消息。
请注意,to号码需要是试用帐户中经过身份验证的号码之一,否则将产生错误。您可以添加更多经过身份验证的号码;请查看 Twilio 文档。
从试用帐户发送的所有消息都将在短信中包含该详细信息,正如您在第 5 步中所看到的。
还有更多...
在某些地区(在撰写本文时为美国和加拿大),短信号码具有发送 MMS 消息(包括图像)的功能。要将图像附加到消息中,请添加media_url参数和要发送的图像的 URL:
client.messages.create(body='An MMS message',
media_url='http://my.image.com/image.png',
from_=FROM,
to='+your authorised number')
客户端基于 RESTful API,并允许您执行多个操作,例如创建新的电话号码,或首先获取一个可用的号码,然后购买它:
available_numbers = client.available_phone_numbers("IE").local.list()
number = available_numbers[0]
new_number = client.incoming_phone_numbers.create(phone_number=number.phone_number)
查看文档以获取更多可用操作,但大多数仪表板的点按操作都可以以编程方式执行。
Twilio 还能够执行其他电话服务,如电话呼叫和文本转语音。请在完整文档中查看。
完整的 Twilio 文档在此处可用:www.twilio.com/docs/。
另请参阅
-
接收短信配方
-
创建 Telegram 机器人配方
接收短信
短信也可以自动接收和处理。这使得可以提供按请求提供信息的服务(例如,发送 INFO GOALS 以接收足球联赛的结果),但也可以进行更复杂的流程,例如在机器人中,它可以与用户进行简单的对话,从而实现诸如远程配置恒温器之类的丰富服务。
每当 Twilio 接收到您注册的电话号码之一的短信时,它会执行对公开可用的 URL 的请求。这在服务中进行配置,这意味着它应该在您的控制之下。这会产生一个问题,即在互联网上有一个在您控制之下的 URL。这意味着仅仅您的本地计算机是行不通的,因为它是不可寻址的。我们将使用 Heroku(heroku.com)来提供一个可用的服务,但也有其他选择。Twilio 文档中有使用grok的示例,它允许通过在公共地址和您的本地开发环境之间创建隧道来进行本地开发。有关更多详细信息,请参见此处:www.twilio.com/blog/2013/10/test-your-webhooks-locally-with-ngrok.html。
这种操作方式在通信 API 中很常见。值得注意的是,Twilio 有一个 WhatsApp 的 beta API,其工作方式类似。请查看文档以获取更多信息:www.twilio.com/docs/sms/whatsapp/quickstart/python。
准备就绪
我们需要在www.twilio.com/为 Twilio 创建一个帐户。有关详细说明,请参阅准备就绪部分中生成短信配方。
对于这个配方,我们还需要在 Heroku(www.heroku.com/)中设置一个 Web 服务,以便能够创建一个能够接收发送给 Twilio 的短信的 Webhook。因为这个配方的主要目标是短信部分,所以在设置 Heroku 时我们将简洁一些,但您可以参考其出色的文档。它非常易于使用:
-
在 Heroku 中创建一个帐户。
-
您需要安装 Heroku 的命令行界面(所有平台的说明都在
devcenter.heroku.com/articles/getting-started-with-python#set-up),然后登录到命令行:
$ heroku login
Enter your Heroku credentials.
Email: your.user@server.com
Password:
-
从
github.com/datademofun/heroku-basic-flask下载一个基本的 Heroku 模板。我们将把它用作服务器的基础。 -
将
twilio客户端添加到requirements.txt文件中:
$ echo "twilio" >> requirements.txt
- 用 GitHub 中的
app.py替换app.py:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter08/app.py。
您可以保留现有的app.py来检查模板示例和 Heroku 的工作原理。查看github.com/datademofun/heroku-basic-flask中的 README。
- 完成后,将更改提交到 Git:
$ git add .
$ git commit -m 'first commit'
- 在 Heroku 中创建一个新服务。它将随机生成一个新的服务名称(我们在这里使用
service-name-12345)。此 URL 是可访问的:
$ heroku create
Creating app... done, ⬢ SERVICE-NAME-12345
https://service-name-12345.herokuapp.com/ | https://git.heroku.com/service-name-12345.git
- 部署服务。在 Heroku 中,部署服务会将代码推送到远程 Git 服务器:
$ git push heroku master
...
remote: Verifying deploy... done.
To https://git.heroku.com/service-name-12345.git
b6cd95a..367a994 master -> master
- 检查 Webhook URL 的服务是否正在运行。请注意,它显示为上一步的输出。您也可以在浏览器中检查:
$ curl https://service-name-12345.herokuapp.com/
All working!
如何做...
- 转到 Twilio 并访问 PHONE NUMBER 部分。配置 Webhook URL。这将使 URL 在每次收到短信时被调用。转到 All Products and Services | Phone Numbers 中的 Active Numbers 部分,并填写 Webhook。请注意 Webhook 末尾的
/sms。单击保存:

- 服务现在已经启动并可以使用。向您的 Twilio 电话号码发送短信,您应该会收到自动回复:

请注意,模糊的部分应该用您的信息替换。
如果您有试用账户,您只能向您授权的电话号码之一发送消息,所以您需要从它们发送文本。
它是如何工作的...
第 1 步设置了 Webhook,因此 Twilio 在电话线上收到短信时调用您的 Heroku 应用程序。
让我们看看app.py中的代码,看看它是如何工作的。这里为了清晰起见对其进行了编辑;请在github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter08/app.py中查看完整文件:
...
@app.route('/')
def homepage():
return 'All working!'
@app.route("/sms", methods=['GET', 'POST'])
def sms_reply():
from_number = request.form['From']
body = request.form['Body']
resp = MessagingResponse()
msg = (f'Awwwww! Thanks so much for your message {from_number}, '
f'"{body}" to you too.')
resp.message(msg)
return str(resp)
...
app.py可以分为三个部分——文件开头的 Python 导入和 Flask 应用程序的启动,这只是设置 Flask(此处不显示);调用homepage,用于测试服务器是否正常工作;和sms_reply,这是魔术发生的地方。
sms_reply函数从request.form字典中获取发送短信的电话号码以及消息的正文。然后,在msg中组成一个响应,将其附加到一个新的MessagingResponse,并返回它。
我们正在将用户的消息作为一个整体使用,但请记住第一章中提到的解析文本的所有技术,让我们开始自动化之旅。它们都适用于在此处检测预定义操作或任何其他文本处理。
返回的值将由 Twilio 发送回发送者,产生步骤 2 中看到的结果。
还有更多...
要能够生成自动对话,对话的状态应该被存储。对于高级状态,它可能应该被存储在数据库中,生成一个流程,但对于简单情况,将信息存储在session中可能足够了。会话能够在 cookies 中存储信息,这些信息在相同的来去电话号码组合之间是持久的,允许您在消息之间检索它。
例如,此修改将返回不仅发送正文,还有先前的正文。只包括相关部分:
app = Flask(__name__)
app.secret_key = b'somethingreallysecret!!!!'
...
@app.route("/sms", methods=['GET', 'POST'])
def sms_reply():
from_number = request.form['From']
last_message = session.get('MESSAGE', None)
body = request.form['Body']
resp = MessagingResponse()
msg = (f'Awwwww! Thanks so much for your message {from_number}, '
f'"{body}" to you too. ')
if last_message:
msg += f'Not so long ago you said "{last_message}" to me..'
session['MESSAGE'] = body
resp.message(msg)
return str(resp)
上一个body存储在会话的MESSAGE键中,会话会被保留。注意使用会话数据需要秘密密钥的要求。阅读此处的信息:flask.pocoo.org/docs/1.0/quickstart/?highlight=session#sessions。
要在 Heroku 中部署新版本,将新的app.py提交到 Git,然后执行git push heroku master。新版本将自动部署!
因为这个食谱的主要目标是演示如何回复,Heroku 和 Flask 没有详细描述,但它们都有很好的文档。Heroku 的完整文档可以在这里找到:devcenter.heroku.com/categories/reference,Flask 的文档在这里:flask.pocoo.org/docs/。
请记住,使用 Heroku 和 Flask 只是为了方便这个食谱,因为它们是很好和易于使用的工具。有多种替代方案,只要您能够公开一个 URL,Twilio 就可以调用它。还要检查安全措施,以确保对此端点的请求来自 Twilio:www.twilio.com/docs/usage/security#validating-requests。
Twilio 的完整文档可以在这里找到:www.twilio.com/docs/。
另请参阅
-
生成短信食谱
-
创建 Telegram 机器人食谱
创建一个 Telegram 机器人
Telegram Messenger 是一个即时通讯应用程序,对创建机器人有很好的支持。机器人是旨在产生自动对话的小型应用程序。机器人的重要承诺是作为可以产生任何类型对话的机器,完全无法与人类对话区分开来,并通过Turing 测试,但这个目标对大部分来说是相当雄心勃勃且不现实的。
图灵测试是由艾伦·图灵于 1951 年提出的。两个参与者,一个人类和一个人工智能(机器或软件程序),通过文本(就像在即时通讯应用程序中)与一个人类评委进行交流,评委决定哪一个是人类,哪一个不是。如果评委只能猜对一半的时间,就无法轻易区分,因此人工智能通过了测试。这是对衡量人工智能的最早尝试之一。
但是,机器人也可以以更有限的方式非常有用,类似于需要按2来检查您的账户,按3来报告遗失的卡片的电话系统。在这个食谱中,我们将看到如何生成一个简单的机器人,用于显示公司的优惠和活动。
准备就绪
我们需要为 Telegram 创建一个新的机器人。这是通过一个名为BotFather的界面完成的,它是一个特殊的 Telegram 频道,允许我们创建一个新的机器人。您可以通过此链接访问该频道:telegram.me/botfather。通过您的 Telegram 帐户访问它。
运行/start以启动界面,然后使用/newbot创建一个新的机器人。界面会要求您输入机器人的名称和用户名,用户名应该是唯一的。
一旦设置好,它将给您以下内容:
-
您的机器人的 Telegram 频道-
https:/t.me/<yourusername>。 -
允许访问机器人的令牌。复制它,因为稍后会用到。
如果丢失令牌,可以生成一个新的令牌。阅读 BotFather 的文档。
我们还需要安装 Python 模块telepot,它包装了 Telegram 的 RESTful 接口:
$ echo "telepot==12.7" >> requirements.txt
$ pip install -r requirements.txt
从 GitHub 上下载telegram_bot.py脚本:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter08/telegram_bot.py。
如何做...
- 将生成的令牌设置到
telegram_bot.py脚本的第 6 行的TOKEN常量中:
TOKEN = '<YOUR TOKEN>'
- 启动机器人:
$ python telegram_bot.py
- 使用 URL 在手机上打开 Telegram 频道并启动它。您可以使用
help,offers和events命令:

工作原理...
第 1 步设置要用于您特定频道的令牌。在第 2 步中,我们在本地启动机器人。
让我们看看telegram_bot.py中的代码结构:
IMPORTS
TOKEN
# Define the information to return per command
def get_help():
def get_offers():
def get_events():
COMMANDS = {
'help': get_help,
'offers': get_offers,
'events': get_events,
}
class MarketingBot(telepot.helper.ChatHandler):
...
# Create and start the bot
MarketingBot类创建了一个与 Telegram 进行通信的接口:
-
当频道启动时,将调用
open方法 -
当收到消息时,将调用
on_chat_message方法 -
如果有一段时间没有回应,将调用
on_idle
在每种情况下,self.sender.sendMessage方法用于向用户发送消息。大部分有趣的部分都发生在on_chat_message中:
def on_chat_message(self, msg):
# If the data sent is not test, return an error
content_type, chat_type, chat_id = telepot.glance(msg)
if content_type != 'text':
self.sender.sendMessage("I don't understand you. "
"Please type 'help' for options")
return
# Make the commands case insensitive
command = msg['text'].lower()
if command not in COMMANDS:
self.sender.sendMessage("I don't understand you. "
"Please type 'help' for options")
return
message = COMMANDS[command]()
self.sender.sendMessage(message)
首先,它检查接收到的消息是否为文本,如果不是,则返回错误消息。它分析接收到的文本,如果是定义的命令之一,则执行相应的函数以检索要返回的文本。
然后,将消息发送回用户。
第 3 步显示了从与机器人交互的用户的角度来看这是如何工作的。
还有更多...
您可以使用BotFather接口向您的 Telegram 频道添加更多信息,头像图片等。
为了简化我们的界面,我们可以创建一个自定义键盘来简化机器人。在定义命令之后创建它,在脚本的第 44 行左右:
from telepot.namedtuple import ReplyKeyboardMarkup, KeyboardButton
keys = [[KeyboardButton(text=text)] for text in COMMANDS]
KEYBOARD = ReplyKeyboardMarkup(keyboard=keys)
请注意,它正在创建一个带有三行的键盘,每行都有一个命令。然后,在每个sendMessage调用中添加生成的KEYBOARD作为reply_markup,例如如下所示:
message = COMMANDS[command]()
self.sender.sendMessage(message, reply_markup=KEYBOARD)
这将键盘替换为仅有定义的按钮,使界面非常明显:

这些更改可以在 GitHub 的telegram_bot_custom_keyboard.py文件中下载,链接在这里:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter08/telegram_bot_custom_keyboard.py。
您可以创建其他类型的自定义界面,例如内联按钮,甚至是创建游戏的平台。查看 Telegram API 文档以获取更多信息。
与 Telegram 的交互也可以通过 webhook 完成,方式与接收短信配方中介绍的类似。在telepot文档中查看 Flask 的示例:github.com/nickoala/telepot/tree/master/examples/webhook。
通过telepot可以设置 Telegram webhook。这要求您的服务位于 HTTPS 地址后,以确保通信是私密的。这可能对于简单的服务来说有点棘手。您可以在 Telegram 文档中查看有关设置 webhook 的文档:core.telegram.org/bots/api#setwebhook。
电报机器人的完整 API 可以在这里找到:core.telegram.org/bots。
telepot模块的文档可以在这里找到:telepot.readthedocs.io/en/latest/。
另请参阅
-
生成短信配方
-
接收短信配方
第九章:为什么不自动化您的营销活动呢?
在本章中,我们将介绍与营销活动相关的以下配方:
-
检测机会
-
创建个性化优惠券代码
-
通过用户的首选渠道向客户发送通知
-
准备销售信息
-
生成销售报告
介绍
在本章中,我们将创建一个完整的营销活动,逐步进行每个自动步骤。我们将在一个项目中利用本书中的所有概念和配方,这将需要不同的步骤。
让我们举个例子。对于我们的项目,我们的公司希望设置一个营销活动来提高参与度和销售额。这是一个非常值得赞扬的努力。为此,我们可以将行动分为几个步骤:
-
我们希望检测启动活动的最佳时机,因此我们将从不同来源收到关键词的通知,这将帮助我们做出明智的决定
-
该活动将包括生成个人代码以发送给潜在客户
-
这些代码的部分将直接通过用户的首选渠道发送给他们,即短信或电子邮件
-
为了监控活动的结果,将编制销售信息并生成销售报告
本章将逐步介绍这些步骤,并提出基于本书介绍的模块和技术的综合解决方案。
尽管这些示例是根据现实生活中的需求创建的,但请注意,您的特定环境总会让您感到意外。不要害怕尝试、调整和改进您的系统,随着对系统的了解越来越多,迭代是创建出色系统的方法。
让我们开始吧!
检测机会
在这个配方中,我们提出了一个分为几个步骤的营销活动:
-
检测启动活动的最佳时机
-
生成个人代码以发送给潜在客户
-
通过用户的首选渠道直接发送代码,即短信或电子邮件
-
整理活动的结果,并生成带有结果分析的销售报告
这个配方展示了活动的第一步。
我们的第一阶段是检测启动活动的最佳时间。为此,我们将监视一系列新闻网站,搜索包含我们定义关键词之一的新闻。任何与这些关键词匹配的文章都将被添加到一份报告中,并通过电子邮件发送。
做好准备
在这个配方中,我们将使用本书中之前介绍的几个外部模块,delorean、requests和BeautifulSoup。如果尚未添加到我们的虚拟环境中,我们需要将它们添加进去:
$ echo "delorean==1.0.0" >> requirements.txt
$ echo "requests==2.18.3" >> requirements.txt
$ echo "beautifulsoup4==4.6.0" >> requirements.txt
$ echo "feedparser==5.2.1" >> requirements.txt
$ echo "jinja2==2.10" >> requirements.txt
$ echo "mistune==0.8.3" >> requirements.txt
$ pip install -r requirements.txt
您需要列出一些 RSS 源,我们将从中获取数据。
在我们的示例中,我们使用以下源,这些源都是知名新闻网站上的技术源:
feeds.reuters.com/reuters/technologyNews
rss.nytimes.com/services/xml/rss/nyt/Technology.xml
feeds.bbci.co.uk/news/science_and_environment/rss.xml
下载search_keywords.py脚本,该脚本将从 GitHub 执行操作,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/search_keywords.py。
您还需要下载电子邮件模板,可以在github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/email_styling.html和github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/email_template.md找到。
在github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/config-opportunity.ini中有一个配置模板。
你需要一个有效的用户名和密码来使用电子邮件服务。在第八章的发送单独的电子邮件示例中检查。
如何做...
- 创建一个
config-opportunity.ini文件,格式如下。记得填写你的详细信息:
[SEARCH]
keywords = keyword, keyword
feeds = feed, feed
[EMAIL]
user = <YOUR EMAIL USERNAME>
password = <YOUR EMAIL PASSWORD>
from = <EMAIL ADDRESS FROM>
to = <EMAIL ADDRESS TO>
你可以使用 GitHub 上的模板github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/config-opportunity.ini来搜索关键词cpu和一些测试源。记得用你自己的账户信息填写EMAIL字段。
- 调用脚本生成电子邮件和报告:
$ python search_keywords.py config-opportunity.ini
- 检查
to电子邮件,你应该收到一份包含找到的文章的报告。它应该类似于这样:

工作原理...
在步骤 1 中创建脚本的适当配置后,通过调用search_keywords.py在步骤 2 中完成网页抓取和发送电子邮件的结果。
让我们看一下search_keywords.py脚本。代码分为以下几部分:
-
IMPORTS部分使所有 Python 模块可供以后使用。它还定义了EmailConfig namedtuple来帮助处理电子邮件参数。 -
READ TEMPLATES检索电子邮件模板并将它们存储以供以后在EMAIL_TEMPLATE和EMAIL_STYLING常量中使用。 -
__main__块通过获取配置参数、解析配置文件,然后调用主函数来启动过程。 -
main函数组合了其他函数。首先,它检索文章,然后获取正文并发送电子邮件。 -
get_articles遍历所有的源,丢弃任何超过一周的文章,检索每一篇文章,并搜索关键词的匹配。返回所有匹配的文章,包括链接和摘要的信息。 -
compose_email_body使用电子邮件模板编写电子邮件正文。注意模板是 Markdown 格式,它被解析为 HTML,以便在纯文本和 HTML 中提供相同的信息。 -
send_email获取正文信息,以及用户名/密码等必要信息,最后发送电子邮件。
还有更多...
从不同来源检索信息的主要挑战之一是在所有情况下解析文本。一些源可能以不同的格式返回信息。
例如,在我们的示例中,你可以看到路透社的摘要包含 HTML 信息,这些信息在最终的电子邮件中被渲染。如果你遇到这种问题,你可能需要进一步处理返回的数据,直到它变得一致。这可能高度依赖于预期的报告质量。
在开发自动任务时,特别是处理多个输入源时,预计会花费大量时间以一致的方式清理输入。但另一方面,要找到平衡,并牢记最终的接收者。例如,如果邮件是要由你自己或一个理解的队友接收,你可以比对待重要客户的情况更宽容一些。
另一种可能性是增加匹配的复杂性。在这个示例中,检查是用简单的in完成的,但请记住,第一章中的所有技术,包括所有正则表达式功能,都可以供您使用。
此脚本可以通过定时作业自动化,如《第二章》中所述,《自动化任务变得容易》。尝试每周运行一次!
另请参阅
-
在《第一章》的“添加命令行参数”中,《让我们开始我们的自动化之旅》
-
在《第一章》的“介绍正则表达式”中,《让我们开始我们的自动化之旅》
-
在《第二章》的“准备任务”中,《自动化任务变得容易》
-
在《第二章》的“设置定时作业”中,《自动化任务变得容易》
-
在《第三章》的“解析 HTML”中,《第一个网络爬虫应用程序》
-
在《第三章》的“爬取网络”中,《第一个网络爬虫应用程序》
-
在《第三章》的“构建您的第一个网络爬虫应用程序”中,订阅提要的食谱
-
在《第八章》的“发送个人电子邮件”中,《处理通信渠道》
创建个性化优惠券代码
在本章中,我们将一个营销活动分为几个步骤:
-
检测最佳时机启动活动
-
生成要发送给潜在客户的个人代码
-
通过用户首选的渠道,即短信或电子邮件,直接发送代码给用户
-
收集活动的结果
-
生成带有结果分析的销售报告
这个食谱展示了活动的第 2 步。
在发现机会后,我们决定为所有客户生成一项活动。为了直接促销并避免重复,我们将生成 100 万个独特的优惠券,分为三批:
-
一半的代码将被打印并在营销活动中分发
-
30 万代码将被保留,以备将来在活动达到一些目标时使用
-
其余的 20 万将通过短信和电子邮件直接发送给客户,我们稍后会看到
这些优惠券可以在在线系统中兑换。我们的任务是生成符合以下要求的正确代码:
-
代码需要是唯一的
-
代码需要可打印且易于阅读,因为一些客户将通过电话口述它们
-
在检查代码之前应该有一种快速丢弃代码的方法(避免垃圾邮件攻击)
-
代码应以 CSV 格式呈现以供打印
做好准备
从 GitHub 上下载create_personalised_coupons.py脚本,该脚本将在 CSV 文件中生成优惠券,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/create_personalised_coupons.py。
如何做...
- 调用
create_personalised_coupons.py脚本。根据您的计算机速度,运行时间可能需要一两分钟。它将在屏幕上显示生成的代码:
$ python create_personalised_coupons.py
Code: HWLF-P9J9E-U3
Code: EAUE-FRCWR-WM
Code: PMW7-P39MP-KT
...
- 检查它是否创建了三个 CSV 文件,其中包含代码
codes_batch_1.csv,codes_batch_2.csv和codes_batch_3.csv,每个文件都包含正确数量的代码:
$ wc -l codes_batch_*.csv
500000 codes_batch_1.csv
300000 codes_batch_2.csv
200000 codes_batch_3.csv
1000000 total
- 检查每个批次文件是否包含唯一代码。您的代码将是唯一的,并且与此处显示的代码不同:
$ head codes_batch_2.csv
9J9F-M33YH-YR
7WLP-LTJUP-PV
WHFU-THW7R-T9
...
它是如何工作的...
步骤 1 调用生成所有代码的脚本,步骤 2 检查结果是否正确。步骤 3 显示代码存储的格式。让我们分析create_personalised_coupons.py脚本。
总之,它具有以下结构:
# IMPORTS
# FUNCTIONS
def random_code(digits)
def checksum(code1, code2)
def check_code(code)
def generate_code()
# SET UP TASK
# GENERATE CODES
# CREATE AND SAVE BATCHES
不同的功能一起工作来创建代码。random_code生成一组随机字母和数字的组合,取自CHARACTERS。该字符串包含所有可供选择的有效字符。
字符的选择被定义为易于打印且不易混淆的符号。例如,很难区分字母 O 和数字 0,或数字 1 和字母 I,这取决于字体。这可能取决于具体情况,因此如有必要,请进行打印测试以定制字符。但是避免使用所有字母和数字,因为这可能会引起混淆。如有必要,增加代码的长度。
checksum函数基于两个代码生成一个额外的数字,这个过程称为哈希,在计算中是一个众所周知的过程,尤其是在密码学中。
哈希的基本功能是从一个输入产生一个较小且不可逆的输出,这意味着很难猜测,除非已知输入。哈希在计算中有很多常见的应用,通常在底层使用。例如,Python 字典广泛使用哈希。
在我们的示例中,我们将使用 SHA256,这是一个众所周知的快速哈希算法,包含在 Python 的hashlib模块中:
def checksum(code1, code2):
m = hashlib.sha256()
m.update(code1.encode())
m.update(code2.encode())
checksum = int(m.hexdigest()[:2], base=16)
digit = CHARACTERS[checksum % len(CHARACTERS)]
return digit
两个代码作为输入添加,然后将哈希的两个十六进制数字应用于CHARACTERS,以获得其中一个可用字符。这些数字被转换为数字(因为它们是十六进制的),然后我们应用模运算符来确保获得其中一个可用字符。
这个校验和的目的是能够快速检查代码是否正确,并且丢弃可能的垃圾邮件。我们可以再次对代码执行操作,以查看校验和是否相同。请注意,这不是加密哈希,因为在操作的任何时候都不需要秘密。鉴于这个特定的用例,这种(低)安全级别对我们的目的来说可能是可以接受的。
密码学是一个更大的主题,确保安全性强可能会很困难。密码学中涉及哈希的主要策略可能是仅存储哈希以避免以可读格式存储密码。您可以在这里阅读有关此的快速介绍:crackstation.net/hashing-security.htm。
generate_code函数然后生成一个随机代码,由四位数字、五位数字和两位校验和组成,用破折号分隔。第一个数字使用前九个数字按顺序生成(四位然后五位),第二个数字将其反转(五位然后四位)。
check_code函数将反转过程,并在代码正确时返回True,否则返回False。
有了基本元素之后,脚本开始定义所需的批次——500,000、300,000 和 200,000。
所有的代码都是在同一个池中生成的,称为codes。这是为了避免在池之间产生重复。请注意,由于过程的随机性,我们无法排除生成重复代码的可能性,尽管这很小。我们允许最多重试三次,以避免生成重复代码。代码被添加到一个集合累加器中,以确保它们的唯一性,并加快检查代码是否已经存在的速度。
sets是 Python 在底层使用哈希的另一个地方,因此它将要添加的元素进行哈希处理,并将其与已经存在的元素的哈希进行比较。这使得在集合中进行检查非常快速。
为了确保过程是正确的,每个代码都经过验证并打印出来,以显示生成代码的进度,并允许检查一切是否按预期工作。
最后,代码被分成适当数量的批次,每个批次保存在单独的.csv文件中。 代码使用.pop()从codes中逐个删除,直到batch达到适当大小为止:
batch = [(codes.pop(),) for _ in range(batch_size)]
请注意,前一行创建了一个包含单个元素的适当大小行的批次。每一行仍然是一个列表,因为对于 CSV 文件来说应该是这样。
然后,创建一个文件,并使用csv.writer将代码存储为行。
作为最后的测试,验证剩余的codes是否为空。
还有更多...
在这个食谱中,流程采用了直接的方法。这与第二章中准备运行任务食谱中介绍的原则相反,简化任务变得更容易。请注意,与那里介绍的任务相比,此脚本旨在运行一次以生成代码,然后结束。它还使用了定义的常量,例如BATCHES,用于配置。
鉴于这是一个独特的任务,设计为仅运行一次,花时间将其构建成可重用的组件可能不是我们时间的最佳利用方式。
过度设计肯定是可能的,而在实用设计和更具未来导向性的方法之间做出选择可能并不容易。要对维护成本保持现实,并努力找到自己的平衡。
同样,这个食谱中的校验和设计旨在提供一种最小的方式来检查代码是否完全虚构或看起来合法。鉴于代码将被检查系统,这似乎是一个明智的方法,但要注意您特定的用例。
我们的代码空间是22 个字符** 9 个数字= 1,207,269,217,792 个可能的代码,这意味着猜测其中一个百万个生成的代码的概率非常小。也不太可能产生相同的代码两次,但尽管如此,我们通过最多三次重试来保护我们的代码。
这些检查以及检查每个代码的验证以及最终没有剩余代码的检查在开发这种脚本时非常有用。这确保了我们朝着正确的方向前进,事情按计划进行。只是要注意,在某些情况下asserts可能不会被执行。
如 Python 文档所述,如果使用-O命令运行 Python 代码,则assert命令将被忽略。请参阅此处的文档docs.python.org/3/reference/simple_stmts.html#the-assert-statement。通常情况下不会这样做,但如果是这种情况可能会令人困惑。避免过度依赖asserts。
学习加密的基础并不像你可能认为的那么困难。有一些基本模式是众所周知且易于学习的。一个很好的介绍文章是这篇thebestvpn.com/cryptography/。Python 也集成了大量的加密函数;请参阅文档docs.python.org/3/library/crypto.html。最好的方法是找一本好书,知道虽然这是一个难以真正掌握的主题,但绝对是可以掌握的。
另请参阅
-
第一章中的介绍正则表达式食谱,让我们开始自动化之旅
-
第四章中的读取 CSV 文件食谱,搜索和阅读本地文件
向客户发送他们首选渠道的通知
在本章中,我们介绍了一个分为几个步骤的营销活动:
-
检测最佳推出活动的时机
-
生成要发送给潜在客户的个别代码
-
直接将代码发送给用户,通过他们首选的渠道,短信或电子邮件
-
收集活动的结果
-
生成带有结果分析的销售报告
这个食谱展示了活动的第 3 步。
一旦我们的代码为直接营销创建好,我们需要将它们分发给我们的客户。
对于这个食谱,从包含所有客户及其首选联系方式信息的 CSV 文件中,我们将使用先前生成的代码填充文件,然后通过适当的方法发送通知,其中包括促销代码。
做好准备
在这个示例中,我们将使用已经介绍过的几个模块——delorean、requests和twilio。如果尚未添加到我们的虚拟环境中,我们需要将它们添加进去:
$ echo "delorean==1.0.0" >> requirements.txt
$ echo "requests==2.18.3" >> requirements.txt
$ echo "twilio==6.16.3" >> requirements.txt
$ pip install -r requirements.txt
我们需要定义一个config-channel.ini文件,其中包含我们用于 Mailgun 和 Twilio 的服务的凭据。可以在 GitHub 上找到此文件的模板:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/config-channel.ini。
有关如何获取凭据的信息,请参阅通过电子邮件发送通知和生成短信的示例第八章,处理通信渠道
文件的格式如下:
[MAILGUN]
KEY = <YOUR KEY>
DOMAIN = <YOUR DOMAIN>
FROM = <YOUR FROM EMAIL>
[TWILIO]
ACCOUNT_SID = <YOUR SID>
AUTH_TOKEN = <YOUR TOKEN>
FROM = <FROM TWILIO PHONE NUMBER>
为了描述所有目标联系人,我们需要生成一个 CSV 文件notifications.csv,格式如下:
| Name | Contact Method | Target | Status | Code | Timestamp |
|---|---|---|---|---|---|
| John Smith | PHONE | +1-555-12345678 | NOT-SENT |
||
| Paul Smith | paul.smith@test.com |
NOT-SENT |
|||
| … |
请注意Code列为空,所有状态应为NOT-SENT或空。
如果您正在使用 Twilio 和 Mailgun 的测试帐户,请注意其限制。例如,Twilio 只允许您向经过身份验证的电话号码发送消息。您可以创建一个只包含两三个联系人的小型 CSV 文件来测试脚本。
应该准备好在 CSV 文件中使用的优惠券代码。您可以使用 GitHub 上的create_personalised_coupons.py脚本生成多个批次,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/create_personalised_coupons.py。
从 GitHub 上下载要使用的脚本send_notifications.py,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/send_notifications.py。
操作步骤...
- 运行
send_notifications.py以查看其选项和用法:
$ python send_notifications.py --help
usage: send_notifications.py [-h] [-c CODES] [--config CONFIG_FILE] notif_file
positional arguments:
notif_file notifications file
optional arguments:
-h, --help show this help message and exit
-c CODES, --codes CODES
Optional file with codes. If present, the file will be
populated with codes. No codes will be sent
--config CONFIG_FILE config file (default config.ini)
- 将代码添加到
notifications.csv文件中:
$ python send_notifications.py --config config-channel.ini notifications.csv -c codes_batch_3.csv
$ head notifications.csv
Name,Contact Method,Target,Status,Code,Timestamp
John Smith,PHONE,+1-555-12345678,NOT-SENT,CFXK-U37JN-TM,
Paul Smith,EMAIL,paul.smith@test.com,NOT-SENT,HJGX-M97WE-9Y,
...
- 最后,发送通知:
$ python send_notifications.py --config config-channel.ini notifications.csv
$ head notifications.csv
Name,Contact Method,Target,Status,Code,Timestamp
John Smith,PHONE,+1-555-12345678,SENT,CFXK-U37JN-TM,2018-08-25T13:08:15.908986+00:00
Paul Smith,EMAIL,paul.smith@test.com,SENT,HJGX-M97WE-9Y,2018-08-25T13:08:16.980951+00:00
...
- 检查电子邮件和电话,以验证消息是否已收到。
工作原理...
第 1 步展示了脚本的使用。总体思路是多次调用它,第一次用于填充代码,第二次用于发送消息。如果出现错误,可以再次执行脚本,只会重试之前未发送的消息。
notifications.csv文件获取将在第 2 步中注入的代码。这些代码最终将在第 3 步中发送。
让我们分析send_notifications.py的代码。这里只显示了最相关的部分:
# IMPORTS
def send_phone_notification(...):
def send_email_notification(...):
def send_notification(...):
def save_file(...):
def main(...):
if __name__ == '__main__':
# Parse arguments and prepare configuration
...
主要函数逐行遍历文件,并分析每种情况下要执行的操作。如果条目为SENT,则跳过。如果没有代码,则尝试填充。如果尝试发送,则会附加时间戳以记录发送或尝试发送的时间。
对于每个条目,整个文件都会被保存在名为save_file的文件中。注意文件光标定位在文件开头,然后写入文件,并刷新到磁盘。这样可以在每次条目操作时覆盖文件,而无需关闭和重新打开文件。
为什么要为每个条目写入整个文件?这是为了让您可以重试。如果其中一个条目产生意外错误或超时,甚至出现一般性故障,所有进度和先前的代码都将被标记为已发送,并且不会再次发送。这意味着可以根据需要重试操作。对于大量条目,这是确保在过程中出现问题不会导致我们重新发送消息给客户的好方法。
对于要发送的每个代码,send_notification 函数决定调用 send_phone_notification 或 send_email_notification。在两种情况下都附加当前时间。
如果无法发送消息,两个 send 函数都会返回错误。这允许您在生成的 notifications.csv 中标记它,并稍后重试。
notifications.csv 文件也可以手动更改。例如,假设电子邮件中有拼写错误,这就是错误的原因。可以更改并重试。
send_email_notification 根据 Mailgun 接口发送消息。有关更多信息,请参阅第八章中的通过电子邮件发送通知配方,处理通信渠道。请注意这里发送的电子邮件仅为文本。
send_phone_notification 根据 Twilio 接口发送消息。有关更多信息,请参阅第八章中的生成短信配方,处理通信渠道。
还有更多...
时间戳的格式故意以 ISO 格式编写,因为它是可解析的格式。这意味着我们可以轻松地以这种方式获取一个正确的对象,就像这样:
>>> import datetime
>>> timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
>>> timestamp
'2018-08-25T14:13:53.772815+00:00'
>>> datetime.datetime.fromisoformat(timestamp)
datetime.datetime(2018, 9, 11, 21, 5, 41, 979567, tzinfo=datetime.timezone.utc)
这使您可以轻松地解析时间戳。
ISO 8601 时间格式在大多数编程语言中都得到很好的支持,并且非常精确地定义了时间,因为它包括时区。如果可以使用它,这是记录时间的绝佳选择。
send_notification 中用于路由通知的策略非常有趣:
# Route each of the notifications
METHOD = {
'PHONE': send_phone_notification,
'EMAIL': send_email_notification,
}
try:
method = METHOD[entry['Contact Method']]
result = method(entry, config)
except KeyError:
result = 'INVALID_METHOD'
METHOD 字典将每个可能的 Contact Method 分配给具有相同定义的函数,接受条目和配置。
然后,根据特定的方法,从字典中检索并调用函数。请注意 method 变量包含要调用的正确函数。
这类似于其他编程语言中可用的 switch 操作。也可以通过 if...else 块来实现。对于这种简单的代码,字典方法使代码非常易读。
invalid_method 函数被用作默认值。如果 Contact Method 不是可用的方法之一(PHONE 或 EMAIL),将引发 KeyError,捕获并将结果定义为 INVALID METHOD。
另请参阅
-
第八章中的通过电子邮件发送通知配方,处理通信渠道
-
第八章中的生成短信配方,处理通信渠道
准备销售信息
在本章中,我们介绍了一个分为几个步骤的营销活动:
-
检测启动广告活动的最佳时机
-
生成要发送给潜在客户的个人代码
-
直接通过用户首选的渠道,短信或电子邮件,发送代码
-
收集广告活动的结果
-
生成带有结果分析的销售报告
这个配方展示了广告活动的第 4 步。
向用户发送信息后,我们需要收集商店的销售日志,以监控情况和广告活动的影响有多大。
销售日志作为与各个关联商店的单独文件报告,因此在这个配方中,我们将看到如何将所有信息汇总到一个电子表格中,以便将信息作为一个整体处理。
做好准备
对于这个配方,我们需要安装以下模块:
$ echo "openpyxl==2.5.4" >> requirements.txt
$ echo "parse==1.8.2" >> requirements.txt
$ echo "delorean==1.0.0" >> requirements.txt
$ pip install -r requirements.txt
我们可以从 GitHub 上获取这个配方的测试结构和测试日志:github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter09/sales。请下载包含大量测试日志的完整sales目录。为了显示结构,我们将使用tree命令(mama.indstate.edu/users/ice/tree/),它在 Linux 中默认安装,并且可以在 macOs 中使用brew安装(brew.sh/)。您也可以使用图形工具来检查目录。
我们还需要sale_log.py模块和parse_sales_log.py脚本,可以在 GitHub 上找到:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/parse_sales_log.py。
如何做...
- 检查
sales目录的结构。每个子目录代表一个商店提交了其销售日志的期间:
$ tree sales
sales
├── 345
│ └── logs.txt
├── 438
│ ├── logs_1.txt
│ ├── logs_2.txt
│ ├── logs_3.txt
│ └── logs_4.txt
└── 656
└── logs.txt
- 检查日志文件:
$ head sales/438/logs_1.txt
[2018-08-27 21:05:55+00:00] - SALE - PRODUCT: 12346 - PRICE: $02.99 - NAME: Single item - DISCOUNT: 0%
[2018-08-27 22:05:55+00:00] - SALE - PRODUCT: 12345 - PRICE: $07.99 - NAME: Family pack - DISCOUNT: 20%
...
- 调用
parse_sales_log.py脚本生成存储库:
$ python parse_sales_log.py sales -o report.xlsx
- 检查生成的 Excel 结果,
report.xlsx:

它是如何工作的...
步骤 1 和 2 展示了数据的结构。步骤 3 调用parse_sales_log.py来读取所有日志文件并解析它们,然后将它们存储在 Excel 电子表格中。电子表格的内容在步骤 4 中显示。
让我们看看parse_sales_log.py的结构:
# IMPORTS
from sale_log import SaleLog
def get_logs_from_file(shop, log_filename):
with open(log_filename) as logfile:
logs = [SaleLog.parse(shop=shop, text_log=log)
for log in logfile]
return logs
def main(log_dir, output_filename):
logs = []
for dirpath, dirnames, filenames in os.walk(log_dir):
for filename in filenames:
# The shop is the last directory
shop = os.path.basename(dirpath)
fullpath = os.path.join(dirpath, filename)
logs.extend(get_logs_from_file(shop, fullpath))
# Create and save the Excel sheet
xlsfile = openpyxl.Workbook()
sheet = xlsfile['Sheet']
sheet.append(SaleLog.row_header())
for log in logs:
sheet.append(log.row())
xlsfile.save(output_filename)
if __name__ == '__main__':
# PARSE COMMAND LINE ARGUMENTS AND CALL main()
命令行参数在第一章中有解释,让我们开始自动化之旅。请注意,导入包括SaleLog。
主要函数遍历整个目录并通过os.walk获取所有文件。您可以在第二章中获取有关os.walk的更多信息,简化任务自动化。然后将每个文件传递给get_logs_from_file来解析其日志并将它们添加到全局logs列表中。
注意,特定商店存储在最后一个子目录中,因此可以使用os.path.basename来提取它。
完成日志列表后,使用openpyxl模块创建一个新的 Excel 表。SaleLog模块有一个.row_header方法来添加第一行,然后所有日志都被转换为行格式使用.row。最后,文件被保存。
为了解析日志,我们创建一个名为sale_log.py的模块。这个模块抽象了解析和处理一行的过程。大部分都很简单,并且正确地结构化了每个不同的参数,但是解析方法需要一点注意:
@classmethod
def parse(cls, shop, text_log):
'''
Parse from a text log with the format
...
to a SaleLog object
'''
def price(string):
return Decimal(string)
def isodate(string):
return delorean.parse(string)
FORMAT = ('[{timestamp:isodate}] - SALE - PRODUCT: {product:d} '
'- PRICE: ${price:price} - NAME: {name:D} '
'- DISCOUNT: {discount:d}%')
formats = {'price': price, 'isodate': isodate}
result = parse.parse(FORMAT, text_log, formats)
return cls(timestamp=result['timestamp'],
product_id=result['product'],
price=result['price'],
name=result['name'],
discount=result['discount'],
shop=shop)
sale_log.py是一个classmethod,意味着可以通过调用SaleLog.parse来使用它,并返回类的新元素。
Classmethods 被调用时,第一个参数存储类,而不是通常存储在self中的对象。约定是使用cls来表示它。在最后调用cls(...)等同于SaleFormat(...),因此它调用__init__方法。
该方法使用parse模块从模板中检索值。请注意,timestamp和price这两个元素具有自定义解析。delorean模块帮助我们解析日期,价格最好描述为Decimal以保持适当的分辨率。自定义过滤器应用于formats参数。
还有更多...
Decimal类型在 Python 文档中有详细描述:docs.python.org/3/library/decimal.html。
完整的openpyxl可以在这里找到:openpyxl.readthedocs.io/en/stable/。还要检查第六章,电子表格的乐趣,以获取有关如何使用该模块的更多示例。
完整的parse文档可以在这里找到:github.com/r1chardj0n3s/parse。第一章中也更详细地描述了这个模块。
另请参阅
-
第一章中的使用第三方工具—parse配方,让我们开始自动化之旅
-
第四章中的爬取和搜索目录配方,搜索和读取本地文件
-
第四章中的读取文本文件配方,搜索和读取本地文件
-
第六章中的更新 Excel 电子表格配方,电子表格的乐趣
生成销售报告
在这一章中,我们提出了一个分为几个步骤的营销活动:
-
检测最佳推出活动的时机
-
生成个人代码以发送给潜在客户
-
直接将代码通过用户首选的渠道,短信或电子邮件发送给用户
-
收集活动的结果
-
生成带有结果分析的销售报告
这个配方展示了活动的第 5 步。
作为最后一步,所有销售的信息都被汇总并显示在销售报告中。
在这个配方中,我们将看到如何利用从电子表格中读取、创建 PDF 和生成图表,以便自动生成全面的报告,以分析我们活动的表现。
准备工作
在这个配方中,我们将在虚拟环境中需要以下模块:
$ echo "openpyxl==2.5.4" >> requirements.txt
$ echo "fpdf==1.7.2" >> requirements.txt
$ echo "delorean==1.0.0" >> requirements.txt
$ echo "PyPDF2==1.26.0" >> requirements.txt
$ echo "matplotlib==2.2.2" >> requirements.txt
$ pip install -r requirements.txt
我们需要在 GitHub 上的sale_log.py模块,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/sale_log.py。
输入电子表格是在前一个配方中生成的,准备销售信息。在那里查找更多信息。
您可以从 GitHub 上下载用于生成输入电子表格的脚本parse_sales_log.py,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/parse_sales_log.py。
从 GitHub 上下载原始日志文件,网址为github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter09/sales。请下载完整的sales目录。
从 GitHub 上下载generate_sales_report.py脚本,网址为github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter09/generate_sales_report.py。
如何做...
- 检查输入文件和使用
generate_sales_report.py:
$ ls report.xlsx
report.xlsx
$ python generate_sales_report.py --help
usage: generate_sales_report.py [-h] input_file output_file
positional arguments:
input_file
output_file
optional arguments:
-h, --help show this help message and exit
- 使用输入文件和输出文件调用
generate_sales_report.py脚本:
$ python generate_sales_report.py report.xlsx output.pdf
- 检查
output.pdf输出文件。它将包含三页,第一页是简要摘要,第二页和第三页是按天和按商店的销售图表:

第二页显示了每天的销售图表:

第三页按商店划分销售额:

它是如何工作的
第 1 步显示如何使用脚本,第 2 步在输入文件上调用它。让我们来看一下generate_sales_report.py脚本的基本结构:
# IMPORTS
def generate_summary(logs):
def aggregate_by_day(logs):
def aggregate_by_shop(logs):
def graph(...):
def create_summary_brief(...):
def main(input_file, output_file):
# open and read input file
# Generate each of the pages calling the other calls
# Group all the pdfs into a single file
# Write the resulting PDF
if __name__ == '__main__':
# Compile the input and output files from the command line
# call main
有两个关键元素——以不同方式(按商店和按天)聚合日志以及在每种情况下生成摘要。摘要是通过generate_summary生成的,它从日志列表中生成一个带有聚合信息的字典。日志的聚合是在aggregate_by函数中以不同的样式完成的。
generate_summary生成一个包含聚合信息的字典,包括开始和结束时间,所有日志的总收入,总单位,平均折扣,以及相同数据按产品进行的详细分解。
通过从末尾开始理解脚本会更好。主要函数将所有不同的操作组合在一起。读取每个日志并将其转换为本地的SaleLog对象。
然后,它将每个页面生成为一个中间的 PDF 文件:
-
create_summary_brief生成一个关于所有数据的总摘要。 -
日志被
aggregate_by_day。创建一个摘要并生成一个图表。 -
日志被
aggregate_by_shop。创建一个摘要并生成一个图表。
使用PyPDF2将所有中间 PDF 页面合并成一个文件。最后,删除中间页面。
aggregate_by_day和aggregate_by_shop都返回一个包含每个元素摘要的列表。在aggregate_by_day中,我们使用.end_of_day来检测一天何时结束,以区分一天和另一天。
graph函数执行以下操作:
-
准备要显示的所有数据。这包括每个标签(日期或商店)的单位数量,以及每个标签的总收入。
-
创建一个顶部图表,显示按产品分割的总收入,以堆叠条形图的形式。为了能够做到这一点,同时计算总收入时,还计算了基线(下一个堆叠位置的位置)。
-
它将图表的底部部分分成与产品数量相同的图表,并显示每个标签(日期或商店)上销售的单位数量。
为了更好地显示,图表被定义为 A4 纸的大小。它还允许我们使用skip_labels在第二个图表的 X 轴上打印每个X标签中的一个,以避免重叠。这在显示日期时很有用,并且设置为每周只显示一个标签。
生成的图表被保存到文件中。
create_summary_brief使用fpdf模块保存一个包含总摘要信息的文本 PDF 页面。
create_summary_brief中的模板和信息被故意保持简单,以避免使这个配方复杂化,但可以通过更好的描述性文本和格式进行复杂化。有关如何使用fpdf的更多详细信息,请参阅第五章,“生成精彩报告”。
如前所示,main函数将所有 PDF 页面分组并合并成一个单一文档,然后删除中间页面。
还有更多...
此配方中包含的报告可以扩展。例如,可以在每个页面中计算平均折扣,并显示为一条线:
# Generate a data series with the average discount
discount = [summary['average_discount'] for _, summary in full_summary]
....
# Print the legend
# Plot the discount in a second axis
plt.twinx()
plt.plot(pos, discount,'o-', color='green')
plt.ylabel('Average Discount')
但要小心,不要在一个图表中放入太多信息。这可能会降低可读性。在这种情况下,另一个图表可能是更好的显示方式。
在创建第二个轴之前小心打印图例,否则它将只显示第二个轴上的信息。
图表的大小和方向可以决定是否使用更多或更少的标签,以便清晰可读。这在使用skip_labels避免混乱时得到了证明。请注意生成的图形,并尝试通过更改大小或在某些情况下限制标签来适应该领域可能出现的问题。
例如,可能的限制是最多只能有三种产品,因为在我们的图表中打印第二行的四个图表可能会使文本难以辨认。请随意尝试并检查代码的限制。
完整的matplotlib文档可以在matplotlib.org/找到。
delorean文档可以在这里找到:delorean.readthedocs.io/en/latest/
openpyxl的所有文档都可以在openpyxl.readthedocs.io/en/stable/找到。
PyPDF2 的 PDF 操作模块的完整文档可以在pythonhosted.org/PyPDF2/找到,pyfdf的文档可以在pyfpdf.readthedocs.io/en/latest/找到。
本食谱利用了第五章中提供的不同概念和技术,用于 PDF 创建和操作,《第六章](404a9dc7-22f8-463c-9f95-b480dc17518d.xhtml)中的与电子表格玩耍,用于电子表格阅读,以及第七章中的开发令人惊叹的图表,用于图表创建。查看它们以了解更多信息。
另请参阅
-
在第五章中的聚合 PDF报告食谱
-
在第六章中的读取 Excel电子表格食谱
-
在第七章中的绘制堆叠条形图食谱
-
在《开发令人惊叹的图表》第七章中的显示多行食谱
-
在《开发令人惊叹的图表》第七章中的添加图例和注释食谱
-
在《开发令人惊叹的图表》第七章中的组合图表食谱
-
在《开发令人惊叹的图表》第七章中的保存图表食谱
第十章:调试技术
在本章中,我们将介绍以下配方:
-
学习 Python 解释器基础知识
-
通过日志调试
-
使用断点调试
-
提高你的调试技能
介绍
编写代码并不容易。实际上,它非常困难。即使是世界上最好的程序员也无法预见代码的任何可能的替代方案和流程。
这意味着执行我们的代码将总是产生惊喜和意外的行为。有些会非常明显,而其他的则会非常微妙,但是识别和消除代码中的这些缺陷的能力对于构建稳固的软件至关重要。
这些软件中的缺陷被称为bug,因此消除它们被称为调试。
仅通过阅读来检查代码并不好。总会有意外,复杂的代码很难跟踪。这就是为什么通过停止执行并查看当前状态的能力是重要的。
每个人,每个人都会在代码中引入 bug,通常稍后会对此感到惊讶。有些人将调试描述为在一部犯罪电影中扮演侦探,而你也是凶手。
任何调试过程大致遵循以下路径:
-
你意识到有一个问题
-
你了解正确的行为应该是什么
-
你发现了当前代码产生 bug 的原因
-
你改变代码以产生正确的结果
在这 95%的时间里,除了步骤 3 之外的所有事情都是微不足道的,这是调试过程的主要部分。
意识到 bug 的原因,本质上使用了科学方法:
-
测量和观察代码的行为
-
对为什么会这样产生假设
-
验证或证明是否正确,也许通过实验
-
使用得到的信息来迭代这个过程
调试是一种能力,因此随着时间的推移会得到改善。实践在培养对哪些路径看起来有希望识别错误的直觉方面起着重要作用,但也有一些一般的想法可能会帮助你:
- 分而治之:隔离代码的小部分,以便理解代码。尽可能简化问题。
这有一个称为狼围栏算法的格式,由爱德华·高斯描述:
"阿拉斯加有一只狼;你怎么找到它?首先在州的中间建造一道围栏,等待狼嚎叫,确定它在围栏的哪一边。然后只在那一边重复这个过程,直到你能看到狼为止。"
-
从错误处向后移动:如果在特定点有明显的错误,那么 bug 可能位于周围。从错误处逐渐向后移动,沿着轨迹直到找到错误的源头。
-
只要你证明了你的假设,你可以假设任何东西:代码非常复杂,无法一次性记住所有内容。您需要验证小的假设,这些假设结合起来将为检测和修复问题提供坚实的基础。进行小实验,这将允许您从头脑中删除实际工作的代码部分,并专注于未经测试的代码部分。
或用福尔摩斯的话说:
"一旦你排除了不可能的,无论多么不可能,剩下的,必定是真相。"
但记住要证明它。避免未经测试的假设。
所有这些听起来有点可怕,但实际上大多数 bug 都是相当明显的。也许是拼写错误,或者一段代码还没有准备好接受特定的值。尽量保持简单。简单的代码更容易分析和调试。
在本章中,我们将看到一些调试工具和技术,并将它们特别应用于 Python 脚本。这些脚本将有一些 bug,我们将作为配方的一部分来修复它们。
学习 Python 解释器基础知识
在这个配方中,我们将介绍一些 Python 内置的功能,以检查代码,调查发生了什么事情,并检测当事情不正常时。
我们还可以验证事情是否按预期进行。记住,能够排除代码的一部分作为错误源是非常重要的。
在调试时,我们通常需要分析来自外部模块或服务的未知元素和对象。鉴于 Python 的动态特性,代码在执行的任何时刻都是高度可发现的。
这个方法中的所有内容都是 Python 解释器的默认内容。
如何做到...
- 导入
pprint:
>>> from pprint import pprint
- 创建一个名为
dictionary的新字典:
>>> dictionary = {'example': 1}
- 将
globals显示到此环境中:
>>> globals()
{...'pprint': <function pprint at 0x100995048>,
...'dictionary': {'example': 1}}
- 以可读格式使用
pprint打印globals字典:
>>> pprint(globals())
{'__annotations__': {},
...
'dictionary': {'example': 1},
'pprint': <function pprint at 0x100995048>}
- 显示
dictionary的所有属性:
>>> dir(dictionary)
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
- 展示
dictionary对象的帮助:
>>> help(dictionary)
Help on dict object:
class dict(object)
| dict() -> new empty dictionary
| dict(mapping) -> new dictionary initialized from a mapping object's
| (key, value) pairs
...
它是如何工作的...
在第 1 步导入pprint(漂亮打印)之后,我们创建一个新的字典作为第 2 步中的示例。
第 3 步显示了全局命名空间包含已定义的字典和模块等内容。globals()显示所有导入的模块和其他全局变量。
本地命名空间有一个等效的locals()。
pprint有助于以第 4 步中更可读的格式显示globals,增加更多空间并将元素分隔成行。
第 5 步显示了如何使用dir()获取 Python 对象的所有属性。请注意,这包括所有双下划线值,如__len__。
使用内置的help()函数将显示对象的相关信息。
还有更多...
dir()特别适用于检查未知对象、模块或类。如果需要过滤默认属性并澄清输出,可以通过以下方式过滤输出:
>>> [att for att in dir(dictionary) if not att.startswith('__')]
['clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
同样,如果要搜索特定方法(例如以set开头的方法),也可以以相同的方式进行过滤。
help()将显示函数或类的docstring。docstring是在定义之后定义的字符串,用于记录函数或类的信息:
>>> def something():
... '''
... This is help for something
... '''
... pass
...
>>> help(something)
Help on function something in module __main__:
something()
This is help for something
请注意,在下一个示例中,这是某物的帮助字符串是在函数定义之后定义的。
docstring通常用三引号括起来,以允许编写多行字符串。Python 将三引号内的所有内容视为一个大字符串,即使有换行符也是如此。您可以使用'或"字符,只要使用三个即可。您可以在www.python.org/dev/peps/pep-0257/找到有关docstrings的更多信息。
内置函数的文档可以在docs.python.org/3/library/functions.html#built-in-functions找到,pprint的完整文档可以在docs.python.org/3/library/pprint.html#找到。
另请参阅
-
提高调试技能的方法
-
通过日志进行调试的方法
通过日志进行调试
毕竟,调试就是检测程序内部发生了什么以及可能发生的意外或不正确的影响。一个简单但非常有效的方法是在代码的战略部分输出变量和其他信息,以跟踪程序的流程。
这种方法的最简单形式称为打印调试,或者在调试时在某些点插入打印语句以打印变量或点的值。
但是,稍微深入了解这种技术,并将其与第二章中介绍的日志技术相结合,轻松实现自动化任务使我们能够创建程序执行的半永久跟踪,这在检测运行中的程序中的问题时非常有用。
准备工作
从 GitHub 下载 debug_logging.py 文件:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter10/debug_logging.py。
它包含了冒泡排序算法的实现(www.studytonight.com/data-structures/bubble-sort),这是对元素列表进行排序的最简单方式。它在列表上进行多次迭代,每次迭代都会检查并交换两个相邻的值,使得较大的值在较小的值之后。这样就使得较大的值像气泡一样在列表中上升。
冒泡排序是一种简单但天真的排序实现方式,有更好的替代方案。除非你有极好的理由,否则依赖列表中的标准 .sort 方法。
运行时,它检查以下列表以验证其正确性:
assert [1, 2, 3, 4, 7, 10] == bubble_sort([3, 7, 10, 2, 4, 1])
我们在这个实现中有一个 bug,所以我们可以将其作为修复的一部分来修复!
如何做...
- 运行
debug_logging.py脚本并检查是否失败:
$ python debug_logging.py
INFO:Sorting the list: [3, 7, 10, 2, 4, 1]
INFO:Sorted list: [2, 3, 4, 7, 10, 1]
Traceback (most recent call last):
File "debug_logging.py", line 17, in <module>
assert [1, 2, 3, 4, 7, 10] == bubble_sort([3, 7, 10, 2, 4, 1])
AssertionError
- 启用调试日志,更改
debug_logging.py脚本的第二行:
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
将前一行改为以下一行:
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
注意不同的 level。
- 再次运行脚本,增加更多信息:
$ python debug_logging.py
INFO:Sorting the list: [3, 7, 10, 2, 4, 1]
DEBUG:alist: [3, 7, 10, 2, 4, 1]
DEBUG:alist: [3, 7, 10, 2, 4, 1]
DEBUG:alist: [3, 7, 2, 10, 4, 1]
DEBUG:alist: [3, 7, 2, 4, 10, 1]
DEBUG:alist: [3, 7, 2, 4, 10, 1]
DEBUG:alist: [3, 2, 7, 4, 10, 1]
DEBUG:alist: [3, 2, 4, 7, 10, 1]
DEBUG:alist: [2, 3, 4, 7, 10, 1]
DEBUG:alist: [2, 3, 4, 7, 10, 1]
DEBUG:alist: [2, 3, 4, 7, 10, 1]
INFO:Sorted list : [2, 3, 4, 7, 10, 1]
Traceback (most recent call last):
File "debug_logging.py", line 17, in <module>
assert [1, 2, 3, 4, 7, 10] == bubble_sort([3, 7, 10, 2, 4, 1])
AssertionError
- 分析输出后,我们意识到列表的最后一个元素没有排序。我们分析代码并发现第 7 行有一个 off-by-one 错误。你看到了吗?让我们通过更改以下一行来修复它:
for passnum in reversed(range(len(alist) - 1)):
将前一行改为以下一行:
for passnum in reversed(range(len(alist))):
(注意移除了 -1 操作。)
- 再次运行它,你会发现它按预期工作。调试日志不会显示在这里:
$ python debug_logging.py
INFO:Sorting the list: [3, 7, 10, 2, 4, 1]
...
INFO:Sorted list : [1, 2, 3, 4, 7, 10]
它是如何工作的...
第 1 步介绍了脚本,并显示代码有错误,因为它没有正确地对列表进行排序。
脚本已经有一些日志来显示开始和结束结果,以及一些调试日志来显示每个中间步骤。在第 2 步中,我们激活了显示 DEBUG 日志的显示,因为在第 1 步中只显示了 INFO。
注意,默认情况下日志会显示在标准错误输出中。这在终端中是默认显示的。如果你需要将日志重定向到其他地方,比如文件中,可以查看如何配置不同的处理程序。查看 Python 中的日志配置以获取更多详细信息:docs.python.org/3/howto/logging.html。
第 3 步再次运行脚本,这次显示额外信息,显示列表中的最后一个元素没有排序。
这个 bug 是一个 off-by-one 错误,这是一种非常常见的错误,因为它应该迭代整个列表的大小。这在第 4 步中得到修复。
检查代码以了解为什么会出现错误。整个列表应该被比较,但我们错误地减少了一个大小。
第 5 步显示修复后的脚本运行正确。
还有更多...
在这个示例中,我们已经有策略地放置了调试日志,但在实际的调试练习中可能不是这样。你可能需要添加更多或更改位置作为 bug 调查的一部分。
这种技术的最大优势是我们能够看到程序的流程,能够检查代码执行的一个时刻到另一个时刻,并理解流程。但缺点是我们可能会得到一大堆不提供关于问题的具体信息的文本。你需要在提供太多和太少信息之间找到平衡。
出于同样的原因,除非必要,尽量限制非常长的变量。
记得在修复 bug 后降低日志级别。很可能你发现一些不相关的日志需要被删除。
这种技术的快速而粗糙的版本是添加打印语句而不是调试日志。虽然有些人对此持反对意见,但实际上这是一种用于调试目的的有价值的技术。但记得在完成后清理它们。
所有的内省元素都可用,因此可以创建显示例如dir(object)对象的所有属性的日志:
logging.debug(f'object {dir(object)}')
任何可以显示为字符串的内容都可以在日志中呈现,包括任何文本操作。
另请参阅
-
学习 Python 解释器基础食谱
-
提高调试技能食谱
使用断点进行调试
Python 有一个现成的调试器叫做pdb。鉴于 Python 代码是解释性的,这意味着可以通过设置断点来在任何时候停止代码的执行,这将跳转到一个命令行,可以在其中使用任何代码来分析情况并执行任意数量的指令。
让我们看看如何做。
准备工作
下载debug_algorithm.py脚本,可从 GitHub 获取:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter10/debug_algorithm.py。
在下一节中,我们将详细分析代码的执行。代码检查数字是否符合某些属性:
def valid(candidate):
if candidate <= 1:
return False
lower = candidate - 1
while lower > 1:
if candidate / lower == candidate // lower:
return False
lower -= 1
return True
assert not valid(1)
assert valid(3)
assert not valid(15)
assert not valid(18)
assert not valid(50)
assert valid(53)
可能你已经认识到代码在做什么,但请跟着我一起交互分析它。
如何做...
- 运行代码以查看所有断言是否有效:
$ python debug_algorithm.py
- 在
while循环之后添加breakpoint(),就在第 7 行之前,结果如下:
while lower > 1:
breakpoint()
if candidate / lower == candidate // lower:
- 再次执行代码,看到它在断点处停止,进入交互式
Pdb模式:
$ python debug_algorithm.py
> .../debug_algorithm.py(8)valid()
-> if candidate / lower == candidate // lower:
(Pdb)
- 检查候选值和两个操作的值。这一行是在检查
candidate除以lower是否为整数(浮点数和整数除法是相同的):
(Pdb) candidate
3
(Pdb) candidate / lower
1.5
(Pdb) candidate // lower
1
- 使用
n继续到下一条指令。看到它结束了 while 循环并返回True:
(Pdb) n
> ...debug_algorithm.py(10)valid()
-> lower -= 1
(Pdb) n
> ...debug_algorithm.py(6)valid()
-> while lower > 1:
(Pdb) n
> ...debug_algorithm.py(12)valid()
-> return True
(Pdb) n
--Return--
> ...debug_algorithm.py(12)valid()->True
-> return True
- 继续执行,直到找到另一个断点,使用
c。请注意,这是对valid()的下一个调用,输入为 15:
(Pdb) c
> ...debug_algorithm.py(8)valid()
-> if candidate / lower == candidate // lower:
(Pdb) candidate
15
(Pdb) lower
14
- 继续运行和检查数字,直到
valid函数的操作有意义。你能找出代码在做什么吗?(如果你不能,不要担心,查看下一节。)完成后,使用q退出。这将停止执行:
(Pdb) q
...
bdb.BdbQuit
工作原理...
代码正在检查一个数字是否是质数,这点你可能已经知道。它试图将数字除以比它小的所有整数。如果在任何时候可以被整除,它将返回False结果,因为它不是质数。
实际上,这是一个检查质数的非常低效的方法,因为处理大数字将需要很长时间。不过,对于我们的教学目的来说,它足够快。如果你有兴趣找质数,可以查看 SymPy 等数学包(docs.sympy.org/latest/modules/ntheory.html?highlight=prime#sympy.ntheory.primetest.isprime)。
在步骤 1 中检查了一般的执行,在步骤 2 中,在代码中引入了一个breakpoint。
当在步骤 3 中执行代码时,它会在breakpoint位置停止,进入交互模式。
在交互模式下,我们可以检查任何变量的值,以及执行任何类型的操作。如步骤 4 所示,有时,通过重现其部分,可以更好地分析一行代码。
可以检查代码并在命令行中执行常规操作。可以通过调用n(ext)来执行下一行代码,就像步骤 5 中多次执行一样,以查看代码的流程。
步骤 6 显示了如何使用c(ontinue)命令恢复执行,以便在下一个断点处停止。所有这些操作都可以迭代以查看流程和值,并了解代码在任何时候正在做什么。
可以使用q(uit)停止执行,如步骤 7 所示。
还有更多...
要查看所有可用的操作,可以在任何时候调用h(elp)。
您可以使用l(ist)命令在任何时候检查周围的代码。例如,在步骤 4 中:
(Pdb) l
3 return False
4
5 lower = candidate - 1
6 while lower > 1:
7 breakpoint()
8 -> if candidate / lower == candidate // lower:
9 return False
10 lower -= 1
11
12 return True
另外两个主要的调试器命令是s(tep),它将执行下一步,包括进入新的调用,以及r(eturn),它将从当前函数返回。
您可以使用pdb命令b(reak)设置(和禁用)更多断点。您需要为断点指定文件和行,但实际上更直接,更不容易出错的方法是改变代码并再次运行它。
您可以覆盖变量以及读取它们。或者创建新变量。或进行额外的调用。或者您能想象到的其他任何事情。Python 解释器的全部功能都在您的服务中!用它来检查某些东西是如何工作的,或者验证某些事情是否发生。
避免使用调试器保留的名称创建变量,例如将列表称为l。这将使事情变得混乱,并在尝试调试时干扰,有时以非明显的方式。
breakpoint()函数是 Python 3.7 中的新功能,但如果您使用该版本,强烈推荐使用它。在以前的版本中,您需要用以下内容替换它:
import pdb; pdb.set_trace()
它们的工作方式完全相同。请注意同一行中的两个语句,这在 Python 中通常是不推荐的,但这是保持断点在单行中的一个很好的方法。
记得在调试完成后删除任何breakpoints!特别是在提交到 Git 等版本控制系统时。
您可以在官方 PEP 中阅读有关新的breakpoint调用的更多信息,该 PEP 描述了其用法:www.python.org/dev/peps/pep-0553/。
完整的pdb文档可以在这里找到:docs.python.org/3.7/library/pdb.html#module-pdb。它包括所有的调试命令。
另请参阅
-
学习 Python 解释器基础食谱
-
改进您的调试技能食谱
改进您的调试技能
在这个食谱中,我们将分析一个小脚本,它复制了对外部服务的调用,分析并修复了一些错误。我们将展示不同的技术来改进调试。
脚本将一些个人姓名 ping 到互联网服务器(httpbin.org,一个测试站点)以获取它们,模拟从外部服务器检索它们。然后将它们分成名和姓,并准备按姓氏排序。最后,它将对它们进行排序。
脚本包含了几个我们将检测和修复的错误。
准备工作
对于这个食谱,我们将使用requests和parse模块,并将它们包含在我们的虚拟环境中:
$ echo "requests==2.18.3" >> requirements.txt
$ echo "parse==1.8.2" >> requirements.txt
$ pip install -r requirements.txt
debug_skills.py脚本可以从 GitHub 获取:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter10/debug_skills.py。请注意,它包含我们将在本食谱中修复的错误。
如何做...
- 运行脚本,将生成错误:
$ python debug_skills.py
Traceback (most recent call last):
File "debug_skills.py", line 26, in <module>
raise Exception(f'Error accessing server: {result}')
Exception: Error accessing server: <Response [405]>
- 分析状态码。我们得到了 405,这意味着我们发送的方法不被允许。我们检查代码并意识到,在第 24 行的调用中,我们使用了
GET,而正确的方法是POST(如 URL 中所述)。用以下内容替换代码:
# ERROR Step 2\. Using .get when it should be .post
# (old) result = requests.get('http://httpbin.org/post', json=data)
result = requests.post('http://httpbin.org/post', json=data)
我们将旧的错误代码用(old)进行了注释,以便更清楚地进行更改。
- 再次运行代码,将产生不同的错误:
$ python debug_skills.py
Traceback (most recent call last):
File "debug_skills_solved.py", line 34, in <module>
first_name, last_name = full_name.split()
ValueError: too many values to unpack (expected 2)
- 在第 33 行插入一个断点,一个在错误之前。再次运行它并进入调试模式:
$ python debug_skills_solved.py
..debug_skills.py(35)<module>()
-> first_name, last_name = full_name.split()
(Pdb) n
> ...debug_skills.py(36)<module>()
-> ready_name = f'{last_name}, {first_name}'
(Pdb) c
> ...debug_skills.py(34)<module>()
-> breakpoint()
运行n不会产生错误,这意味着它不是第一个值。在c上运行几次后,我们意识到这不是正确的方法,因为我们不知道哪个输入是产生错误的。
- 相反,我们用
try...except块包装该行,并在那一点产生一个breakpoint:
try:
first_name, last_name = full_name.split()
except:
breakpoint()
- 我们再次运行代码。这次代码在数据产生错误的时候停止:
$ python debug_skills.py
> ...debug_skills.py(38)<module>()
-> ready_name = f'{last_name}, {first_name}'
(Pdb) full_name
'John Paul Smith'
- 现在原因很明显,第 35 行只允许我们分割两个单词,但如果添加中间名就会引发错误。经过一些测试,我们确定了这行来修复它:
# ERROR Step 6 split only two words. Some names has middle names
# (old) first_name, last_name = full_name.split()
first_name, last_name = full_name.rsplit(maxsplit=1)
- 我们再次运行脚本。确保移除
breakpoint和try..except块。这次,它生成了一个名字列表!并且它们按姓氏字母顺序排序。然而,一些名字看起来不正确:
$ python debug_skills_solved.py
['Berg, Keagan', 'Cordova, Mai', 'Craig, Michael', 'Garc\\u00eda, Roc\\u00edo', 'Mccabe, Fathima', "O'Carroll, S\\u00e9amus", 'Pate, Poppy-Mae', 'Rennie, Vivienne', 'Smith, John Paul', 'Smyth, John', 'Sullivan, Roman']
谁叫O'Carroll, S\\u00e9amus?
- 为了分析这个特殊情况,但跳过其余部分,我们必须创建一个
if条件,只在第 33 行为那个名字中断。注意in,以避免必须完全正确:
full_name = parse.search('"custname": "{name}"', raw_result)['name']
if "O'Carroll" in full_name:
breakpoint()
- 再次运行脚本。
breakpoint在正确的时刻停止了:
$ python debug_skills.py
> debug_skills.py(38)<module>()
-> first_name, last_name = full_name.rsplit(maxsplit=1)
(Pdb) full_name
"S\\u00e9amus O'Carroll"
- 向上移动代码,检查不同的变量:
(Pdb) full_name
"S\\u00e9amus O'Carroll"
(Pdb) raw_result
'{"custname": "S\\u00e9amus O\'Carroll"}'
(Pdb) result.json()
{'args': {}, 'data': '{"custname": "S\\u00e9amus O\'Carroll"}', 'files': {}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'Content-Length': '37', 'Content-Type': 'application/json', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.18.3'}, 'json': {'custname': "Séamus O'Carroll"}, 'origin': '89.100.17.159', 'url': 'http://httpbin.org/post'}
- 在
result.json()字典中,实际上有一个不同的字段,似乎正确地呈现了名字,这个字段叫做'json'。让我们仔细看一下;我们可以看到它是一个字典:
(Pdb) result.json()['json']
{'custname': "Séamus O'Carroll"}
(Pdb) type(result.json()['json'])
<class 'dict'>
- 改变代码,不要解析
'data'中的原始值,直接使用结果中的'json'字段。这简化了代码,非常棒!
# ERROR Step 11\. Obtain the value from a raw value. Use
# the decoded JSON instead
# raw_result = result.json()['data']
# Extract the name from the result
# full_name = parse.search('"custname": "{name}"', raw_result)['name']
raw_result = result.json()['json']
full_name = raw_result['custname']
- 再次运行代码。记得移除
breakpoint:
$ python debug_skills.py
['Berg, Keagan', 'Cordova, Mai', 'Craig, Michael', 'García, Rocío', 'Mccabe, Fathima', "O'Carroll, Séamus", 'Pate, Poppy-Mae', 'Rennie, Vivienne', 'Smith, John Paul', 'Smyth, John', 'Sullivan, Roman']
这一次,一切都正确了!您已成功调试了程序!
它是如何工作的...
食谱的结构分为三个不同的问题。让我们分块分析它:
- 第一个错误-对外部服务的错误调用:
在步骤 1 中显示第一个错误后,我们仔细阅读了产生的错误,说服务器返回了 405 状态码。这对应于不允许的方法,表明我们的调用方法不正确。
检查以下行:
result = requests.get('http://httpbin.org/post', json=data)
它告诉我们,我们正在使用GET调用一个为POST定义的 URL,所以我们在步骤 2 中进行了更改。
请注意,在这个错误中并没有额外的调试步骤,而是仔细阅读错误消息和代码。记住要注意错误消息和日志。通常,这已经足够发现问题了。
我们在步骤 3 中运行代码,找到下一个问题。
- 第二个错误-中间名处理错误:
在步骤 3 中,我们得到了一个值过多的错误。我们在步骤 4 中创建一个breakpoint来分析这一点的数据,但发现并非所有数据都会产生这个错误。在步骤 4 中进行的分析表明,当错误没有产生时停止执行可能会非常令人困惑,必须继续直到产生错误。我们知道错误是在这一点产生的,但只对某种类型的数据产生错误。
由于我们知道错误是在某个时候产生的,我们在步骤 5 中用try..except块捕获它。当异常产生时,我们触发breakpoint。
这使得步骤 6 执行脚本时停止,当full_name是'John Paul Smith'时。这会产生一个错误,因为split期望返回两个元素,而不是三个。
这在步骤 7 中得到了修复,允许除了最后一个单词之外的所有内容都成为名字的一部分,将任何中间名归为第一个元素。这符合我们这个程序的目的,按姓氏排序。
实际上,名字处理起来相当复杂。如果您想对关于名字的错误假设感到惊讶,请查看这篇文章:www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/。
以下行使用rsplit:
first_name, last_name = full_name.rsplit(maxsplit=1)
它通过单词从右边开始分割文本,最多分割一次,确保只返回两个元素。
当代码更改时,第 8 步再次运行代码以发现下一个错误。
- 第三个错误——使用外部服务返回的错误值:
在第 8 步运行代码会显示列表,并且不会产生任何错误。 但是,检查结果,我们可以看到一些名称被错误处理了。
我们选择第 9 步中的一个示例,并创建一个条件断点。 只有在数据满足if条件时才激活breakpoint。
在这种情况下,if条件在任何时候停止"O'Carroll"字符串出现,而不必使用等号语句使其更严格。 对于这段代码要实用主义,因为在修复错误后,您将需要将其删除。
代码在第 10 步再次运行。 从那里,一旦验证数据符合预期,我们就开始向后寻找问题的根源。 第 11 步分析先前的值和到目前为止的代码,试图找出导致不正确值的原因。
然后我们发现我们在从服务器的result返回值中使用了错误的字段。 json字段的值更适合这个任务,而且它已经为我们解析了。 第 12 步检查该值并查看应该如何使用它。
在第 13 步,我们更改代码进行调整。 请注意,不再需要parse模块,而且使用json字段的代码实际上更清晰。
这个结果实际上比看起来更常见,特别是在处理外部接口时。 我们可能会以一种有效的方式使用它,但也许这并不是最好的。 花点时间阅读文档,并密切关注改进并学习如何更好地使用工具。
一旦这个问题解决了,代码在第 14 步再次运行。 最后,代码按姓氏按字母顺序排列。 请注意,包含奇怪字符的其他名称也已修复。
还有更多...
修复后的脚本可以从 GitHub 获取:github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter10/debug_skills_fixed.py。 您可以下载并查看其中的差异。
还有其他创建条件断点的方法。 实际上,调试器支持创建断点,但仅当满足某些条件时才停止。 可以在 Python pdb文档中查看如何创建它:docs.python.org/3/library/pdb.html#pdbcommand-break。
在第一个错误中显示的捕获异常的断点类型演示了在代码中制作条件是多么简单。 只是要小心在之后删除它们!
还有其他可用的调试器具有更多功能。 例如:
-
ipdb(github.com/gotcha/ipdb):添加制表符补全和语法高亮显示 -
pudb(documen.tician.de/pudb/):显示旧式的半图形文本界面,以自动显示环境变量的方式显示 90 年代早期工具的风格 -
web-pdb(pypi.org/project/web-pdb/):打开一个 Web 服务器以访问带有调试器的图形界面
查看它们的文档以了解如何安装和运行它们。
还有更多可用的调试器,通过互联网搜索将为您提供更多选项,包括 Python IDE。 无论如何,要注意添加依赖项。 能够使用默认调试器总是很好的。
Python 3.7 中的新断点命令允许我们使用PYTHONBREAKPOINT环境变量轻松地在调试器之间切换。 例如:
$ PYTHONBREAKPOINT=ipdb.set_trace python my_script.py
这将在代码中的任何断点上启动ipdb。您可以在breakpoint()文档中了解更多信息:www.python.org/dev/peps/pep-0553/#environment-variable。
对此的一个重要影响是通过设置PYTHONBREAKPOINT=0来禁用所有断点,这是一个很好的工具,可以确保生产中的代码永远不会因为错误留下的breakpoint()而中断。
Python pdb文档可以在这里找到:docs.python.org/3/library/pdb.html parse模块的完整文档可以在github.com/r1chardj0n3s/parse找到,requests的完整文档可以在docs.python-requests.org/en/master/找到。
另请参阅
-
学习 Python 解释器基础配方
-
使用断点进行调试配方


浙公网安备 33010602011771号