Posts Python正则表达式实战——WSL与Windows路径(绝对路径)的相互转换
Post
Cancel

Python正则表达式实战——WSL与Windows路径(绝对路径)的相互转换

一、 前言

在前面的文章中, 介绍了python正则表达式的用法, 这次利用正则表达式实现WSL与Windows路径(绝对路径)的相互转换—-对于经常在WSL环境中开发的我来说, 是一个很有用的工具, 所以我把它写成一个独立的模块, 以便日后使用

文章中的代码不一定是最完美的, 后续的更新会同步上传到Github: develop-tools-python

二、 正篇

2.1 问题分析

总体的实现思路就是: 使用正则表达式, 对传入的路径进行匹配, 捕获关键元素 (盘符、目录名称等), 依据它们进行另一种格式路径的重构

要做两种路径的相互转化, 就要了解两种路径的组成规则分别是什么

  • Linux中各层级用斜杠/分隔, 单个斜杠/表示根目录, 文件名和目录名中不允许出现斜杠/
  • Windows的本地路径以盘符和:\开头, 用反斜杠\分隔各个层级, 文件名和目录中不允许出现\/:*?"<>|字符

既然是wsl和windows的路径转换, 那必须再加一条条件:

  • WSL路径以/mnt/[a-zA-Z]/开头, 其中[a-zA-Z]表示可能的Windows盘符

除此之外, 还有一些细节问题, 比如

  • 这个路径是目录还是文件
  • 路径以分隔符(即/\)结尾, 还是以名称结尾

2.2 验证输入合法性的正则表达式

拿到用户的输入之后, 首先需要验证输入的合法性, 所以路径整体的正则表达式是不可或缺的

1
2
3
4
5
# WSL共享目录的合法路径
wsl_re = r'^/mnt/([a-z])/([^/]+/)*[^/]+/?$'	

# windows本地磁盘驱动器合法路径的正则表达式
win_re = r'^([a-zA-Z]):\\([^\\/:*?"<>|\r\n]+\\?)*$'

对于WSL路径:

  • 开头的^和结尾的$表示匹配开头和结尾
  • /mnt/([a-z])/是共享路径开头到盘符的部分, 如/mnt/c/
  • ([^/]+/)*是中间的部分, [^/]+表示对除/以外的所有符号做一次或无数次匹配, 比如workspace这样的字符串; 后面跟/、整体包*表示对类似于workspace/这样的字符串匹配0次或无数次, 即路径的中间部分
  • [^/]+/?结尾部分与中间部分相似, 除了去掉数量词*之外, 结尾的/也是(匹配零次或一次)可选的——文件路径的结尾必没有/, 而路径的结尾可以没有/

Windows路径也是同理, 注意Windows禁用的符号比较多, 还有上面的例子把后两个 部分合为一体(因为正则匹配默认为贪婪模式)

验证之后就需要提取所需的元素了, 我们将需要提取的元素分为三种: 盘符中间目录文件名, 由于用户可能输入目录的路径, 所以文件名是可选的

2.3 提取盘符

提取盘符是最容易的一项:只需在上文中、验证合法性的正则表达式的相应位置添加捕获组即可

上文中已经在盘符位置添加了捕获组

1
2
# 使用match & group提取盘符 (WSL)
drive_letter = wsl_re_c.match(abs_linux_path).group(1)

注意:group(0)是整个匹配结果, group(1)才是第一个捕获组

2.4 提取中间目录(和文件名)

对于中间目录和文件名, 我们可能还想使用现成的正则表达式, 于是给中间部分加上小括号试图捕获

事实证明这个小算盘是失败的: 我们使用了数量词, 试图捕获多个这样的组, 实际上程序只看到了这个位置上只有一个捕获组, 所以它只会捕获到最后一个中间目录(或文件名), 前面的中间目录(或文件名)会被覆盖掉

我们只好重写一组正则表达式, 用来提取中间目录(和文件名)的部分

1
2
3
4
5
6
7
# 路径中盘符右边的部分 (WSL)
# 如对于'mnt/c/workspace/python/', 右边的部分是'/workspace/python/'
wsl_re_right = r'/([^/]+)'

# 路径中盘符右边的部分 (Windows)
# 如对于'C:\workspace\python', 右边的部分是'workspace\python'
win_re_right = r'([^\\/:*?"<>|\r\n]+)\\?'

