(转)python高级FTP

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(转)python高级FTP相关的知识,希望对你有一定的参考价值。

原文地址:http://www.itnose.net/detail/6754889.html
高级FTP服务器
1. 用户加密认证
2. 多用户同时登陆
3. 每个用户有自己的家目录且只能访问自己的家目录
4. 对用户进行磁盘配额、不同用户配额可不同
5. 用户可以登陆server后,可切换目录
6. 查看当前目录下文件
7. 上传下载文件,保证文件一致性
8. 传输过程中现实进度条
9.支持断点续传
10.用户操作日志

服务端 启动参数 start
客户端 启动参数 -s localhost -P 9500

程序结构:
seniorFTP/#综合目录
|- - -ftp_client/#客户端程序目录
| |- - -__init__.py
| |- - -bin/#启动目录
| | |- - -__init__.py
| | |- - -client_ftp.py#客户端视图启动
| |
| |- - -cfg/#配置目录
| | |- - -__init__.py
| | |- - -config.py#配置文件
| |
| |- - -down/#下载文件目录
| |
| |- - -putfile/#上传文件目录
| |
| |
| |- - -REDMAE
|- - -ftp_server/#服务端程序目录
| |- - -__init__.py
| |- - -bin/#启动目录
| | |- - -__init__.py
| | |- - -start.py#服务端视图启动
| | |- - -user_reg.py#用户注册启动
| |
| |- - -cfg/#配置目录
| | |- - -__init__.py
| | |- - -config.py#配置文件
| | |- - -userpwd.cfg#用户信息文件
| |
| |- - -core/#文件目录
| | |- - -__init__.py
| | |- - -ftp_server.py#服务端主要逻辑 类
|           |      |- - -logs.py#日志主要逻辑 类
|           |      |- - -main.py#服务端启动主程序
| |
| |- - -home/#用户文件目录
| | |- - -用户/#个人目录
| |
| |- - -log/#日志文件目录
| |
| |- - -REDMAE
| |
|
|- - -REDMAE

先上流程图:

技术分享

详细代码如下:

|- - -ftp_client/#客户端程序目录
| |- - -__init__.py
| |- - -bin/#启动目录
| | |- - -__init__.py
| | |- - -client_ftp.py#客户端视图启动

  1 #!usr/bin/env python
  2 #-*-coding:utf-8-*-
  3 # Author calmyan
  4 import socket,os,json,getpass,hashlib
  5 import os ,sys,optparse
  6 
  7 STATUS_CODE={
  8     240:‘格式出错,格式:{"action":"get","filename":"filename","size":100}‘,
  9     241:‘指令错误‘,
 10     242:‘用户密码出错‘,
 11     243:‘用户或密码出错‘,
 12     244:‘用户密码通过校验‘,
 13 }
 14 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))#获取相对路径转为绝对路径赋于变量
 15 sys.path.append(BASE_DIR)#增加环境变量
 16 from cfg import config
 17 class FTPClient(object):
 18     def __init__(self):
 19         paresr=optparse.OptionParser()
 20         paresr.add_option(‘-s‘,‘--server‘,dest=‘server‘,help=‘服务器地址‘)
 21         paresr.add_option(‘-P‘,‘--port‘,type="int",dest=‘port‘,help=‘服务器端口‘)
 22         paresr.add_option(‘-u‘,‘--username‘,dest=‘username‘,help=‘用户名‘)
 23         paresr.add_option(‘-p‘,‘--password‘,dest=‘password‘,help=‘密码‘)
 24         (self.options,self.args)=paresr.parse_args()#返回一个字典与列表的元组
 25         self.verify_args(self.options,self.args)#判断参数
 26         self.ser_connect()#连接服务端
 27         self.cmd_list=config.CMD_LIST
 28         self.rat=0#文件断点
 29 
 30     #实例化一个连接端
 31     def ser_connect(self):
 32         self.c=socket.socket()#实例化一个连接端
 33         self.c.connect((self.options.server,self.options.port))#进行连接
 34 
 35     #判断用户与密码是否成对出现
 36     def verify_args(self,options,args):
 37         if (options.username is None and options.password is None) or (options.username is not None and options.password is not None):#判断用户与密码是否成对出现
 38             pass##判断用户与密码单个出现
 39         else:
 40             exit(‘出错:请输入用户与密码!‘)#退出
 41         if options.server and options.port:#端口判断
 42             if options.port>0 and options.port<65535:
 43                 return True
 44             else:
 45                 print(‘端口号:[%s]错误,端口范围:0-65535‘%options.port)
 46 
 47     #登陆方法
 48     def landing(self):#登陆方法
 49         ‘‘‘用户验证‘‘‘
 50         if self.options.username is not None:#判断用户名已经输入
 51             #print(self.options.username,self.options.password)
 52             return self.get_user_pwd(self.options.username,self.options.password)#返回结果
 53         else:
 54             print(‘用户登陆‘.center(60,‘=‘))
 55             ret_count=0#验证次数
 56             while ret_count<5:
 57                 username=input(‘用户名:‘).strip()
 58                 password=getpass.getpass(‘密码:‘).strip()
 59                 if self.get_user_pwd(username,password):
 60                     return self.get_user_pwd(username,password)#调用远程验证用户 返回结果
 61                 else:
 62                     ret_count+=1#次数加一
 63                     print(‘认证出错次数[%s]‘%ret_count)
 64             else:
 65                 print(‘密码出错次数过多!‘)
 66                 exit()
 67 
 68     #‘‘‘用户名与密码检验‘‘‘
 69     def get_user_pwd(self,username,password):
 70         ‘‘‘用户名与密码检验‘‘‘
 71         #发送 头文件
 72         data={
 73             ‘action‘:‘auth‘,
 74             ‘username‘:username,
 75             ‘password‘:password
 76         }
 77         self.c.send(json.dumps(data).encode())#发送到服务器
 78         response = self.get_response()#得到服务端的回复
 79         if response.get(‘status_code‘) == 244:
 80             print(STATUS_CODE[244])
 81             self.user = username#存下用户名
 82             self.user_dir=response.get(‘dir‘)#目录
 83             return True
 84         else:
 85             print(response.get("status_msg") )
 86 
 87     #服务器回复
 88     def get_response(self):#服务器回复
 89         ‘‘‘服务器回复信息‘‘‘
 90         data=self.c.recv(1024)#接收回复
 91         data = json.loads(data.decode())
 92         return data
 93 
 94     #指令帮助
 95     def help(self):#指令帮助
 96         attr=‘‘‘
 97         help            指令帮助
 98         ----------------------------------
 99         info            个人信息
