在本章中,我们将介绍以下配方:
- 准备任务
- 设置 cron 作业
- 捕获错误和问题
- 发送电子邮件通知
为了正确地自动化任务,我们需要一个平台,以便它们在正确的时间自动运行。需要手动运行的任务实际上并不是完全自动化的。
但是,为了能够让它们在后台运行,同时担心更紧迫的问题,该任务需要足以在开火并忘记模式下运行。我们应该能够监控它是否正确运行,确保捕获将来的操作(例如,如果出现有趣的事情,则接收通知),并知道在运行它时是否有任何错误。
确保一个软件始终以高可靠性运行实际上是一件非常重要的事情,要正确完成这项工作,需要具备专业知识和人员,通常称为系统管理员、操作人员或SRE(现场可靠性工程)。亚马逊(Amazon)和谷歌(Google)等网站需要巨额投资,以确保一切都能全天候运行。
这本书的目的远比这要温和得多。您可能不需要每年少于几秒钟的停机时间。以合理的可靠性运行任务是一件容易得多的事情。但是,要知道还有维护工作要做,所以要做好准备。
这一切都是从准确定义需要运行的任务开始的,并以一种不需要人为干预的方式来设计它。
一些理想的特征点如下:
- 单一、清晰的入口点:对要运行的任务没有混淆。
- 参数清晰:如果有参数,应该非常明确。
- 无交互:无法停止执行向用户请求信息。
- 结果应存储:以便能够在与运行时不同的时间进行检查。
- 明确结果:如果我们在结果中交互工作,我们会接受更详细的结果或进度报告。但是,对于自动化任务,最终结果应该尽可能简洁和切中要害。
- 应记录错误:分析出了什么问题
一个命令行程序已经有很多这样的特性了。它有一个明确的运行方式,带有定义的参数,并且可以存储结果,即使只是以文本格式。但是,可以通过一个配置文件和一个输出文件来改进它,以澄清参数。
请注意,第 6 点是捕获错误和问题配方的目标,将在此处介绍。
To avoid interactivity, do not use any command that stops for the user to input, like input. Remember to delete breakpoints for debugging!
首先,我们将遵循一个结构,其中一个主函数将作为入口点,所有参数都将提供给它。
This is the same the basic structure the was presented in the Adding command-line arguments recipe in Chapter 1, Let's Begin Our Automation Journey.
主函数的定义将包含所有显式参数,包括第 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并自动打开。这非常方便,如果文件无效,将引发错误。
Remember to open the file in the correct mode. In Step 5, the config file is opened in read mode (r) and the output file in write mode (w), which will overwrite the file if it exists. You may find the append mode (a), which will add the next piece of data at the end of an existing file.
configparser模块允许我们轻松使用配置文件。如步骤 2 所示,文件的解析如下所示:
config = configparser.ConfigParser()
config.read_file(file)然后,该配置将作为一个字典被分区,然后是值进行访问。请注意,值始终以字符串格式存储,需要转换为其他类型,例如整数:
If you need to obtain boolean values, do not perform value = bool(config[raw_value]) as it will be transformed into True no matter what; for instance, the string False is a true string, as it's not empty. Use the .getboolean method instead, for example, value = config.getboolean(raw_value).
Python3 允许我们将一个file参数传递给print函数,该函数将写入该文件。步骤 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的完整文档 https://docs.python.org/3/library/configparser.html.
在大多数情况下,这个配置解析器应该足够好,但是如果需要更多的功能,您可以使用 YAML 文件作为配置文件。YAML 文件(https://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)ARGUMENTS:
n1: 7
n2: 4- 然后,运行以下命令:
$ python3 prepare_task_yaml.py -c config.yaml
The result is 28还可以设置默认配置文件以及默认输出文件。这可以方便地创建不需要输入参数的纯任务。
As a general rule, try to avoid creating too many input and configuration parameters if the task has a very specific objective in mind. Try to limit the input parameters to different executions of the task. A parameter that never changes is probably fine being defined as a constant. A high number of parameters will make config files or command-line arguments complicated and will create more maintenance in the long run. On the other hand, if your objective is to create a very flexible tool to be used in very different situations, then creating more parameters is probably a good idea. Try to find your own proper balance!
Cron 是一种老式但可靠的执行命令的方法。Unix 从 70 年代就开始使用了,在系统管理中,执行维护(如释放空间、旋转日志、进行备份和其他常见操作)一直是最受欢迎的操作。
This recipe is Unix-specific, so it will work in Linux and MacOS. While it's possible to schedule a task in Windows, it's very different and uses Task Scheduler, which won't be described here. If you have access to a Linux server, it can be a good way of scheduling periodic tasks. The main advantages are as follows:
- 它几乎存在于所有 Unix 或 Linux 系统中,并配置为自动运行。
- 它很容易使用,虽然有点欺骗性。
- 这是众所周知的。几乎所有参与管理任务的人都会对如何使用它有一个大致的想法。
- 它允许简单的周期性命令,具有良好的精度。
但是,它也有一些缺点,如下所示:
- 默认情况下,它可能不会提供太多反馈。检索输出、记录执行和错误是至关重要的。
- 任务应该尽可能独立,以避免环境变量出现问题,例如使用错误的 Python 解释器,或者应该执行什么路径。
- 它是特定于 Unix 的。
- 只有固定的周期时间可用。
- 它不控制同时运行多少任务。每次倒计时开始时,它都会创建一个新任务。例如,一个任务需要一个小时才能完成,并且计划每 45 分钟运行一次,在运行两个任务的情况下,将有 15 分钟的重叠时间。
Don't understate the latest effect. Running multiple expensive tasks at the same time can have bad effects on performance. Having expensive tasks overlapping may result in a race condition where each task is making the others never finish! Allow ample time for your tasks to finish and keep an eye on them.
我们将制作一个名为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'模式所示。 - 使用
-h参数打印帮助时,ArgumentDefaultsHelpFormatter参数会自动添加有关默认值的信息。
检查任务是否产生预期结果,以及您是否可以登录到已知文件:
$ 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请注意,这将使用默认命令行编辑器打开一个编辑终端。
If you haven't set up your default command-line editor, by default, it is likely Vim. This can be disconcerting if you don't have experience with Vim. Press I to start inserting text and *Esc *when you're done. Then, exit after saving the file with :wq. For more information about Vim, see this introduction: https://null-byte.wonderhowto.com/how-to/intro-vim-unix-text-editor-every-hacker-should-be-familiar-with-0174674.
For information on how to change the default command-line editor, see the following: https://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 35crontab 行包括一行,描述运行任务的频率(前六个元素),以及任务。最初的六个元素中的每一个都表示不同的执行时间单位。大多数是星星,意思是任何:
* * * * * *
| | | | | |
| | | | | +-- 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"不要试着猜太多。使用类似于的备忘单 https://crontab.guru/ 用于示例和调整。这里将直接描述大多数常用用法。您还可以编辑公式并获取有关其运行方式的描述性文本。
在描述如何运行 cron 作业之后,包括执行任务的行,如在*如何执行…*部分的步骤 2 中所准备的。
Note that the task is described with all the full paths for every related file—the interpreter, the script, and the output file. This removes all ambiguity related to paths and reduces the chances of possible errors. A very common one is not being able to determine one (or more) of the three elements.
如果在 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在下一个配方中,我们将看到独立捕获错误的方法,以便任务能够顺利运行。
- 第一章中添加命令行选项的配方让我们开始我们的自动化之旅**
- 捕捉错误和问题配方
自动化任务的主要特征是它的发射和遗忘质量。我们不是在积极地观察结果,而是在后台运行它。
此外,由于本书中的大多数食谱都涉及外部信息,如网页或其他报告,因此在运行时发现意外问题的可能性很高。此配方将提供一个自动任务,该任务将安全地将意外行为存储在日志文件中,以便事后检查。
作为起点,我们将使用一个将两个数字分开的任务,如命令行中所述。
This task is very similar to the one presented in Step 5 in the How to do it… section, but instead of multiplying two numbers, we'll divide them.
- 创建
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)请注意,记录异常对于获取错误信息非常重要。
This kind of exception is nicknamed Pokémon, because it can catch'em all, as it will capture any unexpected error at the highest level. Do not use it in other areas of the code, as capturing everything can hide unexpected errors. At the very least, any unexpected exception should be logged to allow for further analysis.
通过exit(1)调用退出状态为 1 的额外步骤通知操作系统我们的脚本出了问题。
logging模块允许我们登录。请注意基本配置,其中包括用于存储日志的可选文件、要显示的日志的格式和级别。
The available level for logs are, from less critical to more critical—DEBUG, INFO, WARNING, ERROR, and CRITICAL. The logging level will set the minimal severity required to log the message. For example, an INFO log won't be stored if the severity is set to WARNING.
创建日志很容易。您可以通过调用方法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。
A good definition of log levels is key to displaying relevant information, while reducing spam. It is not easy to set up sometimes, but especially if more than one person is involved, try to agree on exactly what WARNING versus ERROR means to avoid misinterpretations.
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)Pythonlogging模块有很多功能,比如:
- 进一步调整日志的格式,例如,包括生成的日志的文件和行号。
- 定义不同的记录器对象,每个对象都有自己的配置,如日志级别和格式。这允许以不同的方式向不同的系统生成日志,但通常不用于简单性。
- 将日志发送到多个位置,例如标准输出和文件,甚至远程记录器。
- 自动旋转日志,在特定时间或大小后创建新日志文件。这有助于按天组织日志,并允许压缩或删除旧日志。
- 从文件中读取标准日志记录配置。
Instead of creating complex rules, try to log extensively, but with the proper level, and then filter.
详细内容请查看模块的 Python 文档 https://docs.python.org/3.7/library/logging.html 或处的教程 https://docs.python.org/3.7/howto/logging.html 。
- 第一章中添加命令行选项的配方让我们开始我们的自动化之旅**
- 准备任务配方
电子邮件已成为每个人日常使用的不可避免的工具。如果自动任务检测到某些内容,那么它可能是发送通知的最佳位置。另一方面,电子邮件收件箱中已经充满了垃圾邮件,所以要小心
Spam filters are also a reality. Be careful with who to send emails to and the number of emails to be sent. An email server or address can be labelled as spam, and all emails will be quietly dropped by the internet. This recipe will show how to send a single email, using an already existing email account.
这种方法对于发送给几个人的备用电子邮件是可行的,这是自动化任务的结果,但仅此而已。
对于此配方,我们需要设置有效的电子邮件帐户,其中包括以下内容:
- 有效的电子邮件服务器
- 要连接到的端口
- 地址
- 密码
这四个元素应该足以发送电子邮件。
Some email services, for example, Gmail, will encourage you to set up 2FA, meaning that a password is not enough to send an email. Typically, they'll allow you to create an specific password for apps to use, bypassing the 2FA request. Check your email provider's information for options.
要使用的电子邮件提供程序应在其文档中指明 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 帐户,请填写以下模板。该模板在 GitHub中提供 https://github.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() 就足够了。然后就可以发送整个信息了。
It is technically possible to send an email from a different email than the account used to send it. This is discouraged, though, as it can be considered by your email provider as trying to impersonate a different email. You can use the reply-to header as a way of allowing answering to a different account.
发送电子邮件需要连接到指定的服务器并启动 SMPT 连接。SMPT 是电子邮件通信的标准
这些步骤非常简单:配置服务器、登录服务器、发送准备好的消息,然后退出。
If you need to send more than one message, you can log in, send multiple emails, and then quit, instead of connecting each time.
If the objective is a bigger operation, like a marketing campaign, or even production emails like confirming a user's email, please check Chapter 8, Dealing with Communication Channels
本食谱中使用的电子邮件内容非常简单,但电子邮件可能比这复杂得多。
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;检查您的电子邮件提供商文档。
Remember that this recipe is aimed for simple notifications. Emails can grow quite complex if attaching different information. If your objective is an email for customers or any general group, try to use the ideas in Chapter 8, Dealing with Communication Channels.
- 第一章中添加命令行选项的配方让我们开始我们的自动化之旅**
- 准备任务配方