我正在進行一個項目,需要我提取使用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粉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; };
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; };