Автор Тема: Прост скрипт за търсене на дублиращи се файлове.  (Прочетена 890 пъти)

4096bits

  • Участник
  • *****
  • Публикации: 1921
    • Профил
Почна да не ми стига мястото на лаптопа и външните дискове и вчера си написах това, да ми търси дублиращи се файлове. Може и в bash да се направи с един ред, обаче... Пробвах да го направя да ползва повече ядра и уж да е по-бързо. Не зная, доколко съм направил нещо и ако някой иска, да си каже мнението и да тества. Аз ще го направя, като имам повече време, че терабайти не се проверяват за пет минути. md5 уж е бързо, ма не чак толкова. Днес му добавих само шаренийките и малко коментари.

Python 3.6

Код:
#!/usr/bin/env python3
# Find and prints duplicated files based on their md5 sum
#
import argparse
from collections import defaultdict
from concurrent import futures
from hashlib import md5
import json
import os.path
import os

parser = argparse.ArgumentParser(description='Print duplicated files')
parser.add_argument('path',
                    type=str,
                    default='.',
                    nargs='?',
                    help='Path to a directory to scan')
parser.add_argument('-d', '--dump',
                    dest='res_file',
                    type=str,
                    default='./duplicated.json',
                    help='''Path/filename to store the results
                    Default: ./duplicated.json''')

args = parser.parse_args()
path = args.path
result_file = args.res_file

def human_fsize(size):
    """Return file size in human readable format.
       Argument: file's size
       Type: int
    """

    pref = [('B', 1), ('KB', 1024),
            ('BM', 1024**2), ('GB', 1024**3), ('TB', 1024**4)]
    counter = 0
    res = size
    while True:
        res = res / 1024
        if res < 1:
            break
        else:
            counter += 1

    if size > 1024:
        h_size = round(size / pref[counter][1], 3)
    else:
        h_size = size

    prefix = pref[counter][0]

    return f'{h_size} {prefix}'

def file_hash(file_name):
    """Returns a tuple of md5sum and file name.
       Argument: file's name
       Type: str
    """
    hasher = md5()

    with open(file_name, 'rb') as in_file:
        while True:
            chunk = in_file.read(65536)
            if chunk:
                hasher.update(chunk)
            else:
                break

    md5sum = hasher.hexdigest()
    return md5sum, file_name

# Walk the through directories and look for duplicates
duped = {}
hashed = defaultdict(list)
for root, dirs, files in os.walk(path):
    full_paths = [os.path.join(root, file_) for file_ in files]

    with futures.ProcessPoolExecutor(max_workers=4) as executor:

        for result in executor.map(file_hash, full_paths):
            hashed[result[0]].append(result[1])

            if len(hashed[result[0]]) > 1:
                duped[result[0]] = hashed[result[0]]

# Print the results
lblue, purple, default = ('\033[94m', '\033[35m', '\033[0m')
for key, values in duped.items():
    print(f'md5: {lblue}{key}{default} size: {lblue}{human_fsize(os.stat(values[0]).st_size)}')
    for v in values:
        print(f'    * {purple}{v}{lblue}')
    print(f'{default}')

# Dump the results in a json file
with open(result_file, 'w', encoding='utf-8') as dump_file:
    json.dump(duped, dump_file, indent=4, ensure_ascii=False)

Активен

"As they say in Mexico 'dosvidaniya'. That makes two vidaniyas."

4096bits

  • Участник
  • *****
  • Публикации: 1921
    • Профил
Беше много тъпо, да му кажа смята md5 на всеки файл  :D
Преправих го малко, да смята само на тези, дето са с еднакъв размер.

Някой може ли да каже, какъв размер парче от файла да му задам да чете с оглед на по-добра производителност? В момента е 1М. За малките файлове може да е добре, но не зная за тези гигабайтовите.

