Une fois que l’on a défini une structure de données et des formats pour des index, il faut pouvoir filtrer cette l’arborescence sérialisée. En général les besoins se limitent à des choses simples: exclure des dossiers, sélectionner des extensions de fichiers, sélectionner des répertoires … J’ai ajouté aussi un contrôle en profondeur et des outils basés sur des expressions régulières, que l’on peut combiner en mode AND/OR. Ces fonctions sont regroupées dans le module index_filters.py, avec une orientation : ne pas tout regrouper dans des codes complexes, mais favoriser l’empilement de fonctions plus simples, de manière à implémenter un pipeline. Les fonctions disponibles pour les processus post filtrage correspondent à d’autres modules du paquetage buildez.sys et font l’objet d’une autre note.
Dans une approche à plusieurs étages (pipeline) l’étape de filtrage est plutôt considérée comme une première sélection afin de diminuer le volume des données et de faciliter des opérations ultérieures (recherche par exemple) si elles sont nécessaires.
1. Un filtre d’exclusion en profondeur
La fonction index_simple_filter utilise un filtre dont la syntaxe est basée sur un dictionnaire, de manière à pouvoir évoluer. Au moment d’appeler la fonction il faut initialiser ce dictionnaire pour les valeurs maxdepth, excludes, exts et exts_keep. Les clés os_sep et limit seront modifiées à l’intérieur de la fonction, on les laisse à ces valeurs.
{ 'os_sep':'', 'limit':0, 'maxdepth':20, 'excludes':[], 'exts':[], 'exts_keep':True } |
- La valeur de
maxdepthspécifie la profondeur (le nombre de niveaux de répertoires) à laquelle on veut limiter le filtrage. Par exemple simaxdepth=3, l’index filtré ne portera que sur les 3 premiers niveaux (incluant la racine) de l’arborescence. - La liste
excludesspécifie les noms de répertoires que l’on veut éliminer de l’index, par exempleexcludes=['__pycache__', '_projets'], siexcludesest une liste vide, il n’y aura pas d’exclusions. - La variable
ext_keepnous dit ce que nous allons faire des fichiers sélectionnés par la listeexts. Soit nous les gardons (ext_keep=True) soit nous les éliminons de l’index. - La liste
extsspécifie les extensions de fichiers qui nous intéressent dans l’index, par exempleexts=['py'].
Avec une racine telle que C:\Python3\envs\prod\Lib\site-packages\ (de l’ordre 30000 fichiers et répertoires), une exclusion de l’arbre au delà d’une profondeur de 3, une élimination des répertoires __pycache__, _projets et une restriction aux fichiers dont l’extension est .py, nous avons :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
-> Recursive dir+files list ! Working using format='simple' index_tree_files found 30874 items index_tree_files => done in 0.765 s +++ Counting ... [files, dirs, links, unkn, total] [27998, 2876, 0, 0, 30874] filter => {'os_sep': '', 'limit': 0, 'maxdepth': 3, 'excludes': ['__pycache__', '_projets'], 'exts': ['py'], 'exts_keep': True} start_depth=6 limit=9 from [C:\Python3\envs\prod\Lib\site-packages\] reduced index from 30874 to 9207 items index_simple_filter => done in 0.430 s total in 1.195 s +++ Counting . [files, dirs, links, unkn, total] [9207, 0, 0, 0, 9207] found 9207 items ====================================================================== 0 ['C:\\Python3\\envs\\prod\\Lib\\site-packages', 'brotli.py'] 1 ['C:\\Python3\\envs\\prod\\Lib\\site-packages', 'colour.py'] 2 ['C:\\Python3\\envs\\prod\\Lib\\site-packages', 'jdcal.py'] ... 9204 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\_pytest\\_py', 'error.py'] 9205 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\_pytest\\_py', 'path.py'] 9206 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\_pytest\\_py', '__init__.py'] ====================================================================== |
Nous constatons que le filtre est assez rapide, il ne correspond que pour un tiers dans le temps total (génération + filtrage), c’est la génération d’index qui prends le plus de temps. Le fait de rajouter une extension (par exemple exts = ['py']) à traiter ne change pas grand chose (cf. tableau suivant). La partie lente (2 tiers) du processus correspond à l’indexation, mais les valeurs s’équilibrent au fur et à mesure que l’on va vers de petites arborescences. La fonction index_simple_filter s’applique aussi à des index qui ne sont constitués que de répertoires (générés avec la fonction index_tree_dirs), c’est un cas pratique ou la liste exts ne contient aucun élément.
La fonction de profondeur est une troisième dimension que l’on ne trouve que rarement dans les outils d’indexation mais qui peut être très utile (par exemple si nous voulions rechercher des choses dans /usr/bin, /usr/lib … en fait tous les sous-niveaux de /usr mais sans descendre plus bas). Dans une autre note, je parlerais du module index_search qui correspond à une extension (orientée chemin d’accès) de ce principe.
2. Comparaison avec les anciennes routines
La fonction dir_dirmatrix était basée sur un combo génération de l’index + filtrage, et ne produisait qu’un index au format dirmatrix (au moment de l’édition, elle existe sous le nom deprec_dir_dirmatrix_opts dans le module deprec_dirmatrix.py de buildez.sys). Ce type de fonction était basée qui alimente une queue en fonction de certains critères du filtre (par exemple la profondeur). Les répertoires qui sont inclus dans la queue seront explorés, et d’autres seront ajoutés au fur et à mesure du parcours pour implémenter la récursivité. Plus les critères sont restrictifs, plus la queue sera limitée, plus l’indexeur – filtreur sera rapide . Un travail a été fait pour que l’ex dir_dirmatrix accepte le même filtre que index_simple_filter. Pour comparer vis à vis d’un mécanisme à deux étages, nous allons utiliser un filtre sans l’option exts=['py'] car cette fonctionalité n’était pas prise en compte dans deprec_dir_dirmatrix_opts. Dans le tableau suivant, les valeurs fluctuent, il ne s’agit que d’une indication :
| fonction | filtre | comptage [d, f, l, o, t] | simple (sec) | dirmatrix (sec) |
index_tree_files + index_simple_filter |
‘maxdepth’: 3, ‘excludes’: [‘__pycache__’, ‘_projets’], ‘exts’: [‘py’] | [27998, 2876, 0, 0, 30874] => [9207, 0, 0, 0, 9207] |
0.75 à 0.95 + 0.35 à 0.45 = 1.1 à 1.4 |
0.85 à 0.95 + 0.35 à 0.45 = 1.2 à 1.4 |
index_tree_files + index_simple_filter |
‘maxdepth’: 3, ‘excludes’: [‘__pycache__’, ‘_projets’], ‘exts’: [] | [27998, 2876, 0, 0, 30874] => [14297, 1512, 0, 0, 15809] |
||
deprec_dir_dirmatrix + index_simple_filter |
id | id | – | 0.8 à 0.9 + 0.35 à 0.45 = 1.2 à 1.3 |
deprec_dir_dirmatrix_old + index_simple_filter |
id | id | – | 5.1 à 6 + 0.3 à 0.5 = 5.4 à 6.5 |
deprec_dir_dirmatrix_opts |
id | id | – | 2.8 à 3.3 |
La fonction deprec_dir_dirmatrix est juste une interface à index_tree_files, elle a globalement les mêmes performances. La fonction deprec_dirmatrix_old utilise la boucle avec queue mais sans filtre, c’est l’algorithme original avec un peu d’optimisation, (5x) plus lent que index_tree_files.
La fonction deprec_dir_dirmatrix_opts correspond au même mécanisme mais avec un filtrage intégré qui limite la constitution de la queue. Au final elle reste encore (~2x) plus longue que la combinaison de nouvelles fonctions, mais ce n’était pas si mal que cela. Pour des arbres plus courts (moins de 2000 répertoires + fichiers) les performances s’équilibrent, mais le mécanisme à deux fonction conserve un avantage en termes de lisibilité. A noter que ce filtre inclue des options supplémentaires, par exemple: rem_point pour éliminer tous les fichiers commençant par un ., rem_links pour éliminer tous les items identifiés en tant que liens, et rem_unkn pour éliminer tous les items non identifiés. En gros tout ce qui pourrait être utile et nous arrange pour limiter la queue. Ces options n’ont pas été implémentées dans le filtre utilisé par index_simple_filter mais cela peut être envisagé si nécessaire.
3. Un filtre regex
Un filtre d’exclusion n’est pas suffisant, un autre filtre basé sur des expressions régulières, a été implémenté dans le module index_filters. Il s’agit de la fonction index_regexp_filter du module index_filters. La plupart du remps dans un contexte de filtration dans un arbre, on utilise re.search(exp, ma_chaine) ou l’expression matche une chaine, dans le nom, au début du nom, à la fin du nom, par exemple (non exhaustif) :
'^af0'pour tous les répertoires ou fichiers commençant paraf0.'mdl'pour tous les répertoires ou fichiers (dans le nom ou dans l’extension) incluantmdl.'_pycache+?'pour tous les répertoires ou fichiers incluant_pycache(ce qui peut inclure__pycache__).
Dans ce type d’approche ce qui compte ce n’est pas la complexité des expressions régulières, mais ce qui les accompagne. La fonction index_regexp_filter utilise un filtre, sous la forme d’une liste [expr, apply='name|path', action='get|drop']. Ce qui permet de sélectionner sur une expression régulière, de l’appliquer à un nom (fichier ou répertoire, apply='name') ou à un répertoire seulement (apply='path'), puis de sélectionner une action get (on le garde) ou drop (on le sort de l’index). La syntaxe est simple :
filter = ['pybel+?', 'path', 'get']findex = index_regexp_filter(index, filter, format='simple', verb=1) |
Ou format notifie que l’index entrant et produit sera au format simple ou dirmatrix. Si nous appliquons cet appel à C:\Python3\envs\prod\Lib\site-packages\ nous avons un résultat (pour le format simple) du type :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-> Simple filter is applied ! Working using format='simple' index_regexp_filter ['pybel+?', 'path', 'get'] index_regexp_filter start 30873 items index_regexp_filter return 183 items => done in 0.019 s +++ Counting . [files, dirs, links, unkn, total] [176, 7, 0, 0, 183] found 183 items ====================================================================== 0 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface', ''] 1 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\data', ''] 2 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\test', ''] ... 180 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\__pycache__', 'pybel_smi.cpython-312.pyc'] 181 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\__pycache__', '__init__.cpython-310.pyc'] 182 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\__pycache__', '__init__.cpython-312.pyc'] ====================================================================== |
La fonction nous garde tous les répertoires (apply='path') incluant 'pybel' dans le nom (action='get') et toutes les autres entrées de l’index sont éliminées. Pour le format dirmatrix nous avons le même résultat et le même ordre de temps.
Finalement c’est une approche assez simple qui ressemble à des règles comme des ACL (drop, pass …) et que l’on pourrait définir de manière externe. Dans ce cas, ces règles pourraient être regroupées dans un fichier externe (un fichier CSVM conviendrait tout à fait) qui serait à son tour éditable.
4. Combinaisons de filtres regex
Pour maximiser, il est possible de combiner deux filtres en mode AND ou OR, c’est l’objectif de deux fonctions supplémentaires :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def index_regexp_2filters_union(index, filter1, filter2, format='simple', verb=0): """ Interface for two calls to index_regexp_filter in OR operation (union). Added a mode 'list|dirmatrix' for output format. """ list1 = index_regexp_filter(index, filter1, format=format, verb=verb) list2 = index_regexp_filter(index, filter2, format=format, verb=verb) return sets_list_union(list1, list2, '') def index_regexp_2filters_intersection(index, filter1, filter2, format='simple', verb=0): """ Interface for two calls to index_regexp_filter in AND operation (intersection). Added a mode 'list|dirmatrix' for output format. """ list1 = index_regexp_filter(index, filter1, format=format, verb=verb) list2 = index_regexp_filter(index, filter2, format=format, verb=verb) return sets_list_intersect(list1, list2, '') |
Ces deux fonctions produisent des index dont on fait l’union ou l’intersection avec des routines du module buildez.sets, s’appliquant à des listes. Le principe est que des filtres simples combinés sont plus lisibles que des filtres complexes.
Exemple en mode Union
Je veux sélectionner toutes les molécules dont le nom commence par af0 et tous les fichiers dont le nom inclue csvm, dans un répertoire donné.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
-> Union (OR) of two filters is applied ! Working using format='simple' index_regexp_filter ['^af0', 'name', 'get'] index_regexp_filter start 2007 items index_regexp_filter return 18 items index_regexp_filter ['csvm', 'name', 'get'] index_regexp_filter start 2007 items index_regexp_filter return 190 items => done in 0.005 s +++ Counting . [files, dirs, links, unkn, total] [208, 0, 0, 0, 208] found 208 items ====================================================================== 0 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\test\\index1', 'af01.mdl'] 1 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\test\\index1', 'af02.mdl'] 2 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\test\\index1', 'af03.mdl'] ... 205 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\_projets\\plots\\_depot', 'sab100403.csvm'] 206 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\_projets\\plots\\_depot', 'synth.csvm'] 207 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\_projets\\plots\\_depot\\peak_data', 'model.csvm'] ====================================================================== |
En mode dirmatrix le résultat et les performances sont similaires, en principe je me sers peu du mode OR, mais il a été programmé au cas ou.
Exemple en mode intersection
Je voudrais garder les répertoires qui incluent pybel dans le nom et éliminer tous les répertoires dont le nom est __pycache__, dans ce cas :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
-> Intersection (AND) of two filters is applied ! Working using format='simple' index_regexp_filter ['pybel+?', 'path', 'get'] index_regexp_filter start 2007 items index_regexp_filter return 183 items index_regexp_filter ['_pycache+?', 'path', 'drop'] index_regexp_filter start 2007 items index_regexp_filter return 1729 items => done in 0.017 s +++ Counting . [files, dirs, links, unkn, total] [153, 6, 0, 0, 159] found 159 items ====================================================================== 0 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface', ''] 1 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\data', ''] 2 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\test', ''] ... 156 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\_projets', 'todo.txt'] 157 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\_projets\\test', '01FRT_2IOG.mdl'] 158 ['C:\\Python3\\envs\\prod\\Lib\\site-packages\\buildez\\chem\\pybel_interface\\_projets\\test', 'results_conform.sdf'] ====================================================================== |
En mode dirmatrix le résultat et les performances sont similaires, on peut combiner get|drop et name|path dans les filtres ce qui ouvre quelques perspectives intéressantes. Il serait possible d’optimiser en appliquant le second filtre sur l’index généré par le premier. Je ne le fais pas, je préfère une vraie intersection qui me permet d’oublier l’ordre des filtres. Si pour des raisons de performance (par exemple des index volumineux) il faut le faire, il sera possible d’appliquer directement index_regexp_filter sur la liste filtrée, et de même s’il faut combiner plus de deux niveaux de filtres en mode AND.
Liens et lectures
- Guide des expressions régulières [ https://docs.python.org/fr/3/howto/regex.html ].
- Python RegEx – Guru99 [ https://www.guru99.com/fr/python-regular-expressions-complete-tutorial.html ].
- Cours de Python – Patrick Fuchs et Pierre Poulain / Université Paris Cité [ https://python.sdv.u-paris.fr/17_expressions_regulieres/ ].
- RegexBuddy [ https://www.regular-expressions.info/ ].
- RegexOne [ https://regexone.com/ ].
5. Conclusion
Nous avons donc un premier filtre d’exclusion qui permet d’affiner utilement un index produit de manière récursive, sans trop impacter sur le temps d’exécution, tout en simplifiant la syntaxe et la lisibilité de l’ensemble. Nous constatons que cette approche à deux étages tient le coup par rapport à un filtre plus complexe, basé sur une boucle avec une queue et qui sera plus difficile à maintenir, tout en maintenant des fonctionnalités particulières comme un contrôle de profondeur. Des outils basés sur un mécanisme d’expression régulières viennent compléter ces filtres d’exclusion, en amenant un peu plus de flexibilité. La partie critique est l’indexeur, qui peut être encore améliorée. Je n’ai également pas encore testé une approche basée sur glob, qui existe aussi en Python.
Liens et lectures
- glob – Unix style pathname pattern expansion [ https://docs.python.org/3/library/glob.html#module-glob ].