python ftp下载文件夹_Python实现下载FTP服务器的整个目录(文件夹)

Title(EN): Downloading FTP Directory Recursively with Python

Author: dog2

需求

快速批量下载多个FTP服务器的上的目录(指定目录或整个目录)。

分析

核心问题:针对一个FTP Server,要能够下载其指定目录。即递归遍历所有目录,针对每个目录中的子目录,创建本地相应目录;针对每个目录中的文件,下载到本地相应目录。

下载所有文件的过程最好也是可并发的,以加快整个下载过程。

前车之轮

需求及实现思路已经清楚,剩下的就是编码测试了。

按照惯例,为避免重复造轮子,在开工前有必要上谷歌百度一下,确定前人是否已经造出了好用的轮子。 找到如下几个:

通过python下载FTP上的文件夹的实现代码

python实现的ftp自动上传下载程序(支持目录递归操作)

python实现支持目录FTP上传下载文件的方法

python第三方库 ftputil

python第三方库 pyftpsync

逐一测试,都不太好用。

1在现实时使用os.chdir切换本地下载目录,批量下载(多线程并发下载多个服务器目录)时会产生目录混乱。

3在下载每个文件都需要执行ls函数确认其存在于远程目录,性能很低。

使用2 4 5没有成功下载过,可能是用法不对,粗略扫了下文档和源码,也没找到正确用法。如果你知道正确用法,还请指点。

实现

既然前人的轮子都不太好用,只好再造一个。相对其他协议而言,FTP协议还是比较复杂的,因此最好基于已有的FTP库来实现,python的自带FTP库是ftplib,这里就选用它。

文档

源码

大概实现思路及流程已在0x01中指出,这里可能需要用到ftplib中的两个函数

ftplib.FTP.dir() : 用于列举目录信息,其内部实现调用的是FTP协议中的LIST请求,输出格式类似linux下的命令_ls -alh_。这里需要注意,默认情况下,该函数是不返回目录信息的字符串的,它在内部实现中调用了ftplib.FTP.retrlines()函数得到返回数据的每一行,并默认使用println函数处理每行数据,即打印至标准输出。当然,处理每行数据的函数是可以被替换的,在dir()函数的参数中指出即可。尽管如此还是难以将目录信息存入到一个字符串并返回,以供我们后续调用。在0x02中提到的3是通过是给dir函数传入自定义类的实例函数,并将目录信息存储在自定义类的实例变量中来得到这个值的。当然,也可以通过自定义函数结合全局变量的方式来得到这个值。这种实现略显蹩脚,因此在这里,我们参照原有dir函数的实现方式,在自定义类中实现一个新的dir函数,它不用再传入处理每行数据的函数,且能够返回目录信息。

ftplib.FTP.retrbinary() : 用于指定并发送某种FTP请求,并以二进制数据接收响应。这里使用它来执行FTP协议中的RETR命令,以下载FTP服务器上指定路径的文件。

代码

实现代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205#!/usr/bin/env python

# encoding: utf-8

import os

import sys

import ftplib

import traceback

class FtpDownloader(object):

PATH_TYPE_UNKNOWN = -1

PATH_TYPE_FILE = 0

PATH_TYPE_DIR = 1

def __init__(self, host, user=None, passwd=None, port=21, timeout=10):

self.conn = ftplib.FTP(

host=host,

user=user,

passwd=passwd,

timeout=timeout

)

def dir(self, *args):

'''

by defualt, ftplib.FTP.dir() does not return any value.

Instead, it prints the dir info to the stdout.

So we re-implement it in FtpDownloader, which is able to return the dir info.

'''

info = []

cmd = 'LIST'

for arg in args:

if arg:

cmd = cmd + (' ' + arg)

self.conn.retrlines(cmd, lambda x: info.append(x.strip().split()))

return info

def tree(self, rdir=None, init=True):

'''

recursively get the tree structure of a directory on FTP Server.

args:

rdir - remote direcotry path of the FTP Server.

init - flag showing whether in a recursion.

'''

if init and rdir in ('.', None):

rdir = self.conn.pwd()

tree = []

tree.append((rdir, self.PATH_TYPE_DIR))

dir_info = self.dir(rdir)

for info in dir_info:

attr = info[0] # attribute

name = info[-1]

path = os.path.join(rdir, name)

if attr.startswith('-'):

tree.append((path, self.PATH_TYPE_FILE))

elif attr.startswith('d'):

if (name == '.' or name == '..'): # skip . and ..

continue

tree.extend(self.tree(rdir=path,init=False)) # recurse

else:

tree.append(path, self.PATH_TYPE_UNKNOWN)

return tree

