1

我编写了一个基本程序来检查包含许多 jpeg 文件(500000+)的目录树,验证它们没有损坏(大约 3-5% 的文件似乎以某种方式损坏)然后取一个 sha1sum文件(甚至是损坏的文件)并将信息保存到数据库中。

有问题的 jpeg 文件位于 windows 系统上,并通过 cifs 安装在 linux 机器上。它们的大小大多约为 4 兆字节,尽管有些可能稍大或稍小。

当我运行该程序时,它似乎运行得相当好一段时间,然后它因以下错误而崩溃。这是在它处理了大约 1100 个文件之后(错误表明在尝试打开 4.5 兆的文件时出现了问题)。

现在我明白我可以捕捉到这个错误并继续或重试等,但我很好奇它为什么会首先发生以及捕捉和重试是否真的可以解决问题 - 或者它会在重试时卡住(除非我当然限制重试,但随后会跳过一个文件)。

我在 debian 系统上使用“Python 2.7.5+”来运行它。系统至少有 4 Gig(可能是 8 个)的 ram,并且 top 报告脚本在运行时的任何时候都使用不到 1% 的 ram 和不到 3% 的 cpu。同样,此脚本运行的 jpeginfo 也使用同样少量的内存和 cpu。

为了避免在读取文件时使用过多的内存,我采用了这个答案中给出的方法来回答另一个问题:https ://stackoverflow.com/a/1131255/289545

此外,您可能会注意到“jpeginfo”命令处于等待“[OK]”响应的 while 循环中。这是因为如果“jpeginfo”认为它找不到文件,它会返回 0,因此 subprocess.check_output 调用不会将其视为错误状态。

我确实想知道 jpeginfo 在第一次尝试时似乎无法找到某些文件的事实是否可能相关(我怀疑它是相关的)但返回的错误说无法分配内存而不是找不到文件。

错误:

Traceback (most recent call last):
  File "/home/m3z/jpeg_tester", line 95, in <module>
    main()
  File "/home/m3z/jpeg_tester", line 32, in __init__
    self.recurse(self.args.dir, self.scan)
  File "/home/m3z/jpeg_tester", line 87, in recurse
    cmd(os.path.join(root, name))
  File "/home/m3z/jpeg_tester", line 69, in scan
    with open(filepath) as f:
IOError: [Errno 12] Cannot allocate memory: '/path/to/file name.jpg'

