Brick_Porter 发表于 2022-8-15 09:20:38

导出指定第三方库所需依赖的模块

本帖最后由 Brick_Porter 于 2022-8-18 10:07 编辑

虽然使用Python提供的 pip freeze > <file> 命令可以将所有已安装的第三方库和所需依赖库导出到指定文件中,但该命令无法导出单一第三方库所需依赖。
为解决这个问题,笔者编写了一段小程序以递归的方式搜索某个特定的第三方库及其依赖,然后将搜索结果导出到文件中。文件的每一行就是一个库,并且带有版本信息。

笔者编写这个程序的主要目的是为了把第三方库卸载干净,因为 pip uninstall <lib> 命令只会把传给它的第三方库卸载掉而不会一并卸载其依赖库,如此一来便导致了残留。
使用这个脚本可以方便地导出某个第三方库及其所需的其他库到一个名为 requires.txt 的文件中。有了这个文件可以:
1. 使用 pip uninstall -r requires.txt -y 命令方便地批量卸载库,而且没有残留{:5_95:}
2. 使用 pip install -r requires.txt 命令批量安装库

针对第二点的补充说明:
开发过程中可能遇到这样的情况:自己编写的代码需要第三方库,同时还分享了自己的代码(如,在Github或者Gitee中公开自己的代码)。其他人不知道所需依赖的情况直接下载并运行我们的代码会因为缺少必要的依赖而报错,为了解决这个问题一般都会在自己的项目目录中包含一个名为requires.txt的文件,该文件列举了本项目所需的所有第三方库。通过requires.txt这个文件告知下载代码的人,需要提前安装指定的依赖库否则代码无法运行。

针对上述情况,不论使用笔者的程序还是 pip freeze 命令都可以顺利解决问题。不过如果你没有使用虚拟环境隔离项目,笔者的程序与 pip freeze 命令就各有优劣了:前者需要手动输入每个依赖库的库名,如果依赖较多还会运行缓慢;后者虽然快捷但是会把其他项目的依赖也一并导出造成冗余。笔者建议多项目开发时最好使用虚拟环境隔离各个项目{:10_256:}

除了上面说到的情况外,还有一种微妙的情况。我们使用的是较老版本的第三方库,而 pip install 命令默认安装最新版本,如果要安装指定版本的第三方库则需了解其是否依赖其他库以及依赖库的版本情况,然后按照先安装依赖库后安装主要库的顺序依次安装,这样做显然很复杂。为了解决这个问题,笔者特地把搜索结果进行了逆序,先安装依赖库,最后安装主要的第三方库,而且每个库都带有版本信息,所以避免了自动安装最新版的问题。

请注意运行程序需要Python 3.8及以上版本,另外由于附件不支持直接上传源文件所以打包为压缩文件,解压后就是源码文件,最好使用源文件。
使用说明见源码开头的文档。

最后附上完整代码以供参考:
"""一个用于列出某个第三方库所需依赖的模块。

前置条件:
Python 3.8+

实现思路:
1. 借助 pip show <library> 命令展示第三方库的详细信息;
2. 从上述命令的输出中提取出库名、版本、依赖库信息;
3. 对依赖库进行递归,直到无依赖为止。

补充说明:
执行pip show命令需要借助subprocess.run函数,它最占用运行时间,
此外程序还使用了递归操作,进行类似深度优先的搜索,
所以如果第三方库的依赖较多程序的运行时间就会很长。

本模块提供的主要函数为requires_to_file,它接收第三方库名作为参数,
无返回值,但是会把所有依赖包括第三方库自身写入一个名为requires.txt的文件中。
该函数的具体用法请使用help(requires_to_file)查看。

使用方法:
1. 导入模块
>>> from export_requires import requires_to_file
>>> requires_to_file('requests')

2. 命令行
$ python export_requires.py -l='requests'

$ python export_requires.py --lib='requests'
"""

import optparse
import re
import subprocess
from collections import OrderedDict

