解决python fs(filesystem)库在连接FTP服务器时无法显示文件目录的问题

本文由Markdown语法编辑器编辑完成.

1. 问题提出:

目前有一个需求是:医院的影像数据是存储在FTP服务器上的,医院提供了连接该FTP服务器的host, user, password等参数.(注:该ftp服务是部署在windows操作系统的IIR服务上)
采用的python库是fs(filesystem2)的第三方库.fs==2.0.23版本.
在根据提供的参数,可以正常的连接到该ftp服务器,但是在显示该ftp服务器下的文件目录时,却显示为空.但是,该ftp服务器下的确是有文件夹的.

之前在连接别的医院的ftp服务器时,是可以正常显示ftp服务器下的文件目录的.

通过跟踪代码,发现是由于fs库的内部的一个函数实现,在解析服务器返回的字符串时,返回为空,而导致后续无法检索到文件夹.

定位到的函数是:fs.ftpfs下的_ftp_parse()函数.
IIR服务器返回的lines是:

[
  '01-20-19 04:23AM < DIR > 104794_CT',
  '01-20-19 04:22AM < DIR > 104794_SR',
   '01-19-19 07:45AM < DIR > 104795_CT',
   '01-19-19 07:43AM < DIR > 104795_SR',
   '01-15-19 07:49AM < DIR > 104796_CT',
   '01-14-19 07:47AM < DIR > 104796_SR',
   '01-13-19 08:44AM < DIR > 104797_CT'
 ]

在运行:

_list = [Info(raw_info) for raw_info in ftp_parse.parse(lines)]

后,_list为[], 因此后续无法检索到图像.

2. 问题分析:

要想解决这个问题,需要对比之前可以正常显示ftp服务器下的文件目录和这里的区别.
通过debug代码发现,当ftp服务是部署在linux服务器时,程序运行到上面的位置时,lines的值的格式为:

[
  u'drwxr-xr-x    2 ftp      ftp         69632 Jan 30 03:51 3177498', 
  u'drwxr-xr-x    2 ftp      ftp         73728 Jan 30 03:51 3177512', 
  u'drwxr-xr-x    2 ftp      ftp         77824 Jan 30 03:52 3177517', 
  u'drwxr-xr-x    2 ftp      ftp         73728 Jan 30 03:52 3177529'
]

而fs中解析这个lines的代码为_ftp_parse.py:

re_linux = re.compile(
    """
    ^
    ([ldrwx-]{10})
    \s+?
    (\d+)
    \s+?
    ([\w\-]+)
    \s+?
    ([\w\-]+)
    \s+?
    (\d+)
    \s+?
    (\w{3}\s+\d{1,2}\s+[\w:]+)
    \s+
    (.*?)
    $
    """,
    re.VERBOSE
)

def get_decoders():
    decoders = [
        (re_linux, decode_linux)
    ]
    return decoders


def parse(lines):
    info = []
    for line in lines:
        if not line.strip():
            continue
        raw_info = parse_line(line)
        if raw_info is not None:
            info.append(raw_info)
    return info


def parse_line(line):
    for line_re, decode_callable in get_decoders():
        match = line_re.match(line)
        if match is not None:
            return decode_callable(line, match)
    return None

def decode_linux(line, match):
    perms, links, uid, gid, size, mtime, name = match.groups()
    is_link = perms.startswith('l')
    is_dir = perms.startswith('d') or is_link
    if is_link:
        name, _, _link_name = name.partition('->')
        name = name.strip()
        _link_name = _link_name.strip()
    permissions = Permissions.parse(perms[1:])

    mtime_epoch = _parse_time(mtime)

    name = unicodedata.normalize('NFC', name)

    raw_info = {
        "basic": {
            "name": name,
            "is_dir": is_dir
        },
        "details": {
            "size": int(size),
            "type": int(
                ResourceType.directory
                if is_dir else
                ResourceType.file
            )
        },
        "access": {
            "permissions": permissions.dump()
        },
        "ftp": {
            "ls": line
        }
    }
    access = raw_info['access']
    details = raw_info['details']
    if mtime_epoch is not None:
        details['modified'] = mtime_epoch

    access['user'] = uid
    access['group'] = gid

    return raw_info

从fs的源码中可以看出,_ftp_parse.py中的get_decoders()函数中,decoders这个变量数组中,只提供了一组解码器,即内部的 (re_linux, decode_linux). 因此,当ftp服务器不是部署在linux操作系统上,而是部署在windows等其他操作系统上时,ftp返回的字符串格式,就不再符合解码器中的正则表达式.

