利用脚本让计算机调度程序、定时运行或者在午夜让计算机执行CPU密集型任务是一件非常有效率的事情。
time模块
time模块为python内置模块,可以读取系统时钟的当前时间。unix纪元是编程中经常参考的时间:1970年1月1日0点,即协调世界时间UTC。time.time()函数返回自那一刻以来的秒数,是一个浮点值,这个时间被称为UNIX纪元时间戳。
# 纪元时间戳可以用于剖析代码
import time
def calcProd():
# Calculate the product of the first 100,000 numbers.
product = 1
for i in range(1, 100000):
product = product * i
return product
startTime = time.time()
prod = calcProd()
endTime = time.time()
print('The result is %s digits long.' % (len(str(prod))))
print('Took %s seconds to calculate.' % (endTime - startTime))
# 另外,cProfile.run()函数也可以剖析代码并提供更多信息,这里不再介绍
# 让时间暂停1秒钟
import time
>>> for i in range(3):
print('Tick')
time.sleep(1)
print('Tock')
time.sleep(1)
Tick
Tock
Tick
Tock
Tick
Tock
>>> time.sleep(5)
#请注意,在 IDLE 中按 Ctrl-C 不会中断 time.sleep()调用。IDLE 会等待到暂停结束,再抛出 KeyboardInterrupt 异常。
#要绕过这个问题,不要用一次 time.sleep(30)调用来暂停 30 秒,而是使用 for 循环执行 30 次 time.sleep(1)调用。
# 数字四舍五入
import time
>>> now = time.time()
>>> now
1425064108.017826
>>> round(now, 2)
1425064108.02
>>> round(now, 4)
1425064108.0178
>>> round(now)
1425064108
项目:超级秒表
假设要记录在没有自动化的枯燥任务上花了多少时间。你没有物理秒表,但是要为
笔记本或智能手机找到一个免费的秒表应用(没有广告,且不会将你的浏览历史发
送给市场营销人员)又出乎意料地困难。你可以自己用 Python 写一个简单的秒表程序。
总的来说,你的程序需要完成:
- 记录从按下回车键开始,每次按键的时间,每次按键都是一个新的“单圈”。
- 打印圈数、总时间和单圈时间。
这意味着代码将需要完成以下任务: - 在程序开始时,通过调用 time.time()得到当前时间,将它保存为一个时间戳。
在每个单圈开始时也一样。 - 记录圈数,每次用户按下回车键时加 1。
- 用时间戳相减,得到计算流逝的时间。
- 处理 KeyboardInterrupt 异常,这样用户可以按 Ctrl-C 退出。
打开一个新的文件编辑器窗口,并保存为 stopwatch.py。
#第 1 步:设置程序来记录时间
#秒表程序需要用到当前时间,所以要导入的 time 模块。程序在调用 input()之前,也应该向用户打印一些简短的说明,这样计时器可以在用户按下回车键后开始。
#! python3
# stopwatch.py - A simple stopwatch program.
import time
# Display the program's instructions.
print('Press ENTER to begin. Afterwards, press ENTER to "click" the stopwatch.
Press Ctrl-C to quit.')
input() # press Enter to begin
print('Started.')
startTime = time.time() # get the first lap's start time
lastTime = startTime
lapNum = 1
# 第 2 步:记录并打印单圈时间
# Start tracking the lap times.
try:
while True:
input()
lapTime = round(time.time() - lastTime, 2)
totalTime = round(time.time() - startTime, 2)
print('Lap #%s: %s (%s)' % (lapNum, totalTime, lapTime), end='')
lapNum += 1
lastTime = time.time() # reset the last lap time
except KeyboardInterrupt:
# Handle the Ctrl-C exception to keep its error message from displaying.
print('\nDone.')
类似程序的想法
时间追踪为程序打开了几种可能性。虽然可以下载应用程序来做其中一些事
情,但自己编程的好处是它们是免费的,而且不会充斥着广告和无用的功能。可以
编写类似的程序来完成以下任务:
- 创建一个简单的工时表应用程序,当输入一个人的名字时,用当前的时间记录
下他们进入或离开的时间。 - 为你的程序添加一个功能,显示自一项处理开始以来的时间,诸如利用 requests
模块进行的下载(参见第 11 章)。 - 间歇性地检查程序已经运行了多久,并为用户提供了一个机会,取消耗时太长
的任务。
datetime模块
time模块用于取得unix时间戳,但是如果需要更方便的显示日期或者对日期进行算术运算,要使用datetime模块。datetime模块有自己的datetime数据类型,datetime值表示一个特定时刻。
>>> import datetime
# 返回一个datetime对象,这个对象包含当前时刻的年,月,日,时,分,秒和微秒
>>> datetime.datetime.now()
datetime.datetime(2015, 2, 27, 11, 10, 49, 55, 53)
# 通过传递参数,也可以创建一个datetime对象
>>> dt = datetime.datetime(2015, 10, 21, 16, 29, 0)
>>> dt.year, dt.month, dt.day
(2015, 10, 21)
>>> dt.hour, dt.minute, dt.second
(16, 29, 0)
# 返回一个datetime对象,表示unix纪元后1000000秒的时刻
>>> datetime.datetime.fromtimestamp(1000000)
datetime.datetime(1970, 1, 12, 5, 46, 40)
# 返回当前的datetime对象,和datetime.datetime.now()一样
>>> datetime.datetime.fromtimestamp(time.time())
datetime.datetime(2015, 2, 27, 11, 13, 0, 604980)
# 比较时间的先后顺序
>>> halloween2015 = datetime.datetime(2015, 10, 31, 0, 0, 0)
>>> newyears2016 = datetime.datetime(2016, 1, 1, 0, 0, 0)
>>> oct31_2015 = datetime.datetime(2015, 10, 31, 0, 0, 0)
>>> halloween2015 == oct31_2015
True
>>> halloween2015 > newyears2016
False
>>> newyears2016 > halloween2015
True
>>> newyears2016 != oct31_2015
True
#timedelta对象,表示一段时间
# 通过timedelta创建对象,但不接受month和year关键字参数,因为这两个参数是可变的。
# timedelta对象拥有的总时间以天,秒,微妙来表示,将timedelta对象传入str函数,会返回可读性良好的字符串表示。
>>> delta = datetime.timedelta(days=11, hours=10, minutes=9, seconds=8)
>>> delta.days, delta.seconds, delta.microseconds
(11, 36548, 0)
>>> delta.total_seconds()
986948.0
>>> str(delta)
'11 days, 10:09:08'
# 算术运算符可以用于对datetime的值进行日期运算
>>> dt = datetime.datetime.now()
>>> dt
datetime.datetime(2015, 2, 27, 18, 38, 50, 636181)
>>> thousandDays = datetime.timedelta(days=1000)
>>> dt + thousandDays
datetime.datetime(2017, 11, 23, 18, 38, 50, 636181)
>>> oct21st = datetime.datetime(2015, 10, 21, 16, 29, 0)
>>> aboutThirtyYears = datetime.timedelta(days=365 * 30)
>>> oct21st
datetime.datetime(2015, 10, 21, 16, 29)
>>> oct21st - aboutThirtyYears
datetime.datetime(1985, 10, 28, 16, 29)
>>> oct21st - (2 * aboutThirtyYears)
datetime.datetime(1955, 11, 5, 16, 29)
# 暂停至特定的日期
import datetime
import time
halloween2016 = datetime.datetime(2016, 10, 31, 0, 0, 0)
while datetime.datetime.now() < halloween2016:
time.sleep(1)
#将datetime对象转换为字符串
#Unix 纪元时间戳和 datetime 对象对人类来说都不是很友好可读。利用 strftime()方法,可以将 datetime 对象显示为字符串。
#strftime()函数名中的 f 表示格式,format。
# strftime 指令 含义
# %Y 带世纪的年份,例如'2014'
# %y 不带世纪的年份,'00'至'99'(1970 至 2069)
# %m 数字表示的月份, '01'至'12'
# %B 完整的月份,例如'November'
# %b 简写的月份,例如'Nov'
# %d 一月中的第几天,'01'至'31'
# %j 一年中的第几天,'001'至'366'
# %w 一周中的第几天,'0'(周日)至'6'(周六)
# %A 完整的周几,例如'Monday'
# %a 简写的周几,例如'Mon'
# %H 小时(24 小时时钟),'00'至'23'
# %I 小时(12 小时时钟),'01'至'12'
# %M 分,'00'至'59'
# %S 秒,'00'至'59'
# %p 'AM'或'PM'
# %% 就是'%'字符
#向 strftime()传入一个定制的格式字符串,其中包含格式化指定(以及任何需要的斜线、冒号等),strftime()将返回一个格式化的字符串,表示 datetime 对象的信息。
>>> oct21st.strftime('%Y/%m/%d %H:%M:%S')
'2015/10/21 16:29:00'
>>> oct21st.strftime('%I:%M %p')
'04:29 PM'
>>> oct21st.strftime("%B of '%y")
"October of '15"
# 将字符串转换成datetime对象
# 格式字符串传入 strptime(),这样它就知道如何解析和理解日期字符串(strptime()
# 函数名中 p 表示解析,parse)。
>>> datetime.datetime.strptime('October 21, 2015', '%B %d, %Y')
datetime.datetime(2015, 10, 21, 0, 0)
>>> datetime.datetime.strptime('2015/10/21 16:29:00', '%Y/%m/%d %H:%M:%S')
datetime.datetime(2015, 10, 21, 16, 29)
>>> datetime.datetime.strptime("October of '15", "%B of '%y")
datetime.datetime(2015, 10, 1, 0, 0)
>>> datetime.datetime.strptime("November of '63", "%B of '%y")
datetime.datetime(2063, 11, 1, 0, 0)
多线程
import threading, time
print('Start of program.')
def takeANap():
time.sleep(5)
print('Wake up!')
# 注意,关键字参数是 target=takeANap,而不是 target=takeANap()。这是因为你想将 takeANap()函数本身作为参数,而不是调用 takeANap(),并传入它的返回值
#创建线程对象
threadObj = threading.Thread(target=takeANap)
# 创建新线程
threadObj.start()
print('End of program.')
# 向线程的目标函数传递参数,目标函数的常规参数可以作为列表传给args关键字参数,关键字参数可以传递给thread的kwargs。
>>> import threading
>>> threadObj = threading.Thread(target=print, args=['Cats', 'Dogs', 'Frogs'],
kwargs={'sep': ' & '})
>>> threadObj.start()
Cats & Dogs & Frogs
'''
线程的并发问题:可以轻松地创建多个新线程,让它们同时运行。但多线程也可能会导致所谓的
并发问题。如果这些线程同时读写变量,导致互相干扰,就会发生并发问题。并发
问题可能很难一致地重现,所以难以调试。
多线程编程本身就是一个广泛的主题,超出了本书的范围。必须记住的是:为了避
免并发问题,绝不让多个线程读取或写入相同的变量。当创建一个新的 Thread 对象时,
要确保其目标函数只使用该函数中的局部变量。这将避免程序中难以调试的并发问题。
在 http://nostarch.com/automatestuff/,有关于多线程编程的初学者教程。 '''
项目:多线程 XKCD 下载程序
在第 11 章,你编写了一个程序,从 XKCD 网站下载所有的 XKCD 漫画。这是
一个单线程程序:它一次下载一幅漫画。程序运行的大部分时间,都用于建立网络
连接来开始下载,以及将下载的图像写入硬盘。如果你有宽带因特网连接,单线程
程序并没有充分利用可用的带宽。
多线程程序中有一些线程在下载漫画,同时另一些线程在建立连接,或将漫画图
像文件写入硬盘。它更有效地使用 Internet 连接,更迅速地下载这些漫画。打开一个新
的文件编辑器窗口,并保存为 multidownloadXkcd.py。你将修改这个程序,添加多线程。
# 第一步: 修改程序以使用函数
#! python3
# multidownloadXkcd.py - Downloads XKCD comics using multiple threads.
import requests, os, bs4, threading
os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd
def downloadXkcd(startComic, endComic):
for urlNumber in range(startComic, endComic):
# Download the page.
print('Downloading page http://xkcd.com/%s...' % (urlNumber))
res = requests.get('http://xkcd.com/%s' % (urlNumber))
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text)
# Find the URL of the comic image.
comicElem = soup.select('#comic img')
if comicElem == []:
print('Could not find comic image.')
else:
comicUrl = comicElem[0].get('src')
# Download the image.
print('Downloading image %s...' % (comicUrl))
res = requests.get(comicUrl)
res.raise_for_status()
# Save the image to ./xkcd.
imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)), 'wb')
for chunk in res.iter_content(100000):
imageFile.write(chunk)
imageFile.close()
# 第 2 步:创建并启动线程
# Create and start the Thread objects.
downloadThreads = [] # a list of all the Thread objects
for i in range(0, 1400, 100): # loops 14 times, creates 14 threads
downloadThread = threading.Thread(target=downloadXkcd, args=(i, i + 99))
downloadThreads.append(downloadThread)
downloadThread.start()
第 3 步:等待所有线程结束
# Wait for all threads to end.
for downloadThread in downloadThreads:
downloadThread.join()
print('Done.')
python启动其他程序
# p代表process
>>> import subprocess
>>> subprocess.Popen('C:\\Windows\\System32\\calc.exe')
<subprocess.Popen object at 0x0000000003055A58>
>>> calcProc = subprocess.Popen('c:\\Windows\\System32\\calc.exe')
# poll是轮询的意思,就是判断是否执行完这个进程,如果进程仍在运行,则返回None,如果已经完成,则返回整数退出代码,用于说明进程是否是无错终止,0表示无错。
>>> calcProc.poll() == None
True
# wait会阻塞主进程,直到启动的进程终止。如果你希望你的程序暂停直到用户完成其他程序,则很有用。这里,我们关掉计算器就会显示进程已无错终止。
>>> calcProc.wait()
0
>>> calcProc.poll()
0
# 向Popen()传递命令行参数
# 向 Popen()传递一个列表,作为唯一的参数。该列表中的第一个字符串是要启动的程序的可执行文件名,所有后续的字符串将是该程序启动时,传递给该程序的命令行参数。
# 实际上,这个列表将作为被启动程序的 sys.argv 的值。
>>> subprocess.Popen(['C:\\Windows\\notepad.exe', 'C:\\hello.txt'])
<subprocess.Popen object at 0x00000000032DCEB8>
# 这不仅会启动记事本应用程序,也会让它立即打开 C:\hello.txt。
''' Task Scheduler、launchd 和 cron
如果你精通计算机,可能知道 Windows 上的 Task Scheduler,OS X 上的
launchd,或 Linux 上的 cron 调度程序。这些工具文档齐全,而且可靠,它们都允许
你安排应用程序在特定的时间启动。如果想更多地了解它们,可以在 http://nostarch.
com/automatestuff/找到教程的链接。
利用操作系统内置的调度程序,你不必自己写时钟检查代码来安排你的程序。但
是,如果只需要程序稍作停顿,就用 time.sleep()函数。或者不使用操作系统的调度程
序,代码可以循环直到特定的日期和时间,每次循环时调用 time.sleep(1)。 '''
# 不同于将 Python 程序导入为一个模块,如果 Python 程序启动了另一个 Python程序,两者将在独立的进程中运行,不能分享彼此的变量。
'hello.py'])
<subprocess.Popen object at 0x000000000331CF28>
# 使用默认应用程序打开文件
#每个操作系统都有一个程序,其行为等价于双击文档文件来打开它。在 Windows上,这是 start 程序。在 OS X 上,这是 open 程序。在 Ubuntu Linux 上,这是 see 程序。
#在交互式环境中输入以下代码,根据操作系统,向 Popen()传入'start'、'open'或'see':
>>> fileObj = open('hello.txt', 'w')
>>> fileObj.write('Hello world!')
12
>>> fileObj.close()
>>> import subprocess
>>> subprocess.Popen(['start', 'hello.txt'], shell=True)
'''Unix 哲学
程序精心设计,能被其他程序启动,这样的程序比单独使用它们自己的代码
更强大。Unix 的哲学是一组由 UNIX 操作系统(现代的 Linux 和 OS X 也是基于
它)的程序员建立的软件设计原则。它认为:编写小的、目的有限的、能互操作
的程序,胜过大的、功能丰富的应用程序。
较小的程序更容易理解,通过能够互操作,它们可以是更强大的应用程序的
构建块。智能手机应用程序也遵循这种方式。如果你的餐厅应用程序需要显示一
间咖啡店的方位,开发者不必重新发明轮子,编写自己的地图代码。餐厅应用程
序只是启动一个地图应用程序,同时传入咖啡店的地址,就像 Python 代码调用
一个函数,并传入参数一样。
你在本书中编写的 Python 程序大多符合 Unix 哲学,尤其是在一个重要的方
面:它们使用命令行参数,而不是 input()函数调用。如果程序需要的所有信息都
可以事先提供,最好是用命令行参数传入这些信息,而不是等待用户键入它。这
样,命令行参数可以由人类用户键入,也可以由另一个程序提供。这种互操作的方式,让你的程序可以作为另一个程序的部分而复用。
唯一的例外是,你不希望口令作为命令行参数传入,因为命令行可能记录它们,
作为命令历史功能的一部分。在需要输入口令时,程序应该调用 input()函数。
在 https://en.wikipedia.org/wiki/Unix_philosophy/,你可以阅读更多有关 Unix
哲学的内容。
'''
项目:简单的倒计时程序
就像很难找到一个简单的秒表应用程序一样,也很难找到一个简单的倒计时程
序。让我们来写一个倒计时程序,在倒计时结束时报警。
总的来说,程序要做到:
- 从 60 倒数。
- 倒数至 0 时播放声音文件(alarm.wav)。
这意味着代码将需要做到以下几点: - 在显示倒计时的每个数字之间,调用 time.sleep()暂停一秒。
- 调用 subprocess.Popen(),用默认的应用程序播放声音文件。
打开一个新的文件编辑器窗口,并保存为 countdown.py。
# 第 1 步:倒计时
#! python3
# countdown.py - A simple countdown script.
import time, subprocess
timeLeft = 60
while timeLeft > 0:
print(timeLeft, end='')
time.sleep(1)
timeLeft = timeLeft - 1
# 第2步: 播放声音文件
# At the end of the countdown, play a sound file.
subprocess.Popen(['start', 'alarm.wav'], shell=True)
类似程序的想法
倒计时是简单的延时,然后继续执行程序。这也可以用于其他应用程序和功能,
诸如:
- 利用 time.sleep()给用户一个机会,按下 Ctrl-C 取消的操作,例如删除文件。你
的程序可以打印“Press Ctrl-C to cancel”,然后用 try 和 except 语句处理所有
KeyboardInterrupt 异常。 - 对于长期的倒计时,可以用 timedelta 对象来测量直到未来某个时间点(生日?
周年纪念?)的天、时、分和秒数。