《Python3程序开发指南(第二版)》例题之多进程文件查找单词

专业书籍上的例子对于新手来说有点难以接受,但是如果愿意努力去了解,会发现有很多有趣的用法.
比如最近在学习的python语言,老师在讲解的时候用的往往是简单的例子,但是老师推荐的书籍《Python3程序开发指南(第二版)》中的例子在我看来,对于代码规范熟悉,编程思想的锻炼都很好,但是有的时候设计的函数可能在书的前面部分出现,或者根本不出现, 我会单独开一个这本书的分组,用来记录自己分析并补充这些代码的过程.

今天是第一篇,程序功能如标题题

开始

这是有关多线程这一章节的一个例题, 用"多进程"的方式实现在给定的目录或者文件中查找是否存在相关单词.

大概思路

程序分为主程序和子程序,主程序调用子程序.

主程序

  1. 主程序分析参数列表,得到需要查找的文件,需要查找的单词,每个子程序需要处理的文件数量
  2. 主程序循环调用子程序. 比如需要查找100个文件, 每个文件查找7个文件,那么需要14个子程序,(多余的文件放在第一个程序内)
  3. 将查找的单词和每个程序对应的查找文件传入管道给子程序

子程序

  1. 如果文件太大一次性读取会导致程序出问题,所以使用块查找方式,设定块大小
  2. 循环读取文件
  3. 循环读取文件块
  4. 判断查找单词是否存在块内(注意如果单词可能处于两个块的尾和头中),如果在跳出循环,查找下一个单词

代码

(完整代码在最下面)

主程序

文件名: 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()操作, 然后子进程开始执行代码

你可能感兴趣的:(《Python3程序开发指南(第二版)》例题之多进程文件查找单词)