完整的程序代码:

  1 #!/usr/bin/env python
  2
  3 import os
  4 import time
  5 import subprocess
  6 import argparse
  7 import hashlib
  8 import oursql as sql
  9
 10
 11
 12 class main:
 13     def __init__(self):
 14         parser = argparse.ArgumentParser(description='Check jpeg files in a given directory for errors')
 15         parser.add_argument('dir',action='store', help="absolute path to the directory to check")
 16         parser.add_argument('-r, --recurse', dest="recurse", action='store_true', help="should we check subdirectories")
 17         parser.add_argument('-s, --scan', dest="scan", action='store_true', help="initiate scan?")
 18         parser.add_argument('-i, --index', dest="index", action='store_true', help="should we index the files?")
 19
 20         self.args = parser.parse_args()
 21         self.results = []
 22
 23         if not self.args.dir.startswith("/"):
 24                 print "dir must be absolute"
 25                 quit()
 26
 27         if self.args.index:
 28                 self.db = sql.connect(host="localhost",user="...",passwd="...",db="fileindex")
 29                 self.cursor = self.db.cursor()
 30
 31         if self.args.recurse:
 32                 self.recurse(self.args.dir, self.scan)
 33         else:
 34                 self.scan(self.args.dir)
 35
 36         if self.db:
 37                 self.db.close()
 38
 39         for line in self.results:
 40                 print line
 41
 42
 43
 44     def scan(self, dirpath):
 45         print "Scanning %s" % (dirpath)
 46         filelist = os.listdir(dirpath)
 47         filelist.sort()
 48         total = len(filelist)
 49         index = 0
 50         for filen in filelist:
 51                 if filen.lower().endswith(".jpg") or filen.lower().endswith(".jpeg"):
 52                         filepath = os.path.join(dirpath, filen)
 53                         index = index+1
 54                         if self.args.scan:
 55                                 try:
 56                                         procresult = subprocess.check_output(['jpeginfo','-c',filepath]).strip()
 57                                         while "[OK]" not in procresult:
 58                                                 time.sleep(0.5)
 59                                                 print "\tRetrying %s" % (filepath)
 60                                                 procresult = subprocess.check_output(['jpeginfo','-c',filepath]).strip()
 61                                         print "%s/%s: %s" % ('{:>5}'.format(str(index)),total,procresult)
 62                                 except subprocess.CalledProcessError, e:
 63                                         os.renames(filepath, os.path.join(dirpath, "dodgy",filen))
 64                                         filepath = os.path.join(dirpath, "dodgy", filen)
 65                                         self.results.append("Trouble with: %s" % (filepath))
 66                                         print "%s/%s: %s" % ('{:>5}'.format(str(index)),total,e.output.strip())
 67                         if self.args.index:
 68                                 sha1 = hashlib.sha1()
 69                                 with open(filepath) as f:
 70                                         while True:
 71                                                 data = f.read(8192)
 72                                                 if not data:
 73                                                         break
 74                                                 sha1.update(data)
 75                                 sqlcmd = ("INSERT INTO `index` (`sha1`,`path`,`filename`) VALUES (?, ?, ?);", (buffer(sha1.digest()), dirpath, filen))
 76                                 self.cursor.execute(*sqlcmd)
 77
 78
 79     def recurse(self, dirpath, cmd, on_files=False):
 80         for root, dirs, files in os.walk(dirpath):
 81             if on_files:
 82                 for name in files:
 83                     cmd(os.path.join(root, name))
 84             else:
 85                 cmd(root)
 86                 for name in dirs:
 87                     cmd(os.path.join(root, name))
 88
 89
 90
 91
 92
 93
 94 if __name__ == "__main__":
 95     main()
4

1 回答 1

5

在我看来,Python 只是从底层open()调用传递了一个错误,而真正的罪魁祸首是 Linux CIFS 支持——我怀疑 Python 会合成ENOMEM,除非系统内存真的耗尽(甚至可能我希望 Linux要调用OOM 杀手ENOMEM而不是获取)。

不幸的是,它可能需要一些 Linux 文件系统专家来弄清楚那里发生了什么,但是查看Linux 内核中 CIFS 的源代码ENOMEM,我可以看到当各种内核特定资源耗尽时返回的各种地方,而不是到总系统内存,但我对它还不够熟悉,无法说出它们中的任何一个的可能性。

要排除任何特定于 Python 的内容,您可以在其下运行该过程,strace以便查看 Python 从 Linux 获取的确切返回码。为此,请运行如下命令:

strace -eopen -f python myscript.py myarg1 myarg2 2>strace.log

-f跟随子进程(即jpeginfo您运行的命令),并且-eopen将只显示您的open()调用,而不是所有系统调用(strace默认情况下)。这可以生成合理数量的输出,这就是我在上面的示例中将其重定向到文件的原因,但如果您愿意,可以将其显示在终端上。

我希望您在遇到异常之前会看到类似的内容:

open("/path/to/file name.jpg", O_RDONLY) = -1 ENOMEM (Cannot allocate memory)

如果是这样,则此错误直接来自文件系统open()调用,您在 Python 脚本中几乎无能为力。如果失败,您可以捕获异常并重试(可能在短暂的延迟后)jpeginfo,但如果不知道导致错误的原因,很难说这种策略会有多成功。

当然,您可以在本地复制文件,但这听起来会很痛苦,因为文件太多了。

编辑:顺便说一句,您会看到很多open()与您的脚本无关的调用,因为strace它正在跟踪Python 进行的每个调用,例如,它包括打开自己的文件.py.pyc文件。忽略那些不引用您感兴趣的文件的文件。

于 2013-07-04T15:21:52.653 回答