Python cho người Việt

Python cho người Việt

Entries tagged “glob”

Duyệt qua các file trong một thư mục

written by vithon, on Jun 20, 2009 10:45:00 AM.

Bạn thường gặp bài toán liệt kê danh sách tất cả các file trong một thư mục cho trước. Dưới đây là một vài các cách khác nhau tùy theo hoàn cảnh.

1. Sử dụng os.listdir

Câu lệnh sau sẽ lấy danh mục các file trong thư mục Test nằm trong thư mục cá nhân của bạn.

>>> import os
>>> path = "~/Test"
>>> os.listdir(os.path.expanduser(path))
['file 3', 'file 1', 'file 2']

os.listdir chỉ cho bạn tên file hoặc thư mục nằm trong đường dẫn đó. Ví dụ:

>>> path = "/mnt/data/pictures/"
>>> os.listdir(path)
['Old', 'Icons', 'baby.jpg']

Để có đường dẫn đầy đủ bạn có thể viết:

>>> FJoin = os.path.join
>>> files = [FJoin(path, f) for f in os.listdir(path)]
>>> files
['/mnt/data/pictures/Old', '/mnt/data/pictures/Icons', '/mnt/data/pictures/baby.jpg']

Để đảm bảo vấn đề tương thích mã khi chuyển qua lại giữa các hệ điều hành bạn luôn luôn nên dùng: os.path.join để nối các đường dẫn, hoặc dùng os.path.sep để cộng các đường dẫn.

không nên:

path = path1 + "\path2"

nên viết:

path = os.path.join(path1, "path2") 

hoặc

path = path1 + os.path.sep + path2 