100         ----------------------------------
101         ls              查看当前目录(linux/windows)
102         ----------------------------------
103         pwd             查看当前路径(linux/windows)
104         ----------------------------------
105         cd 目录         切换目录(linux/windows)
106         ----------------------------------
107         get filename    下载文件
108         ----------------------------------
109         put filename    上传文件
110         ----------------------------------
111         --md5           使用md5  在get/put 后
112         ----------------------------------
113         mkdir name      创建目录(linux/windows)
114         ----------------------------------
115         rmdir name      删除目录(linux/windows)
116         ----------------------------------
117         rm filename     删除文件 (linux/windows)
118         ----------------------------------
119         exit            退出
120         ----------------------------------
121         ‘‘‘.format()
122         print(attr)
123 
124     ##交互
125     def inter(self):#交互
126         if  self.landing():#通过用户密码认证
127             print(‘指令界面‘.center(60,‘=‘))
128             self.help()
129             while True:
130                 cmd = input(‘[%s]-->指令>>>:‘%self.user_dir).strip()
131                 if len(cmd)==0:continue#输入空跳过
132                 if cmd==‘exit‘:exit()#退出指令
133                 cmd_str=cmd.split()#用空格分割 取命令到列表
134                 #print(cmd_str)
135                 #print(len(cmd_str))
136                 if len(cmd_str)==1 and cmd_str[0] in self.cmd_list:#如果是单个命令 并且在命令列表中
137                 #if len(cmd_str)==1:#如果是单个命令 并且在命令列表中
138                     if cmd_str[0]==config.HELP:
139                         self.help()
140                         continue
141                     func=getattr(self,‘cmd_compr‘)#调用此方法
142                     ret=func(cmd_str)
143                     if ret:
144                         continue
145                     else:
146                         pass
147                 elif len(cmd_str)>1:
148                     if hasattr(self,‘cmd_%s‘%cmd_str[0]):#判断类中是否有此方法
149                         func=getattr(self,‘cmd_%s‘%cmd_str[0])#调用此方法
150                         func(cmd_str)#执行
151                         continue
152                 else:
153                     print(‘指令出错!‘)
154                     self.help()#
155 
156     #‘‘‘是否要md5‘‘‘
157     def cmd_md5_(self,cmd_list):
158         ‘‘‘是否要md5‘‘‘
159         if ‘--md5‘ in cmd_list:
160             return True
161 
162     #进度条
163     def show_pr(self,total):#进度条
164         received_size = 0 #发送的大小
165         current_percent = 0 #
166         while received_size < total:
167              if int((received_size / total) * 100 )   > current_percent :
168                   print("#",end="",flush=True)#进度显示
169                   current_percent = int((received_size / total) * 100 )
170              new_size = yield #断点跳转 传入的大小
171              received_size += new_size
172 
173     #单个命令
174     def cmd_compr(self,cmd_str,**kwargs):
175         mag_dict={
176                     "action":"compr",
177                     ‘actionname‘:cmd_str[0]
178                 }
179         self.c.send(json.dumps(mag_dict).encode(‘utf-8‘))#发送数据
180         cmd_res_attr=self.get_response()#得到服务器的回复
181         if type(cmd_res_attr) is not int:#如果不int 类型
182             if cmd_res_attr["status_code"] ==241:#命令不对
183                 print(cmd_res_attr[‘status_msg‘])
184                 return
185             if cmd_res_attr["status_code"] ==240:#命令不对
186                 print(cmd_res_attr[‘status_msg‘])
187                 return
188         size_l=0#收数据当前大小
189         self.c.send(‘准备好接收了,可以发了‘.encode(‘utf-8‘))
190         receive_data= ‘‘.encode()
191         while size_l< cmd_res_attr:
192             data=self.c.recv(1024)#开始接收数据
193             size_l+=len(data)#加上
194             receive_data += data
195         else:
196             receive_data=receive_data.decode()
197             try:
198                 receive_data=eval(receive_data)#转为列表 或字典
199             except Exception as e:
200                 pass
201             if type(receive_data) is dict:#如果是字典
202                 for i in receive_data:
203                     print(i,receive_data[i])
204                 return 1
205             if type(receive_data) is list:#如果是列表
206                 for i in receive_data:
207                     print(i)
208                 return 1
209             print(receive_data)
210             return 1
211 
212     #切换目录
213     def cmd_cd(self,cmd_list,**kwargs):
214         ‘‘‘切换目录‘‘‘
215         mag_dict={
216                     "action":"cd",
217                     ‘actionname‘:cmd_list[1]
218                 }
219         self.c.send(json.dumps(mag_dict).encode(‘utf-8‘))#发送数据
220         msg_l=self.c.recv(1024)#接收数据 消息
221         data=json.loads(msg_l.decode())
222         if data["status_code"] ==251:#目录不可切换
223             print(data[‘status_msg‘])
224             return
225         elif data["status_code"] ==252:#目录可以换
226             print(data[‘status_msg‘])
227             self.c.send(b‘1‘)#发送到服务器,表示可以了
228             data=self.c.recv(1024)
229             print(data.decode())
230             user_dir=data.decode()
231             print(user_dir)
232             self.user_dir=user_dir
233             return
234         elif data["status_code"] ==256:#目录不存在
235             print(data[‘status_msg‘])
236             return
237 
238     #删除文件
239     def cmd_rm(self,cmd_list,**kwargs):
240         mag_dict={
241                     "action":"rm",
242                     ‘filename‘:cmd_list[1]
243                 }
244         self.c.send(json.dumps(mag_dict).encode(‘utf-8‘))#发送文件信息
245         data=self.get_response()#得到服务器的回复
246         if data["status_code"] ==245:#文件不存在
247             print(data[‘status_msg‘])
248             #print(‘删除前空间:‘,data[‘剩余空间‘])
249             return
250         elif data["status_code"] ==254:#文件删除完成
251             print(data[‘status_msg‘])
252             print(‘删除前空间:‘,data[‘剩余空间‘])
253             pass
254         self.c.send(b‘1‘)#发送到服务器,表示可以
255         data=self.get_response()#得到服务器的回复
256         if data["status_code"] ==255:#文件删除完成
257             print(data[‘status_msg‘])
258             print(‘删除后空间:‘,data[‘剩余空间‘])
259             return
260 
261     #创建目录
262     def cmd_mkdir(self,cmd_list,**kwargs):
263         mag_dict={
264                     "action":"mkdir",
265                     ‘filename‘:cmd_list[1]
266                 }
267         self.c.send(json.dumps(mag_dict).encode(‘utf-8‘))#发送文件信息
268         data=self.get_response()#得到服务器的回复
269         if data["status_code"] ==257:#目录已经存在
270             print(data[‘status_msg‘])
271             return
272         elif data["status_code"] ==256:#目录创建中
273             print(data[‘目录‘])
274             pass
275         self.c.send(b‘1‘)#发送到服务器,表示可以
276         data=self.get_response()#得到服务器的回复
277         if data["status_code"] ==258:#目录创建中完成
278             print(data[‘status_msg‘])
279             return
280         pass
281 
282     #删除目录
283     def cmd_rmdir(self,cmd_list,**kwargs):
284         mag_dict={
285                     "action":"rmdir",
286                     ‘filename‘:cmd_list[1]
287                 }
288         self.c.send(json.dumps(mag_dict).encode(‘utf-8‘))#发送文件信息
289         data=self.get_response()#得到服务器的回复
290         if data["status_code"] ==256:#目录不存在
291             print(data[‘status_msg‘])
292             return
293         elif data["status_code"] ==260:#目录不为空
294             print(data[‘status_msg‘])
295             print(data[‘目录‘])
296             return
297         elif data["status_code"] ==257:#目录删除中
298             print(data[‘目录‘])
299             pass
300         self.c.send(b‘1‘)#发送到服务器,表示可以
301         data=self.get_response()#得到服务器的回复
302         if data["status_code"] ==259:#目录删除完成
303             print(data[‘status_msg‘])
304             return
305         pass
306 
307     #上传方法
308     def cmd_put(self,cmd_list,**kwargs):#上传方法
309         if len(cmd_list) > 1:
310             filename=cmd_list[1]#取文件名
311             filename_dir=config.PUT_DIR+filename#拼接文件名路径
312 
313             if os.path.isfile(filename_dir):#是否是一个文件
314                 filesize=os.stat(filename_dir).st_size#获取文件大小
315                 #执行行为 名字,大小,是否
316                 mag_dict={
317                     "action":"put",
318                     ‘filename‘:filename,
319                     ‘size‘:filesize,
320                     ‘overridden‘:True,
321                     ‘md5‘:False
322                 }
323                 if self.cmd_md5_(cmd_list):#判断是否进行MD5
324                     mag_dict[‘md5‘] = True
325                 self.c.send(json.dumps(mag_dict).encode(‘utf-8‘))#发送文件信息
326                 data=self.get_response()#得到服务器的回复
327                 if data["status_code"] ==250:#磁盘空间不足
328                     print(data[‘status_msg‘])
329                     print(mag_dict[‘size‘])
330                     return
331                 if data["status_code"] ==249:#磁盘空间足够
332                     print(data[‘status_msg‘])
333                     print(‘剩余空间‘,data[‘剩余空间‘])
334                     self.c.send(b‘1‘)#发送到服务器,表示可以上传文件了
335                     data=self.get_response()#得到服务器的回复
336                     if data["status_code"] ==230:#断点续传
337                         print(data[‘status_msg‘])
338                         print(data[‘文件大小‘])
339                         self.rat=data[‘文件大小‘]#文件指针位置
340                         pass
341                     elif data["status_code"] ==231:#非断点续传
342                         print(data[‘status_msg‘])
343                         self.rat=0#文件指针位置
344                         pass
345                     f=open(filename_dir,‘rb‘)#打开文件
346                     f.seek(self.rat)#移动到位置
347                     print(mag_dict[‘md5‘])
348                     self.c.send(b‘1‘)#发送到服务器,表示可以上传文件了
349                     if mag_dict[‘md5‘]==True:
350                         md5_obj = hashlib.md5()#定义MD5
351                         progress = self.show_pr(mag_dict[‘size‘]) #进度条 传入文件大小
352                         progress.__next__()
353                         while self.rat<filesize:
354                             line=f.read(1024)
355                             self.c.send(line)
356                             try:
357                                 progress.send(len(line))#传入当前数据大小
358                             except StopIteration as e:
359                                 print("100%")
360                                 break
361                             md5_obj.update(line)#计算MD5
362 
363                         else:
364                             print(filename,‘发送完成!‘)
365                             f.close()
366                             md5_val = md5_obj.hexdigest()
367                             md5_from_server = self.get_response()#服务端的MD5
368                             if md5_from_server[‘status_code‘] == 248:
369                                 if md5_from_server[‘md5‘] == md5_val:
370                                     print("%s 文件一致性校验成功!" % filename)
371                                     return
372                     else:
373                         progress = self.show_pr(mag_dict[‘size‘]) #进度条 传入文件大小
374                         progress.__next__()
375                         #for line in f:
376                         while self.rat<filesize:
377                             line=f.read(1024)
378                             self.c.send(line)
379                             try:
380                                 progress.send(len(line))#传入当前数据大小
381                             except StopIteration as e:
382                                 print("100%")
383                                 break
384                             #print(line)
385                         else:
386                             print(filename,‘发送完成!‘)
387                             f.close()
388                             return
389             else:
390                 print(filename,‘文件不存在!‘)
391 
392     #下载方法
393     def cmd_get(self,cmd_list,**kwargs):#下载方法
394         #cmd_split= args[0].split()#指令解析
395         # if len(cmd_list) == 1:
396         #     print("没有输入文件名.")
397         #     return
398         #down_filename = cmd_list[1].split(‘/‘)[-1]#文件名
399         down_filename=cmd_list[1]#取文件名
400         file_path=‘%s/%s‘%(config.GET_DIR,down_filename)#拼接文件路径 用户down目录
401         if os.path.isfile(file_path):#文件是否存
402             filesize=os.stat(file_path).st_size#获取文件大小
403             name_down=True
404         else:
405             filesize=0
406             name_down=False
407         mag_dict={
408                     "action":"get",
409                     ‘filename‘:cmd_list[1],
410                     ‘name_down‘:name_down,
411                     ‘size‘:filesize
412                 }
413         if self.cmd_md5_(cmd_list):#判断是否进行MD5
414             mag_dict[‘md5‘] = True
415         self.c.send(json.dumps(mag_dict).encode())#发送
416         self.c.send(b‘1‘)#发送到服务器,防粘包
417 
418         response = self.get_response()#服务器返回文件 的信息
419         if response["status_code"] ==247:#如文件存在
420             if name_down==True and response[‘file_size‘]==filesize:
421                 print(‘文件已经下载完成‘)
422                 self.c.send(b‘2‘)
423                 return
424             self.c.send(b‘1‘)#发送到服务器,表示可以接收文件了
425             #if name_down:
426             received_size = filesize#当前接收的数据大小
427             #else:
428             #received_size = 0#当前接收的数据大小
429 
430             file_obj = open(file_path,"ab")#打开文件
431             if self.cmd_md5_(cmd_list):
432                 md5_obj = hashlib.md5()
433                 progress = self.show_pr(response[‘file_size‘]) #进度条 传入文件大小
434                 progress.__next__()
435                 while received_size< response[‘file_size‘]:
436                     if response[‘file_size‘] - received_size>1024:#表示接收不止一次
437                         size=1024
438                     else:#最后一次
439                         size=response[‘file_size‘] - received_size
440                         #print(‘最后一个大小‘,size)
441                     data= self.c.recv(size)#接收数据
442 
443                     try:
444                         progress.send(len(data))#传入当前数据大小
445                     except StopIteration as e:
446                         print("100%")
447                     received_size+=len(data)#接收数据大小累加
448                     file_obj.write(data)#写入文件
449                     md5_obj.update(data)#进行MD5验证
450                 else:
451                     print("下载完成".center(60,‘-‘))
452                     file_obj.close()
453                     md5_val = md5_obj.hexdigest()#获取MD5
454                     #print(md5_val)
455                     md5_from_server = self.get_response()#服务端的MD5
456                     #print(md5_from_server[‘md5‘])
457                     if md5_from_server[‘status_code‘] == 248:
458                         if md5_from_server[‘md5‘] == md5_val:
459                             print("%s 文件一致性校验成功!" % down_filename)
460                     pass
461             else:
462                 progress = self.show_pr(response[‘file_size‘]) #进度条 传入文件大小
463                 progress.__next__()
464                 while received_size< response[‘file_size‘]:
465                     if response[‘file_size‘] - received_size>1024:#表示接收不止一次
466                         size=1024
467                     else:#最后一次
468                         size=response[‘file_size‘] - received_size
469                         #print(‘最后一个大小‘,size)
470                     data= self.c.recv(size)#接收数据
471 
472                     try:
473                       progress.send(len(data))#传入当前数据大小
474                     except StopIteration as e:
475                       print("100%")
476                     received_size+=len(data)#接收数据大小累加
477                     file_obj.write(data)#写入文件
478                     pass
479 
480                 else:
481                     print("下载完成".center(60,‘-‘))
482                     file_obj.close()
483                     pass
484             self.c.send(b‘1‘)#发送到服务器,表示可以接收文件了
485 
486 if __name__==‘__main__‘:
487 
488     c=FTPClient()
489     c.inter()
|           |- - -cfg/#配置目录
| | |- - -__init__.py
| | |- - -config.py#配置文件
 1 #!usr/bin/env python
 2 #-*-coding:utf-8-*-
 3 # Author calmyan
 4 
 5 import os ,sys
 6 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))#获取相对路径转为绝对路径赋于变量
 7 sys.path.append(BASE_DIR)#增加环境变量
 8 #print(BASE_DIR)
 9 
