Odczyt folderu rekurencyjnego w Pythonie

224

Mam doświadczenie w C ++ / Obj-C i właśnie odkrywam Python (piszę to od około godziny). Piszę skrypt do rekurencyjnego odczytu zawartości plików tekstowych w strukturze folderów.

Mam problem z tym, że kod, który napisałem, będzie działał tylko w jednym folderze. Rozumiem, dlaczego w kodzie (patrz #hardcoded path) po prostu nie wiem, jak mogę iść naprzód z Pythonem, ponieważ moje doświadczenie z nim jest zupełnie nowe.

Kod Python:

import os
import sys

rootdir = sys.argv[1]

for root, subFolders, files in os.walk(rootdir):

    for folder in subFolders:
        outfileName = rootdir + "/" + folder + "/py-outfile.txt" # hardcoded path
        folderOut = open( outfileName, 'w' )
        print "outfileName is " + outfileName

        for file in files:
            filePath = rootdir + '/' + file
            f = open( filePath, 'r' )
            toWrite = f.read()
            print "Writing '" + toWrite + "' to" + filePath
            folderOut.write( toWrite )
            f.close()

        folderOut.close()
Brock Woolf
źródło

Odpowiedzi:

346

Upewnij się, że rozumiesz trzy zwracane wartości os.walk:

for root, subdirs, files in os.walk(rootdir):

ma następujące znaczenie:

  • root: Bieżąca ścieżka, którą „przeszedł”
  • subdirs: Pliki w rootkatalogu typu
  • files: Pliki w root(nie w subdirs) typu innego niż katalog

I proszę używać os.path.joinzamiast łączenia z ukośnikiem! Twój problem polega na tym, filePath = rootdir + '/' + fileże musisz połączyć bieżący folder zamiast najwyższego folderu. Tak musi być filePath = os.path.join(root, file). „Plik” BTW jest wbudowany, więc zwykle nie używasz go jako nazwy zmiennej.

Kolejnym problemem są twoje pętle, które powinny wyglądać tak: na przykład:

import os
import sys

walk_dir = sys.argv[1]

print('walk_dir = ' + walk_dir)

# If your current working directory may change during script execution, it's recommended to
# immediately convert program arguments to an absolute path. Then the variable root below will
# be an absolute path as well. Example:
# walk_dir = os.path.abspath(walk_dir)
print('walk_dir (absolute) = ' + os.path.abspath(walk_dir))

for root, subdirs, files in os.walk(walk_dir):
    print('--\nroot = ' + root)
    list_file_path = os.path.join(root, 'my-directory-list.txt')
    print('list_file_path = ' + list_file_path)

    with open(list_file_path, 'wb') as list_file:
        for subdir in subdirs:
            print('\t- subdirectory ' + subdir)

        for filename in files:
            file_path = os.path.join(root, filename)

            print('\t- file %s (full path: %s)' % (filename, file_path))

            with open(file_path, 'rb') as f:
                f_content = f.read()
                list_file.write(('The file %s contains:\n' % filename).encode('utf-8'))
                list_file.write(f_content)
                list_file.write(b'\n')

Jeśli nie wiedziałeś, withoświadczenie dotyczące plików jest skrótem:

with open('filename', 'rb') as f:
    dosomething()

# is effectively the same as

f = open('filename', 'rb')
try:
    dosomething()
finally:
    f.close()
AndiDog
źródło
4
Znakomite, wiele odbitek, aby zrozumieć, co się dzieje i działa idealnie. Dzięki! +1
Brock Woolf
16
Podchodzi do każdego tak głupiego / nieświadomego jak ja ... ten przykładowy kod zapisuje plik txt w każdym katalogu. Cieszę się, że przetestowałem to w folderze z kontrolą wersji, chociaż wszystko, czego potrzebuję do napisania skryptu czyszczenia, jest tutaj :)
Steazy,
ten drugi (najdłuższy) fragment kodu zadziałał bardzo dobrze, zaoszczędził mi dużo nudnej pracy
amfibia
1
Ponieważ prędkość, choć oczywiście najważniejszy aspekt, os.walknie jest zła, chociaż wymyśliłem jeszcze szybszą drogę os.scandir. Wszystkie globrozwiązania są znacznie wolniejsze niż walk& scandir. Moja funkcja, a także pełną analizę prędkości, można znaleźć tutaj: stackoverflow.com/a/59803793/2441026
user136036
112

Jeśli używasz języka Python 3.5 lub nowszego, możesz to zrobić w 1 linii.

import glob

for filename in glob.iglob(root_dir + '**/*.txt', recursive=True):
     print(filename)

Jak wspomniano w dokumentacji

Jeśli rekurencja jest prawdą, wzorzec „**” będzie pasował do dowolnych plików i zero lub więcej katalogów i podkatalogów.