Trên Windows bạn có thể viết: path = "~/Test" hoặc "~/test" đều được. Linux phân biệt chữ hoa chữ thường nên phải viết chính xác Test. Ký tự phân tách đường dẫn trên linux là "/", trên Windows bạn có thể viết "/" hoặc tôi hay dùng "\\" (thay vì "\").

2. Sử dụng walk

os.listdir chỉ liệt kê các file và thư mục trong một thư mục. Để lấy được cả danh sách đệ quy các file trong thư mục bạn có thể dùng os.walk hoặc thư viện ngoài glob.

Hàm os.walk là một “generator function” và os.walk(path) là một “generator object”. Nghĩa là hàm trạng thái trả về các kết quả kế tiếp nhau theo yêu cầu. Mối một item của walk có ba thành phần: 1. Thư mục hiện tại, 2. Các thư mục con, 3. Các file bên trong. Dưới đây là hàm sử dụng walk để liệt kê tất cả các file và thư mục con trong một thư mục cho trước, kết quả trả về chứa đường dẫn đầy đủ:

import os

FJoin = os.path.join

def GetFiles(path):
    """ Output: file_list là danh sách tất cả các file trong path và trong tất cả các
         thư mục con bên trong nó. dir_list là danh sách tất cả các thư mục con
         của nó. Các output đều chứa đường dẫn đầy đủ."""

    file_list, dir_list = [], []
    for dir, subdirs, files in os.walk(path):
        file_list.extend([FJoin(dir, f) for f in files])
        dir_list.extend([FJoin(dir, d) for d in subdirs])
    return file_list, dir_list

if __name__ == "__main__":
    files, dirs = GetFiles(os.path.expanduser("~/Music"))
    for file in files:
        print file
    for dir in dirs:
        print dir

Hàm này chạy tốt trên Windows, trên Linux nó lấy cả các link. Nói chung các link sẽ không có ý nghĩa trong đa số các trường hợp của bạn. Để loại bỏ các link bạn viêt lại hàm GetFiles như sau:

def GetFiles(path):
    file_list, dir_list = [], []
    for dir, subdirs, files in os.walk(path):
        file_list.extend([FJoin(dir, f) for f in files])
        dir_list.extend([FJoin(dir, d) for d in subdirs])
    file_list = filter(lambda x: not os.path.islink(x), file_list)   
    dir_list = filter(lambda x: not os.path.islink(x), dir_list)   
    return file_list, dir_list

Chú ý rằng một object vừa có thể là file vừa có thể là link. Vì vậy bạn không thể dùng hàm os.path.isfile để lọc các link được:

>>> os.path.islink('/home/hoaiptm/Music/mylink')
True
>>> os.path.isfile('/home/hoaiptm/Music/mylink')
True

Tương tự như vậy với hàm os.path.isdir, nó cũng trả về True nếu object là link liên kết đến một thư mục khác.

3. Sử dụng glob

glob là cách liệt kê file và thư mục theo pattern, các pattern có thể chứa các ký tự đại diện như *, ?, [], nó cho phép bạn tìm kiếm chỉ các file có tên thỏa mãn quy tắc pattern cho trước. Ví dụ:

# liệt kê các file và thư mục trong thư mục path
glob.glob(os.path.join(path, "*"))
# liệt kê các file và thư mục cấp 2 bên trong thư mục path.
# chẳng hạn path chứa thư mục A, B, C thì lệnh trên sẽ
# liệt kê hết các file và thư mục bên trong A, B, C.
glob.glob(os.path.join(path, "*", "*"))
# liệt kê các file và thư mục trong thư mục path bắt đầu với chữ cái a.
glob.glob(os.path.join(path, "[a]*"))
# liệt kê các file và thư mục trong thư mục path
# có tên kết thúc bằng chữ cái p và có đúng 3 ký tự.
glob.glob(os.path.join(path, "??p"))

Chú ý rằng không giống như listdir hoặc walk, glob lấy đường dẫn đầy đủ.

Hàm glob không thực hiện đệ quy. Hàm sau đây sử dụng glob để tìm kiếm đệ quy tất cả các file và thư mục bên trong thư mục cho trước:

import os
from os import path
import glob
 
def dirwalk(dir, bag, wildcards):
    bag.extend(glob.glob(path.join(dir, wildcards)))
    for f in os.listdir(dir):
        fullpath = os.path.join(dir, f)
        if os.path.isdir(fullpath) and not os.path.islink(fullpath):  
            dirwalk(fullpath, bag, wildcards)

files = []

# Lấy tất cả các file và thư mục con trong thư mục path:
dirwalk(path, files,  "*")

# Lấy tất cả các file trong thư mục path (có thể có lẫn thư mục):
dirwalk(path, files, "*.*")

# Lấy tất cả các file python trong thư mục path:
dirwalk(path, files,  "*.py")

# Lấy tất cả các file hoặc thư mục bắt đầu bằng "py" trong thư mục path:
dirwalk(path, files,  "py*.*")

Hàm dirwalk ở triển khai này làm được nhiều việc hơn hàm GetFiles triển khai ở mục 2. Nó tổng quát hơn.

Một cách triển khai khác có thể viết như thế này:

def dirwalk2(dir, bag, wildcards):
    """ bag là một list chứa các file thỏa mãn quy tắc wildcards """

    if glob.glob(path.join(dir, "*")):
        bag.extend(glob.glob(path.join(dir, wildcards)))
        dirwalk2(path.join(dir, "*"), bag, wildcards)

dirwalk2 tuy ngắn gọn nhưng khó hiểu hơn dirwalk. nó hoàn toàn tương tự ngoại trừ việc không kiểm tra một object có thể là link (chỉ có ý nghĩa trên linux).

4. Thư mục có chứa tên file tiếng Việt

Các ví dụ phía trên giả thiết rằng bạn có các tên file và thư mục là tiếng anh thông thường hoặc tiếng việt không dấu. Thư mục có thể chứa các file có tên tiếng việt hoặc tiếng Tàu, tiếng Hàn, … bạn phải truyền đối số unicode cho các hàm listdir, walk, glob… Quy tắc này áp dụng cho tất cả các hàm và thủ tục khác thao tác với file. Quan sát ví dụ sau (trên Windows):

>>> import os
>>> FJoin = os.path.join
>>> FExists = os.path.exists
>>> path = "D:/abc"
>>> os.listdir(path)
['V? mi?n t\xe2y']
>>> [FJoin(path, f) for f in os.listdir(path)]
['D:/abc\\V? mi?n t\xe2y']
>>> [FExists(FJoin(path, f)) for f in os.listdir(path)]
[False]

Thư mục abc có chứa duy nhất file “Về miền tây”, tên file là tiếng Việt có dấu, kết quả kiểm tra sự tồn tại cho thấy file không tồn tại. Nguyên nhân là từ hàm listdir. Nêu đối số của hàm listdir là unicode các tên file lấy về cũng là unicode, nếu là ascii, các tên file trả về cũng là dạng ascii. Vì vậy tên file trả về của listdir trong trường hợp này không đúng và bạn không thể thao tác với file đó được. Để lấy đúng tên file trong trường hợp này bạn phải truyền đối số unicode cho hàm listdir:

>>> path = u"D:/abc"
>>> [FJoin(path, f) for f in os.listdir(path)]
[u'D:/abc\\V\u1ec1 mi\u1ec1n t\xe2y']
>>> [FExists(FJoin(path, f)) for f in os.listdir(path)]
[True]

Tuy nhiên trên Linux, bạn không gặp vấn đề này.

5. Bài tập ví dụ:

Viết chương chình đổi tên tất cả các file trong thư mục “c:\data” và các thư mục con của nó. Đổi tên các file có phần mở rộng là “*.htm” thành “*.docx”.

Dưới đây là một triển khai của bài tập này:

# -*- coding: utf-8 -*-
#!/usr/bin/env python
 
from os import path
import os
import glob
 
def dirwalk(dir, bag, wildcards):
    """ bag là một list chứa các file thỏa mãn quy tắc wildcards """
     
    if glob.glob(path.join(dir, "*")):
        bag.extend(glob.glob(path.join(dir, wildcards)))
        dirwalk(path.join(dir, "*"), bag, wildcards)
 
def rename(oldName, newExt):
    """ Thay thế phần mở rộng cũ thành mở rộng mới. Giả thiết oldName luôn có
     phần mở rộng. """
     
    newName = oldName[0:oldName.rfind(".") + 1] + newExt
    os.rename(oldName, newName)
    return newName
 
def WalkAndRename(dir, oldExt, newExt):
    files = []
    dirwalk(dir, files, u"*" + oldExt)
     
    for f in files:
        print f, "-->", rename(f, newExt)
 
if __name__ == "__main__":
    WalkAndRename('c:\\data', "htm", 'docx')


# The End

(Bài viết được chuyển từ diễn đàn do bạn Phạm Thị Minh Hoài đóng góp)