专业书籍上的例子对于新手来说有点难以接受,但是如果愿意努力去了解,会发现有很多有趣的用法.
比如最近在学习的python语言,老师在讲解的时候用的往往是简单的例子,但是老师推荐的书籍《Python3程序开发指南(第二版)》中的例子在我看来,对于代码规范熟悉,编程思想的锻炼都很好,但是有的时候设计的函数可能在书的前面部分出现,或者根本不出现, 我会单独开一个这本书的分组,用来记录自己分析并补充这些代码的过程.
今天是第一篇,程序功能如标题题
这是有关多线程这一章节的一个例题, 用"多进程"的方式实现在给定的目录或者文件中查找是否存在相关单词.
程序分为主程序和子程序,主程序调用子程序.
(完整代码在最下面)
文件名: grepword_p.py
主要代码位于main()方法
def main():
这里我用的是执行后输入参数,这样便于调试,如果想用命令行的话,将变量的值设置为sys.argv[1:]
就好,sys.argv[0]
是程序文件名
# 手动输入参数
args_list = [item for item in input().split()]
用parse_option方法来解析输入的参数
# 获取用户指定的命令行选项, 待搜索单词, 待搜索文件列表
opts, word, args = parse_option(args_list)
parse_option(args_list)
函数使用了optparse模块,optparse模块是一个选项解析器,
(但是在我写这篇博客的时候发现这个模块已经弃用且不再维护, 新的开发转向argparse模块了(╥╯^╰╥))
主要用法有optparse.OptionParser()
创建OptionParser对象
OptionParser.add_option()
用于填充解析器,用法看代码就能看懂了
OptionParser.parse_args(list)
用于解析参数,返回相关对象以及未匹配的参数,具体用法可以查看 optparse模块
def parse_option(args_list = None):
args_list = args_list if args_list else sys.argv[1:]
parse = optparse.OptionParser()
parse.add_option("-r", "--recurse", dest="recurse", default="True", help="subdirectory recursion or not" )
parse.add_option("-n", "--numprocess", type=int, dest="numprocess", default=7, help="number of process")
# parse.add_option("-f", "--filename", dest="filename", help="Filename that need to be searched") 不用参数, 文件直接在所有参数后面输入, 最后会解析到args上面
parse.add_option("-w", "--word", dest="word", help="Searched words")
parse.add_option("-d", "--debug", dest="debug", default="debug", help="if now debug")
options, args = parse.parse_args(args_list)
# print("123")
# 返回 相应的opts, 查询词, 未匹配的词
return options, options.word, args
执行程序时的样例可以是
-w grepword -r true -n 7 ./
如果设置了默认值就可以不输入
现在回到main函数
由输入的文件名和opts里面的recurse参数来获得需要查询的所有文件名
# 带读取的文件列表(文件列表, 是否递归搜索)
file_list = get_files(args, opts.recurse)
get_files函数如下:
使用os.path
模块来判断是否是文件和文件夹,如果需要递归搜索则用os.walk()
即可,具体用法自己试一下就好了,或者查看文档 os.path — 路径操作
返回的是所有的文件名(包括路径,相对还是绝对路径取决于传入的路径)
def get_files(args, recurse):
file_list = []
# 假设目录一定以/结尾且不存在小数点
for filename in args:
# isfile, isdir...
if os.path.isfile(filename):
file_list.append(filename)
# recurse
if "true" in recurse.lower() and os.path.isdir(filename):
for root, dirs, files in os.walk(filename):
for file in files:
file_list.append(str(os.path.join(root, file)))
for item in file_list:
print(item)
return file_list
继续回到main()
每一行的意思见注释
# 每个进程被分配的文件数量
files_per_process = len(file_list) // opts.numprocess
# 分片(多余的文件给第一个进程)
start, end = 0, (files_per_process + (len(file_list) % opts.numprocess))
# 用于debug模式
number = 1
我们还需要获得子程序的文件名
# 获取子程序的名称
child = os.path.join(os.path.dirname(__file__), "grepword_pchild.py")
现在来创建一个管道数组来存取子程序的管道
pipes = []
pipes里面存取的是subprocess.Popen
对象,subprocess.Popen
是模块subprocess
中的一个接口,可用于底层的操作,具体用法可查看 Popen对象
现在是main()函数
接下来进行每个子函数文件的分配,直接上全部代码, 具体意思查看注释即可
涉及到的函数有
Popen.stdin
: 如果传入的参数是PIPE,则返回的是由open()返回的可写流对象
Popen.wait(timeout=None)
: 等待子进程终止。设置并返回returncode属性。
具体解释可查看→ Popen对象
while start < len(file_list):
# 每个进程具有一个Popen对象, 其stdin内输入搜索单词以及相关数量的文件名(路径加名字)
# 命令列表 - python解释器(sys.executable可以轻松访问)和子程序列表
command = [sys.executable, child]
if opts.debug:
command.append(str(number))
# 返回一个Popen类
pipe = subprocess.Popen(command, stdin=subprocess.PIPE)
# 严格来说保持对每个进程的引用并不是完全需要的, ,
# 因为pipe变量每次循环的时候会绑定到新的Popen对象,因为每个进程是独立运行的,
# 但我们还是把它加入一个列表中,这样可以打断程序的运行
pipes.append(pipe)
# 将搜索单词写入stdin,占位一行
pipe.stdin.write(word.encode("utf-8")+b"\n")
for filename in file_list[start:end]:
# 将搜索文件写入stdin,每个文件占位一行
pipe.stdin.write(filename.encode("utf-8")+b"\n")
# 关闭标准输入通道
pipe.stdin.close()
number += 1
start, end = end, end + files_per_process
while pipes:
pipe = pipes.pop()
# 在所有进程启动后,我们将等待每个子进程结束。
# 这并不是必需的,但在UNIX类系统上,这种做法可以确保在所有进程完成工作后返回到控制台提示符(否则,在
# 所有进程结束后必须按Enter键)。这种等待的另一个好处是,如果我们中断了程序(比
# 如,通过按Crl+c组合键),那么所有正在运行的进程也将被中断进而终止,并产生
# 一个未捕获的Keyboardinterrupt异常——如果我们不等待,那么主程序将结束(因而
# 不是可打断的),而子进程将继续运行(除非由kill程序或任务管理器终止)。
pipe.wait()
有个问题,子程序什么时候被调用的???(先记录)
stdin,buffer啥的我不懂啊啊啊啊啊所以略过了,到时候写个专门的博客吧
主程序grepword_p.py
到此为止,接下来我们来编写子程序grepword_pchild.py
.
正如上面所说,如果文件太大一次性读取会导致程序出问题,所以使用块查找方式,设定块大小
BLOCK_SIZE = 8000
获取程序参数的值:
# subprocess模块只能读写二进制数据并总是使用本地编码,所以需要读取sys.stdin的底层二进制数据缓冲区,并执行解码操作
stdin = sys.stdin.buffer.read()
lines = stdin.decode("utf8", "ignore").splitlines()
word = lines[0].rstrip()
遍历文件: (剩下的看注释吧)
for i in range(1, len(lines)):
filename = lines[i]
try:
# 以二进制模式进行读操作
with open(filename, 'rb') as fh:
print("begin find file {0}:".format(filename))
# 要保持对前面文件块的读取,确保不会因为待搜索单词落在两个文件块之间而失去匹配的机会
previous = fh.read(0).decode('utf-8', 'ignore')
while True:
# 如果文件很大,一次性读取这些文件会出现问题,所以我们以块的形式读入每个文件,
# 这个方法的另一个好处是如果提前出现了可以不用往下找,因为我们目的只是判断是否存在
# 要保持对前面文件块的读取,确保不会因为待搜索单词落在两个文件块之间而失去匹配的机会
current = fh.read(BLOCK_SIZE)
# 文件读取完毕
if not current:
break
# 假定所有文件都是用utf-8编码
current = current.decode('utf-8', 'ignore')
if (word in current) or (word in previous[-len(word):] + current[:len(word)]):
print("{0}{1}".format(number, filename))
break
if len(current) != BLOCK_SIZE:
break
previous = current
print("end find file {0}:".format(filename))
except EnvironmentError as err:
print("{0}{1}".format(number, err))
好了,虽然只要将上面所有的代码全部复制,调整一下函数的顺序即可执行,但是我还是贴一下完整顺序的代码,给测试使用
grepword_p.py
文件:
"""
作者: 子狼 日期: 2019/8/3 12:59
项目名称: python_week3
文件名称: grepword_p.py
"""
import os
import argparse
import sys
import subprocess
import optparse
# subprocess模块允许你生成新进程,连接到其输入/输出/错误管道,并获取其返回码。
def parse_option(args_list = None):
args_list = args_list if args_list else sys.argv[1:]
parse = optparse.OptionParser()
parse.add_option("-r", "--recurse", dest="recurse", default="True", help="subdirectory recursion or not" )
parse.add_option("-n", "--numprocess", type=int, dest="numprocess", default=7, help="number of process")
# parse.add_option("-f", "--filename", dest="filename", help="Filename that need to be searched") 文件直接在所有参数后面输入
parse.add_option("-w", "--word", dest="word", help="Searched words")
parse.add_option("-d", "--debug", dest="debug", default="debug", help="if now debug")
options, args = parse.parse_args(args_list)
print("123")
# 返回 相应的opts, 查询词, 未匹配的词
return options, options.word, args
def get_files(args, recurse):
file_list = []
# 假设目录一定以/结尾且不存在小数点
for filename in args:
# isfile, isdir...
if os.path.isfile(filename):
file_list.append(filename)
# recurse
if "true" in recurse.lower() and os.path.isdir(filename):
for root, dirs, files in os.walk(filename):
for file in files:
file_list.append(str(os.path.join(root, file)))
# for item in file_list:
# print(item)
return file_list
def main():
# 手动输入参数
args_list = [item for item in input().split()]
# 获取子程序的名称
child = os.path.join(os.path.dirname(__file__), "grepword_pchild.py")
# print(child)
# 获取用户指定的命令行选项, 待搜索单词, 待搜索文件列表
opts, word, args = parse_option(args_list)
# 带读取的文件列表(文件列表, 是否递归搜索)
file_list = get_files(args, opts.recurse)
# 每个进程被分配的文件数量
files_per_process = len(file_list) // opts.numprocess
# 分片(多余的文件给第一个进程)
start, end = 0, (files_per_process + (len(file_list) % opts.numprocess))
number = 1
# print("start, end", start, end)
pipes = []
while start < len(file_list):
# 每个进程具有一个Popen对象, 其stdin内输入搜索单词以及相关数量的文件名(路径加名字)
# 命令列表 - python解释器(sys.executable可以轻松访问)和子程序列表
command = [sys.executable, child]
if opts.debug:
command.append(str(number))
# 返回一个Popen类
pipe = subprocess.Popen(command, stdin=subprocess.PIPE)
# 严格来说保持对每个进程的引用并不是完全需要的, ,
# 因为pipe变量每次循环的时候会绑定到新的Popen对象,因为每个进程是独立运行的,
# 但我们还是把它加入一个列表中,这样可以打断程序的运行
pipes.append(pipe)
# 将搜索单词写入stdin,占位一行
pipe.stdin.write(word.encode("utf-8")+b"\n")
for filename in file_list[start:end]:
# 将搜索文件写入stdin,每个文件占位一行
pipe.stdin.write(filename.encode("utf-8")+b"\n")
# 关闭标准输入通道
pipe.stdin.close()
number += 1
start, end = end, end + files_per_process
while pipes:
pipe = pipes.pop()
# 在所有进程启动后,我们将等待每个子进程结束。
# 这并不是必需的,但在UNIX类系统上,这种做法可以确保在所有进程完成工作后返回到控制台提示符(否则,在
# 所有进程结束后必须按Enter键)。这种等待的另一个好处是,如果我们中断了程序(比
# 如,通过按Crl+c组合键),那么所有正在运行的进程也将被中断进而终止,并产生
# 一个未捕获的Keyboardinterrupt异常——如果我们不等待,那么主程序将结束(因而
# 不是可打断的),而子进程将继续运行(除非由kill程序或任务管理器终止)。
pipe.wait()
if __name__ == '__main__':
# print(sys.argv)
main()
grepword_pchild.py
文件:
"""
作者: 子狼 日期: 2019/8/3 13:15
项目名称: python_week3
文件名称: grepword2
"""
# 首先获取子程序的名称
# 获取用户指定的命令行选项
import os
import optparse
import sys
import subprocess
def parse_option(argv_list=None):
argv_list = sys.argv if not argv_list else argv_list
parse = optparse.OptionParser()
parse.add_option('-w', '--word', dest="word", default="wordgrep", help="search word")
parse.add_option('-r', '--recurse', dest='recurse', default="True", help='subdirectory recursion or not')
parse.add_option('-d', '--debug', dest='debug', default="True", help='debug mode or not')
parse.add_option('-n', '--numprocess', dest='numprocess', default=7, type=int, help='number of process')
print("123")
options, argv = parse.parse_args(argv_list)
word = options.word
return options, word, argv
def get_file(args, recurse):
"""
获取需要查找的文件列表
:param args: 需要查找的文件或者文件夹
:param recurse: 是否遍历
:return: 返回涉及到的文件的列表
"""
file_list = []
# 假设目录一定以/结尾且不存在小数点
for filename in args:
# isfile, isdir...
if os.path.isfile(filename):
file_list.append(filename)
# recurse
if "true" in recurse.lower() and os.path.isdir(filename):
for root, dirs, files in os.walk(filename):
for file in files:
file_list.append(str(os.path.join(root, file)))
for item in file_list:
print(item)
return file_list
def main():
# 手动输入参数
argv_list = input().split()
child = os.path.join(os.path.dirname(__file__), 'grepword_pchild2.py')
# 获取参数的相关值
opts, word, args = parse_option(argv_list)
# 获取需要查找的文件列表
print(opts, word, args)
file_list = get_file(args, opts.recurse)
# 给每个文件分配数量
files_per_process = len(file_list) // opts.numprocess
# 分片
start, end = 0, (files_per_process + len(file_list) % opts.numprocess)
# 用于调试
number = 1
# 管道
pipes = []
while start < len(file_list):
command = [sys.executable, child]
if "true" in opts.debug.lower():
command.append(str(number))
pipe = subprocess.Popen(command, stdin=subprocess.PIPE)
pipes.append(pipe)
pipe.stdin.write(word.encode('utf-8')+b'\n')
for filename in file_list[start:end]:
pipe.stdin.write(filename.encode('utf-8')+b'\n')
pipe.stdin.close()
number += 1
start, end = end, (end + files_per_process)
while pipes:
pipe = pipes.pop()
pipe.wait()
if __name__ == '__main__':
main()
示例输入:
-w grepword ./
8月9日补充: 子进程其实也是一个线程,也会阻塞, 在Popen对象被创建的时候, 就相当于我们的普通的命令行`python.exe child_filename.py",然后subprocess.stdin就是我们的输入框,stdin.close()相当于输入框输入结束,然后开始执行程序.
在stdin.write的时候,主程序的循环会继续执行,但是write操作会被阻塞(因为时间较多), 直到这个子进程对应的write完成后才会执行stdin.close()操作, 然后子进程开始执行代码