Код:
#!/usr/bin/env python3
# Find and prints duplicated files based on their md5 sum
#
import argparse
from collections import defaultdict
from concurrent import futures
from hashlib import md5
import json
import os.path
import os

parser = argparse.ArgumentParser(description='Print duplicated files')
parser.add_argument('path',
                    type=str,
                    default='.',
                    nargs='?',
                    help='Path to a directory to scan')
parser.add_argument('-d', '--dump',
                    dest='res_file',
                    type=str,
                    default='./duplicated.json',
                    help='''Path/filename to store the results
                    Default: ./duplicated.json''')

args = parser.parse_args()
path = args.path
result_file = args.res_file


def human_fsize(size):
    """Return file size in human readable format.
       Argument: file's size
       Type: int
    """

    pref = [('B', 1), ('KB', 1024),
            ('MB', 1024**2), ('GB', 1024**3), ('TB', 1024**4)]
    counter = 0
    res = size
    while True:
        res = res / 1024
        if res < 1:
            break
        else:
            counter += 1

    if size > 1024:
        h_size = round(size / pref[counter][1], 3)
    else:
        h_size = size

    prefix = pref[counter][0]
    return f'{h_size} {prefix}'


def file_hash(file_name):
    """Returns a tuple of md5sum and file name.
       Argument: file's name
       Type: str
    """
    hasher = md5()

    with open(file_name, 'rb') as in_file:
        while True:
            chunk = in_file.read(1048576) # 1MB - 1024**2
            if chunk:
                hasher.update(chunk)
            else:
                break

    md5sum = hasher.hexdigest()
    return md5sum, file_name

# Walk through the directories and look for equal file sizes
for_hashing = {}
eq_sized = defaultdict(list)
for root, dirs, files in os.walk(path):
    full_paths = [os.path.join(root, file_) for file_ in files]

    for full_name in full_paths:
        size = os.stat(full_name).st_size
        eq_sized[size].append(full_name)

        if len(eq_sized[size]) > 1:
            for_hashing[size] = eq_sized[size]

# Hashing the files
duped = {}
hashed = defaultdict(list)
for size, files in for_hashing.items():

    with futures.ProcessPoolExecutor(max_workers=2) as executor:

        for result in executor.map(file_hash, files):
            hashed[result[0]].append(result[1])

            if len(hashed[result[0]]) > 1:
                duped[result[0]] = hashed[result[0]]


# Print the results
lblue, purple, default = ('\033[94m', '\033[35m', '\033[0m')
sizes = 0
duplicates = 0
for key, values in duped.items():
    size = os.stat(values[0]).st_size
    sizes += (len(values) - 1) * size
    duplicates += len(values) - 1
    print(f'md5: {lblue}{key}{default} size: {lblue}{len(values)} {default}* {lblue}{human_fsize(size)}')

    for v in values:
        print(f'    * {purple}{v}{lblue}')

    print(f'{default}')

# Dump the results in a json file
with open(result_file, 'w', encoding='utf-8') as dump_file:
    json.dump(duped, dump_file, indent=4, ensure_ascii=False)

print(f'Dumped as JSON in: {lblue}{result_file}{default}\n')

print(f'Summarize:\n')
print(f'    Files: {lblue}{len(duped)}{default}\n    Duplicates: {lblue}{duplicates}\n')
print(f'{default}Deleting the duplicates will free {human_fsize(sizes)}!')

« Последна редакция: Окт 04, 2017, 11:06 от 4096bits »
Активен

"As they say in Mexico 'dosvidaniya'. That makes two vidaniyas."

spec1a

  • Участник
  • *****
  • Публикации: 314
    • Профил
   Със каква файлова с-ма си ?
   Може би по-доброто решение е да ползваш т.нар. "дедупликация"
   Ако има няколко файла с еднакво съдържание,де факто физически
съществува само един, всички останали всъщност са само линкове
към "оригиналния".
   Мисля,нещо подобно има в btrfs
