2007-07-31

 

Python Cookbook 2.27 从Microsoft Word文档中获取文本

需求:

你需要将目录树中的Microsoft Word文档的内容提取出来,并保存到对应的文本文档中.

讨论:

使用PyWin32扩展,我们可以使用COM来访问Word,这样能实现需求:

import fnmatch, os, sys, win32com.client
wordapp = win32com.client.gencache.EnsureDispatch("Word.Application")
try:
    for path, dirs, files in os.walk(sys.argv[1]):
        for filename in files:
            if not fnmatch.fnmatch(filename, '*.doc'): continue
            doc = os.path.abspath(os.path.join(path, filename))
            print "processing %s" % doc
            wordapp.Documents.Open(doc)
            docastxt = doc[:-3] + 'txt'
            wordapp.ActiveDocument.SaveAs(docastxt,
                FileFormat= win32com.client.constants.wdFormatText)
            wordapp.ActiveDocument.Close( )
finally:
    # ensure Word is properly shut down even if we get an exception
    wordapp.Quit( )

对于Windows应用程序来说,有一个很好的特点是脚本可以使用COM来访问它们.而PyWin32扩展使得用Python使用COM变得异常容易.这个扩展能让你通过脚本完成许多windows系统任务,本节的脚本驱动Microsoft Word将目录树下的.doc文档中取出文本并存到对应的txt文档中.使用os.walk函数,我们可以简单的用一个for循环来遍历目录树,使用fnmatch.fnmatch函数 ,我们可以判断一个文件是否满足指定的扩展名,这里是'.doc',一旦我们确定文件是word文件,我们使用os.path来获得它的绝对路径,然后使用Word打开它,并保存文本内容为txt.
如果你没有安装Word,你需要使用完全不同的方法,一个方案是使用Open Office,它可以加载Word文档.另外的方法就是使用别的程序来读Word文档,如AntiWord等.我们在这里不做讨论了.

2007-07-30

 

Python Cookbook 2.26 从OpenOffice文档中获取文本

需求:

你需要从OpenOffice文档中获取文本内容(保留或者不保留XML标记).

讨论:

OpenOffice是由用一系列定义好的XML文档压缩(zip)而成.要获取文档的原始信息,我们甚至可以不用安装OpenOffice:

import zipfile, re
rx_stripxml = re.compile("<[^>]*?>", re.DOTALL|re.MULTILINE)
def convert_OO(filename, want_text=True):
    """ Convert an OpenOffice.org document to XML or text. """
        zf = zipfile.ZipFile(filename, "r")
        data = zf.read("content.xml")
        zf.close( )
        if want_text:
            data = " ".join(rx_stripxml.sub(" ", data).split( ))
        return data
if _ _name_ _=="_ _main_ _":
    import sys
    if len(sys.argv)>1:
        for docname in sys.argv[1:]:
            print 'Text of', docname, ':'
            print convert_OO(docname)
            print 'XML of', docname, ':'
            print convert_OO(docname, want_text=False)
    else:
        print 'Call with paths to OO.o doc files to see Text and XML forms.'

OpenOffice文档实际上是zip文档,除了别的内容外,其中会包含content.xml,本节的目的就是要获取这个文档的信息.默认情况下,本节的代码使用简单的正则式去掉了xml标记.当然,我们可以使用一个xml解析器来获得更多的信息,可是如果只需要文本内容的话,使用最直接的方法就可以了.
正则式rx_stripxml 会匹配所有的xml标记,从"<"开始到">" 结束.在函数Convert_OO中,在if want_text后面,我们将xml标记都替换成空格,然后使用split将它拆分,最后使用"".join将其合并.基本上,使用分割-处理-合并方式将一系列空格转换为一个空字符,更多使用xml文件的方法将在12.3节中给出.


 

Python Cookbook 2.25 在Windows系统上改变文件属性

需求:

你需要在windows系统下设置文件的属性,比如要将文件属性设置为只读,归档等.

讨论:

PyWin32's的win32api模块提供了方法SetFileAttributes,使得这个问题的解决方案变得异常简单:

import win32con, win32api, os
# create a file, just to show how to manipulate it
thefile = 'test'
f = open('test', 'w')
f.close( )
# to make the file hidden...:
win32api.SetFileAttributes (thefile, win32con.FILE_ATTRIBUTE_HIDDEN)
# to make the file readonly:
win32api.SetFileAttributes(thefile, win32con.FILE_ATTRIBUTE_READONLY)
# to be able to delete the file we need to set it back to normal:
win32api.SetFileAttributes(thefile, win32con.FILE_ATTRIBUTE_NORMAL)
# and finally we remove the file we just made
os.remove(thefile)

使用win32api.SetFileAttributes的一个好处是可以增强删除文件功能.使用os.remove方法在windows平台上可能会失败,如果文件属性不是默认的情况下.我们可以先使用SetFileAttributes将它设置为默认属性 ,然后删除之.不过需要注意的是,你要清楚的知道自己删除的是什么文件.不要造成不必要的损失.

相关说明:

win32api的文档在:

http://ASPN.ActiveState.com/ASPN/Python/Reference/Products/ActivePython/PythonWin32Extensions/win32file.html



 

Python Cookbook 2.24 在Mac OS系统上统计PDF文档的页数

需求:

假如你的系统是Mac OS(10.3或以后),你希望统计一个PDF文档的页数.

讨论:

PDF格式和Python都是Mac OS默认支持的,实现本节的需求异常简单:

#!/usr/bin python
import CoreGraphics
def pageCount(pdfPath):
    "Return the number of pages for the PDF document at the given path."
    pdf = CoreGraphics.CGPDFDocumentCreateWithProvider(
              CoreGraphics.CGDataProviderCreateWithFilename(pdfPath)
          )
    return pdf.getNumberOfPages( )
if _ _name_ _ == '_ _main_ _':
    import sys
    for path in sys.argv[1:]:
        print pageCount(path)

另一个解决方法是使用Python的PyObjC扩展,这样能使代码在Mac OS的Foundation和AppKit框架下都能工作,这样你的脚本在比较旧的Mac  OS版本上也能使用了, 比如10.2 Jaguar.在10.3以后的Mac OS版本中,请确保Python安装在你的系统下,你才能使用一些比较有用的Python模块,比如CoreGraphics,这样你才能用一些Apple's的Quartz图形引擎.

相关说明:

我没有见过Mac OS,本节的内容基本不明白,立此存照吧.

2007-07-27

 

Python Cookbook 2.23 从控制台读不回显字符--跨平台方式

需求:

你的程序需要从控制台读入字符,一次一个,不显示在屏幕上,而且要跨平台.

讨论:

当面对跨平台问题时,我们需要封装代码,让他们看起来都是一样的:

try:
    from msvcrt import getch
except ImportError:
    ''' we're not on Windows, so we try the Unix-like approach '''
    def getch( ):
        import sys, tty, termios
        fd = sys.stdin.fileno( )
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

