Как программно подсчитать количество файлов в архиве с помощью 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()
  • Это действительно единственный способ? Я не могу найти любую другую команду, но кажется немного странным, что я не могу просто попросить количество файлов
  • Как насчет проверки ошибок? Достаточно было бы изменить это на:

    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 будут достаточно для стартеров

Ответ 1

Чтобы подсчитать количество членов архива в 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 = partial (p.stdout.read, 1 < 15)?

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

Ответ 2

Так как у меня уже есть 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 - wait vs communication 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))

Изменим это с моими выводами.