« Последна редакция: Окт 04, 2017, 12:49 от spec1a »
Активен

4096bits

  • Участник
  • *****
  • Публикации: 1921
    • Профил
В момента на тази система с ext4, имам едно Ubuntu MATE  с btrfs, но основно ще се преравят външни носители, които са на ntfs. Мислех си да използвам mmap, но не съм сигурен, дали системата ще си реши сама, колко да качи в паметта, вместо да го задавам ръчно.
Активен

"As they say in Mexico 'dosvidaniya'. That makes two vidaniyas."

remotexx

  • Участник
  • *****
  • Публикации: 395
    • Профил
Колега, реших да те оставя да се побориш малко сам.. и виждам - не съм сгрешил
Ето няколко съвета и от мен
1. Нали се сещаш че и с много (100) нишки няма да стане много по бързо отколкото с 2-4 просто няма как да стане по бързо от скоростта на четене от диска, в сл. Два диска
2. Той този, както и повечето алгоритми са блокови, но ако разглеждаме един блок като неделим то на ниво блок са си поточни, та може да спреш изчисленията при първия блок с разлики т.е. Не знам там тоя клас дето ползваш дали позволява на наколко пъти т.е. След всеки блок да викаш „колко е текущата сума до момента” и ако се различава спираш веднага. Ако класът който ползваш не го позволява виж 3.
3. Ползвай по прост алгоритъм напр. CRC32 който направо си е поточен на ниво int32, можеш да ги разпишеш на ръка дето се вика и да спреш при първата разлика
Внимавай само за следното, еднакви суми не гарантират еднакви файлове т.е. Ползвай само за бърза проверка за различни файлове, ако црц32 съвпада пак се налага мд5

А ако очакваш повечето да са еднакви смятай го паралелно с мд5, колкото ако мд5 не позволява изкарване на междинна дума црц32 да ти каже кога да спреш
Активен

makeme

  • Участник
  • *****
  • Публикации: 187
  • Distribution: Many
  • Window Manager: Mate
    • Профил
В Ubuntu Mate има програмче за тази работа :)
Код:
sudo fdupes -r /home/
Знам, че не е скрипт, но ако искаш да си свършиш работата може да го пробваш. Доста е добро и върши същата работа.

ПС: Даже ще ми е интересно да ги пуснеш да се състезават с твоя скрипт. Може да си направил нещо по-бързо :)
« Последна редакция: Окт 04, 2017, 17:06 от makeme »
Активен

Distributions:  UbuntuMate 14.04; 15.10; 16.04, CentOS 6.x, 7.x, Kali 2.0 ...

4096bits

  • Участник
  • *****
  • Публикации: 1921
    • Профил
Напълно ясно ми е, че всичко зависи от скоростта на трансфер на файловете от дисковете. Нямаше да се блъскам, ако не беше бройката на файлчетата, някои големички. Става въпрос за архиви на години. Музика, снимки, видео и филми, свалени сайтове или части от такива. Тонове pdf документи и книги

Затова се и опитвам да впрегна осемте ядра. Иначе програмки за тази работа има, да. Ако знаех, дали ще се справят по-бързо, нямаше да се занимавам. Неприятната част е, че това не ми е професия и сигурно основни неща не зная. Щом в началото бях почнал да смятам всеки файл, направете си сметката  ;D Не се бях сетил, че мога да спра насред сметките на петия или щестия блок например. Ще проверя задължително, как стоят нещата с това. Ако hashlib не го може, ще пробвам с друга библиотека. Искаше ми се да не е такава от трета страна, щото може да трябва да го пренасям.
Остава и въпроса с комуникацията между процесите, ако тръгна да правя и това. Ще трябва някак да споделят предишно състояние на чексумите. Redis може би, не зная. Или сокети. Ще стане май мътна и кървава  :D

----------------------