在windows平台上,Python标准库msvcrt提供了一个很方便的方法getch,来一次一个从键盘读字符,且不回显.然而,这个模块不是类Unix平台上的标准模块之一,我们可以使用tty和termios模块来实现这个功能(当然,这两个模块也不是windows版本Python的标准模块).
在应用程序的角度,我们不应该考虑这些问题,而且,我们应该用跨平台的思路来写程序,而不是去记忆不同平台的标准库有什么区别.Python解决了大部分跨平台的问题,但不是全部.本节的例子就是就是它没有解决的跨平台问题之一.
当我们不能找到一个现成的跨平台的包时,我们应该自己封装一个来使用.本节的代码不仅说明了如何解决问题,也提供了一个编写Python的包的好的方式,
你自己写的模块中,如果要使用与平台相关的标准库时,要用try语句将import包起来,并处理好expect ImportError异常.在expect块的代码中,你可以自己处理如何在另一个平台运行的代码.对于罕见的情况,有时不只需要判断两个平台.但大多数情况下,所有非windows平台都可以属于类unix平台.


 

算法学习:楼梯问题

需求:

有一个台阶数为N的楼梯,可以有两种等楼梯的方法:一次上一阶,或者一次上两阶.问对于N阶楼梯,共有多少种不同的上法?
如:
N=1:  1种
N=3:  3种(1+1+1, 1+2, 2+1)
N=4:  5种(1+1+1+1, 1+2+1, 1+1+2, ,2+1+1, 2+2)

算法:

1.这个题目咋一看似乎是数的拆分问题的一种,可以用解决拆分问题的方法来解决.

2.比较简单的方法是递推求解:
设Y为台阶数为N时,上楼梯的方法数.则:
N=1, Y=1
N=2, Y=2
N=3, Y=3
N=4, Y=5
N=5, Y=8
......
如果仔细观察Y值,可以发现满足Fibonacci数列.可是我不会证明,记得以前我们高中老师证明过一次,可是当时就没有看懂.
如果这样,解法就相当简单了.
def Stairways(N):
    a,b=0,1
    for i in xrange(n):
        a,b=b,a+b
    return b

讨论:

如果题目要求打印出所有的方法步骤,还是使用拆分发比较好.

2007-07-26

 

Python Cookbook 2.22 在两个路径间计算相对路径

需求:

你需要知道从一个目录到另一个目录的相对路径,比如,想创建 一个符号连接或者在URL中引用.

讨论:

最简单的办法就是将路径分割成目录列表,然后依次处理.我们可以写一个助手方法来实现:

import os, itertools
def all_equal(elements):
    ''' return True if all the elements are equal, otherwise False. '''
    first_element = elements[0]
    for other_element in elements[1:]:
        if other_element != first_element: return False
    return True
def common_prefix(*sequences):
    ''' return a list of common elements at the start of all sequences,
        then a list of lists that are the unique tails of each sequence. '''
    # if there are no sequences at all, we're done
    if not sequences: return [  ], [  ]
    # loop in parallel on the sequences
    common = [  ]
    for elements in itertools.izip(*sequences):
        # unless all elements are equal, bail out of the loop
        if not all_equal(elements): break
        # got one more common element, append it and keep looping
        common.append(elements[0])
    # return the common prefix and unique tails
    return common, [ sequence[len(common):] for sequence in sequences ]
def relpath(p1, p2, sep=os.path.sep, pardir=os.path.pardir ):
    ''' return a relative path from p1 equivalent to path p2.
        In particular: the empty string, if p1 == p2;
                       p2, if p1 and p2 have no common prefix.
    '''
    common, (u1, u2) = common_prefix(p1.split(sep), p2.split(sep))
    if not common:
        return p2      # leave path absolute if nothing at all in common
    return sep.join( [pardir]*len(u1) + u2 )
def test(p1, p2, sep= os.path.sep):
    ''' call function relpath and display arguments and results. '''
    print "from", p1, "to", p2, " -> ", relpath(p1, p2, sep)
if _ _name_ _ == '_ _main_ _':
    test('/a/b/c/d', '/a/b/c1/d1', '/')
    test('/a/b/c/d', '/a/b/c/d', '/')
    test('c:/x/y/z', 'd:/x/y/z', '/')

在本节的代码中,使用最多的是简单却又最常使用的common_prefix函数,给定该函数一个长度为N的序列,它返回序列中元素的共同前缀, 并分别返回由不同部分组成的列表.为了计算两个路径间的相对路径,我们可以忽略它们中相同的部分.我们只关心向上的层数就可以了(一般来说,用os.path,pardir表示,在Unix系统中用../来表示,我们需要用它来表示从开始路径向下的层数),然后添加不同的后缀就可以了.所以,函数relpath将路径分割成目录,然后调用common_prefix,接着计算共同的层数.
common_prefix代码中间的循环语句for elements in itertools.izip(*sequences),因为izip语句只有当它遇到最短的元素的结尾时才会中止,所以我们的程序中需要自己判断获得的字串是不是在所有元素中都存在,并且将所有的公共元素都添加到common中,当循环结束的时候,我们通过common来对各个元素进行切片处理,以获得不同部分的列表.
函数all_euqal可以用另外一种简单的方式来实现,也是很有趣的:

def all_equal(elements):
    return len(dict.fromkeys(elements)) == 1

或者,用更简单的方法,在Python2.4后有效:

def all_equal(elements):
    return len(set(elements)) == 1

因为所有元素相等等价于所有元素组成的集合的大小为1,是使用dict.fromkeys的版本中,我们使用dict来模拟set的功能,所以它可以在Python2.3和2.4中工作 ,而set版本只能在2.4中工作.

相关说明:

class izip(__builtin__.object)
 |  izip(iter1 [,iter2 [...]]) --> izip object
 |
 |  Return a izip object whose .next() method returns a tuple where
 |  the i-th element comes from the i-th iterable argument.  The .next()
 |  method continues until the shortest iterable in the argument sequence
 |  is exhausted and then it raises StopIteration.  Works like the zip()
 |  function but consumes less memory by returning an iterator instead of
 |  a list.

2007-07-25

 

Python Cookbook 2.21 动态修改Python搜索路径

需求:

只有当模块在Python的搜索路径中时,它们才能被引用.可是如果因此而设置巨大的搜索路径,很影响效率.因为我们需要动态调整搜索路径.

讨论:

我们只要简单的给sys.path添加目录就可以了,需要注意的是避免重复字段:

def AddSysPath(new_path):
    """ AddSysPath(new_path): adds a "directory" to Python's sys.path
    Does not add the directory if it does not exist or if it's already on
    sys.path. Returns 1 if OK, -1 if new_path does not exist, 0 if it was
    already on sys.path.
    """
    import sys, os
    # Avoid adding nonexistent paths
    if not os.path.exists(new_path): return -1
    # Standardize the path.  Windows is case-insensitive, so lowercase
    # for definiteness if we are on Windows.
    new_path = os.path.abspath(new_path)
    if sys.platform == 'win32':
        new_path = new_path.lower( )
    # Check against all currently available paths
    for x in sys.path:
        x = os.path.abspath(x)
        if sys.platform == 'win32':
            x = x.lower( )
        if new_path in (x, x + os.sep):
            return 0
    sys.path.append(new_path)
    # if you want the new_path to take precedence over existing
    # directories already in sys.path, instead of appending, use:
    # sys.path.insert(0, new_path)
    return 1
