首頁  >  問答  >  主體

解析'pip install'命令以取得已安裝軟體包文字中的範圍

我正在進行一個項目,需要我提取使用pip install命令安裝的Python套件的名稱和位置。

一個網頁包含一個code元素,其中包含多行文字和bash命令。我想編寫一個JS程式碼,可以解析這個文字並找到套件和它們在文字中的位置。

例如,如果文字是:

$ pip install numpy
pip install --global-option build_ext -t ../ pandas>=1.0.0,<2
sudo apt update
pip uninstall numpy
pip install "requests==12.2.2"

我想得到類似這樣的結果:

[
    {
        "name": "numpy",
        "position": 14
    },
    {
        "name": "pandas",
        "position": 65
    },
    {
        "name": "requests",
        "position": 131
    }
]

我該如何在JavaScript中實作這個功能?

P粉431220279P粉431220279408 天前530

全部回覆(2)我來回復

  • P粉773659687

    P粉7736596872023-09-08 15:53:55

    您可以在此答案中查看我解釋的程式碼。

    這裡還有另一種類似的解決方案,更基於正規表示式:

    const pipOptionsWithArg = [
      '-c',
      '--constraint',
      '-e',
      '--editable',
      '-t',
      '--target',
      '--platform',
      '--python-version',
      '--implementation',
      '--abi',
      '--root',
      '--prefix',
      '-b',
      '--build',
      '--src',
      '--upgrade-strategy',
      '--install-option',
      '--global-option',
      '--no-binary',
      '--only-binary',
      '--progress-bar',
      '-i',
      '--index-url',
      '--extra-index-url',
      '-f',
      '--find-links',
      '--log',
      '--proxy',
      '--retires',
      '--timeout',
      '--exists-action',
      '--trusted-host',
      '--cert',
      '--client-cert',
      '--cache-dir',
    ];
    const optionWithArgRegex = `( (${pipOptionsWithArg.join('|')})(=| )\S+)*`;
    const options = /( -[-\w=]+)*/;
    const packageArea = /["']?(?<package_part>(?<package_name>\w[\w.-]*)([=<>~!]=?[\w.,<>]+)?)["']?(?=\s|$)/g;
    const repeatedPackages = `(?<packages>( ${packageArea.source})+)`;
    const whiteSpace = / +/;
    const PIP_COMMAND_REGEX = new RegExp(
      `(?<command>pip install${optionWithArgRegex}${options.source})${repeatedPackages}`.replaceAll(' ', whiteSpace.source),
      'g'
    );
    export const parseCommand = (command) => {
      const matches = Array.from(command.matchAll(PIP_COMMAND_REGEX));
    
      const results = matches.flatMap((match) => {
        const packagesStr = match?.groups.packages;
        if (!packagesStr) return [];
    
        const packagesIndex = command.indexOf(packagesStr, match.index + match.groups.command.length);
    
        return Array.from(packagesStr.matchAll(packageArea))
          .map((packageMatch) => {
            const packagePart = packageMatch.groups.package_part;
            const name = packageMatch.groups.package_name;
    
            const startIndex = packagesIndex + packagesStr.indexOf(packagePart, packageMatch.index);
            const endIndex = startIndex + packagePart.length;
    
            return {
              type: 'pypi',
              name,
              version: undefined,
              startIndex,
              endIndex,
            };
          })
          .filter((result) => result.name !== 'requirements.txt');
      });
    
      return results;
    };
    

    回覆
    0
  • P粉194541072

    P粉1945410722023-09-08 10:50:01

    這裡是一個可選的解決方案,嘗試使用循環而不是正規表示式:

    思路是找到包含 pip install 文字的行,這些行是我們感興趣的行。然後,將命令分解成單詞,並在它們上進行循環,直到達到命令的包部分。

    首先,我們將定義一個用於套件的正規表示式。請記住,一個套件可以是像 pip install 'stevedore>=1.3.0,<1.4.0' "MySQL_python==1.2.2" 這樣的東西:

    const packageArea = /(?<=\s|^)["']?(?<package_part>(?<package_name>\w[\w.-]*)([=<>~!]=?[\w.,<>]+)?)["']?(?=\s|$)/;
    

    注意到命名分組package_part 用於識別「帶版本的套件」字串,而package_name 用於提取包名。


    關於參數

    我們有兩種類型的命令列參數:選項標誌

    選項的問題在於我們需要理解下一個單字不是包名,而是 選項 值。

    所以,我先列出了 pip install 指令中的所有選項:

    const pipOptionsWithArg = [
      '-c',
      '--constraint',
      '-e',
      '--editable',
      '-t',
      '--target',
      '--platform',
      '--python-version',
      '--implementation',
      '--abi',
      '--root',
      '--prefix',
      '-b',
      '--build',
      '--src',
      '--upgrade-strategy',
      '--install-option',
      '--global-option',
      '--no-binary',
      '--only-binary',
      '--progress-bar',
      '-i',
      '--index-url',
      '--extra-index-url',
      '-f',
      '--find-links',
      '--log',
      '--proxy',
      '--retires',
      '--timeout',
      '--exists-action',
      '--trusted-host',
      '--cert',
      '--client-cert',
      '--cache-dir',
    ];
    

    然後我編寫了一個稍後將使用的函數,用於在看到一個參數時決定要做什麼:

    const handleArgument = (argument, restCommandWords) => {
      let index = 0;
      index += argument.length + 1; // +1 是为了去掉 split 时的空格
    
      if (argument === '-r' || argument === '--requirement') {
        while (restCommandWords.length > 0) {
          index += restCommandWords.shift().length + 1;
        }
        return index;
      }
    
      if (!pipOptionsWithArg.includes(argument)) {
        return index;
      }
    
      if (argument.includes('=')) return index;
    
      index += restCommandWords.shift().length + 1;
      return index;
    };
    

    這個函數接收了辨識出的參數和指令的其餘部分,分割成單字。

    (在這裡你開始看到「索引計數器」。由於我們還需要找到每個發現的位置,我們需要追蹤原始文字中的目前位置)。

    在函數的最後幾行中,你可以看到我處理了 --option=something--option something 兩種情況。


    解析器

    現在主解析器將原始文字分割成行,然後再分割成單字。

    每個操作都必須更新全域索引,以追蹤我們在文字中的位置,並且這個索引幫助我們在文字中搜尋和查找,而不會陷入錯誤的子字串中,使用indexOf(str, counterIndex)

    export const parseCommand = (multilineCommand) => {
      const packages = [];
      let counterIndex = 0;
    
      const lines = multilineCommand.split('\n');
      while (lines.length > 0) {
        const line = lines.shift();
    
        const pipInstallMatch = line.match(/pip +install/);
        if (!pipInstallMatch) {
          counterIndex += line.length + 1; // +1 是为了换行符
          continue;
        }
    
        const pipInstallLength = pipInstallMatch.index + pipInstallMatch[0].length;
        const argsAndPackagesWords = line.slice(pipInstallLength).split(' ');
        counterIndex += pipInstallLength;
    
        while (argsAndPackagesWords.length > 0) {
          const word = argsAndPackagesWords.shift();
    
          if (!word) {
            counterIndex++;
            continue;
          }
    
          if (word.startsWith('-')) {
            counterIndex += handleArgument(word, argsAndPackagesWords);
            continue;
          }
    
          const packageMatch = word.match(packageArea);
          if (!packageMatch) {
            counterIndex += word.length + 1;
            continue;
          }
    
          const startIndex = multilineCommand.indexOf(packageMatch.groups.package_part, counterIndex);
          packages.push({
            type: 'pypi',
            name: packageMatch.groups.package_name,
            version: undefined,
            startIndex,
            endIndex: startIndex + packageMatch.groups.package_part.length,
          });
    
          counterIndex += word.length + 1;
        }
      }
    
      return packages;
    };
    

    回覆
    0
  • 取消回覆