10 PUT_DIR=BASE_DIR+‘\putfile\\‘#定义用户上传目录文件路径变量
11 GET_DIR=BASE_DIR+‘\down\\‘#定义用户下载目录文件路径变量
12 HELP=‘help‘
13 CMD_LIST=[‘ls‘,‘pwd‘,‘info‘,‘help‘]
|- - -ftp_server/#服务端程序目录
| |- - -__init__.py
| |- - -bin/#启动目录
| | |- - -__init__.py
| | |- - -start.py#服务端视图启动
 1 #!usr/bin/env python
 2 #-*-coding:utf-8-*-
 3 # Author calmyan
 4 import socket,os,json
 5 import os ,sys
 6 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))#获取相对路径转为绝对路径赋于变量
 7 sys.path.append(BASE_DIR)#增加环境变量
 8 
 9 from core import main
10 
11 if __name__ == ‘__main__‘:
12 
13     main.ArvgHandler()
|           |      |- - -user_reg.py#用户注册启动
 1 #!usr/bin/env python
 2 #-*-coding:utf-8-*-
 3 # Author calmyan
 4 
 5 import configparser
 6 import os ,sys
 7 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))#获取相对路径转为绝对路径赋于变量
 8 sys.path.append(BASE_DIR)#增加环境变量
 9 from cfg import config