if _ _name_ _ == '_ _main_ _':
    # Test and show usage
    import sys
    print 'Before:'
    for x in sys.path: print x
    if sys.platform == 'win32':
          print AddSysPath('c:\\Temp')
          print AddSysPath('c:\\temp')
    else:
          print AddSysPath('/usr/lib/my _modules')
    print 'After:'
    for x in sys.path: print x

模块在被引用前,必须存在于Python的搜索路径中,而我们不能因此就给Python搜索路径添加巨多的项目,这样会影响别的Python应用程序的执行效率.本节采用的方法是动态给sys.path添加目录,而且这个目录必须是存在且sys.path中没有的.
sys.path是一个列表,所以很容易通过sys.path.append向它的后面添加元素,添加完毕后,Python会在新的搜索路径中依次搜索目录,这就是说,你可以使用sys.path.insert (0..),这样保证Python总是先搜索我们添加的目录.
如果给sys.path中添加重复目录或者无效的目录都没有问题,因为Python的import很聪明,能自动去除这些项.每次这样的问题发生的时候(如,遇到无效目录,或者重复目录,要处理系统报出的错误),都要花掉一点时间来处理.为了节约这些时间,本节给出的方法是避免给sys.path添加无效的或者重复的目录.这些添加的目录,只有在程序运行时有效,不会影响别的程序.


 

Python Cookbook 2.20 在Python搜索路径中查找文件

需求:

一个很大的Python应用,引用了很多资源文件(比如:项目文件,SQL脚本,图片等),你希望将它们放到一起,以便打包.

讨论:

需要在Python搜索路径中查找,该路径保存在sys.path中:

import sys, os
class Error(Exception): pass
def _find(pathname, matchFunc=os.path.isfile):
    for dirname in sys.path:
        candidate = os.path.join(dirname, pathname)
        if matchFunc(candidate):
            return candidate
    raise Error("Can't find file %s" % pathname)
def findFile(pathname):
    return _find(pathname)
def findDir(path):
    return _find(path, matchFunc=os.path.isdir)

大的Python应用由Python代码文件和很多资源文件组成.将它们放在一起对以后的使用和发布都是极为便利的 ,使用本节的方法或者2.18节的方法都很容易能找到它们.

相关说明:

isdir(path)
    Test whether a path is a directory
isfile(path)
    Test whether a path is a regular file

2007-07-24

 

算法学习:排序

需求:

讨论一些常见算法的实现.

讨论:

1.插入排序

原理:找到适合的位置i,然后将i之后的元素后移,将元素放在i位置

def InsertSort(array):
    n=len(array)
    for j in xrange(1,n):
        key=array[j]
        i=j-1
        while i>=0 and array[i]>key:
            array[i+1]=array[i]
            i-=1
        array[i+1]=key
    return array
if __name__ == '__main__':
    print InsertSort([5,4,7,8,9,3,6,1,2,0])

2.归并排序

原理:首先将元素递归分组,确保每一组的元素是排序好的,然后将这些组合并即可.

def MergeSort(array,left,right):
    if left < right:
        middle = (left + right) >> 1
        MergeSort(array,left,middle)
        MergeSort(array,middle+1,right)
        Merge(array,left,middle,right)
    return array
def Merge(array,left,middle,right):
    L=array[left:middle+1]
    L+=[65535]
    R=array[middle+1:right+1]
    R+=[65535]
    i=j=0
    for k in xrange(left,right+1):
        try:
            if L[i]<=R[j]:
                array[k]=L[i]
                i+=1
            else:
                array[k]=R[j]
                j+=1
        except:
            print 'i=%d,j=%d'%(i,j)
if __name__ == '__main__':
    print MergeSort([5,4,7,8,9,3,6,1,2,0],0,9)

其中加入两个边界值,当到边界值时,表示一组已经复制完毕.

3.堆排序

原理:建立最大堆,然后将root(最大)元素和堆的最后一个元素互换(最小)即可.然后减少堆的大小,循环执行.

heapSize = 0
def MaxHeapify(array,i):
    global heapSize
    l=(i+1 << 1) - 1
    r=(i+1) << 1
    largest=0
    if l<heapSize and array[l] > array[i]:
        largest = l
    else:
        largest = i
    if r<heapSize and array[r] > array[largest]:
        largest = r
    if largest != i:
        tmp = array[i]
        array[i] = array[largest]
        array[largest] = tmp
        MaxHeapify(array,largest)

def BuildMaxHeap(array):
    global heapSize
    heapSize= len(array)
    for i in xrange(heapSize>>1,-1,-1):
        MaxHeapify(array,i)

def HeapSort(array):
    global heapSize
    BuildMaxHeap(array)
    for i in xrange(len(array)-1,0,-1):
        tmp = array[0]
        array[0] = array[i]
        array[i] = tmp
        heapSize-=1
        MaxHeapify(array,0)
    return array
if __name__ == '__main__':
    print HeapSort([5,4,7,8,9,3,6,1,2,0])

4.快速排序

原理:利用分执法的优点,进行交换排序.在元素多的情况下,快速排序的表现最好.

def QuickSort(array,left,right):
    if left < right:
        middle = Partition(array,left,right)
        QuickSort(array,left,middle-1)
        QuickSort(array,middle+1,right)
    return array
       
def Partition(array,left,right):
    x = array[right]
    i = left - 1
    for j in xrange(left,right):
        if array[j] <= x:
            i+=1
            tmp = array[i]
            array[i] = array[j]
            array[j] = tmp
    i+=1
    tmp = array[i]
    array[i] = array[right]
    array[right] = tmp
    return i
if __name__ == '__main__':
    print QuickSort([5,4,7,8,9,3,6,1,2,0],0,9)

5.给出一个生成随机序列的方法,我就是用这个方法测试的.

def ArrayGenerator(n):
    array = range(0,n)
    from random import randint
    for i in xrange(0,n):
        p = randint(i,n-1)
        tmp = array[i]
        array[i] = array[p]
        array[p] = tmp
    return array

2007-07-23

 

算法学习:求组合数

需求:

给定一个数N,求出由序列[1..N]取出M个数的方法.

讨论:

求组合数有很多中算法,我认为最方便的就是二进制算法,算法的原理依据二项式定理,N的全组合数目为2^n-1.我们可以使用这个数的二进制形式来获得N的全组合.

Python实现:

def main(n,m=0):
    max = 2**n-1
    def makeResult(num):
        i=1;
        ret=[]
        while num > 0:
            if num & 1:
                ret.append(i)
            num>>=1
            i+=1
        return ret
    while max > 0:
        out=makeResult(max)
        if m!=0:
            if len(out)==m:
                print out
        else:
            print out
        max-=1
if __name__ == '__main__':
    main(5,3)

其中,max为2^n-1,我们让它递减至0,然后获得二进制中为1的位,并输出该位的位置,即获得的组合数.main的第二个参数是标志位,如果是0,求出全组合,如果不是0,取出特定个数的组合.

 

Python Cookbook 2.19 在给定的路径中查找特定模式的文件

需求:

给定一个搜索路径(用分隔符分割的目录列表),需要找出目录下符合特定模式的所有文件.

讨论:

基本上,需要遍历搜索路径中所有的目录,循环方法可以封装在一个生成器中:

import glob, os
def all_files(pattern, search_path, pathsep=os.pathsep):
    """ Given a search path, yield all files matching the pattern. """
    for path in search_path.split(pathsep):
        for match in glob.glob(os.path.join(path, pattern)):
            yield match

使用生成器有一个好处,你可以获得返回的第一个值,或者全部,以及其中的任何一个.比如,打印在你的PATH中满足'*.pye'的第一个文件名:

print all_files('*.pye', os.environ['PATH']).next( )

要打印全部的,而且每行一个:

for match in all_files('*.pye', os.environ['PATH']):
    print match

用列表的形式全部打出:

print list(all_files('*.pye', os.environ['PATH']))

我经常在main方法中使用all_file,来打印在PATH中的满足模式的所有文件名,它不仅能获得首先发现的,也能发现其它的:

if _ _name_ _ == '_ _main_ _':
    import sys
    if len( sys.argv) != 2 or sys.argv[1].startswith('-'):
        print 'Use: %s <pattern>' % sys.argv[0]
        sys.exit(1)
    matches = list(all_files(sys.argv[1], os.environ['PATH']))
    print '%d match:' % len(matches)
    for match in matches:
        print match

相关说明:

glob.glob(pathname)
    Return a list of paths matching a pathname pattern.

    The pattern may contain simple shell-style wildcards a la fnmatch.


 

Python Cookbook 2.18 在给定的路径中查找文件

需求:

给定一个路径(一个目录列表,中间有分隔符分割),需要在路径中找到需要的文件.

讨论:

基本上,你需要遍历给定的路径:

import os
def search_file(filename, search_path, pathsep=os.pathsep):
    """ Given a search path, find file with requested name """
    for path in search_path.split(pathsep):
        candidate = os.path.join(path, filename)
        if os.path.isfile(candidate):
            return os.path.abspath(candidate)
    return None
if _ _name_ _ == '_ _main_ _':
    search_path = '/bin' + os.pathsep + '/usr/bin'  # ; on Windows, : on Unix
    find_file = search_file('ls', search_path)
    if find_file:
        print "File 'ls' found at %s" % find_file
    else:
        print "File 'ls' not found"

本节讨论的问题是很常见的问题,Python为解决它提供了很多方便.
实现搜索的循环还可以写成很多方式,但是一旦找到目标文件,就立刻返回了.在方法最后写的return None并不是必需的,因为在方法的结束是,Python默认会返回一个None.在这里写这一句是让代码看起来更清晰.

相关说明:

os.path.isfile(path)
    Test whether a path is a regular file

os.path.abspath(path)
    Return the absolute version of a path.

2007-07-20

 

Python Cookbook 2.17 替换目录树下文件的扩展名

需求:

你需要对一个目录树的文件进行重命名操作,用给定的扩展名替换它们的扩展名.

讨论:

对一个目录树下的所有文件进行操作,使用Python标准库的os.walk是比较方便的:

import os
def swapextensions(dir, before, after):
    if before[:1] != '.':
        before = '.'+before
    thelen = -len(before)
    if after[:1] != '.':
        after = '.'+after
    for path, subdirs, files in os.walk(dir):
        for oldfile in files:
            if oldfile[thelen:] == before:
                oldfile = os.path.join(path, oldfile)
                newfile = oldfile[:thelen] + after
                os.rename(oldfile, newfile)
if _ _name_ _=='_ _main_ _':
    import sys
    if len(sys.argv) != 4:
        print "Usage: swapext rootdir before after"
        sys.exit(100)
    swapextensions(sys.argv[1], sys.argv[2], sys.argv[3])

上面的代码说明了如何替换指定目录树下所有文件的扩展名,用于需要批量更新文件名的地方很合适,比如网站.也可以用于更改用批处理下载的文件名称的错误.
上面的代码也被设计成模块,以便别的程序使用,或者通过命令行,它是与平台无关的.你可以传递给它传递带.或者不带.的参数,因为在代码中进行了处理.(当然,这也带来一个负面效果,它不能处理不带任何扩展名的文件,尤其是在类Unix系统下)
上面的代码中使用的默写技术可能让理想主义者认为底层.它直接操作了文件名,而不是使用os.path下面的方法.其实并没有关系,因为Python提供的字符串处理功能也是很方便的.

相关说明:
rename(...)
    rename(old, new)

    Rename a file or directory.

join(a, *p)
    Join two or more pathname components, inserting "\" as needed



2007-07-19

 

Python Cookbook 2.16 遍历目录树

需求:

需要检查一个目录,或者以特定目录为跟的目录树,对树中的每一个匹配的文件或目录进行操作.

讨论:

Python的标准模块os中提供的生成器os.walk就是为这个需求设计的.我们也可以封装它来实现自己的需求:

import os, fnmatch
def all_files(root, patterns='*', single_level=False, yield_folders=False):
    # Expand patterns from semicolon-separated string to list
    patterns = patterns.split(';')
    for path, subdirs, files in os.walk(root):
        if yield_folders:
            files.extend(subdirs)
        files.sort( )
        for name in files:
            for pattern in patterns:
                if fnmatch.fnmatch(name, pattern):
                    yield os.path.join(path, name)
                    break
        if single_level:
            break

标准库提供的os.walk是灵活 ,强大而且易用的.不过,正是因为它是标准的,所以它不够灵活,比如它不能选择特定类型的文件,或者以排序模式访问文件,或者只访问单层目录,而不是目录树等.本节演示了上述的功能如何添加到walk中,它将walk封装到自己写的生成器中,并使用标准库的fnmath来匹配特定类型的文件,如果要匹配多个类型,用分号分隔.
举例说明,你要获得/tmp目录下所有的Python和HTML文件,包括子目录,可以这样写:

thefiles = list(all_files('/tmp', '*.py;*.htm;*.html'))

如果你只是想一次一个的处理,可以不用构造一个列表,用循环就可以了:

for path in all_files('/tmp', '*.py;*.htm;*.html'):
    print path

如果你使用的平台对大小写敏感,你想使用大小写敏感的模式匹配,可以修改模式字符串,如:"*.[Hh][Tt][Mm][Ll]"而不是"*.html".

相关说明:

fnmatch(name, pat)
    Test whether FILENAME matches PATTERN.

    Patterns are Unix shell style:

    *       matches everything
    ?       matches any single character
    [seq]   matches any character in seq
    [!seq]  matches any char not in seq

    An initial period in FILENAME is not special.
    Both FILENAME and PATTERN are first case-normalized
    if the operating system requires it.
    If you don't want this, use fnmatchcase(FILENAME, PATTERN).

需要区别os.walk和os.path.walk!

2007-07-18

 

Python Cookbook 2.15 适配(Adapting)类文件对象到文件对象

需求:

你需要传递一个"类文件"(file like)对象(如urllib.nrlopen的返回值)到一个只接收文件对象为参数的方法中(如marshal.load).

讨论:

为了适应类型检查,你需要将类文件对象的内容写入临时文件中,然后再操作这个临时文件,下面的代码实现了这个想法:

import types, tempfile
CHUNK_SIZE = 16 * 1024
def adapt_file(fileObj):
    if isinstance(fileObj, file): return fileObj
    tmpFileObj = tempfile.TemporaryFile
    while True:
        data = fileObj.read(CHUNK_SIZE)
        if not data: break
        tmpFileObj.write (data)
    fileObj.close( )
    tmpFileObj.seek(0)
    return tmpFileObj

