Как программно подсчитать количество файлов в архиве с помощью python

в программе, которую я поддерживаю, это делается так:

# count the files in the archive
length = 0
command = ur'"%s" l -slt "%s"' % (u'path/to/7z.exe', srcFile)
ins, err = Popen(command, stdout=PIPE, stdin=PIPE,
                 startupinfo=startupinfo).communicate()
ins = StringIO.StringIO(ins)
for line in ins: length += 1
ins.close()
  1. это действительно единственный способ ? Кажется, я не могу найти любая другая команда но кажется немного странным, что я не могу просто попросить количество файлов
  2. что насчет ошибок ? Было бы достаточно изменить это на:

    proc = Popen(command, stdout=PIPE, stdin=PIPE,
                 startupinfo=startupinfo)
    out = proc.stdout
    # ... count
    returncode = proc.wait()
    if returncode:
        raise Exception(u'Failed reading number of files from ' + srcFile)
    

    или я должен фактически проанализировать вывод Popen ?

EDIT: интересует 7z, rar, zip архивы (которые поддерживается 7z.exe) - но 7z и zip было бы достаточно для начала

2 ответов


чтобы подсчитать количество членов архива в zip-архиве на Python:

#!/usr/bin/env python
import sys
from contextlib import closing
from zipfile import ZipFile

with closing(ZipFile(sys.argv[1])) as archive:
    count = len(archive.infolist())
print(count)

он может использовать zlib, bz2, lzma модули, если таковые имеются, для распаковки архива.


чтобы подсчитать количество обычных файлов в архиве tar:

#!/usr/bin/env python
import sys
import tarfile

with tarfile.open(sys.argv[1]) as archive:
    count = sum(1 for member in archive if member.isreg())
print(count)

он может поддерживать gzip, bz2 и lzma сжатие в зависимости от версии Python.

вы можете найти сторонний модуль, который обеспечит аналогичную функциональность для 7z архивов.


получить количество файлов в архиве, используя 7z утилиты:

import os
import subprocess

def count_files_7z(archive):
    s = subprocess.check_output(["7z", "l", archive], env=dict(os.environ, LC_ALL="C"))
    return int(re.search(br'(\d+)\s+files,\s+\d+\s+folders$', s).group(1))

вот версия, которая может использовать меньше памяти, если в архиве много файлов:

import os
import re
from subprocess import Popen, PIPE, CalledProcessError

def count_files_7z(archive):
    command = ["7z", "l", archive]
    p = Popen(command, stdout=PIPE, bufsize=1, env=dict(os.environ, LC_ALL="C"))
    with p.stdout:
        for line in p.stdout:
            if line.startswith(b'Error:'): # found error
                error = line + b"".join(p.stdout)
                raise CalledProcessError(p.wait(), command, error)
    returncode = p.wait()
    assert returncode == 0
    return int(re.search(br'(\d+)\s+files,\s+\d+\s+folders', line).group(1))

пример:

import sys

try:
    print(count_files_7z(sys.argv[1]))
except CalledProcessError as e:
    getattr(sys.stderr, 'buffer', sys.stderr).write(e.output)
    sys.exit(e.returncode)

для подсчета количества строк в выходных данных универсального подпроцесса:

from functools import partial
from subprocess import Popen, PIPE, CalledProcessError

p = Popen(command, stdout=PIPE, bufsize=-1)
with p.stdout:
    read_chunk = partial(p.stdout.read, 1 << 15)
    count = sum(chunk.count(b'\n') for chunk in iter(read_chunk, b''))
if p.wait() != 0:
    raise CalledProcessError(p.returncode, command)
print(count)

он поддерживает неограниченный выход.


не могли бы вы объяснить, почему buffsize=-1 (в отличие от buffsize=1 в вашем предыдущем ответе: stackoverflow.com/a/30984882/281545)