10 #修改个信息 磁盘大小
11 def set_info(name,pwd,size):
12     config_info=configparser.ConfigParser()#读数据
13     config_info.read(config.AUTH_FILE)#读文件 用户名密码
14     #print(config_info.options(name))
15     config_info[name]={}
16     config_info.set(name,config.PWD,pwd)#密码
17     config_info.set(name,config.QUOTATION,size)#磁盘信息
18     config_info.write(open(config.AUTH_FILE,‘w‘))#写入文件
19     file_path=‘%s/%s‘%(config.USER_HOME,name)#拼接目录路径
20     os.mkdir(file_path)#创建目录
21     print(‘创建完成‘.center(60,‘=‘))
22     print(‘用户名:[%s]\n密码:[%s]\n磁盘空间:[%s]‘%(name,pwd,size))
23 
24 if __name__ == ‘__main__‘:
25     name=input(‘name:‘)
26     pwd=input(‘pwd:‘)
27     size=input(‘size:‘)
28     set_info(name,pwd,size)
|           |      |- - -userpwd.cfg#用户信息文件
 1 #!usr/bin/env python
 2 #-*-coding:utf-8-*-
 3 # Author calmyan
 4 
 5 import os ,sys
 6 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))#获取相对路径转为绝对路径赋于变量
 7 sys.path.append(BASE_DIR)#增加环境变量
 8 #HOME_PATH = os.path.join(BASE_DIR, "home")
 9 
