Get-ChildItem na sterydach – jak szybko sprawdzić ile jest plików w folderze?

Zawsze jeśli chcę wylistować pliki (np. zdjęcia), które mają określoną konwencję nazewniczą używam cmd z poleceniem dir – jest znacząco szybsze niż Powershellowy odpowiednik Get-ChildItem, jednakże do dalszej obróbki w skrypcie, polecenie to nie nadaje się tak dobrze, bo zwraca tekst, a nie obiekty. Jak za filtrowanie plików brać się optymalniej?

Standardowo w PowerShell podalibyśmy poniższą komendę:

Get-ChildItem -Path ścieżka

Jeśli chcemy przejrzeć nasze zdjęcia w formacie .jpg, to poniższe polecenie zmodyfikowalibyśmy intuicyjnie w następujący sposób:

Get-ChildItem -Path ścieżka\*.jpg

A co jeśli chcemy wybrać tylko najnowszy plik?

Get-ChildItem -Path ścieżka\*.jpg | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1

Cel osiągnięty, ale bardzo dużym kosztem. Dlaczego? Otóż gwiazdka w tej postaci pracuje na zupełnie innym poziomie niż gwiazdka w poleceniu dir w cmd. Cmd pracuje ściśle z Windows API, dir jest więc poleceniem wysoko zoptymalizowanym dające podstawowe parametry systemu plików takie jak LastWriteTime, Name, Length. W podstawowym scenariuszu tj. wylistowanie zawartości folderu nie znajdziecie chyba lepszej metody.

PowerShell korzysta z dobrodziejstwa frameworku .NET – dostarcza więcej informacji, ale obróbka tych danych też zajmuje mu więcej czasu. Co więcej, każdy wylistowany plik w skrypcie jest obiektem, a więc przechowywanie takich danych w pamięci odpowiednio więcej zajmuje. Kilka lat temu kiedy używałem PowerShella do rozpakowania kilku milionów plików, zajmował on całą dostępną pamięć komputera (ok. 11GB RAM).

Jak znacząco poprawić wydajność skryptu? Wystarczy zmienić położenie wyżej wymienionej gwiazdki do parametru -Filter:

Get-ChildItem -Filter *.jpg -Path ścieżka | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1

Dla dużej ilości plików odnotujecie znaczący wzrost wydajności. Czym jest to spowodowane? W pierwszym przypadku PowerShell najpierw zbiera wszystkie pliki a potem stosuje filtr, mocno upraszczając sprawę, ma miejsce taka kolej rzeczy:

Get-ChildItem -Path ścieżka | Where-Object { $_.Name -match „.*jpg”}

W drugim przypadku PowerShell odrzuca niepasujące do filtru pliki, tak jak na kontroli jakości odrzuca się nieprawidłowe produkty. Po zakończeniu listowania mamy więc tylko wyselekcjonowane pliki, a nie jak w pierwszym przypadku – cały worek, z którego zaczynamy wyciągać to co chcemy.

Jeśli naszym celem nie jest wybranie najnowszego albo największego pliku, a każdy plik będzie podlegał jakiejś obróbce np. przenoszeniu do innego katalogu, wydajniej jest procesować pliki porcjami, niż trzymanie wszystkiego w pamięci – empirycznie zauważyłem wzrost wydajności, a wielkość porcji należy dopierać dość indywidualnie. Jak wygląda takie porcjowanie plików w praktyce?

Get-ChildItem -Filter *.jpg -Path ścieżka -OutBuffer 1000 | Move-Item $_.FullName -Destination docelowy_folder

W tym wydaniu będziemy listować do 1000 plików i je przemieszczać do innego folderu. W tym celu optymalizujemy w szczególności wykorzystanie pamięci RAM.

A jak szybko policzyć ile plików jest w folderze, jeśli nawet Eksplorator Plików nie daje rady? PowerShell też może dostać zadyszki po poniższym poleceniu:

(Get-ChildItem -Filter *.jpg -Path ścieżka | Measure-Object).Count

Żeby zrobić to znacząco szybciej (bo chodzi nam aby uzyskać tylko liczbę plików) musimy zejść o jeden poziom niżej – bezpośrednio do metod .NET:

[System.IO.Directory]::GetFiles(„ścieżka”, „*”).Count

Jeśli macie czas na testowanie, to ta metoda działa jak strzała w porównaniu z PowerShell 😉

Marcin Kuchczyński

Wielki fan automatyzowania wszystkiego, co trzeba wykonać więcej niż 2 razy. Cierpliwie rozwijający się w kierunku rozwiązań chmurowych i raczkujący w sferze sztucznej inteligencji. W IT od 2016 roku.

Leave a Reply