name_regex = re.compile(r'(?<=Name:\s).*?(?=\r?\n)')
version_regex = re.compile(r'(?<=Version:\s).*?(?=\r?\n)')
requires_regex = re.compile(r'(?<=Requires:\s).*?(?=\r?\n)')


class _Lib:
    """_Lib类表示一个第三方库。实例对象包含库名、版本、依赖三项信息。"""

    def __init__(self, s: str) -> None:
      """从命令行输出中提取第三方库的信息然后创建一个第三方库对象。

      :param s: str, 命令行输出
      :returns: None
      """
      self.name = self.__extract_info(s, name_regex)
      self.version = self.__extract_info(s, version_regex)
      self.requires = self.__extract_info(s, requires_regex)

    def __str__(self) -> str:
      """第三方库的统一输出格式为:库名==库版本"""
      return f'{self.name}=={self.version}'

    __repr__ = __str__# f字符串需要用到__repr__函数

    @staticmethod
    def __extract_info(s: str, regex: re.Pattern) -> str:
      """extract_info方法使用正则表达式从字符串s中提取信息。

      :param s: str, 标准输出
      :returns: str, 提取出的信息
      """
      result = regex.search(s)
      if result:
            return result.group()
      return ''


def list_libraries(lib: str) -> OrderedDict:
    """list_libraries函数使用命令`pip show`以有序字典的形式返回指定库的所有依赖。
    字典的键是依赖库的名字,值是_Lib对象。函数使用subprocess.PIPE捕获`pip show`命令的输出,
    然后对输出进行分析获取所需依赖。为避免依赖库重复,所有依赖都以字典形式保存从而实现去重效果。
    之所以保留顺序是为了方便安装第三方库。

    :param lib: str, 待分析依赖的第三方库
    :returns: OrderedDict, 一个字典,其中的每个值都是所需依赖
    """
    command = f'pip show {lib}'
    result = subprocess.run(command, stdout=subprocess.PIPE)
    try:
      output = result.stdout.decode('gbk')
    except UnicodeDecodeError:
      output = result.stdout.decode('utf-8')

    library = _Lib(output)
    dependencies = OrderedDict()
    dependencies = library# 键值对 库名(str): 库对象(_Lib)
    if not library.requires:# 空字符串表示无依赖
      return dependencies

    # 分割字符串为子字符串,每个字符串都是一个依赖库名
    requires = library.requires.split(', ')
    for i in requires:
      for k, v in list_libraries(i).items():
            if k not in dependencies:
                dependencies = v
    return dependencies


def requires_to_file(lib: str) -> None:
    """requires_to_file函数分析指定第三方库的所有依赖,然后将这些依赖
    写入一个名为requires.txt的文件中。

    使用requires.txt文件可以方便地安装或卸载第三方库,例如:
    1. 批量安装:pip install -r requires.txt
    2. 批量卸载:pip uninstall -r requires.txt -y
    注意,-y用于跳过确认,也就是直接卸载,如果不提供-y则需要手动确认卸载

    :param lib: str, 待分析依赖的第三方库
    :returns: None
    """
    # 下列代码对字典的键进行反序,这样做是为了方便安装第三方库时
    # 优先把依赖库安装好。特殊情况下可能需要较老版本的依赖库,
    # 反序后先安装指定版本的依赖库,避免安装最新版
    libs = (str(x) for x in reversed(list_libraries(lib).values()) if x.name)
    with open('requires.txt', 'w', encoding='utf-8') as f:
      f.write('\n'.join(libs))
    print('完成')


def cmd() -> None:
    """cmd函数用于解析命令行参数,然后执行相应操作。"""
    parser = optparse.OptionParser()
    parser.add_option('-l', '--lib', help='待分析的第三方库名')
    options, _ = parser.parse_args()
    if options.lib:
      requires_to_file(options.lib)
    else:
      print('未提供库名')


__all__ = ['requires_to_file']


if __name__ == "__main__":
    cmd()

2022-08-15更新,修改代码拼写问题及文档字符串不一致问题。

2022-08-18更新,解决库名相同但仍重复递归的BUG,附件已更新。
页: [1]
查看完整版本: 导出指定第三方库所需依赖的模块