使用findall()进行正则匹配, 返回匹配结果都会根据捕获组的捕获范围进行调整

举个例子: 对于’/workspace/python/’进行findall()匹配, 匹配到的是/workspace/python, 但由于只捕获括号内的部分, findall()返回的结果为['workspace', 'python'], 即中间目录(和文件名)的部分

1
2
3
# 使用findall捕获盘符下目录层次
# 如对于'/mnt/c/workspace/python/', 捕获到的是['workspace','python']
result_right = wsl_re_right_c.findall(abs_linux_path[6:])

通过指定字符串的下标, 我们可以将匹配范围限定在子串内: 比如’/mnt/c/workspace/python/’限定[6:]后的子串为’/workspace/python/’, 无论是Linux还是Windows, 盘符及其以前的字符串长度都是一致的, 所以我们可以使用这种方法方便地提取后半部分的子串

2.5 是目录还是文件?

我们还需要解决的一个问题是: 当用户传进一个文件时, 往往希望可以获得文件名和文件目录的路径, 以便其进行重命名等操作. 这时我们就需要将文件名和路径分开后返回, 给用户更大的发挥空间

1
return win_path, filename

诚然, 我们拥有ps.path.split()这样方便的函数帮助我们提取文件名, 但是这个函数的具体表现依赖于运行环境——在Linux下只能用于Linux路径, 在Windows下只能用于Windows路径, 这就导致了: 如果将这一环节交给用户, 很可能会因错误使用split而发生意料之外的错误

这里有一个”是目录还是文件?”的问题, 如果路径以/或者\结尾, 我们可以断言它一定是目录, 但请看下面这种情况

1
/mnt/c/workspace/python 

我们可以将其看成以下两种情况

  • python是一个目录
  • python是一个文件(即使其没有拓展名)

它们几乎是无法辨别的, 所以我们不得不把这个问题交给用户回答——在传入参数的时候需要选择模式

1
2
3
4
5
FOLDER_MODE = 0
FILE_MODE = 1

def abs_wsl2win(abs_linux_path: str, mode: int):
def abs_win2wsl(abs_win_path: str, mode: int):

为了保证返回形式的一致性, 所有都返回两个值, 但只有是文件模式时第二个值才有意义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if mode == FOLDER_MODE:	
    # 文件夹模式
    for node in result_right:
        win_path += ('\\' + node)
    # 结尾与输入保持一致
    if abs_linux_path.endswith('/'):
        win_path += '\\'
    return win_path, None
elif mode == FILE_MODE:
    # 文件模式
    # 将文件名排除路径
    for node in result_right[:-1]:
        win_path += ('\\' + node)
    # 结尾为'\'
    win_path += '\\'
    # 提取文件名
    filename = result_right[-1]
    return win_path, filename
else:
    # 模式错误
    return None, None

用户在以文件夹模式/文件模式调用的时候可以这样做

1
2
3
import path_convertor as pc
path, throw = pc.abs_wsl2win('/mnt/c/workspace/python', pc.FOLDER_MODE)
path, filename = pc.abs_wsl2win('/mnt/c/workspace/python', pc.FILE_MODE)

throw只是用来占位, 它会在文件夹模式下被赋予None, 用户可以直接丢掉它的值

2.6 使用示例

代码1:

1
2
3
4
import path_convertor as pc

path, filename = pc.abs_win2wsl(r'C:\games\galgames\senrenbanka.exe', pc.FILE_MODE)
print(path); print(filename)

输出1:

1
2
/mnt/c/games/galgames/
senrenbanka.exe


代码2:

1
2
3
4
import path_convertor as pc

path, throw = pc.abs_win2wsl(r'C:\games\galgames', pc.FOLDER_MODE)
print(path); print(throw)

输出2:

1
2
/mnt/c/games/galgames
None


代码3:

1
2
3
4
import path_convertor as pc

path, throw = pc.abs_wsl2win('/mnt/c/games/galgames/', pc.FOLDER_MODE)
print(path); print(throw)

输出3:

1
2
C:\games\galgames\
None

三、 参考资料

  • David Beazley & Brian K.Jones - Python Cookbook (3rd edition)
This post is licensed under CC BY 4.0 by the author.

Python正则表达式

为jekyll快速添加文件的Python脚本