Тъй, значи паралелно с размера да следя и crc32 и да добавям файлове в списъка за чексума, само, ако съвпада и размера и crc32?
« Последна редакция: Окт 04, 2017, 20:03 от 4096bits »
Активен

"As they say in Mexico 'dosvidaniya'. That makes two vidaniyas."

remotexx

  • Участник
  • *****
  • Публикации: 395
    • Профил
ами за малък файл (особено пък ако се очаква повечето да са рaзлични - може и за по-големичък .. напр. 16/32 MB или там колкото ти е буфера на диска) може и само CRC32, като очакването е да не се налага МД5 или айде да кажем очаква се да се наложи МД5 в под 5% от случаите (само когато CRC32 им съвпада)

- но ако файла е голям - напр. 4 ГБ и се очаква да е еднакъв (то там обик. ако размера съвпада вероятността да е същия е много голяма) то тогава ще трябва да се смятат и двете едновременно (за да не се четат 2 х 4 ГБ)

т.е. за малки файлове може и поотделно (надяваш се да са различни и да спестиш от несмятане и на МД5) - забавянето е пренебрежимо ако се наложи после и МД5 (данните вече са кеширани - 16/32 Мб - не ги знам колко са големи днешно време дисковите буфери), но за големи (ГБ, ТБ файлове) за да не го четеш по два пъти смятай едновременно, като CRC ще ти каже кога да спреш.

CRC-8/16/32/64 е толкова просто че не ти трябва и клас - директно по формулата и ако искаш на всеки 8/16/32/64 бита сравнявай
https://en.wikipedia.org/wiki/Cyclic_redundancy_check

П.П. би трябвало и МД5 да може да спре - е не на всеки байт, но поне на/след всеки блок и да ти каже колко е сумата до момента, но това само ако ти си го разпишеш, а като те гледам ми се видя готова библиотека да ползваш, а те рядко дават шанс - та само за това предложих CRC32 понеже лесно се смята ...ако библиотеката поддържа показване на междиини МД5 (без да прецаква крайния резултат) тогава карай само с него.

П.П.П. Сетих се и друго - Ако стане ноемра само с МД5 тогава внимавай размера на буфера ти т.е. парчето данни което добавяш на всяко четене към МД5 сметалката да е кратно на размера на блока за сътоветното МД4/5/...256, защото ако не е кратна .. е тогава ще си го допълни с нули - добрата новина е че и на двата файла ще го смята еднакво грешно т.е. по този начин сметнати МД5 ще съвпадат, проблема е ако решиш да провериш после на ръка или с друга утилка - да не се чудиш що няма съвпадение... Така като гледам класа който ползваш може и да може да му се извика повече от веднъж да каже колко е сумата до момента, но имай предвид че ще го лови горния БЪГ т.е. ако последния/буфера не е кратен на размера на блока с който работи алгоритъма - ще го допълва (на случайни места с нули - колкото пъти поискаш текущата сума и блока иска допълване) - както вече казах това не е проблем ако на едни и същи локации по двата файла правиш сметките, но няма да съвпадат сумите с истинските (генерирани от нормалните инструменти)
« Последна редакция: Окт 04, 2017, 20:32 от remotexx »
Активен

remotexx

  • Участник
  • *****
  • Публикации: 395
    • Профил
Не разбрах какво имаш предвид със "въпроса с комуникацията между процесите," - аз мислех че един процес ти е един файл (което пак може да озори диска да чете от много различни места хеле па ако му е и малък ...буфера)
имай предвид, че тези суми не можеш да ги смяташ паралелно (затова са и толкова бавни) - т.е. ако си мислиш да го разпаралелиш (малей каква сложна дума) един файл на 8 части - няма да стане - тези сметалки искат последнователен достъп и напр. втория блок започва с начално състояние  сметнато от първия та не може да го разделиш 1 файл на 8 да сметнеш паралелно 8те МД5 - нали накрая ще имаш 8 МД5??? е да ако една е различна значи и файловете са разлчини, но пак ще си ги сметнал и осемте! плюс това понеже нишките се състезават няма гаранция напр. че нишка 1 при файл 1 няма да грабне напр. блок 1,9,... а при файл 2 да грабне 1,10.. (щото позакъсняла малко в сметките) и тогава става мазало, та единствения начин е по една нишка на файл и с динамично управление на броя - напр. 4 БГ файл - само една (иначе ще се омота диска да чете различни големи файлове от различни места), мног малки файлове: повече нишки, но не мнгоо повече отколкото буфера на диска може да събере