本节讲了一个罕见的用Python使用适配器模式的例子(你拥有X但需要用Y来做).设计模式是面向对象中对常考虑的问题,大多通过写类来实现,而本质上并不需要它们.在本节中,我们不需要引入新的类,因为adapt_file方法已经能很好的完成我们的需求,因为,我们尊重奥卡姆剃刀原则(Occam's Razor) ,不引不必要的项目.
在使用适配器的时候,各方面都要考虑到,当请优先检查类型,即使要使用一些低级的工具或者方法.考虑好要用什么适配为什么类型,总比用时报出异常要好.这样,你的代码能更灵活,而且重用性更好.

相关说明:

奥卡姆剃刀(Occam's Razor, Ockham's Razor)
      是由14世纪逻辑学家、圣方济各会修士奥卡姆的威廉(William of Occam)提出的一个原理。奥卡姆(Ockham)在英格兰的萨里郡,那是他出生的地方。
  这个原理称为"如无必要,勿增实体"(Entities should not be multiplied unnecessarily).
      人们常常引用奥卡姆剃刀的一个强形式,叙述如下:

  如果你有两个原理,它们都能解释观测到的事实,那么你应该使用简单的那个,直到发现更多的证据。
  对于现象最简单的解释往往比较复杂的解释更正确。
  如果你有两个类似的解决方案,选择最简单的。
  需要最少假设的解释最有可能是正确的。

  ……或者以这种自我肯定的形式出现:

  让事情保持简单!


 

Python Cookbook 2.14 重定位到输入文件到文件头

需求:

你构造了一个文件对象(从网络或者文件句柄中读数据),需要定位到文件头,以便读取它的信息.

讨论:

将文件操作封装到一个类里面:

from cStringIO import StringIO
class RewindableFile(object):
    """ Wrap a file handle to allow seeks back to the beginning. """
    def _ _init_ _(self, input_file):
        """ Wraps input_file into a file-like object with rewind. """
        self.file = input_file
        self.buffer_file = StringIO( )
        self.at_start = True
        try:
            self.start = input_file.tell( )
        except (IOError, AttributeError):
            self.start = 0
        self._use_buffer = True
    def seek(self, offset, whence=0):
        """ Seek to a given byte position.
        Must be: whence == 0 and offset == self.start
        """
        if whence != 0:
            raise ValueError("whence=%r; expecting 0" % (whence,))
        if offset != self.start:
            raise ValueError("offset=%r; expecting %s" % (offset, self.start))
        self.rewind( )
    def rewind(self):
        """ Simplified way to seek back to the beginning. """
        self.buffer_file.seek(0)
        self.at_start = True
    def tell(self):
        """ Return the current position of the file (must be at start).  """
        if not self.at_start:
            raise TypeError("RewindableFile can't tell except at start of file")
        return self.start
    def _read(self, size):
        if size < 0:             # read all the way to the end of the file
            y = self.file.read( )
            if self._use_buffer:
                self.buffer_file.write(y)
            return self.buffer_file.read ( ) + y
        elif size == 0:          # no need to actually read the empty string
            return ""
        x = self.buffer_file.read(size)
        if len(x) < size:
            y = self.file.read(size - len(x))
            if self._use_buffer:
                self.buffer_file.write(y)
            return x + y
        return x
    def read(self, size=-1):
        """ Read up to 'size' bytes from the file.
        Default is -1, which means to read to end of file.
        """
        x = self._read(size)
        if self.at_start and x:
            self.at_start = False
        self._check_no_buffer( )
        return x
    def readline(self):
        """ Read a line from the file. """
        # Can we get it out of the buffer_file?
        s = self.buffer_file.readline ( )
        if s[-1:] == "\n":
            return s
        # No, so read a line from the input file
        t = self.file.readline ( )
        if self._use_buffer:
            self.buffer_file.write (t)
        self._check_no_buffer( )
        return s + t
    def readlines(self):
        """read all remaining lines from the file"""
        return self.read( ).splitlines(True)
    def _check_no_buffer(self):
        # If 'nobuffer' has been called and we're finished with the buffer file,
        # get rid of the buffer, redirect everything to the original input file.
        if not self._use_buffer and \
               self.buffer_file.tell( ) == len(self.buffer_file.getvalue( )):
            # for top performance, we rebind all relevant methods in self
            for n in 'seek tell read readline readlines'.split( ):
                setattr(self, n, getattr(self.file, n, None))
            del self.buffer_file
    def nobuffer(self):
        """tell RewindableFile to stop using the buffer once it's exhausted"""
        self._use_buffer = False

有时,从网络或文件句柄中读取的数据并不是我们所期望的.比如,你从一个有问题的服务器读数据,本来应该返回XML流,可是它返回了一些没有格式的错误消息.(这种情况很常见,因为很多服务器并没有很好的处理异常输入情况).
本节的RewindableFile类可以帮助你解决这个问题,r = RewindableFile(f) 将原始的输入流 f 封装到一个"可重定位的文件"对象 r 中,并提供了缓冲区功能.对 r 的读操作被转发给 f ,数据先保存到了缓冲区,然后返回给调用者,而缓冲区里面保存了所有的数据.
r 可以调用rewind方法,即能重定位到文件头,下一次的读操作将首先从缓冲区中读数据,读完后,再从输入流读,而新读入的数据也会添加到缓冲区中.
当不需要缓冲区的时候,我们调用 r 的nobuffer方法来释放它.也就是说,当我们读完数据的时候,可以调用这个方法来释放缓冲区,当调用nobuffer后,seek就没有意义了.
举例说明,你要访问的服务器会返回错误信息:ERROR:can't do that,或者XML信息,<?xml...:

    import RewindableFile
    infile = urllib2.urlopen("http://somewhere/")
    infile = RewindableFile.RewindableFile(infile)
    s = infile.readline( )
    if s.startswith("ERROR:"):
          raise Exception(s[:-1])
    infile.seek(0)
    infile.nobuffer( )   # Don't buffer the data any more
     ... process the XML from infile ...

在本节中,一个常用的Python技巧不适用了:你不能够很好的隐藏RewindableFie的绑定方法(如果你不知道什么是绑定方法,那没有关系,这样的话,你更不可能将它们隐藏起来).这个问题的原因是,当缓冲区是空的时,RewindableFile重新对read,readline等方法进行赋值,并将它们做为self对象的一个变量.比起使用不常用的隐藏绑定方法技巧 ,这样做性能会好一些.
tell方法用于获得当前文件指针的位置,它只能在封装完RewindalbeFile后调用,而且在进行读操作之前.RewindableFile实现的tell方法获得封装后的文件的真实位置,并将它做为起始位置.如果封装的文件对象不支持tell,那么RewindableFile类的tell实现将返回0.



2007-07-17

 

算法学习:用Horner算法计算多项式求和

需求:

计算表达式,
a0 + a1 * x + a2 * x^2 + a3 * x^3 + ... + an * x^n

解法:

使用Horner算法解答:

原式 = a0 + x(a1 + x(a2 + x(a3 + ...x(an)...))

result <-- a[n]
i <-- n
while i >= 0
    result <-- reslut  * x  +  a[i]
    i <-- i - 1

Python描述:

def Horner(a, n, x):
    result = a[n]
    i = n - 1
    while i >= 0:
        result = result * x + a[i]
        i -= 1
    return result

 

Python Cookbook 2.13 使用类C++的流操作语法

需求:

你喜欢C++操作I/O的方式,使用操作符(不同的对象对输入其的数据有不同的处理)来处理,也想在Python这样做.

讨论:

Python允许你重载操作符,通过重写特定的方法(特定方法指那些命名中以两个下划线开始和结束的方法).要使用<<做为输出,就像C++中那样,需要定义一个输出流操作类并定义函数__lshift__:

class IOManipulator(object):
    def _ _init_ _(self, function=None):
        self.function = function
    def do(self, output):
        self.function(output)
def do_endl(stream):
    stream.output.write ('\n')
    stream.output.flush( )
endl = IOManipulator(do_endl)
class OStream(object):
    def _ _init_ _(self, output=None):
        if output is None:
            import sys
            output = sys.stdout
        self.output = output
        self.format = '%s'
    def _ _lshift_ _(self, thing):
        ''' the special method which Python calls when you use the <<
            operator and the left-hand operand is an OStream '''
        if isinstance(thing, IOManipulator):
            thing.do (self)
        else:
            self.output.write(self.format % thing)
            self.format = '%s'
        return self
def example_main( ):
    cout = OStream( )
    cout<< "The average of " << 1 << " and " << 3 << " is " << (1+3)/2 <<endl
# emits The average of 1 and 3 is 4
if _ _name_ _ == '_ _main_ _':
    example_main( )

封装Python的类文件对象来模拟C++操作是比较容易的,本节讲了如果用<<来实现插入操作.本节也实现了一个IOManipulator类(向C++中一样)来实现对流的任意插入操作,也预定义了endl(猜猜从哪里来的?)来实现换行和刷新缓冲区.
在类OStream的实例中,我们保留了一个format属性, 并在每次调用sys.stdout.write后,将其值设置为默认的'%s',这样做是因为你可以构造自己需要的流输出格式,如:

def do_hex(stream):
    stream.format = '%x'
hex = IOManipulator(do_hex)
cout << 23 << ' in hex is ' << hex << 23 << ', and in decimal ' << 23 << endl
# 输出 23 in hex is 17, and in decimal 23

有些人喜欢C++的cout<<something写法,有些人不喜欢.在Python中,我们可以使用更简单和可读的代码:

print>>somewhere, "The average of %d and %d is %f\n" % (1, 3, (1+3)/2)

这是Python方式的写法(和C很像).一切都取决于你更多使用C++还是C.不过无论怎样,本节都给出了选则方案,即使你不会使用本节讨论的内容,也能从其中看到在Python中进行操作符重载是很简单和方便的.

 

Python Cookbook 2.12 在windows平台下向stdout输出二进制数据

需求:

需要输出二进制数据(如,一个图像)到windows平台的标准输出.

讨论:

这里可以使用setmode函数,它位于Python中依赖平台(windows)的msvcrt模块中,用法如下:

import sys
if sys.platform == "win32":
    import os, msvcrt
    msvcrt.setmode(sys.stdout.fileno( ), os.O_BINARY)

现在你就可以调用sys.stdout.write向标准输出输出任意二进制数据.
在类Unix系统中,不会(也不需要)区分二进制文件和文本文件,然而在windows平台上,如果你要输出图像文件, 必须以二进制方式打开.而在Python中,sys.stdout文件对象默认的打开方式是文本模式,所以你需要做如上处理.
你可以让Python以二进制模式打开标准输出,通过给命令行解释器添加'-u'参数.比如,你的CGI脚本运行在Apache服务器下,在你的代码的第一行,可以这样写:

#! c:/python23/python.exe -u

在Python2.3后的安装版本下,上面的方式是可以工作的.问题是,你并不是总是能预计你的脚本会工作在哪个版本的Python下,所以本节开始给出的方法还是推荐使用的.setmode函数通过msvcrt模块让你能修改标准输出的打开模式,在这种情况下,你能保证是以二进制模式打开sys.stdout的.

相关说明:

sys.stdout.fileno(...)
    fileno() -> integer "file descriptor".

    This is needed for lower-level file interfaces, such os.read().



2007-07-16

 

Python Cookbook 2.11 打包目录树中的文件到压缩文件

需求:

你需要打包一个目录下的所有文件(包括子目录)到压缩文件中去,压缩的方法可以使用流行的gzip方法或者压缩比更高的bzip2方法.

讨论:

Python标准库中的tarfile模块就支持上述的两种方式:你只需要指出自己需要的方法,将它做为参数传递给tarfile.TarFile.open方法即可 ,如:

import tarfile, os
def make_tar(folder_to_backup, dest_folder, compression='bz2'):
    if compression:
        dest_ext = '.' + compression
    else:
        dest_ext = ''
    arcname = os.path.basename(folder_to_backup)
    dest_name = '%s.tar%s' % (arcname, dest_ext)
    dest_path = os.path.join(dest_folder, dest_name)
    if compression:
        dest_cmp = ':' + compression
    else:
        dest_cmp = ''
    out = tarfile.TarFile.open(dest_path, 'w'+dest_cmp)
    out.add(folder_to_backup, arcname)
    out.close( )
    return dest_path

你也可以传递'gz'做为open的第二个参数,来使用gzip方式压缩文件,而非默认的bzip2方式,或者传递''从而不使用任何压缩方式.生成文件的扩展名是通过tarfile.TarFile.open的第二个参数来决定的,可以是.tar,.tar.gz,或者.tar.bz2,对应的参数为:'w','w:gz'和'w:bz2'.
除了open之外,类tarfile.TarFile也提供了其它的类方法,你可以使用适合的来生成实例.我认为open是最方便的,因为它使用open的模式参数来表示压缩方式,当然,如果你希望无条件的使用bz2方法,也可以使用bz2open来代替.
一旦我们拥有了tarfile.TarFile的实例,并且设置了适当的压缩方式,剩下的工作都可以交给add方法来完成.在实际中,如果fold_to_backup是一个目录,而不是一个特定的文件, add会递归添加该目录下所有的文件.有些情况下,我们想控制添加的方式,可以传递给add第二个参数recursive=False来关掉递归方式.当add完成后,make_tar方法剩下的工作就是关闭文件,和返回压缩后文件的路径,这样是为了给调用者更多的信息.

相关说明:

open(cls, name=None, mode='r', fileobj=None, bufsize=10240) method of __builtin__.type instance
    Open a tar archive for reading, writing or appending. Return
    an appropriate TarFile class.

    mode:
    'r' or 'r:*' open for reading with transparent compression
    'r:'         open for reading exclusively uncompressed
    'r:gz'       open for reading with gzip compression
    'r:bz2'      open for reading with bzip2 compression
    'a' or 'a:'  open for appending
    'w' or 'w:'  open for writing without compression
    'w:gz'       open for writing with gzip compression
    'w:bz2'      open for writing with bzip2 compression

    'r|*'        open a stream of tar blocks with transparent compression
    'r|'         open an uncompressed stream of tar blocks for reading
    'r|gz'       open a gzip compressed stream of tar blocks
    'r|bz2'      open a bzip2 compressed stream of tar blocks
    'w|'         open an uncompressed stream for writing
    'w|gz'       open a gzip compressed stream for writing
    'w|bz2'      open a bzip2 compressed stream for writing

 

Python Cookbook 2.10 处理内存中的zip文件

需求:

你的程序接收到一个字符串,里面包含了一个zip文件的内容,你需要获得这些信息.

讨论:

对于这一类问题,使用标准库的cStringIO最合适:

import cStringIO, zipfile
class ZipString(ZipFile):
    def _ _init_ _(self, datastring):
        ZipFile._ _init_ _(self, cStringIO.StringIO(datastring))

这样的问题会经常遇到,比如从数据库中的BLOB字段读取zip文件,或者从网络连接中读取.以前的时候,可以使用临时文件,用zipfile处理完数据后,要保证临时文件被删除;后来使用了cStringIO后,就再也没有使用过临时文件了.

cStringIO模块能够让你以文件的方式来访问字符串.另外,你也可以将数据以文件的方式写入cStringIO的实例,然后获得一个包含文件内容的字符串.大部分能处理file对象的Python模块并不检查你处理的对象是否真正是文件对象,只要是"类文件"对象就可以了.这些模块只是在需要的时候调用文件操作的方法,只要类文件对象提供类似的接口和适当的返回值,一切都没有问题.这就说明的多态的强大,以至于你几乎不需要写任何类型测试的代码(比如,if type(x) is y,或者if isinstance(x,y)).一些比较底层的模块,如marshl,需要使用真正的file对象,而zipfile是不需要的,所以我们的代码才能如此简单.
如果你使用的Python版本不是C语言版本,通常被成为"CPython",那你在标准库中可能没有cStringIO模块.以c开头的模块,暗示是以C语言实现的模块,它们能优化代码的效率,但是不保证所有的Python版本都通用.当然,一些别的版本的Python也提供了相关的模块.(如Jython,用java实现的版本并运行在jvm上,PyPy,用Python实现机器码,还有IronPython,用C#实现的,可以运行在.Net CLR环境里面).不用太担心,Python标准库总是提供了用标准Python实现的模块StringIO,它和cStringIO具有相同的实现(只是没有cStringIO效率高),你只需要修改import语句,让它能在cStringIO不存在的时候正确引用StringIO,比如:

import zipfile
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO
class ZipString(ZipFile):
    def _ _init_ _(self, datastring):
        ZipFile._ _init_ _(self, StringIO(datastring))

这样修改的话,本节的代码就能在Jython或者其它版本的Python中运行了.

相关说明:

StringIO(...)
    StringIO([s]) -- Return a StringIO-like stream for reading or writing


2007-07-11

 

Python Cookbook 2.9 从Zip文件中读数据

需求:

不解压缩zip文件,实现对文件内容的获取.

讨论:

zip文件是比较流行的压缩文件的方式,它也是跨平台的.标准Python库提供了zipfile模块来简化相关的操作:

import zipfile
z = zipfile.ZipFile("zipfile.zip", "r")
for filename in z.namelist ( ):
    print 'File:', filename,
    bytes = z.read(filename)
    print 'has', len(bytes), 'bytes'

Python可以直接操作zip文件中的数据,可以获得文件列表或者直接获得文件的内容.本节中给出的例子获得了zipfile.zip中的文件列表和包含文件的长度 .
Python目前还不能处理分卷压缩的zip文件和带有注释的zip文件.需要注意的时候,打开zip文件时要使用'r'参数而不是'rb',尽管后者看起来更合理一些(尤其在windows系统下).因为在ZipFile下面,并不识别'rb'选项,这个和open是不同的.'r'选项就表示了是对zip文件进行读操作.
假如一个zip文件包含了zip模块(py或者pyw文件),你可以在sys.path中添加这个文件的路径 ,并能使用import来引用zip文件中的模块.下面是一个小例子,仅仅用来说明问题,它创建一个zip文件,应引用了它,最后再删除:

import zipfile, tempfile, os, sys
handle, filename = tempfile.mkstemp('.zip')
os.close(handle)
z = zipfile.ZipFile(filename, 'w')
z.writestr('hello.py', 'def f( ): return "hello world from "+_ _file_ _\n')
z.close( )
sys.path.insert(0, filename)
import hello
print hello.f( )
os.unlink(filename)

用执行这个例子后输出:

hello world from /tmp/tmpESVzeY.zip/hello.py

除了说明Python能从zip中引入文件外,这个例子也说明如何创建(并删除)一个临时文件,而且也说明了如何使用writestr来给zip文件添加一个文件,而不是先在磁盘上创建一个.
需要注意的是,import引入的文件路径类似一个目录(在本例中是/tmp/tmpESVzeY.zip/hello.py,因为我们使用的是临时文件,所以不同时刻和不同系统下运行的结果可能是不一样的),特别的,全局变量__file__,它表示了/tmp/tmpESVzeY.zip/hello.py,具有一个类似与目录结构的路径,当然,实际上它并不是目录结构的,你用open方法是不能这样打开zip中的文件的.要想打开zip中的文件,要使用zipfile.

相关说明:

z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True)
 | 
 |  file: Either the path to the file, or a file-like object.
 |        If it is a path, the file will be opened and closed by ZipFile.
 |  mode: The mode can be either read "r", write "w" or append "a".
 |  compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib).
 |  allowZip64: if True ZipFile will create files with ZIP64 extensions when
 |              needed, otherwise it will raise an exception when this would
 |              be necessary.

mkstemp(suffix='', prefix='tmp', dir=None, text=False)
    mkstemp([suffix, [prefix, [dir, [text]]]])
    User-callable function to create and return a unique temporary
    file.  The return value is a pair (fd, name) where fd is the
    file descriptor returned by os.open, and name is the filename.
   
    If 'suffix' is specified, the file name will end with that suffix,
    otherwise there will be no suffix.
   
    If 'prefix' is specified, the file name will begin with that prefix,
    otherwise a default prefix is used.
   
    If 'dir' is specified, the file will be created in that directory,
    otherwise a default directory is used.
   
    If 'text' is specified and true, the file is opened in text
    mode.  Else (the default) the file is opened in binary mode.  On
    some operating systems, this makes no difference.
   
    The file is readable and writable only by the creating user ID.
    If the operating system uses permission bits to indicate whether a
    file is executable, the file is executable by no one. The file
    descriptor is not inherited by children of this process.
   
    Caller is responsible for deleting the file when done with it.

writestr(self, zinfo_or_arcname, bytes)
    unbound zipfile.ZipFile method
    Write a file into the archive.  The contents is the string
    'bytes'.  'zinfo_or_arcname' is either a ZipInfo instance or
    the name of the file in the archive.

 

Python Cookbook 2.8 使用随机方式修改

需求:

要从一个大的二进制文件中读取 一块数据,修改完成后,要写回文件.

讨论:

打开文件,做必要的偏移计算,读取数据,处理完后,重新定位到数据偏移处,写入数据即可.看代码吧,比说的更清楚一些:

import struct
format_string = '8l'                # e.g., say a record is 8 4-byte integers
thefile = open('somebinfile', 'r+b')
record_size = struct.calcsize(format_string)
thefile.seek(record_size * record_number)
buffer = thefile.read(record_size)
fields = list(struct.unpack(format_string, buffer))
# Perform computations, suitably modifying fields, then:
buffer = struct.pack(format_string, *fields)
thefile.seek(record_size * record_number)
thefile.write(buffer)
thefile.close( )

这种方式只有在二进制文件的数据块是固定大小的时候适用,对于一般的文本文件不适用,另外, 数据库的大小要和struct中定义的一样.代码中的struct的典型定义'8l',表示每一个字段是8个4字节的整数构成,它们都是有符号整数类型,被转换为Python的int型数据.这样,上面的代码中fields表示了8个整数为单位的列表.需要注意的是unpack返回的是一个元组,而元组是不可修改的,所以要将它重新绑定为一个列表,因为列表是可以修改的.但要注意的是,不要修改列表的大小,因为如果你要用struct.pack写回的时候 ,会抛出异常.同样,本节的方法也不适用于文件中数据块大小不固定的情况.
当再次定位文件指针的时候,可以不使用seek(record_size * record_number),而使用反向偏移定位:

thefile.seek(-record_size, 1)

其中第二个参数1表示从文件的当前位置开始定位,由于第一个参数是负值, 所以是向前定位了.seek默认是从文件的起始位置开始定位,当然你也可以显示的将第二个参数设置为0.
当你调用第一个seek之前,不必再次调用open打开文件,同样,也不用在调用完write方法后立刻close它.当你正确打开了文件对象(二进制更新模式,而不是文本模式),你可以任意次数的更新文件,最后再关闭.
文件需要以更新模式打开(同时支持读和写),这就是'r+b'的含义:为读和写操作打开文件,并且不做任何隐含的转换,因为是二进制模式的.('b'在Unix和类unix系统上是可选的,不过还是建议加上,当然,在windows系统是,是必须的),如果你只是想创建一个草稿文件,可是仍要做一些读写处理,可以使用'w+b' 做为open的第二个参数.当然,我从来没有见过这么奇怪的需求.二进制文件一般都是先被创建(使用'wb'参数,写数据,然后关闭),再用'r+b'参数打开处理.
当然,虽然本节提供的方法只适用于固定数据块大小的二进制文件,也可以适用于更高级的情况:有一个索引文件表示块的大小和位置,这种通过索引文件访问的技术已经不是什么新技术了,可是它还是非常重要的.在今天,虽然人们处理的情况大多数是数据库,文本文件(还包括html,xml等),还有偶尔的固定块大小的二进制文件, 也不能排出遇到使用索引文件的可能性.如果遇到这样的情况,处理的方法和本节是非常类似的,只是块大小和偏移位置不是计算的,而是从索引文件中读取的.

相关说明:

seek(...)
    seek(offset[, whence]) -> None.  Move to new file position.
   
    Argument offset is a byte count.  Optional argument whence defaults to
    0 (offset from start of file, offset should be >= 0); other values are 1
    (move relative to current position, positive or negative), and 2 (move
    relative to end of file, usually negative, although many platforms allow
    seeking beyond the end of a file).  If the file is opened in text mode,
    only offsets returned by tell() are legal.  Use of other offsets causes
    undefined behavior.
    Note that not all file objects are seekable.

2007-07-10

 

Python Cookbook 2.7 使用随机方式访问I/O

需求:

需要访问一个大的二进制文件,并读取指定位置的数据,而不是一个个的读到那里.

讨论:

指定的偏移地址等于数据块大小乘以块数.可以以此来定位到指定的数据块,并读取数据.比如,从文件的第七块来读取数据,块大小是48字节:

thefile = open('somebinfile', 'rb')
record_size = 48
record_number = 6
thefile.seek(record_size * record_number)
buffer = thefile.read(record_size)

需要注意的是,块数从0开始计数,所以record_number等于6.

上面的方法只适用于固定块大小的二进制文件,对于一般的文本文件可能就不适用了.在代码中,是以二进制方式打开文件的,只要是以二进制方式打开,你可以随意的seek和read,在关闭文件之前,你不用为了使用seek而再打开它一次.

相关说明:

seek(...)
    seek(offset[, whence]) -> None.  Move to new file position.
   
    Argument offset is a byte count.  Optional argument whence defaults to
    0 (offset from start of file, offset should be >= 0); other values are 1
    (move relative to current position, positive or negative), and 2 (move
    relative to end of file, usually negative, although many platforms allow
    seeking beyond the end of a file).  If the file is opened in text mode,
    only offsets returned by tell() are legal.  Use of other offsets causes
    undefined behavior.
    Note that not all file objects are seekable.

 

Python Cookbook 2.6 处理文件中的每一个单词

需求:

要对文件中的每一个单词进行处理.

讨论:

实现这个需求可以用两层的循环嵌套来实现,一个处理文件中的行,另一个处理行中的单词:

for line in open(thefilepath):
    for word in line.split( ):
        dosomethingwith(word)

内层的for语句将单词定义为用空格分隔的字符序列(unix中的wc命令也是这样做的),另外的定义单词的方法,你可以使用正则式:

import re
re_word = re.compile (r"[\w'-]+")
for line in open(thefilepath):
    for word in re_word.finditer(line):
        dosomethingwith(word.group(0))

在这里,单词被定义为由字母,连字符和撇号构成的字符序列.

如果你要重新定义单词,那你需要改变正则表达式,而外层的循环是不用改变的.
当遇到迭代处理的时候,可以把迭代操作封装到一个迭代对象中,这样的封装可简化代码和操作(使用生成器yield):

def words_of_file(thefilepath, line_to_words=str.split):
    the_file = open(thefilepath):
    for line in the_file:
        for word in line_to_words(line):
            yield word
    the_file.close( )
for word in words_of_file(thefilepath):
    dosomethingwith(word)

这种方式让你将问题分开和简化为两个部分:怎样迭代操作对象(本例中是文件中的单词),另一个是怎样处理迭代出的元素.一旦你清晰的封装出迭代操作 (在这里,使用了生成器),剩下的操作就是简单的for循环了.在程序中,可以多次使用这个迭代器,即使在维护代码的时候,也可以只是修改迭代器而不是所有的代码.这个好处类似于人们写一个个的函数而不是到处粘贴代码,在Python中,你可以使用这个技巧来封装循环结构.
在上面的代码中,我们在处理完文件后确保它关闭,这是一个良好的做法,也通过使用split方法把行变成了单词.当然,假如我们要使用正则式来定义单词,可是使用下面的'hook'代码来实现:

import re
def words_by_re(thefilepath, repattern=r"[\w'-]+"):
    wre = re.compile(repattern)
    def line_to_words(line):
        for mo in wre.finditer(line):
            return mo.group(0)
    return words_of_file(thefilepath, line_to_words)

在上面的代码中,我们同样定义了默认的正则式,当然也运行用户重写,一般情况下,将较通用的属性写成默认值,会增加代码的易用性和通用性.

相关说明:

re.finditer(pattern, string, flags=0)
    Return an iterator over all non-overlapping matches in the
    string.  For each match, the iterator returns a match object.
   
    Empty matches are included in the result.



This page is powered by Blogger. Isn't yours?