因此,要修改这个问题,就需要根据当前IIR服务器返回的字符串的格式,撰写相应的正则表达式来解码,然后加入到_ftp_parse.py中的decoders数组中.

要解决这个问题,就需要对python中的正则表达式有一定的了解.相关的教程可以阅读:正则表达式.
https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386832260566c26442c671fa489ebc6fe85badda25cd000

3. 解决方案:

在了解了正则表达式的规范后,模拟re_linux和decode_linux的函数,可以增加re_windows和decode_windows这两个函数.增加后的函数如下:

re_windows = re.compile(
    """
    ^
    ([\w-]{8}\s+[\w:]+)
    \s+?
    ()
    \s+?
    (.*?)
    $
    """,
    re.VERBOSE
)

def decode_windows(line, match):
    mtime, dir, name = match.groups()
    name = name.strip()
    raw_info = {
        "basic": {
            "name": name,
            "is_dir": True
        },
        "ftp": {
            "ls": line
        }
    }
    return raw_info

def get_decoders():
    decoders = [
        (re_linux, decode_linux),
        #这个解码组合是新增的,针对windows操作系统的解码器.
        (re_windows, decode_windows)
    ]
    return decoders

当然,这里的正则表达式是根据之前返回的字符串来撰写的.如果返回的字符串和文章中开头的字符串样式不同,还需要另外修改正则表达式.

备注(2019.02.20新增):

虽然通过自己撰写正则表达式,解决了fs不支持windows系统的FTP服务器的问题,但是总感觉实现方式还不够优雅,也不知道后续还会遇到什么样的FTP系统.

今天正准备将自己修改后的库,重新打一个包,然后上传到公司的离线安装包管理器时,无意中发现了fs的库,早已经进行了多次版本的升级.而最新的fs的版本已经升级到了2.4.0,是2019年2月15日更新的.而我们之前出问题的那个版本,还是2018年5月份发布的版本2.0.23版本.而通过查看2.4.0的源码,发现已经解决了之前的这个问题,而且写的代码更优雅.

因此,原来遇到的那个问题,只要升级一下原来的版本,从2.0.23升级到2.4.0,就迎刃而解了.这也应验了我之前领导曾经跟我说过的一句话,"你别以为你遇到的这个问题,是世界上第一个遇到的.一定已经有很多人遇到了.

虽然,这次花费了很大的功夫才解决了这个问题,而没有想到首先去查看这个库是否有最新的更新,也没有去查看这个库的wiki等.但是,也还是有一点小成就感的.因为,原来总觉得第三方库的源代码是神圣不可侵犯的,不能随便修改第三方库的源码.总觉得人家已经是实现最好的了.但是,当通过修改第三方库的源码,而解决了真实情况中的问题时,才觉得,第三方库的源代码也是人写的嘛.只要理会了源代码的逻辑,是可以根据实际情况,进行更改的呀!

以下是fs 2.4.0版本的_ftp_parse.py源代码中,关于以上问题的解决方案的代码:

RE_WINDOWSNT = re.compile(
    r"""
    ^
    (?P.*(AM|PM))
    \s*
    (?P(|\d*))
    \s*
    (?P.*)
    $
    """,
    re.VERBOSE)

def get_decoders():
    """
    Returns all available FTP LIST line decoders with their matching regexes.
    """
    decoders = [
        (RE_LINUX, decode_linux),
        (RE_WINDOWSNT, decode_windowsnt),
    ]
    return decoders

def decode_windowsnt(line, match):
    """
    Decodes a Windows NT FTP LIST line like these two:

    `11-02-18  02:12PM                 images`
    `11-02-18  03:33PM                 9276 logo.gif`
    """
    is_dir = match.group("size") == ""

    raw_info = {
        "basic": {
            "name": match.group("name"),
            "is_dir": is_dir,
        },
        "details": {
            "type": int(ResourceType.directory if is_dir else ResourceType.file),
        },
        "ftp": {"ls": line},
    }

    if not is_dir:
        raw_info["details"]["size"] = int(match.group("size"))

    modified = _parse_time(match.group("modified"), formats=["%d-%m-%y %I:%M%p"])
    if modified is not None:
        raw_info["details"]["modified"] = modified

    return raw_info

你可能感兴趣的:(技术问题和解决)