Archive for the 'Python' Category

断点上传程序的一点心得

2009-4-25 21:42 | by 2ndboy

  最近两天花时间把之前做的基于文件的断点上传 Python 程序改成了基于数据块的断点上传,粒度变小之后,上传被打断后可以接着单个文件被打断的地方继续上传,而不用再传被打断之前就已经上传过的部分。心得记录如下:

1) 普通的文件上传用的是 STOR(STORE) 命令,断点上传需要用 APPE(APPEND) 命令:
两者的格式和参数一样,APPE 也没有用来指定 offset 的地方,所以 client 自己必须知道已经上传了多少字节,然后打开文件后把文件读指针设置到相应的地方再进行上传,Server 会把收到的数据附加到指定文件的后面。

2) 不能信赖 storbinary() 回调给出的上传数据 size:
由于上传期间随时可能断线,所以我在程序里维护了一个上传队列,里面记录了每个文件已经上传的 size,这个值是通过 storbinary() 的 callback 拿到的,示例代码如下:

  1. def UpdateSize( total_size, delta_size ):
  2.      total_size[0] = total_size[0] + delta_size
  3.  
  4.  ftp = ftplib.FTP( 'localhost' )
  5.  ftp.login( 'anonymous', '1@1.com' )
  6.  size = [0]
  7.  file = open( sys.path[0] + os.sep + 'test.txt', 'rb' )
  8.  ftp.storbinary( 'STOR /test.txt', file, 8192, lambda data: UpdateSize( size, len( data ) ) )
  9.  file.close()
  10.  print( size[0] )

上面这段示例代码有两个需要注意的地方:
a. lambda 里不能用赋值语句,所以要通过再去调用一个函数来达到计数的目的
b. Python 里 int 类型的变量不能传引用,内容一旦改变就会另建一个新的 int 变量,所以示例里用了一个 list 来保存上传的 size 而没有直接用一个 int 变量
(这里有更优雅的方法来达到同样的目的吗?路过的高人请留言:))

但是经过测试,用 storbinary() 的 callback 记录下来的已上传 size 在断线的情况下几乎都是不准确的,全部大于真正上传的数据 size。所以这个数值不是可信的,为了解决这个问题,我用了 ftplib 里的 FTP.size() 对这个值进行校正,效果不错。

顺便提一句,SIZE 命令不是标准所定义的,所以你在 RFC959 上是找不到这个命令的,但是大多数主流的 FTP Server 都支持这个命令,基本上可以放心使用。

3) 上传时处理文件要用双重 try-except:
《A Byte of Python》的 Chapter 13 当中在介绍不管有无异常发生,文件都能被关闭时用了如下一段代码:

  1. try:
  2.      f = file( 'test.txt' )
  3.      #... ...
  4.  finally:
  5.      f.close()

不管是不是由于 Python 2.x 跟 3.0 的语法差异导致的,总之这段代码在我的 Python 3 环境下是运行不了的,看来 try 跟 finally 块的变量作用域是不同的。倒是在《Dive Into Python》的 6.2.3 找到一段不错的实现:

  1. try:
  2.      fsock = open( filename, 'rb' )
  3.      try:
  4.          #... ...
  5.      finally:
  6.          fsock.close()
  7.  except:
  8.      pass

貌似最近有关技术的 post 很多,所以,没有输入就没有输出:D

FTP 的“断点续传”