10 
11 
12 #USER_DIR=‘%s\\data\\‘%BASE_DIR#定义用户数据目录文件路径变量
13 #USER_DIR=‘%s/data‘%BASE_DIR#定义用户数据目录文件路径变量
14 #USER_HOME=‘%s\\home\\‘%BASE_DIR#定义用户家目录文件路径变量
15 USER_HOME=‘%s/home‘%BASE_DIR#定义用户家目录文件路径变量
16 #LOG_DIR=‘%s\\log\\‘%BASE_DIR#日志目录
17 USER_LOG=‘%s/log/user_log.log‘%BASE_DIR#日志登陆文件
18 USER_OPERT=‘%s/log/user_opert.log‘%BASE_DIR#日志操作文件
19 
20 LOG_LEVEL=‘DEBUG‘#日志级别
21 
22 AUTH_FILE=‘%s/cfg/userpwd.cfg‘%BASE_DIR#用户名密码文件
23 HOST=‘0.0.0.0‘# IP
24 PORT=9500#端口
25 QUOTATION=‘Quotation‘#磁盘空间
26 PWD=‘PWD‘#密码
|           |- - -core/#服务端主要文件目录
| | |- - -__init__.py
| | |- - -ftp_server.py#服务端主要逻辑 类
  1 #!usr/bin/env python
  2 #-*-coding:utf-8-*-
  3 # Author calmyan
  4 import socketserver,os,json,pickle,configparser,time
  5 time_format=‘%Y%m%d%H%M%S‘#定义时间格式
  6 times=time.strftime(time_format)#定义时间
  7 
  8 STATUS_CODE={
  9     230:‘文件断点继传‘,
 10     231:‘新文件‘,
 11     240:‘格式出错,格式:{"action":"get","filename":"filename","size":100}‘,
 12     241:‘指令错误‘,
 13     242:‘用户名或密码为空‘,
 14     243:‘用户或密码出错‘,
 15     244:‘用户密码通过校验‘,
 16     245:‘文件不存在或不是文件‘,
 17     246:‘服务器上该文件不存在‘,
 18     247:‘准备发送文件,请接收‘,
 19     248:‘md5‘,
 20     249:‘准备接收文件,请上传‘,
 21     250:‘磁盘空间不够‘,
 22     251:‘当前已经为主目录‘,
 23     252:‘目录正在切换‘,
 24     253:‘正在查看路径‘,
 25     254:‘准备删除文件‘,
 26     255:‘删除文件完成‘,
 27     256:‘目录不存在‘,
 28     257:‘目录已经存在‘,
 29     258:‘目录创建完成‘,
 30     259:‘目录删除完成‘,
 31     260:‘目录不是空的‘,
 32 }
 33 import os ,sys,hashlib
 34 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))#获取相对路径转为绝对路径赋于变量
 35 sys.path.append(BASE_DIR)#增加环境变量
 36 from cfg import config
 37 from core.logs import log_log
 38 from core.logs import user_opert
 39 
 40 
 41 class MyTCPHandler (socketserver.BaseRequestHandler):#
 42 
 43     def setup(self):
 44        print(‘监听中。。。‘)
 45     #‘‘‘用户名与密码是否为空‘‘‘
 46     def cmd_auth(self,*args,**kwargs):#用户校验
 47         ‘‘‘用户名与密码是否为空‘‘‘
 48         data=args[0]#获取 传来的数据
 49         if data.get(‘username‘) is None or data.get(‘password‘) is None:#如果用户名或密码为空
 50             self.send_mge(242)#发送错误码
 51         name=data.get(‘username‘)#用户名
 52         pwd=data.get(‘password‘)#密码
 53         print(name,pwd)
 54         user=self.authusername(name,pwd)#用户名与密码的校验 获名用户名
 55         if user is None:#用户名不存在
 56             self.send_mge(243)
 57         else:
 58             self.user=name#保存用户名
 59             self.home_dir=‘%s/%s‘%(config.USER_HOME,self.user)#拼接 用户home目录路径 用户根目录
 60             self.user_home_dir=self.home_dir#当前所在目录
 61             # self.user_dir=self.user_home_dir.split(‘/‘)[-1]#当前所在目录 相对
 62             self.dir_join()#进行目录拼接
 63             self.send_mge(244,data={‘dir‘:self.user_dir})#相对 目录
 64 
 65     #目录拼接
 66     def dir_join(self,*args,**kwargs):
 67         self.user_dir=self.user_home_dir.split(self.home_dir)[-1]+‘/‘#当前所在目录 相对
 68         print(self.user_dir)
 69 
 70     #‘‘‘用户名与密码的校验‘‘
 71     def authusername(self,name,pwd):
 72         ‘‘‘用户名与密码的校验‘‘‘
 73         config_info=configparser.ConfigParser()#读数据
 74         config_info.read(config.AUTH_FILE)#读文件 用户名密码
 75         if name in config_info.sections():#用户名存
 76             password=config_info[name][‘PWD‘]
 77             if password==pwd:#密码正确
 78                 print(‘通过校验!‘)
 79                 config_info[name][‘USERname‘]=name#名字的新字段
 80                 info_str=‘用户[%s],成功登陆‘%name
 81                 self.log_log.warning(info_str)#记录日志
 82                 #log_log(info_str)
 83                 return config_info[name]
 84             else:
 85                 info_str=‘用户[%s],登陆错误‘%name
 86                 #log_log(info_str)
 87                 self.log_log.warning(info_str)#记录日志
 88                 return 0
 89 
 90     #判断文件 是否存在
 91     def file_name(self,file_path):
 92         if os.path.isfile(file_path):#文件是否存
 93             return  True
 94         else:
 95             return False
 96 
 97     #判断目录是否存在
 98     def file_dir(self,file_path):
 99         if os.path.isdir(file_path):#目录是否存