Jeśli chcesz każdy plik, możesz użyć

import glob

for filename in glob.iglob(root_dir + '**/*', recursive=True):
     print(filename)
ChillarAnand
źródło
TypeError: iglob () otrzymał nieoczekiwany argument słowa kluczowego „rekurencyjny”
Jewenile
1
Jak wspomniano na początku, dotyczy to tylko
języka
9
katalog_główny musi mieć ukośnik końcowy (w przeciwnym razie jako pierwszy argument otrzymasz coś takiego jak „folder ** / *” zamiast „folder / ** / *”). Możesz użyć os.path.join (katalog_katalogu głównego, „ * / ”), ale nie wiem, czy dopuszczalne jest użycie os.path.join ze ścieżkami symboli zastępczych (ale działa to w mojej aplikacji).
drojf
@ChillarAnand Czy możesz dodać komentarz do kodu w tej odpowiedzi, który root_dirwymaga ukośnika końcowego? Zaoszczędzi to ludziom czasu (a przynajmniej zaoszczędziłoby mi czasu). Dzięki.
Dan Nissenbaum
1
Jeśli uruchomiłem to jak w odpowiedzi, nie zadziałało to rekurencyjnie. Do tej pracy rekurencyjnie musiałem go zmienić na: glob.iglob(root_dir + '**/**', recursive=True). Pracuję w Python 3.8.2
Mikey
38

Zgadzam się z Dave'em Webbem, os.walkda przedmiot dla każdego katalogu w drzewie. Faktem jest, że po prostu nie musisz się tym przejmować subFolders.

Taki kod powinien działać:

import os
import sys

rootdir = sys.argv[1]

for folder, subs, files in os.walk(rootdir):
    with open(os.path.join(folder, 'python-outfile.txt'), 'w') as dest:
        for filename in files:
            with open(os.path.join(folder, filename), 'r') as src:
                dest.write(src.read())
Łaskawy
źródło
3
Niezłe. To też działa. Wolę jednak wersję AndiDoga, nawet jeśli jest dłuższa, ponieważ łatwiej jest zrozumieć ją jako początkującą od Pythona. +1
Brock Woolf
20

TL; DR: Jest to odpowiednik find -type fprzejścia do wszystkich plików we wszystkich folderach poniżej, łącznie z bieżącym:

for currentpath, folders, files in os.walk('.'):
    for file in files:
        print(os.path.join(currentpath, file))

Jak już wspomniano w innych odpowiedziach, os.walk()jest to odpowiedź, ale można ją lepiej wyjaśnić. To całkiem proste! Przejdźmy przez to drzewo:

docs/
└── doc1.odt
pics/
todo.txt

Z tym kodem:

for currentpath, folders, files in os.walk('.'):
    print(currentpath)

Jest currentpathto bieżący folder, na który patrzy. Spowoduje to wygenerowanie:

.
./docs
./pics

Więc zapętla się trzy razy, ponieważ istnieją trzy foldery: bieżący docsi pics. W każdej pętli wypełnia zmienne foldersoraz fileswszystkie foldery i pliki. Pokażmy im:

for currentpath, folders, files in os.walk('.'):
    print(currentpath, folders, files)

To pokazuje nam:

# currentpath  folders           files
.              ['pics', 'docs']  ['todo.txt']
./pics         []                []
./docs         []                ['doc1.odt']

Więc w pierwszym wierszu widzimy, że jesteśmy w folderze ., że zawiera dwa foldery mianowicie picsi docs, i że jest jeden plik, mianowicie todo.txt. Nie musisz nic robić, aby ponownie znaleźć się w tych folderach, ponieważ, jak widzisz, automatycznie się powtarza i po prostu udostępnia pliki w dowolnych podfolderach. I wszelkie podfoldery tego (choć nie mamy ich w tym przykładzie).

Jeśli chcesz po prostu przeglądać wszystkie pliki, odpowiednik find -type f, możesz to zrobić:

for currentpath, folders, files in os.walk('.'):
    for file in files:
        print(os.path.join(currentpath, file))

To daje:

./todo.txt
./docs/doc1.odt
Luc
źródło
9

pathlibBiblioteka jest naprawdę wielki dla pracy z plikami. Możesz zrobić rekurencyjną glob na takim Pathobiekcie.

from pathlib import Path

for elem in Path('/path/to/my/files').rglob('*.*'):
    print(elem)
chorbs
źródło
6

Jeśli chcesz mieć płaską listę wszystkich ścieżek pod danym katalogiem (jak find .w powłoce):

   files = [ 
       os.path.join(parent, name)
       for (parent, subdirs, files) in os.walk(YOUR_DIRECTORY)
       for name in files + subdirs
   ]

