Read Excel data with Powershell and write to a variable

后端 未结 4 919
面向向阳花
面向向阳花 2020-12-14 13:01

Using PowerShell I would like to capture user input, compare the input to data in an Excel spreadsheet and write the data in corresponding cells to a variable. I am fairly n

相关标签:
4条回答
  • 2020-12-14 13:18

    I find it preferable to use an OleDB connection to interact with Excel. It's faster than COM interop and less error prone than import-csv. You can prepare a collection of psobjects (one psobject is one row, each property corresponding to a column) to match your desired target grid and insert it into the Excel file. Similarly, you can insert a DataTable instead of a PSObject collection, but unless you start by retrieving data from some data source, PSObject collection way is usually easier.

    Here's a function i use for writing a psobject collection to Excel:

    function insert-OLEDBData ($file,$sheet,$ocol) {
    
        {
            "xlsb$" 
                {"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=`"$File`";Extended Properties=`"Excel 12.0;HDR=YES;IMEX=1`";"}
            "xlsx$"
                {"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=`"$File`";Extended Properties=`"Excel 12.0 Xml;HDR=YES;IMEX=1`";"}
        }
        $OLEDBCon = New-Object System.Data.OleDb.OleDbConnection($cs)
    
        $hdr = $oCol|gm -MemberType NoteProperty|%{$_.name}
    
        $names = '[' + ($hdr-join"],[") + ']'
        $vals = (@("?")*([array]$hdr).length)-join','
    
        $sql = "insert into [$sheet`$] ($names) values ($vals)"
    
        $sqlCmd = New-Object system.Data.OleDb.OleDbCommand($sql)
        $sqlCmd.connection = $oledbcon
    
        $cpary = @($null)*([array]$hdr).length
    
        $i=0
        [array]$hdr|%{([array]$cpary)[$i] = $sqlCmd.parameters.add($_,"VarChar",255);$i++}
        $oledbcon.open()
    
        for ($i=0;$i-lt([array]$ocol).length;$i++)
        {
            for ($k=0;$k-lt([array]$hdr).length;$k++)
            {
                ([array]$cpary)[$k].value = ([array]$oCol)[$i].(([array]$hdr)[$k])
            }
            $res = $sqlCmd.ExecuteNonQuery()
        }
        $OLEDBCon.close()
    
    }
    
    0 讨论(0)
  • 2020-12-14 13:22

    This does not seem to work anymore. I swear it used to, but maybe an update to O365 killed it? or I last used it on Win 7, and have long since moved to Win 10:

    $GoLiveDate = $ws.Cells.Item($i, 2).Value
    

    I can still use .Value for writing to a cell, but not for reading it into a variable. instead of the contents of the cell, It returns: "Variant Value (Variant) {get} {set}"

    But after some digging, I found this does work to read a cell into a variable:

    $GoLiveDate = $ws.Cells.Item($i, 2).Text
    

    In regards to the next question / comment squishy79 asks about slowness, and subsequent OleDB solutions, I can't seem to get those to work in modern OS' either, but my own performance trick is to have all my Excel PowerShell scripts write to a tab delimited .txt file like so:

    Add-Content -Path "C:\FileName.txt" -Value $Header1`t$Header2`t$Header3...
    Add-Content -Path "C:\FileName.txt" -Value $Data1`t$Data2`t$Data3...
    Add-Content -Path "C:\FileName.txt" -Value $Data4`t$Data5`t$Data6...
    

    then when done writing all the data, open the .txt file using the very slow Com "Excel.Application" just to do formatting then SaveAs .xlsx (See comment by SaveAs):

    Function OpenInExcelFormatSaveAsXlsx
    {
        Param ($FilePath)
        If (Test-Path $FilePath)
        {
            $Excel = New-Object -ComObject Excel.Application
            $Excel.Visible = $true
            $Workbook = $Excel.Workbooks.Open($FilePath)
            $Sheet = $Workbook.ActiveSheet
            $UsedRange = $Sheet.UsedRange
            $RowMax = ($Sheet.UsedRange.Rows).count
            $ColMax = ($Sheet.UsedRange.Columns).count
            # This code gets the Alpha character for Columns, even for AA AB, etc.      
            For ($Col = 1; $Col -le $ColMax; $Col++)
            {
                $Asc = ""
                $Asc1 = ""
                $Asc2 = ""
                If ($Col -lt 27)
                {
                    $Asc = ([char]($Col + 64))
                    Write-Host "Asc: $Asc"
                }
                Else
                {
                    $First = [math]::truncate($Col / 26)
                    $Second = $Col - ($First * 26)
                    If ($Second -eq 0)
                    {
                        $First = ($First - 1)
                        $Second = 26
                    }
                    $Asc1 = ([char][int]($First + 64))
                    $Asc2 = ([char][int]($Second + 64))
                    $Asc = "$Asc1$Asc2"
                }
            }
            Write-Host "Col: $Col"
            Write-Host "Asc + 1: $Asc" + "1"
            $Range = $Sheet.Range("a1", "$Asc" + "1")
            $Range.Select() | Out-Null
            $Range.Font.Bold = $true
            $Range.Borders.Item(9).LineStyle = 1
            $Range.Borders.Item(9).Weight = 2
            $UsedRange = $Sheet.UsedRange
            $UsedRange.EntireColumn.AutoFit() | Out-Null
            $SavePath = $FilePath.Replace(".txt", ".xlsx")
            # I found scant documentation, but you need a file format 51 to save a .txt file as .xlsx
            $Workbook.SaveAs($SavePath, 51)
            $Workbook.Close
            $Excel.Quit()
        }
        Else
        {
            Write-Host "File Not Found: $FilePath"
        }
    }
    
    $TextFilePath = "C:\ITUtilities\MyTabDelimitedTextFile.txt"
    OpenInExcelFormatSaveAsXlsx -FilePath $TextFilePath
    

    If you don't care about formatting, you can just open the tab delimited .txt files as-is in Excel. Of course, this is not very good for inserting data into an existing Excel spreadsheet unless you are OK with having the script rewrite the whole sheet it each time an insert is made. It will still run much faster than using COM in most cases.

    0 讨论(0)
  • 2020-12-14 13:26

    I found this, and Yevgeniy's answer. I had to do a few minor changes to the above function in order for it to work. Most notably the handeling of NULL or empty valued values in the input array. Here is Yevgeniy's code with a few minor changes:

    function insert-OLEDBData {
        PARAM (
            [Parameter(Mandatory=$True,Position=1)]
            [string]$file,
            [Parameter(Mandatory=$True,Position=2)]
            [string]$sheet,
            [Parameter(Mandatory=$True,Position=3)]
            [array]$ocol
        )
        $cs = Switch -regex ($file)
        {
            "xlsb$"
                {"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=`"$File`";Extended Properties=`"Excel 12.0;HDR=YES`";"}
            "xlsx$"
                {"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=`"$File`";Extended Properties=`"Excel 12.0 Xml;HDR=YES`";"}
        }
        $OLEDBCon = New-Object System.Data.OleDb.OleDbConnection($cs)
    
        $hdr = $oCol | Get-Member -MemberType NoteProperty,Property | ForEach-Object {$_.name}
    
        $names = '[' + ($hdr -join "],[") + ']'
        $vals = (@("?")*([array]$hdr).length) -join ','
    
        $sql = "insert into [$sheet`$] ($names) values ($vals)"
    
        $sqlCmd = New-Object system.Data.OleDb.OleDbCommand($sql)
        $sqlCmd.connection = $oledbcon
    
        $cpary = @($null)*([array]$hdr).length
    
        $i=0
        [array]$hdr|%{([array]$cpary)[$i] = $sqlCmd.parameters.add($_,"VarChar",255);$i++}
        $oledbcon.open()
    
        for ($i=0;$i -lt ([array]$ocol).length;$i++)
        {
            for ($k=0;$k -lt ([array]$hdr).length;$k++)
            {
                IF (([array]$oCol)[$i].(([array]$hdr)[$k]) -notlike "") {
                    ([array]$cpary)[$k].value = ([array]$oCol)[$i].(([array]$hdr)[$k])
                } ELSE {
                    ([array]$cpary)[$k].value = ""
                }
            }
            $res = $sqlCmd.ExecuteNonQuery()
        }
        $OLEDBCon.close()
    }   
    
    0 讨论(0)
  • 2020-12-14 13:28

    User input can be read like this:

    $num = Read-Host "Store number"
    

    Excel can be handled like this:

    $xl = New-Object -COM "Excel.Application"
    $xl.Visible = $true
    $wb = $xl.Workbooks.Open("C:\path\to\your.xlsx")
    $ws = $wb.Sheets.Item(1)
    

    Looking up a value in one column and assigning the corresponding value from another column to a variable could be done like this:

    for ($i = 1; $i -le 3; $i++) {
      if ( $ws.Cells.Item($i, 1).Value -eq $num ) {
        $GoLiveDate = $ws.Cells.Item($i, 2).Value
        break
      }
    }
    

    Don't forget to clean up after you're done:

    $wb.Close()
    $xl.Quit()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($xl)
    
    0 讨论(0)
提交回复
热议问题