и за домашно - интересния вариант когато двата диска имат различен размер буфери :-)
Активен

makeme

  • Участник
  • *****
  • Публикации: 187
  • Distribution: Many
  • Window Manager: Mate
    • Профил
Ако си решил да си развиваш знанията - това го разбирам и не чети надолу :)

Ако пък ли искаш решение - програмата, която цитирах я ползвам и пак казвам е добро решение.

За да ти дам база за сравнение я пуснах на домашния сървър да ти събера няколко аутпута. Партишъна, който съм сканирал е мой такъв заделен именно за бекъпи. Става въпрос за обикновен евтин хард на 7200 оборота. Когато имам време ще ти пусна и от ссд.

Цитат
големина на файловете:
# du -sh /storage/
411G   /storage/

брой файлове в дяла:
# find /storage -type f | wc -l
56363

време отнело на програмата да завърши:
# time fdupes -r /storage/
real   6m30.874s

намерени дубликати (всъщност брой редове, но ако разделиш цифрата на 3 или 4 ще получиш относително и файловете :) ):
# fdupes -r /storage/ | wc -l
13144

През целия процес процесора почти не е мърдал. Както каза колегата по горе, харда при тези неща е ботълнека и няма смисъл от нишки и тнт (по-голям оувърхед).

Не искам да те обезкуражавам по никакъв начин, просто се опитвам да помогна.

Успех.

ПП: Имаше един лаф: Какво ще правим след това с героите..
Та и при сравнението е така. Ще се чудиш какво да правиш с толкова много еднакви файлове (бил съм там :) )
По горе спомена, че имаш сайтове. Ако са и еднакви ЦиЕМеси там аутпута ще е страшен. Може да помислиш върху пропускане на тези директории.

ПП2: При огромни задания, каквото изглежда твоето, оптимизацията на скрипт има лимит. След това следва оптимизиране на процеса, като ръчно (или не) го орязваш според това, което искаш да постигнеш.

edit 1: Обещаните изходи от SSD

Цитат
размер:
$ sudo du -sh /home/
321G   /home/

реален размер:
$ df -h
Filesystem             Size  Used Avail Use% Mounted on
/home/user/.Private  227G  176G   40G  82% /home/user

намерени дубликати (всъщност брой редове, но ако разделиш цифрата на 3 или 4 ще получиш относително и файловете :) ):
$ fdupes -r /home/ | wc -l
97594

време отнело на програмата да завърши:
$ time fdupes -r /home/
real   4m26.771s

Тук едното ядро се натовари (~80%) понеже работим с ссд. Както се вижда имаме много повече файлове (както за сканиране, така и като резултат), но по-малко като общ размер и времето е с около 30% по-малко.
Като извод може да се каже, че единствено зависиш от броя файлове, а не от тяхната големина. Та както вече казах, помисли върху пропускане на някой директории или ги измести от пътя за сканиране.

edit 2: Исках да сравня и скрипта ти (втората версия) с програмата, но като гледам нямам шанс да го подкарам. Инсталирах питон3.6 , но явно има доста други неща. Като за начало срещне ли счупен симлинк и спира, тръгнах да сканирам друга /home папка на друга машина, но зацикли нещо (буквално спря да прави каквото и да е) и когато му набих ctrl+c извади това:

Цитат
# time python3.6 asd.py /home/
Traceback (most recent call last):
  File "asd.py", line 81, in <module>
    size = os.stat(full_name).st_size
FileNotFoundError: [Errno 2] No such file or directory: '/home/user/.local/share/Steam/ubuntu12_32/steam-runtime/i386/usr/share/doc/libstdc++6'
Error in sys.excepthook:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 63, in apport_excepthook
    from apport.fileutils import likely_packaged, get_recent_crashes
  File "/usr/lib/python3/dist-packages/apport/__init__.py", line 5, in <module>
    from apport.report import Report
  File "/usr/lib/python3/dist-packages/apport/report.py", line 30, in <module>
    import apport.fileutils
  File "/usr/lib/python3/dist-packages/apport/fileutils.py", line 23, in <module>
    from apport.packaging_impl import impl as packaging
  File "/usr/lib/python3/dist-packages/apport/packaging_impl.py", line 23, in <module>
    import apt
  File "/usr/lib/python3/dist-packages/apt/__init__.py", line 23, in <module>
    import apt_pkg
ModuleNotFoundError: No module named 'apt_pkg'

Original exception was:
Traceback (most recent call last):
  File "asd.py", line 81, in <module>
    size = os.stat(full_name).st_size
FileNotFoundError: [Errno 2] No such file or directory: '/home/user/.local/share/Steam/ubuntu12_32/steam-runtime/i386/usr/share/doc/libstdc++6'

Знам, че го играя малко QA, но понеже с друго не мога, помагам с това :)
« Последна редакция: Окт 05, 2017, 01:21 от makeme »
Активен

Distributions:  UbuntuMate 14.04; 15.10; 16.04, CentOS 6.x, 7.x, Kali 2.0 ...

4096bits

  • Участник
  • *****
  • Публикации: 1921
    • Профил
Понеже програмирането не ми е професия, не ми е отръки все още и както казах, със сигурност ми липсват тухлички някъде надолу, до основите. Та, останал съм с впечатлението, че комуникацията между процеси, не е от най-лесните неща, а това не съм го правил до този момент. Малко сокетчета и това е било. Това във връзка със споделянето на междинната чексума на всеки файл или на crc32.
Това, което се пробвам да направя, е не да разцепя всеки файл на парчета и да смятам тях, а да работя върху няколко файла едновременно. Одеве пусках на по-голяма папка на локалния си диск за проба и видях, че натоварването на процесора направо липсва, та сега ми се мота идеята, вместо да се занимавам да тичам по задника на осем ядра, да пусна всичко да се бачка на едно ядро и да ползвам нещо от рода на asyncio. Това животно тепърва започна да ми се изяснява едва преди половин месец. Още ме е шубе от него, а имам върху какво да го пробвам. По-малки скриптчета, в които ProcessPoolExecutor се справи забележително добре.
Активен

"As they say in Mexico 'dosvidaniya'. That makes two vidaniyas."

4096bits

  • Участник
  • *****
  • Публикации: 1921
    • Профил
Така...
Понеже се оказа, че скрипта не работи както трябва - някакви безмислици в output-а - трябваше да го доизчистя.
Резултатите от fdupes:

real   0m9.005s
user   0m1.040s
sys   0m3.668s

Резултатите от скрипта:

real   0m3.786s
user   0m3.852s
sys   0m1.119s

Паралелната обработка на файловете си казва думата.
Пуснах го да рови в локалната директория с музика на единия лаптоп дето го пиша на него.
Не съм правил промените свързани с предложението за проверка на crc32.

Тепърва ще пробвам и това, а след това ще пробвам и на едно ядро, но с async щуротийките. Няма да се учудя, ако този вариант се окаже по-бърз.

Скрипта в момента е в този си вид:

Код:
#!/usr/bin/env python3
# Find and prints duplicated files based on their md5 sum
#
import argparse
from collections import defaultdict
from concurrent import futures
from hashlib import md5
import json
import mmap
import os.path
import os
import subprocess
import sys