def downloadFile(self, rfile, lfile):

'''

download a file with path %rfile on a FTP Server and save it to locate

path %lfile.

'''

ldir = os.path.dirname(lfile)

if not os.path.exists(ldir):

os.makedirs(ldir)

f = open(lfile, 'wb')

self.conn.retrbinary('RETR %s' % rfile, f.write)

f.close()

return True

def treeStat(self, tree):

numDir = 0

numFile = 0

numUnknown = 0

for path, pathType in tree:

if pathType == self.PATH_TYPE_DIR:

numDir += 1

elif pathType == self.PATH_TYPE_FILE:

numFile += 1

elif pathType == self.PATH_TYPE_UNKNOWN:

numUnknown += 1

return numDir, numFile, numUnknown

def downloadDir(self, rdir='.', ldir='.', tree=None,

errHandleFunc=None, verbose=True):

'''

download a direcotry with path %rdir on a FTP Server and save it to

locate path %ldir.

args:

tree - the tree structure return by function FtpDownloader.tree()

errHandleFunc - error handling function when error happens in

downloading one file, such as a function that writes a log.

By default, the error is print to the stdout.

'''

if not tree:

tree = self.tree(rdir=rdir, init=True)

numDir, numFile, numUnknown = self.treeStat(tree)

if verbose:

print 'Host %s tree statistic:' % self.conn.host

print '%d directories, %d files, %d unknown type' % (

numDir,

numFile,

numUnknown

)

if not os.path.exists(ldir):

os.makedirs(ldir)

ldir = os.path.abspath(ldir)

numDownOk = 0

numDownErr = 0

for rpath, pathType in tree:

lpath = os.path.join(ldir, rpath.strip('/').strip('\\'))

if pathType == self.PATH_TYPE_DIR:

if not os.path.exists(lpath):

os.makedirs(lpath)

elif pathType == self.PATH_TYPE_FILE:

try:

self.downloadFile(rpath, lpath)

numDownOk += 1

except Exception as err:

numDownErr += 1

if errHandleFunc:

errHandleFunc(err, rpath, lpath)

elif verbose:

print 'An Error occurred when downloading '\

'remote file %s' % rpath

traceback.print_exc()

print

if verbose:

print 'Host %s: %d/%d/%d(ok/err/total) files downloaded' % (

self.conn.host,

numDownOk,

numDownErr,

numFile

)

elif pathType == self.PATH_TYPE_UNKNOWN:

if verbose:

print 'Unknown type romote path got: %s' % rpath

if verbose:

print 'Host %s directory %s download finished:' % (

self.conn.host, rdir

)

print '%d directories, %d(%d failed) files, %d unknown type.' % (

numDir,

numFile,

numDownErr,

numUnknown

)

return numDir, numFile, numUnknown, numDownErr

if __name__ == '__main__':

import sys

import traceback

from pprint import pprint as pr

flog = open('err.log', 'wb')

def run(host):

try:

fd = FtpDownloader(

host=host,

user='test',

passwd='test',

port=21,

timeout=10

)

numDir, numFile, numUnknown, numDownErr = fd.downloadDir(

rdir='.',

ldir='download',

tree=None,

errHandleFunc=None,

verbose=True

)

flog.write(

'%s\nok\n'

'%d directories, %d(%d failed) files, %d unknown type\n\n\n' % (

host,

numDir,

numFile,

numDownErr,

numUnknown

)

)

except Exception as err:

traceback.print_exc()

flog.write(

'%s\nerror\n%s\n\n\n' % (

host,

traceback.format_exc()

)

)

pr(run(sys.argv[1]))

flog.close()

也可移步至github获取代码,欢迎完善。

这里仅抛砖引玉,实现了下载单个FTP的整个目录的核心功能,可以基于该代码继续实现并发下载多个FTP服务器上的指定文件夹的功能。

值得一提的是,这里针对单个FTP服务器上的多个文件下载还是串行的,如想要实现并发下载单个FTP服务器上的多个文件,则可以先通过tree函数得到FTP服务器的目录树,然后再并发下载相应的文件。但并发下载时若多个下载线程共用一个ftplib.FTP类的实例,并调用该实例的retrbinary函数进行下载,则不同线程之间可能会相互影响,具体可以参考ftplib的源码。

当然,要解决这个问题,可以为每个下载线程创建一个独有的ftplib.FTP类的实例,但这样就加大了FTP服务器处理的并发连接数,最大连接数及下载性能还是会受限于FTP服务器,存在不确定性。

我们将实现的程序与Filezilia进行了对比测试,发现它对多个文件的下载过程也是串行了,而最终下载文件的总数及速度二者相近。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值