bufsize=-1 означает использовать размер буфера ввода-вывода по умолчанию вместо bufsize=0 (без буфера) на Python 2. Это повышение производительности на Python 2. Это значение по умолчанию для последних версий Python 3. Вы можете получить короткое чтение (потерять данные), если на некоторых более ранних версиях Python 3, где bufsize для bufsize=-1.

этот ответ читается кусками, и поэтому поток полностью буферизован для эффективности. решение, которое вы связали ориентирован на линию. bufsize=1 означает "линия буферизованный". Существует минимальная разница от bufsize=-1 иначе.

, а также то, что read_chunk = частичный(стр. поток stdout.читай, 1

это эквивалентно read_chunk = lambda: p.stdout.read(1<<15) но обеспечивает больше самоанализа в целом. Он используется для реализовать wc -l в Python эффективно.


Так как у меня уже есть 7z.exe в комплекте с приложением, и я, конечно, хочу избежать стороннего lib, в то время как мне нужно разобрать RAR и 7z архивы, я думаю, что я пойду с:

regErrMatch = re.compile(u'Error:', re.U).match # needs more testing
r"""7z list command output is of the form:
   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2015-06-29 21:14:04 ....A       <size>               <filename>
where ....A is the attribute value for normal files, ....D for directories
"""
reFileMatch = re.compile(ur'(\d|:|-|\s)*\.\.\.\.A', re.U).match

def countFilesInArchive(srcArch, listFilePath=None):
    """Count all regular files in srcArch (or only the subset in
    listFilePath)."""
    # https://stackoverflow.com/q/31124670/281545
    command = ur'"%s" l -scsUTF-8 -sccUTF-8 "%s"' % ('compiled/7z.exe', srcArch)
    if listFilePath: command += u' @"%s"' % listFilePath
    proc = Popen(command, stdout=PIPE, startupinfo=startupinfo, bufsize=-1)
    length, errorLine = 0, []
    with proc.stdout as out:
        for line in iter(out.readline, b''):
            line = unicode(line, 'utf8')
            if errorLine or regErrMatch(line):
                errorLine.append(line)
            elif reFileMatch(line):
                length += 1
    returncode = proc.wait()
    if returncode or errorLine: raise StateError(u'%s: Listing failed\n' + 
        srcArch + u'7z.exe return value: ' + str(returncode) +
        u'\n' + u'\n'.join([x.strip() for x in errorLine if x.strip()]))
    return length

проверка ошибок как в Python Popen-подождите vs общаться vs CalledProcessError by @JFSebastien


мой окончательный (ish) на основе принятого ответа - unicode может не понадобиться, пока я использую его везде. Также сохранено регулярное выражение (которое я могу расширить, я видел такие вещи, как re.compile(u'^(Error:.+|.+ Data Error?|Sub items Errors:.+)',re.U). Придется заглянуть в check_output и CalledProcessError.

def countFilesInArchive(srcArch, listFilePath=None):
    """Count all regular files in srcArch (or only the subset in
    listFilePath)."""
    command = [exe7z, u'l', u'-scsUTF-8', u'-sccUTF-8', srcArch]
    if listFilePath: command += [u'@%s' % listFilePath]
    proc = Popen(command, stdout=PIPE, stdin=PIPE, # stdin needed if listFilePath
                 startupinfo=startupinfo, bufsize=1)
    errorLine = line = u''
    with proc.stdout as out:
        for line in iter(out.readline, b''): # consider io.TextIOWrapper
            line = unicode(line, 'utf8')
            if regErrMatch(line):
                errorLine = line + u''.join(out)
                break
    returncode = proc.wait()
    msg = u'%s: Listing failed\n' % srcArch.s
    if returncode or errorLine:
        msg += u'7z.exe return value: ' + str(returncode) + u'\n' + errorLine
    elif not line: # should not happen
        msg += u'Empty output'
    else: msg = u''
    if msg: raise StateError(msg) # consider using CalledProcessError
    # number of files is reported in the last line - example:
    #                                3534900       325332  75 files, 29 folders
    return int(re.search(ur'(\d+)\s+files,\s+\d+\s+folders', line).group(1))

редактировать это с моими выводами.