Aby uwzględnić tylko pełne ścieżki do plików w katalogu podstawowym, pomiń + subdirs.

Scott Smith
źródło
6
import glob
import os

root_dir = <root_dir_here>

for filename in glob.iglob(root_dir + '**/**', recursive=True):
    if os.path.isfile(filename):
        with open(filename,'r') as file:
            print(file.read())

**/**służy do rekurencyjnego pobierania wszystkich plików, w tym directory.

if os.path.isfile(filename)służy do sprawdzenia, czy filenamezmienna jest filelub directory, jeśli jest to plik możemy odczytać tego pliku. Tutaj drukuję plik.

Neeraj Sonaniya
źródło
6

Za najłatwiejsze uważam:

from glob import glob
import os

files = [f for f in glob('rootdir/**', recursive=True) if os.path.isfile(f)]

Korzystanie glob('some/path/**', recursive=True)pobiera wszystkie pliki, ale obejmuje także nazwy katalogów. Dodanie if os.path.isfile(f)warunku filtruje tę listę tylko do istniejących plików

Michael Silverstein
źródło
3

użyj, os.path.join()aby skonstruować swoje ścieżki - jest ładniejszy:

import os
import sys
rootdir = sys.argv[1]
for root, subFolders, files in os.walk(rootdir):
    for folder in subFolders:
        outfileName = os.path.join(root,folder,"py-outfile.txt")
        folderOut = open( outfileName, 'w' )
        print "outfileName is " + outfileName
        for file in files:
            filePath = os.path.join(root,file)
            toWrite = open( filePath).read()
            print "Writing '" + toWrite + "' to" + filePath
            folderOut.write( toWrite )
        folderOut.close()
ghostdog74
źródło
Wygląda na to, że ten kod działa tylko dla folderów 2 poziomów (lub głębszych). Nadal zbliża mnie to.
Brock Woolf,
1

os.walkdomyślnie wykonuje marsz rekurencyjny. Dla każdego katalogu, zaczynając od katalogu głównego, daje 3-krotki (dirpath, dirnames, nazwy plików)

from os import walk
from os.path import splitext, join

def select_files(root, files):
    """
    simple logic here to filter out interesting files
    .py files in this example
    """

    selected_files = []

    for file in files:
        #do concatenation here to get full path 
        full_path = join(root, file)
        ext = splitext(file)[1]

        if ext == ".py":
            selected_files.append(full_path)

    return selected_files

def build_recursive_dir_tree(path):
    """
    path    -    where to begin folder scan
    """
    selected_files = []

    for root, dirs, files in walk(path):
        selected_files += select_files(root, files)

    return selected_files
b1r3k
źródło
1
W Pythonie 2.6 walk() Do powrotu rekurencyjnej listę. Próbowałem twojego kodu i dostałem listę z wieloma powtórzeniami ... Jeśli po prostu usuniesz wiersze pod komentarzem „# rekurencyjne wywołania na podfolderach” - działa dobrze
borisbn
1

Spróbuj tego:

import os
import sys

for root, subdirs, files in os.walk(path):

    for file in os.listdir(root):

        filePath = os.path.join(root, file)

        if os.path.isdir(filePath):
            pass

        else:
            f = open (filePath, 'r')
            # Do Stuff
Diego
źródło
Dlaczego miałbyś zrobić inną listdir (), a następnie isdir (), gdy już masz katalog podzielony na pliki i katalogi z walk ()? Wygląda na to, że byłoby to raczej powolne na dużych drzewach (wykonaj trzy wywołania systemowe zamiast jednego: 1 = spacer, 2 = listdir, 3 = isdir, zamiast po prostu chodzić i przeglądać „podkatalogi” i „pliki”).
Luc
0

Myślę, że problem polega na tym, że nie przetwarzasz os.walkpoprawnie danych wyjściowych .

Po pierwsze zmień:

filePath = rootdir + '/' + file

do:

filePath = root + '/' + file

rootdirjest twoim stałym katalogiem początkowym; rootto katalog zwrócony przez os.walk.

Po drugie, nie musisz wciskać pętli przetwarzania plików, ponieważ nie ma sensu uruchamiać tego dla każdego podkatalogu. Dostaniesz rootzestaw do każdego podkatalogu. Nie musisz ręcznie przetwarzać podkatalogów, chyba że chcesz coś zrobić z samymi katalogami.

Dave Webb
źródło
Mam dane w każdym podkatalogu, więc muszę mieć osobny plik tekstowy dla zawartości każdego katalogu.
Brock Woolf
@Brock: część plików to lista plików w bieżącym katalogu. Zatem wcięcie jest rzeczywiście błędne. Piszesz do filePath = rootdir + '/' + file, co nie brzmi dobrze: plik jest z listy bieżących plików, więc piszesz do wielu istniejących plików?
Alok Singhal,