将每个包含多个工作表的多个 Excel 工作簿转换为 CSV:如何提取由公式构造的 url?

Convert multiple Excel workbooks each with multiple worksheets to CSV: How do I extract a url that is constructed by a formula?

我正在编写 PowerShell 脚本以将多工作表 Excel xlsx 工作簿的集合转换为单个 csv 文件。我想要获取的内容之一是由 HYPERLINK 公式创建的 hyperlink 的计算文本。例如,一个单元格包含 =HYPERLINK(CONCATENATE("http://foo/bar.aspx?pid=",A2),"Click Here")

我可以使用 $currentCell = $sheet.Cells.Item($r, $c) 获取单元格。我可以使用 $currentCell.Text 获取 link 文本 Click Here 我可以通过测试 $currentCell.HasFormula 来检测单元格是否有公式。我可以使用 $currentCell.Formula 获取公式并使用正则表达式解析它以检测它是否包含 HYPERLINK 公式。但是,我想要的是获得执行公式的结果。我可以使用 $currentCell.Calculate() 执行公式,但我不知道如何获取结果(当我将 $currentCell.Calculate() 的结果分配给变量时,变量最终作为 System.DBNull).

如何以编程方式获取单元格 Calculate 方法的结果?

更新

在思考了 Benoît Mayer 的回答后,我意识到我不明白我自己问题的基础。我试图概括包含公式的单元格的处理,但这行不通。正在计算单元格的公式,即,当我提取单元格的文本(具有 HYPERLINK 和 CONCATENATE 公式的单元格)时,我得到 Click Here 这是执行公式的结果(例如 =HYPERLINK(CONCATENATE("http://foo/bar.aspx?pid=",A2),"Click Here") ).我需要检测和解析 HYPERLINK 和 CONCATENATE 公式,并使用 Benoit 描述的方法。

这是我的代码。它转换多个 Excel 工作簿,每个工作簿都有多个工作表,并提取我需要处理的工作表中特定公式的结果。请参阅第 136 和 145 行之后的代码。

**代码。 5 月 7 日更新,修复了错误并提供了检测特定公式数据并从中提取数据的代码 **

cls

#region Functions