parser = argparse.ArgumentParser(description='Print duplicated files')
parser.add_argument('path',
                    type=str,
                    default='.',
                    nargs='?',
                    help='Path to a directory to scan')
parser.add_argument('-d', '--dump',
                    dest='res_file',
                    type=str,
                    default='./duplicated.json',
                    help='''Path/filename to store the results
                    Default: ./duplicated.json''')

args = parser.parse_args()
path = args.path
result_file = args.res_file


def human_fsize(size):
    """Return file size in human readable format.
       Argument: file's size
       Type: int
    """

    pref = [('B', 1), ('KB', 1024),
            ('MB', 1024**2), ('GB', 1024**3), ('TB', 1024**4)]
    counter = 0
    res = size
    while True:
        res = res / 1024
        if res < 1:
            break
        else:
            counter += 1

    if size > 1024:
        h_size = round(size / pref[counter][1], 3)
    else:
        h_size = size

    prefix = pref[counter][0]
    return f'{h_size} {prefix}'


def file_hash(file_name):
    """Returns a tuple of md5sum and file name.
       Argument: file's name
       Type: str
    """
    if os.stat(file_name).st_size == 0:
        return
    hasher = md5()
    # chunk_size = 1024**2
    with open(file_name, 'rb') as in_file:
            with mmap.mmap(in_file.fileno(), 0, access=mmap.ACCESS_READ) as mapped:
                while True:
                    chunk = mapped.read()
                    if chunk:
                        hasher.update(chunk)
                    else:
                        break
        # while True:
        #     chunk = in_file.read(chunk_size)
        #     if chunk:
        #         hasher.update(chunk)
        #     else:
        #         break

            md5sum = hasher.hexdigest()
            return md5sum, file_name

def tty_clear():
    try:
        subprocess.call(['cls'])
    except FileNotFoundError:
        subprocess.call(['clear'])

try:
    lblue, purple, default = ('\033[94m', '\033[35m', '\033[0m')

    # Switch the terminal cursor off
    subprocess.call(['setterm', '-cursor', 'off'])

    print('Matching file sizes!\n')

    # Walk through the directories and look for equal file sizes
    cl ="\x1b[K" # clear the tty line
    counter = 0 # number of equal sized files
    eq_sized = defaultdict(list)
    total_files = 0
    for root, dirs, files in os.walk(path):
        total_files += len(files)

        for name in files:
            counter += 1
            size = os.stat(os.path.join(root, name)).st_size
            eq_sized[size].append(os.path.join(root, name))
            if len(name) > 60:
                print(f'\r{cl} {counter:<12}{name[:25]} ..... {name[-25:]}\r', end='', flush=True)
            else:
                print(f'\r{cl} {counter:<12}{name}\r', end='', flush=True)

    for_hashing = {}
    for key, value in eq_sized.items():
        if len(value) > 1:
            for_hashing[key] = value

    counter = sum([len(values) for keys, values in for_hashing.items()])

    tty_clear()
    print(f'Calculating md5 sums.\n')

    # Hashing the files
    duped = {}
    hashed = defaultdict(list)
    num_files = len(for_hashing)
    count = 0
    with futures.ProcessPoolExecutor(max_workers=8) as executor:
        for size, files in for_hashing.items():

            for result in executor.map(file_hash, files):
                if result:
                    hashed[result[0]].append(result[1])
                    count += 1
                    print(f"\r{cl} Done {lblue}{round(count / counter * 100, 3)}%{default} --> {count} from {counter}", end='', flush=True)
                    if len(hashed[result[0]]) > 1:
                        duped[result[0]] = hashed[result[0]]

    # tty_clear()
    print()

    # Print the results
    sizes = 0
    duplicates = 0
    for key, values in duped.items():
        size = os.stat(values[0]).st_size
        sizes += (len(values) - 1) * size
        duplicates += len(values) - 1
        print(f'md5: {lblue}{key}{default} size: {lblue}{len(values)} {default}* {lblue}{human_fsize(size)}')

        for v in values:
            print(f'    * {purple}{v}{lblue}')

        print(f'{default}')



    # Dump the results in a json file
    with open(result_file, 'w', encoding='utf-8') as dump_file:
        json.dump(duped, dump_file, indent=4, ensure_ascii=False)

    print(f'Dumped as JSON in: {lblue}{result_file}{default}\n')

    print(f'Summarize:\n')
    print(f'    Found {lblue}{total_files}{default} foles in {purple}{path}{default}.')
    print(f'    Found {lblue}{len(duped)}{default} files with {lblue}{duplicates} duplicates.\n')
    print(f'{default}Deleting the duplicates will free {human_fsize(sizes)}.')

    # Switch the terminal cursor on
    subprocess.call(['setterm', '-cursor', 'on'])