100             return  True
101         else:
102             return False
103 
104     #删除文件
105     def cmd_rm(self,*args,**kwargs):
106         cmd_dict=args[0]#获取字典
107         action=cmd_dict["action"]
108         filename =cmd_dict[‘filename‘]#文件名
109         file_path=‘%s/%s‘%(self.user_home_dir,filename)#拼接文件路径
110         if not self.file_name(file_path):
111             self.send_mge(245)#文件不存在
112             return
113         else:
114             user_size=self.disk_size()#获取磁盘信息
115             self.send_mge(254,data={‘剩余空间‘:user_size})#准备删除文件
116             file_size=os.path.getsize(file_path)#获取文件大小
117             pass
118         self.request.recv(1) #客户端确认 防粘包
119         os.remove(file_path)
120         new_size=float((float(user_size)+float(file_size))/1024000)#空间大小增加
121         self.set_info(str(new_size))#传入新大小
122         self.send_mge(255,data={‘剩余空间‘:new_size})#删除文件完成
123         info_str=self.log_str(‘删除文件‘)#生成日志信息
124         self.user_opert.critical(info_str)#记录日志
125         return
126 
127     #创建目录
128     def cmd_mkdir(self,*args,**kwargs):
129         cmd_dict=args[0]#获取字典
130         action=cmd_dict["action"]
131         filename =cmd_dict[‘filename‘]#目录名
132         file_path=‘%s/%s‘%(self.user_home_dir,filename)#拼接目录路径
133         if self.file_dir(file_path):
134             self.send_mge(257)#目录已经 存在
135             return
136         else:
137             self.send_mge(256,data={‘目录‘:‘创建中...‘})#目录创建中
138             self.request.recv(1) #客户端确认 防粘包
139             os.mkdir(file_path)#创建目录
140             self.send_mge(258)#目录完成
141             info_str=self.log_str(‘创建目录‘)#生成日志信息
142             self.user_opert.critical(info_str)#记录日志
143             return
144 
145     #删除目录
146     def cmd_rmdir(self,*args,**kwargs):
147         cmd_dict=args[0]#获取字典
148         action=cmd_dict["action"]
149         filename =cmd_dict[‘filename‘]#目录名
150         file_path=‘%s/%s‘%(self.user_home_dir,filename)#拼接目录路径
151         if not self.file_dir(file_path):
152             self.send_mge(256)#目录不存在
153             return
154         elif os.listdir(file_path):
155             self.send_mge(260,data={‘目录‘:‘无法删除‘})#目录不是空的
156             return
157         else:
158             self.send_mge(257,data={‘目录‘:‘删除中...‘})#目录创建中
159             self.request.recv(1) #客户端确认 防粘包
160             os.rmdir(file_path)#删除目录
161             self.send_mge(259)#目录删除完成
162             info_str=self.log_str(‘删除目录‘)#生成日志信息
163             self.user_opert.critical(info_str)#记录日志
164             return
165 
166     #磁盘空间大小
167     def disk_size(self):
168         attr_list=self.user_info()#调用个人信息
169         put_size=attr_list[1]#取得磁盘信息
170         user_size=float(put_size)*1024000#字节
171         return user_size
172 
173     #‘‘‘客户端上传文件 ‘‘‘
174     def cmd_put(self,*args,**kwargs):
175         ‘‘‘客户端上传文件 ‘‘‘
176         cmd_dict=args[0]#获取字典
177         filename =cmd_dict[‘filename‘]#文件名
178         file_size= cmd_dict[‘size‘]#文件大小
179         #user_home_dir=‘%s/%s‘%(config.USER_HOME,self.user)#拼接 用户home目录路径
180         file_path=‘%s/%s‘%(self.user_home_dir,filename)#拼接文件路径
181         user_size=self.disk_size()#取得磁盘信息
182         if float(file_size)>float(user_size):#空间不足
183             self.send_mge(250,data={‘剩余空间‘:user_size})
184             return
185         self.send_mge(249,data={‘剩余空间‘:user_size})#发送一个确认
186         self.request.recv(1) #客户端确认 防粘包
187         if self.file_name(file_path):#判断文件名是否存在,
188             s_file_size=os.path.getsize(file_path)##获取服务器上的文件大小
189             if file_size>s_file_size:#如果服务器上的文件小于要上传的文件进
190                 tmp_file_size=os.stat(file_path).st_size#计算临时文件大小
191                 reversed_size=tmp_file_size#接收到数据大小
192                 self.send_mge(230,data={‘文件大小‘:reversed_size})#发送临时文件大小
193                 pass
194             else:# file_size==s_file_size:#如果大小一样
195                 file_path=file_path+‘_‘+times#命名新的文件 名
196                 reversed_size=0#接收到数据大小
197                 self.send_mge(231)#发送 不是断点文件
198                 pass
199         else:
200             reversed_size=0#接收到数据大小
201             self.send_mge(231)#发送 不是断点文件
202             pass
203 
204         f=open(file_path,‘ab‘)
205         self.request.recv(1) #客户端确认 防粘包
206         if cmd_dict[‘md5‘]:#是否有 md5
207             md5_obj = hashlib.md5() #   进行MD5
208             while reversed_size< int(file_size):#接收小于文件 大小
209                 if int(file_size) - reversed_size>1024:#表示接收不止一次
210                     size=1024
211                 else:#最后一次
212                     size=int(file_size) - reversed_size
213                     #print(‘最后一个大小‘,size)
214                 data= self.request.recv(size)#接收数据
215                 md5_obj.update(data)
216                 reversed_size+=len(data)#接收数据大小累加
217                 f.write(data)#写入文件
218             else:
219                 f.close()
220                 print(‘[%s]文件上传完毕‘.center(60,‘-‘)%filename)
221                 md5_val = md5_obj.hexdigest()#得出MD5
222                 print(md5_val)
223                 self.send_mge(248,{‘md5‘:md5_val})#发送md5给客户端
224         else:
225             while reversed_size< int(file_size):#接收小于文件 大小
226                 if int(file_size) - reversed_size>1024:#表示接收不止一次
227                     size=1024
228                 else:#最后一次
229                     size=int(file_size) - reversed_size
230                     #print(‘最后一个大小‘,size)
231                 data= self.request.recv(size)#接收数据
232                 reversed_size+=len(data)#接收数据大小累加
233                 f.write(data)#写入文件
234             else:
235                 print(‘[%s]文件上传完毕‘%filename.center(60,‘-‘))
236                 f.close()
237         new_size=float((float(user_size)-float(file_size))/1024000)#扣除空间大小
238         self.set_info(str(new_size))#传入新大小
239         info_str=self.log_str(‘文件上传‘)#生成日志信息
240         self.user_opert.critical(info_str)#记录日志
241         return
242 
243     #用户下载文件
244     def cmd_get(self,*args,**kwargs):#用户下载文件
245         ‘‘‘ 用户下载文件‘‘‘
246         data=args[0]
247         print(data)
248         if data.get(‘filename‘) is None:#判断文件名不为空
249             self.send_mge(245)
250             return
251 
252         self.request.recv(1) #客户端确认 防粘包
253         file_path=‘%s/%s‘%(self.user_home_dir,data.get(‘filename‘))#拼接文件路径 用户文件路径
254         if os.path.isfile(file_path):#判断文件是否存在
255             file_obj=open(file_path,‘rb‘)#打开文件句柄256             file_size=os.path.getsize(file_path)#获取文件大小
257             if data[‘name_down‘]:
258                 send_size=data[‘size‘]#已经发送数据大小
259                 #self.send_mge(230,data={‘文件大小‘:file_size})#断点续传
260             else:
261                 send_size=0
262                 #self.send_mge(231)#非断点续传
263             #self.request.recv(1) #客户端确认 防粘包
264             file_obj.seek(send_size)#移动到
265             self.send_mge(247,data={‘file_size‘:file_size})#发送相关信息
266             attr=self.request.recv(1024) #客户端确认 防粘包
267             if attr.decode()==‘2‘:return #如果返回是
268             if data.get(‘md5‘):
269                 md5_obj = hashlib.md5()
270                 while send_size<file_size:
271                     line=file_obj.read(1024)
272                 #for line in file_obj:
273                     self.request.send(line)
274                     md5_obj.update(line)
275                 else:
276                     file_obj.close()
277                     md5_val = md5_obj.hexdigest()
278                     self.send_mge(248,{‘md5‘:md5_val})
279                     print("发送完毕.")
280             else:
281                 while send_size<file_size:
282                     line=file_obj.read(1024)
283                 #for line in file_obj:
284                     self.request.send(line)
285                 else:
286                     file_obj.close()
287                     print("发送完毕.")
288             self.request.recv(1) #客户端确认 防粘包
289             info_str=self.log_str(‘下载文件‘)#生成日志信息
290             #user_opert(info_str)#记录日志
291             self.user_opert.critical(info_str)#记录日志
292             return
293 
294     #切换目录
295     def cmd_cd(self,cmd_dict,*args,**kwargs):
296         ‘‘‘切换目录‘‘‘
297         cmd_attr=cmd_dict[‘actionname‘]#获取命令
298         if cmd_attr==‘..‘ or cmd_attr==‘../..‘:
299             if (self.home_dir)==self.user_home_dir:
300                 self.send_mge(251)
301                 return
302             elif cmd_attr==‘../..‘:
303                 self.send_mge(252)#可以切换到上级目录
304                 self.user_home_dir=self.home_dir#绝对目录 = home
305                 self.user_dir=‘/‘
306                 clinet_ack=self.request.recv(1024)#为了去粘包
307                 self.request.send(self.user_dir.encode())#返回相对目录
308                 return
309             else:
310                 self.send_mge(252)#可以切换到上级目录
311                 print(self.user_home_dir)#绝对目录
312                 print(os.path.dirname(self.user_home_dir))#父级目录
313                 self.user_home_dir=os.path.dirname(self.user_home_dir)#父级目录
314                 self.dir_join()#目录拼接切换
315                 clinet_ack=self.request.recv(1024)#为了去粘包
316                 self.request.send(self.user_dir.encode())#返回相对目录
317                 return
318 
319         elif os.path.isdir(self.user_home_dir+‘/‘+cmd_attr):#如果目录存在
320             self.send_mge(252)
321             self.user_home_dir=self.user_home_dir+‘/‘+cmd_attr#目录拼接
322             self.dir_join()#相对目录拼接切换
323             clinet_ack=self.request.recv(1024)#为了去粘包
324             print(clinet_ack.decode())
325             self.request.send(self.user_dir.encode())
326             return
327         else:
328             self.send_mge(256)#目录不存在
329             return
330 
331     #查看目录路径 CD
332     def cmd_pwd(self,cmd_dict):
333         self.request.send(str(len(self.user_dir.encode(‘utf-8‘))).encode(‘utf-8‘))#发送大小
334         clinet_ack=self.request.recv(1024)#为了去粘包
335         self.request.send(self.user_dir.encode())#发送相对路径
336         info_str=self.log_str(‘查看目录路径‘)#生成日志信息
337         #logger.warning
338         self.user_opert.critical(info_str)#记录日志
339         return
340 
341     #修改个信息 磁盘大小
342     def set_info(self,new_size):
343         config_info=configparser.ConfigParser()#读数据
344         config_info.read(config.AUTH_FILE)#读文件 用户名密码
345         print(config_info.options(self.user))
346         config_info.set(self.user,config.QUOTATION,new_size)
347         config_info.write(open(config.AUTH_FILE,‘w‘))
348 
349     #读取个人信息
350     def user_info(self):
351         config_info=configparser.ConfigParser()#读数据
352         config_info.read(config.AUTH_FILE)#读文件 用户名密码
353         print(config_info.options(self.user))
354         pwds=config_info[self.user][config.PWD]#密码
355         Quotation=config_info[self.user][config.QUOTATION]#磁盘配额 剩余
356         user_info={}
357         user_info[‘用户名‘]=self.user
358         user_info[‘密码‘]=pwds
359         user_info[‘剩余磁盘配额‘]=Quotation
360         return user_info,Quotation
361 
362     #查看用户信息
363     def cmd_info(self,*args,**kwargs):
364         attr=self.user_info()
365         info_dict=attr[0]
366         self.request.send(str(len(json.dumps(info_dict))).encode(‘utf-8‘))#
367         clinet_ack=self.request.recv(1024)#为了去粘包
368         self.request.send(json.dumps(info_dict).encode(‘utf-8‘))#发送指令
369         info_str=self.log_str(‘查看用户信息‘)#生成日志信息
370         self.user_opert.critical(info_str)#记录日志
371         return
372 
373     #日志信息生成
374     def log_str(self,msg,**kwargs):
375         info_str=‘用户[%s]进行了[%s]操作‘%(self.user,msg)
376         return info_str
377 
378 
379     #目录查看
380     def cmd_ls(self,*args,**kwargs):
381         data=os.listdir(self.user_home_dir)#查看目录文件
382         print(data)
383         datas=json.dumps(data)#转成json格式
384         self.request.send(str(len(datas.encode(‘utf-8‘))).encode(‘utf-8‘))#发送大小
385         clinet_ack=self.request.recv(1024)#为了去粘包
386         self.request.send(datas.encode(‘utf-8‘))#发送指令
387         info_str=self.log_str(‘目录查看‘)#生成日志信息
388         self.user_opert.critical(info_str)#记录日志
389         return
390     ##单个命令
391     def cmd_compr(self,cmd_dict,**kwargs):
392         attr=cmd_dict[‘actionname‘]#赋于变量
393         if hasattr(self,‘cmd_%s‘%attr):#是否存在
394             func=getattr(self,‘cmd_%s‘%attr)#调用
395             func(cmd_dict)
396             return
397         else:
398             print(‘没有相关命令!‘)
399             self.send_mge(241)
400             return
401 
402     #‘‘‘发送信息码给客户端‘‘‘
403     def send_mge(self,status_code,data=None):
404         ‘‘‘发送信息码给客户端‘‘‘
405         mge={‘status_code‘:status_code,‘status_msg‘:STATUS_CODE[status_code]}#消息
406         if data:#不为空
407             mge.update(data)#提示码进行更新
408         print(mge)
409         self.request.send(json.dumps(mge).encode())#发送给客户端
410 
411     #重写handle方法
412     def handle(self):#重写handle方法
413         while True:
414             #try:
415             self.data=self.request.recv(1024).strip()#接收数据
416             print(‘ip:{}‘.format(self.client_address[0]))#连接的ip
417             print(self.data)
418             self.log_log=log_log()#登陆日志
419             self.user_opert=user_opert()#操作日志
420             if not self.data:
421                 print("[%s]客户端断开了!."%self.user)
422                 info_str=‘用户[%s],退出‘%self.user
423 
424                 break
425             cmd_dict=json.loads(self.data.decode())#接收 数据
426             if cmd_dict.get(‘action‘) is not None:#判断数据格式正确
427                 action=cmd_dict[‘action‘]#文件 头
428                 if hasattr(self,‘cmd_%s‘%action):#是否存在
429                     func=getattr(self,‘cmd_%s‘%action)#调用
430                     func(cmd_dict)
431                 else:
432                     print(‘没有相关命令!‘)
433                     self.send_mge(241)
434             else:
435                 print(‘数据出错!‘)
436                 self.send_mge(240)
437             #except Exception as e:
438              #  print(‘客户端断开了!‘,e)
439               # break
|           |      |- - -logs.py#日志主要逻辑 类
 1 #!usr/bin/env python
 2 #-*-coding:utf-8-*-
 3 # Author calmyan
 4 import os,logging,time
 5 from cfg import config
 6 LOG_LEVEL=config.LOG_LEVEL
 7 
 8 
 9 def log_log():#登陆日志,传入内容