Function Remove-WhiteSpaceFromNonQuoted($inString)
{

    $quoted = $false
    $newString = ""

    for ($i = 0; $i -lt $inString.Length; $i++)
    {
        if ($inString[$i] -eq "`"")
        {
            $quoted = $quoted -xor $true
        }

        if (($inString[$i] -match "\S" -and !$quoted) -or ($quoted))
        {
            $newString = $newString + $inString[$i] 
        }
    }

    return $newString
}

#endregion

$sortedFieldNameList = New-Object -TypeName System.Collections.SortedList

$fqBookNames = New-Object -TypeName System.Collections.SortedList

$fqBookNames.Add("C:\foo\bar1.xlsx", "")
$fqBookNames.Add("C:\foo\bar2.xlsx", "")
$fqBookNames.Add("C:\foo\barN.xlsx", "")

$global:workBook = $null
$global:excel =  $null

try
{   

    $global:excel = New-Object -Com Excel.Application
    $global:excel.Visible = $false

    write-host ("Scan for column names")

    #Scan all sheets in all books and create an object with all the column names encountered 
    foreach ($fqBookName in $fqBookNames.Keys)
    {
        $global:workBook = $global:excel.Workbooks.Open($fqBookName)

        foreach ($sheet in $global:workBook.Sheets)
        {
            $columnIndexMax = $sheet.UsedRange.Column + $sheet.UsedRange.Columns.Count - 1
            write-host ("Workbook=" + $global:workBook.Name + ". Sheet=" + $sheet.Name)
            $rowOne = $sheet.Rows(1)

            for ($columnIndex = 1; $columnIndex -le $columnIndexMax; $columnIndex++)
            {
                $columnName = $rowOne.Cells($columnIndex).Text.Trim().ToUpper()

                if ($columnName.Length -gt 0)
                {
                    if (!$sortedFieldNameList.ContainsKey($columnName)) 
                    {
                        $sortedFieldNameList.Add($columnName, "")
                    }
                }
                else
                {
                    break
                }
            }
        }

        $global:workBook.Close($false)
        Clear-Variable workBook
    }

    #Create a class that represents the worst-case collection of columns that will be output to, e.g., a grid or CSV file
    #
    Invoke-Expression @"
    Class ClsExportCsv {
    $(($sortedFieldNameList.Keys).ForEach({"[string] `${$($_)}`n "}))
    }
"@

    #create array to hold list of rows that will be output to, e.g., a grid or CSV file
    $itemList = New-Object System.Collections.ArrayList
    $itemList.clear()

    write-host ("Scan for data")

    foreach ($fqBookName in $fqBookNames.Keys)
    {
        $global:workBook = $global:excel.Workbooks.Open($fqBookName)

        foreach ($sheet in $global:workBook.Sheets)
        {
            write-host -NoNewline ("Workbook=" + $global:workBook.Name + ". Sheet=" + $sheet.Name + ". Rows=")

            $columnNameLookup = @{}
            $columnNameLookup.Clear()

            $columnIndexMax = $sheet.UsedRange.Column + $sheet.UsedRange.Columns.Count - 1
            $rowOne = $sheet.Rows(1)

            #create column name index lookup table for this sheet
            for ($columnIndex = 1; $columnIndex -le $columnIndexMax; $columnIndex++)
            {
                $columnNameLookup.Add($columnIndex, $rowOne.Cells($columnIndex).Text.Trim().ToUpper())
            }

            for ($rowIndex = 2; $rowIndex -le $sheet.Cells.EntireRow.Count; $rowIndex++)
            {
                $rowCurrent = $sheet.Rows($rowIndex)

                if (($rowCurrent.Cells(1).Text).Length -gt 0)
                {

                    $listRow = New-Object -TypeName ClsExportCsv

                    for ($columnIndex = 1; $columnIndex -le $columnIndexMax; $columnIndex++)
                    {
                        if (($columnNameLookup.$columnIndex).Length -gt 0)
                        {

                            $cellObject = $rowCurrent.Cells($columnIndex)
                            $textFromFormula = ""

                            if ($cellObject.HasFormula)
                            {
                                $formulaNoWhiteSpace = Remove-WhiteSpaceFromNonQuoted -inString $cellObject.Formula

                                #detect and parse cells with =HYPERLINK(CONCATENATE("http://xxxx.aspx?pid=",A2),"Click Here")
                                if ($formulaNoWhiteSpace -match '^(?:\=HYPERLINK\(CONCATENATE\(\")(?<URL>.*)(?:\"\,)(?<A1>.*)(?:\)\,.*)$')
                                {
                                    if (($Matches["URL"] -ne $null) -and ($Matches["A1"] -ne $null))
                                    {
                                        $textFromFormula = ($Matches["URL"] + $sheet.Range($Matches["A1"]).Text) 
                                    }
                                }

                                #detect and parse cells with =HYPERLINK("http://xxxx","Click Here")
                                if ($formulaNoWhiteSpace -match '^(?:\=HYPERLINK\(\")(?<URL>.*)(?:\"\,\".*\"\))$')
                                {
                                    if ($Matches["URL"] -ne $null)
                                    {
                                        $textFromFormula = $Matches["URL"] 
                                    }
                                }
                            }

                            if ($textFromFormula.Length -eq 0)
                            {
                                $listRow.($columnNameLookup.$columnIndex) = $rowCurrent.Cells($columnIndex).Text.Trim()
                            }
                            else
                            {
                                $listRow.($columnNameLookup.$columnIndex) = $textFromFormula
                            }

                        } # if (($columnNameLookup.$columnIndex).Length -gt 0)

                    } # for ($columnIndex = 1; ...

                    $itemList.Add($listRow) | out-null
                }
                else
                {
                    write-host ($rowIndex - 2).ToString()
                    break
                }

            } # for ($rowIndex = 2; .....

        } # foreach ($sheet in $global:workBook.Sheets)

        $global:workBook.Close($false)
        Clear-Variable workBook
    }

    $global:excel.Quit()
    Clear-Variable excel

    $itemList | Export-CSV -LiteralPath "C:\Users\foo\combined.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ',' $itemList | Out-GridView -Title "Rows"

}
finally
{

    if ($global:excel -ne $null)
    {
        if ($global:workBook -ne $null)
        {
            $global:workBook.Close($false)
        }

        $global:excel.Quit()
        Clear-Variable excel
    }
}

似乎无法直接获取 Concatenate 函数生成的地址,例如

为什么使用如下正则表达式的解决方案不合适?

$split = $currentCell.Formula -split 'CONCATENATE' | Select -Last 1 | %{$_ -replace `
'[" ()]','' -split ','}
$calculatedResult = $split[0] + $sheet.Range("$($split[1])").Text