except KeyboardInterrupt:
    print(f'\n Stoped!\n Keyboard interupt.')
    # Switch the terminal cursor on
    subprocess.call(['setterm', '-cursor', 'on'])


Между другото, не зная защо съм вмъкнал и windows-ката команда за чистене на екрана - 'cls' - след като малко по-надолу ползвам 'setterm', кояко съм сигурен я нама в тази система.
« Последна редакция: Окт 05, 2017, 14:43 от 4096bits »
Активен

"As they say in Mexico 'dosvidaniya'. That makes two vidaniyas."

makeme

  • Участник
  • *****
  • Публикации: 187
  • Distribution: Many
  • Window Manager: Mate
    • Профил
Поздравления! Третия вариант работи много добре. Много ми хареса как си украсил аутпута :)

Относно сравнението. На голяма папка е по-бавен:

4096bits_dupes.py:

Summarize:

    Found 48257 foles in /storage/share/.
    Found 3316 files with 4246 duplicates.

Deleting the duplicates will free 7.443 GB.

real   6m0.450s

fdupes:

real   3m45.325s

големина:
du -sh /storage/share/
270G   /storage/share/

Не знам дали ти остана време да сканираш папка със счупен симлинк, но все още скрипта спира на него.
Активен

Distributions:  UbuntuMate 14.04; 15.10; 16.04, CentOS 6.x, 7.x, Kali 2.0 ...

4096bits

  • Участник
  • *****
  • Публикации: 1921
    • Профил
Може евентуално да му се каже, да не следва symlinks. По-рано днес се замислих, дали да не оставя и един файл със пълния път като ключ и чексумите като стойност. В момента е обратното. Може да послужи евентуално за следене, дали има промени по файловете в някоя папка/дял. Може и сегашния да послужи. Относно големите файлове, предполагам, че ще са бави там и то не малко. Чудя се, дали не мога просто да пусна де се сметнат само първия мегабайт или там четири-пет от гигабайтовите файлове. Има някои доста големички, които са компресирани архиви и там не зная, какво ще стане. Може да го оставя да си работи и да ида събота и неделя на планина или нещо такова  :D Четох из интернет за разни варианти през няколко мегабайта или десетки мегабайта да се изчислява чексума на няколко стотин К. Ще видя големите точно какво ще ги правя.

Резултатите, кито дадох, нещо не успях да ги получа след това. Има нещо при питона, което не зная съвсем ясно, как се случва. Като се пусне .py файл да се изпълнява, интерпретатора създава .pyc файл и следващия път скрипта се изпълнява по-бързо - от точно този файл. Има вариант да преправя кода със cython-ки синтаксис. Това се компилира после и става наистина бързо. Но първо ще пробвам варианта със asyncio.

п.п А може и да се добави проверка, дали обекта е файл
« Последна редакция: Окт 05, 2017, 21:56 от 4096bits »
Активен

"As they say in Mexico 'dosvidaniya'. That makes two vidaniyas."