10     logger=logging.getLogger(‘用户成功登陆日志‘)#设置日志模块
11     logger.setLevel(logging.DEBUG)
12     fh=logging.FileHandler(config.USER_LOG,encoding=‘utf-8‘)#写入文件
13     fh.setLevel(config.LOG_LEVEL)#写入信息的级别
14     fh_format=logging.Formatter(‘%(asctime)s %(message)s‘,datefmt=‘%m/%d/%Y %I:%M:%S %p‘)#日志格式
15     fh.setFormatter(fh_format)#关联格式
16     logger.addHandler(fh)#添加日志输出模式
17     #logger.warning(info_str)
18     return logger
19 
20 def user_opert():#用户操作日志,传入内容
21     logger=logging.getLogger(‘用户操作日志‘)#设置日志模块
22     logger.setLevel(logging.CRITICAL)
23     fh=logging.FileHandler(config.USER_OPERT,encoding=‘utf-8‘)#写入文件
24     fh.setLevel(config.LOG_LEVEL)#写入信息的级别
25     fh_format=logging.Formatter(‘%(asctime)s %(message)s‘,datefmt=‘%m/%d/%Y %I:%M:%S %p‘)#日志格式
26     fh.setFormatter(fh_format)#关联格式
27     logger.addHandler(fh)#添加日志输出模式
28     #logger.critical(info_str)
29     return logger
|           |      |- - -main.py#服务端启动主程序
 1 #!usr/bin/env python
 2 #-*-coding:utf-8-*-
 3 # Author calmyan
 4 
 5 import socketserver,os,json,pickle
 6 import os ,sys
 7 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))#获取相对路径转为绝对路径赋于变量
 8 sys.path.append(BASE_DIR)#增加环境变量
 9 from cfg import config