2009-3-22 15:33 | by 2ndboy

  最近写了个 FTP 上传程序,考虑到网络断线等因素造成的上传中断,所以打算给这个程序加个“断点续传”的功能,说是断点续传,其实跟现在 FlashGet 或者迅雷等下载工具的断点续传还是不一样的,我这里断点续传指的是断线以后只上传刚才还没有被上传的和没有完整上传的文件。比如总共有 100 个文件需要上传,只完整上传了 49 个文件,在第 50 个文件只传了一半的时候断线了,那么再次连接之后只需要重新上传第 50 个文件和后面 50 个文件就可以啦。

  为了达到这个目的,只要等取到 FTP 上的文件列表就 OK。初次使用 Python 的 ftplib,尝试了几种方法,FTP.nlst() 只能拿到文件名,拿不到文件大小。最后决定用 FTP.retrlines() 搞定,但是这个接口的麻烦之处是需要一个 callback 来接收从 server 返回的每一行,对 C/C++ 这类语言来说,由于涉及两个函数调用过程,所以上下文关联稍稍有些麻烦,但是 Python 的 lambda 用在这里真是方便(话又说回来,FTP.retrlines() 直接返回一个 tuple 或者 list 不是更干脆?)。

  1. def GetFileInfo( ftp ):
  2.      file_list = []
  3.      try:
  4.          ftp.retrlines( 'LIST', lambda item: file_list.append( item ) )
  5.      except:
  6.          return( {} )
  7.  
  8.      ro = re.compile( '(.).{9}\s+\d+\s+(?:[^ ]+\s+){2}(\d+)\s+(?:[^ ]+\s+){3}(.+)' )
  9.      file_info = {}
  10.  
  11.      for item in file_list:
  12.          m = ro.match( item )
  13.          if m:
  14.              file_info[m.group(3)] = [ m.group(1), m.group(2) ]
  15.  
  16.      return( file_info )

这个函数返回一个 dictionary,然后对于一个本地文件,用 if file in file_info 就能判断 server 上是否已经有了这个文件,有的话,file_info[file][1] 就是 server 上这个文件的大小,这个大小跟本地文件大小不符就重新上传。

用 Python 遍历目录、清空临时文件

2009-3-12 22:01 | by 2ndboy

  前几天由于调试时需要频繁的清空临时文件夹,同事给了我一个他用 Python 写的脚本,目录及文件遍历用的是 os.walk(),后来在查文档的时候又发现了一个 os.path.walk(),Google 了一下,据说 os.walk() 是 Python 2.3 中才增加的,之前只有 os.path.walk()。但是从 Python 3.0 的文档来看,os.path.walk() 已经被废止了,只剩下 os.walk()。

  但是 os.walk() 用起来比较不顺手,不能像 Windows API 那样递归的进行目录树遍历,而是一次性的扔给你某路径下的所有文件和子目录。查了查文档,用 os.listdir() 实现了递归式层层深入的目录和文件遍历:

  1. import os, stat
  2.  
  3.  def WalkDir( dir, dir_callback = None, file_callback = None ):
  4.      for item in os.listdir( dir ):
  5.          fullpath = dir + os.sep + item
  6.          if os.path.isdir( fullpath ):
  7.              WalkDir( fullpath, dir_callback, file_callback )
  8.              if dir_callback: dir_callback( fullpath )
  9.          else:
  10.              if file_callback: file_callback( fullpath )

由于发现目录时会首先递归调用 WalkDir() 自己,然后再给目录 callback 一个执行机会,所以这算是一个深度优先的遍历实现,调用 WalkDir() 时传入相应的目录回调和文件回调就可以实现特定的功能,我利用刚刚实现的基础设施重新实现了一个清空临时文件夹的脚本:

  1. def DeleteDir( dir ):
  2.      try:
  3.          os.rmdir( dir )
  4.      except WindowsError, e:
  5.          print( "Can't delete directory " + dir + ", err = " + str( e.winerror ) )
  6.  
  7.  def DeleteFile( file ):
  8.      try:
  9.          os.unlink( file )
  10.      except WindowsError, e:
  11.          if 32 == e.winerror:
  12.              print( "Can't delete " + file + ", err = " + str( e.winerror ) )
  13.          elif 5 == e.winerror:
  14.              os.chmod( file, stat.S_IWRITE )  # Try to remove readonly attribute
  15.              try:
  16.                  os.unlink( file )  # Try again
  17.              except WindowsError, e:
  18.                  print( "Can't delete " + file + ", err = " + str( e.winerror ) )
  19.  
  20.  WalkDir( os.environ['TEMP'], DeleteDir, DeleteFile )

  最近抽时间看了些 Python 的电子文档,经常写些练手的小东西,以后打算拿这个做日常使用的脚本语言。至于 PHP,还是在 Web 领域比较专精和适用。

[2011/09/14 Update]
“del /f /s /q %temp%”可以部分替代上述 Python 脚本,缺点是不能删除空目录。