python print directories listing from input list of files - python

I want to print a high level directory structure (without duplication) from given input list of files.
Ex: Input files list is,
li=['a/b/c.txt','a/b/d/cc.txt','a/e/f.txt', 'g/h/i.txt','j/k.txt','l/m.txt']
and output to be like
a
+----b
+----d
+----e
g
+----h
j
l
I did go through similar posts on stack overflow (before posting this question), but most of the posts had inputs with no duplicates or tree like structure or directory listing from local (and none of those cases match with the problem I'm looking at)

Here is the solution that worked for me.
def generate_nested_dirs(dir_list):
nested_dirs={}
for d in dir_list:
temp=nested_dirs
for sub_dir in d.split("/"):
if temp.get(sub_dir) is None:
temp[sub_dir]={}
temp=temp[sub_dir]
return nested_dirs
def print_dirs(input_dict,indent):
for dir in list(input_dict):
if indent == 0:
print(dir)
else:
print('\t'*indent,'+--->',dir)
if input_dict[d]:
print(input_dict[dir], indent+1)
And finally calling the above two functions,
li=['a/b/c.txt','a/b/d/cc.txt','a/e/f.txt', 'g/h/i.txt','j/k.txt','l/m.txt']
all_dirs=[]
for f in li:
all_dirs.append("/".join(f.split("/")[1:-1]))
all_dirs=sorted(set(all_dirs))
to_print=generate_nested_dirs(all_dirs)
print_dirs(to_print)
And output will be
a
+----b
+----d
+----e
g
+----h
j
l
Note: Part of the solution was through 'trie' approach

Related

Is there a better way to do this? Counting Files, and directories via for loop vs map

Folks,
I'm trying to optimize this to help speed up the process...
What I am doing is creating a dictionary of scandir entries...
e.g.
fs_data = {}
for item in Path(fqpn).iterdir():
# snipped out a bunch of normalization code
fs_data[item.name.title().strip()] = item
{'file1': <file1 scandisk data>, etc}
and then later using a function to gather the count of files, and directories in the data.
Now I suspect that the new code, using map could be optimized to be faster than the old code. I suspect that having to run the list comprehension twice, once for files, and once for directories.
But I can't think of a way to optimize it to only have to run once.
Can anyone suggest a way to sum the files, and directories at the same time in the new version? (I could fall back to the old code, if necessary)
But I might be over optimizing at this point?
Any feedback would be welcome.
def new_fs_counts(fs_entries) -> (int, int):
"""
Quickly count the files vs directories in a list of scandir entries
Used primary by sync_database_disk to count a path's files & directories
Parameters
----------
fs_entries (list) - list of scandir entries
Returns
-------
tuple - (# of files, # of dirs)
"""
def counter(fs_entry):
return (fs_entry.is_file(), not fs_entry.is_file())
mapdata = list(map(counter, fs_entries.values()))
files = sum(files for files, _ in mapdata)
dirs = sum(dirs for _, dirs in mapdata)
return (files, dirs)
vs
def old_fs_counts(fs_entries) -> (int, int):
"""
Quickly count the files vs directories in a list of scandir entries
Used primary by sync_database_disk to count a path's files & directories
Parameters
----------
fs_entries (list) - list of scandir entries
Returns
-------
tuple - (# of files, # of dirs)
"""
files = 0
dirs = 0
for fs_item in fs_entries:
is_file = fs_entries[fs_item].is_file()
files += is_file
dirs += not is_file
return (files, dirs)
map is fast here if you map the is_file function directly:
files = sum(map(os.DirEntry.is_file, fs_entries.values()))
dirs = len(fs_entries) - files
(Something with filter might be even faster, at least if most entries aren't files. Or filter with is_dir if that works for you and most entries aren't directories. Or itertools.filterfalse with is_file. Or using itertools.compress. Also, counting True with list.count or operator.countOf instead of summing bools might be faster. But all of these ideas take more code (and some also memory). I'd prefer my above way.)
Okay, map is definitely not the right answer here.
This morning I got up and created a test using timeit...
and it was a bit of a splash of reality to the face.
Without optimizations, new vs old, the new map code was roughly 2x the time.
New : 0.023185124970041215
old : 0.011841499945148826
I really ended up falling for a bit of click bait, and thought that rewriting with MAP would gain some better efficiency.
For the sake of completeness.
from timeit import timeit
import os
new = '''
def counter(fs_entry):
files = fs_entry.is_file()
return (files, not files)
mapdata = list(map(counter, fs_entries.values()))
files = sum(files for files, _ in mapdata)
dirs = sum(dirs for _, dirs in mapdata)
#dirs = len(fs_entries)-files
'''
#dirs = sum(dirs for _, dirs in mapdata)
old = '''
files = 0
dirs = 0
for fs_item in fs_entries:
is_file = fs_entries[fs_item].is_file()
files += is_file
dirs += not is_file
'''
fs_location = '/Volumes/4TB_Drive/gallery/albums/collection1'
fs_data = {}
for item in os.scandir(fs_location):
fs_data[item.name] = item
print("New : ", timeit(stmt=new, number=1000, globals={'fs_entries':fs_data}))
print("old : ", timeit(stmt=old, number=1000, globals={'fs_entries':fs_data}))
And while I was able close the gap with some optimizations.. (Thank you Lee for your suggestion)
New : 0.10864979098550975
old : 0.08246175001841038
It is clear that the for loop solution is easier to read, faster, and just simpler.
The speed difference between new and old, doesn't seem to be map specifically.
The duplicate sum statement added .021, and The biggest slow down was from the second fs_entry.is_file, it added .06x to the timings...

os.walk returns one empty list and then my actual list

I'm working on getting some subdirectories to output while ignoring another. I noticed in the output despite everything else seemingly working correctly, that I have an empty list also being returned.
/usr/lib/python3.9/os.py(407)_walk()->('/home/presto.../pB/media/370', ['images5', 'images'], [])
'images' is removed later on in the code but that final empty list remains.
/usr/lib/python3.9/os.py(407)_walk()->('/home/presto.../pB/media/370', ['images5'], [])
for root, subdirectories, files, in os.walk(wk):
for subgals in subdirectories:
if subgals == primary_gallery:
subdirectories.remove(subgals)
else:
subgal_path.append(os.path.join(root, subgals))
for file in sorted(files):
raw_subgal_file_paths.append(os.path.join(root, file))
for x in raw_subgal_file_paths:
split_root = raw_prim_path_length - len(x)
compiled_subgal_file_paths.append(x[split_root:])
print(compiled_subgal_file_paths)
The final output looks like this.
[]
['/images5/10.gif', '/images5/11.gif', '/images5/20.gif', '/images5/21.gif']
How do I fix this?

How to modify iteration list?

Following scenario of traversing dir structure.
"Build complete dir tree with files but if files in single dir are similar in name list only single entity"
Example tree ( let's assume they're are not sorted ):
- rootDir
-dirA
fileA_01
fileA_03
fileA_05
fileA_06
fileA_04
fileA_02
fileA_...
fileAB
fileAC
-dirB
fileBA
fileBB
fileBC
Expected output:
- rootDir
-dirA
fileA_01 - fileA_06 ...
fileAB
fileAC
-dirB
fileBA
fileBB
fileBC
So I did already simple def findSimilarNames that for fileA_01 (or any fileA_) will return list [fileA_01...fileA_06]
Now I'm in os.walk and I'm doing loop over files so every file will be checked against similar filenames so e.g fileA_03 I've got rest of them [fileA_01 - fileA_06] and now I want to modify the list that I iterate over to just skip items from findSimilarNames, without need of using another loop or if's inside.
I searched here and people are suggesting avoidance of modifying iteration list, but doing so I would avoid every file iteration.
Pseudo code:
for root,dirs,files in os.walk( path ):
for file in files:
similarList = findSimilarNames( file )
#OVERWRITE ITERATION LIST SOMEHOW
files = (set(files)-set(similarList))
#DEAL WITH ELEMENT
What I'm trying to avoid is below - checking each file because maybe it's already found by findSimilarNames.
for root,dirs,files in os.walk( path ):
filteredbysimilar = files[:]
for file in files:
similar = findSimilarNames( file )
filteredbysimilar = list(set(filteredbysimilar)-set(similar))
#--
for filteredFile in filteredbysimilar:
#DEAL WITH ELEMENT
#OVERWRITE ITERATION LIST SOMEHOW
You can get this effect by using a while-loop style iteration. Since you want to do set subtraction to remove the similar groups anyway, the natural approach is to start with a set of all the filenames, and repeatedly remove groups until nothing is left. Thus:
unprocessed = set(files)
while unprocessed:
f = unprocessed.pop() # removes and returns an arbitrary element
group = findSimilarNames(f)
unprocessed -= group # it is not an error that `f` has already been removed.
doSomethingWith(group) # i.e., "DEAL WITH ELEMENT" :)
How about building up a list of files that aren't similar?
unsimilar = set()
for f in files:
if len(findSimilarNames(f).intersection(unsimilar))==0:
unsimilar.add(f)
This assumes findSimilarNames yields a set.

Recursively list all files in directory (Unix)

I am trying to list all files a directory recursively using python. I saw many solutions using os.walk. But I don't want to use os.walk. Instead I want to implement recursion myself.
import os
fi = []
def files(a):
f = [i for i in os.listdir(a) if os.path.isfile(i)]
if len(os.listdir(a)) == 0:
return
if len(f) > 0:
fi.extend(f)
for j in [i for i in os.listdir(a) if os.path.isdir(i)]:
files(j)
files('.')
print fi
I am trying to learn recursion. I saw following Q?A, but I am not able to implement correctly it in my code.
Python recursive directory reading without os.walk
os.listdir return only the filename (without the full path)
so I think calling files(j) will not work correctly.
try using files(os.path.join(dirName,j))
or something like this:
def files(a):
entries = [os.path.join(a,i) for i in os.listdir(a)]
f = [i for i in entries if os.path.isfile(i)]
if len(os.listdir(a)) == 0:
return
if len(f) > 0:
fi.extend(f)
for j in [i for i in entries if os.path.isdir(i)]:
files(j)
I tried to stay close to your structure. However, I would write it with only one loop over the entries, something like that:
def files(a):
entries = [os.path.join(a,i) for i in os.listdir(a)]
if len(entries) == 0:
return
for e in entries:
if os.path.isfile(e):
fi.append(e)
elif os.path.isdir(e):
files(e)
Another way is not to use a global variable. This can be done using the following. Just modified the previous answer a little bit. I think this might be a little more readable ...
def files(a):
entries = [os.path.join(a,i) for i in os.listdir(a)]
folders = filter(os.path.isdir, entries)
normalFiles = filter(os.path.isfile, entries)
for f in folders:
normalFiles += files(f)
return normalFiles

removing file names from a list python

I have all filenames of a directory in a list named files. And I want to filter it so only the files with the .php extension remain.
for x in files:
if x.find(".php") == -1:
files.remove(x)
But this seems to skip filenames. What can I do about this?
How about a simple list comprehension?
files = [f for f in files if f.endswith('.php')]
Or if you prefer a generator as a result:
files = (f for f in files if f.endswith('.php'))
>>> files = ['a.php', 'b.txt', 'c.html', 'd.php']
>>> [f for f in files if f.endswith('.php')]
['a.php', 'd.php']
Most of the answers provided give list / generator comprehensions, which are probably the way you want to go 90% of the time, especially if you don't want to modify the original list.
However, for those situations where (say for size reasons) you want to modify the original list in place, I generally use the following snippet:
idx = 0
while idx < len(files):
if files[idx].find(".php") == -1:
del files[idx]
else:
idx += 1
As to why your original code wasn't working - it's changing the list as you iterator over it... the "for x in files" is implicitly creating an iterator, just like if you'd done "for x in iter(files)", and deleting elements in the list confuses the iterator about what position it is at. For such situations, I generally use the above code, or if it happens a lot in a project, factor it out into a function, eg:
def filter_in_place(func, target):
idx = 0
while idx < len(target):
if func(target[idx)):
idx += 1
else:
del target[idx]
Just stumbled across this old question. Many solutions here will do the job but they ignore a case where filename could be just ".php". I suspect that the question was about how to filter PHP scripts and ".php" may not be a php script. Solution that I propose is as follows:
>>> import os.path
>>> files = ['a.php', 'b.txt', 'c.html', 'd.php', '.php']
>>> [f for f in files if os.path.splitext(f)[1] == ".php"]

Categories