10 
11 
12 from  core.ftp_server import  MyTCPHandler
13 
14 import optparse
15 class ArvgHandler(object):
16     def __init__(self):#   可  传入系统参数
17         self.paresr=optparse.OptionParser()#启用模块
18         #self.paresr.add_option(‘-s‘,‘--host‘,dest=‘host‘,help=‘服务绑定地址‘)
19         #self.paresr.add_option(‘-s‘,‘--port‘,dest=‘host‘,help=‘服务端口‘)
20         (options,args)=self.paresr.parse_args()#返回一个字典与列表的元组
21 
22         self.verufy_args(options,args)#进行校验
23     def verufy_args(self,options,args):
24         ‘‘‘校验与调用‘‘‘
25         if hasattr(self,args[0]):#反射判断参数
26             func=getattr(self,args[0])#生成一个实例
27             func()#开始调用
28         else:
29             self.paresr.print_help()#打印帮助文档
30     def start(self):
31         print(‘服务启动中....‘)
32         s=socketserver.ThreadingTCPServer((config.HOST,config.PORT),MyTCPHandler)#实例化一个服务端对象
33         s.serve_forever()#运行服务器
34         print(‘服务关闭‘)










































































以上是关于(转)python高级FTP的主要内容,如果未能解决你的问题,请参考以下文章

转 Python爬虫入门四之Urllib库的高级用法

Python机器学习从入门到高级:带你玩转特征转换(含详细代码)

展讯NAND Flash高级教程转

正则(高级)(转)

强烈推荐这款神器,把网站转pdf还能编程高级定制!建议收藏

R语言系列3:高级数据管理