powershell excel access without installing Excel - excel

I need to be able to read an existing (password protected) Excel spreadsheet (an .xlsx file) from Powershell - but I don't want to install Excel. Every approach I've found assumes that Excel is installed on the workstation where the script is running.
I've tried the Excel viewer, but it doesn't seem to work; it won't invoke properly. I've looked at other solutions on stackoverflow, but all of them seem to want to update the excel spreadsheet, and I'm hoping I don't have to go that far.
Am I missing something obvious?

See the Detailed Article from Scripting Guy here. You have to use classic COM ADO in your Powershell Script.
Hey, Scripting Guy! How Can I Read from Excel Without Using Excel?
Relevant Powershell Snippet:
$strFileName = "C:\Data\scriptingGuys\Servers.xls"
$strSheetName = 'ServerList$'
$strProvider = "Provider=Microsoft.Jet.OLEDB.4.0"
$strDataSource = "Data Source = $strFileName"
$strExtend = "Extended Properties=Excel 8.0"
$strQuery = "Select * from [$strSheetName]"
$objConn = New-Object System.Data.OleDb.OleDbConnection("$strProvider;$strDataSource;$strExtend")
$sqlCommand = New-Object System.Data.OleDb.OleDbCommand($strQuery)
$sqlCommand.Connection = $objConn
$objConn.open()
$DataReader = $sqlCommand.ExecuteReader()
While($DataReader.read())
{
$ComputerName = $DataReader[0].Tostring()
"Querying $computerName ..."
Get-WmiObject -Class Win32_Bios -computername $ComputerName
}
$dataReader.close()
$objConn.close()
That said, you have stated that your Excel file is password protected.
According to this Microsoft Support article, you cannot open password protected Excel files using OLEDB Connections.
From the Article:
On the Connection tab, browse to your workbook file. Ignore the "User
ID" and "Password" entries, because these do not apply to an Excel
connection. (You cannot open a password-protected Excel file as a data
source. There is more information on this topic later in this
article.)

If you don't have Excel installed, EPPlus is the best solution I know of to access Excel files from PowerShell. Refer to my answer here to setup EPPlus for PowerShell.
The following code creates a passwort protected Excel file containing the output of Get-Process and then reads back the process information from the password protected file:
# Load EPPlus
$DLLPath = "C:\Windows\System32\WindowsPowerShell\v1.0\Modules\EPPlus\EPPlus.dll"
[Reflection.Assembly]::LoadFile($DLLPath) | Out-Null
$FileName = "$HOME\Downloads\Processes.xlsx"
$Passwort = "Excel"
# Create Excel File with Passwort
$ExcelPackage = New-Object OfficeOpenXml.ExcelPackage
$Worksheet = $ExcelPackage.Workbook.Worksheets.Add("FromCSV")
$ProcessesString = Get-Process | ConvertTo-Csv -NoTypeInformation | Out-String
$Format = New-object -TypeName OfficeOpenXml.ExcelTextFormat -Property #{TextQualifier = '"'}
$null=$Worksheet.Cells.LoadFromText($ProcessesString,$Format)
$ExcelPackage.SaveAs($FileName,$Passwort)
# Open Excel File with Passwort
$ExcelPackage = New-Object OfficeOpenXml.ExcelPackage -ArgumentList $FileName,$Passwort
# Select First Worksheet
$Worksheet = $ExcelPackage.Workbook.Worksheets[1]
# Get Process data from Cells
$Processes = 0..$Worksheet.Dimension.Columns | % {
# Get all Cells in a row
$Row = $Worksheet.Cells[($Worksheet.Dimension.Start.Row+$_),$Worksheet.Dimension.Start.Column,($Worksheet.Dimension.Start.Row+$_),$Worksheet.Dimension.End.Column]
# Join values of all Cells in a row to a comma separated string
($Row | select -ExpandProperty Value) -join ','
} | ConvertFrom-Csv
Refer to my answer here for more options to protect Excel files.

Related

Exception from HRESULT: 0x800A03EC (Trying to modify cell in Excel through PowerShell)

I just recently joined an IAM team, and this month had to send out hundreds of emails to people notifying them of an account expiration (they are asked to either request for an extension or termination of the account). Thankfully, there's already a script made to do that part, but for dealing with the responses there is not. There's an excel spreadsheet where I record what is to happen to each account. I was hoping to make a script that can go through each of the responses and mark in the desired field in the spreadsheet accordingly. I've been having trouble with the part of the script where I modify the value under the desired field for the user.
I'm fairly new to PowerShell, so I'm not sure what the issue is. I already spent a few hours looking online and found quite a few possible solutions, but none of them have worked for me. A common problem is apparently using an older excel file, but it's fresh and it's Excel 2016. Another one is not having the correct file type, but I checked and that's not it either. The line of code in question is $extend.Cells.Item($modifyCell.Cells.Row) = "$data".
Any ideas what the problem could be?
Code:
# Path to .msg files
$msgDir = "C:\Users\me\Desktop\Test"
# Array to store results
$msgArray = New-Object System.Collections.Generic.List[object]
# Loop throuch each .msg file
Get-ChildItem "$msgDir" -Filter *.msg |
ForEach-Object {
# Open .msg file
$outlook = New-Object -comobject outlook.application
$msg = $outlook.Session.OpenSharedItem($_.FullName)
# Add .msg file Subject and Body to array
$msgArray.Add([pscustomobject]#{Subject=$msg.Subject;Body=$msg.Body;})
$msg.Close(0) # Close doesn't always work, see KB2633737 -- restart ISE/PowerShell
}
# Loop though / parse each message
ForEach ($message in $msgArray) {
$subject = $message.subject
$body = $message.body
$regex = [regex] '\s*(\w*)\s*\|$'
If ($body -match $regex) {
$username = $body
}
$parse = $body | Select-String -Pattern "Please extend"
If ($parse -eq "Please extend") {
$data = "Y"
}
}
# Open Excel
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $True
$OpenFile = $Excel.Workbooks.Open("C:\Users\me\Desktop\test.xlsx")
$Workbook = $OpenFile.Worksheets
$Worksheet = $Workbook.Item(1)
# Get the values for each column
$samacctname = $Worksheet.Cells | where {$_.value2 -eq "SAM Account Name"} | select -First 1
$extend = $Worksheet.Cells | where {$_.value2 -eq "Extend"} | select -First 1
# Get the values for each row in SAM Account Name
$userValues = #()
for($i=2; $samacctname.Cells.Item($i).Value2 -ne $null; $i++ ){
$userValues += $samacctname.Cells.Item($i)
}
# Get the values where the cell value of SAM Account matches the username
$modifyCell = $userValues | where {$_.Value2 -eq $username}
# Modify the Extend cell using the username's row position
$extend.Cells.Item($modifyCell.Cells.Row) = "$data"
# Save the file
$OpenFile.Save()
Edit 1: I went back into my code and first tried to hard-code the data value I was trying to add to the cell, but I still got the same error. I then tried hard-coding it right when I call the line $extend.Cells.Item($modifyCell.Cells.Row) = "Y" and it works as it should. So how I'm trying to use regex to pull the username is likely not right. Probably how I'm pulling the data as well.

Merge multiple CSV into one without using Excel.Application

I created a PowerShell script that allows me to merge multiple .CSV into one .XLSX file.
It works well on my computer:
$path = "C:\Users\Francesco\Desktop\CSV\Results\*"
$csvs = Get-ChildItem $path -Include *.csv
$y = $csvs.Count
Write-Host "Detected the following CSV files: ($y)"
Write-Host " "$csvs.Name"`n"
$outputfilename = "Final Registry Results"
Write-Host Creating: $outputfilename
$excelapp = New-Object -ComObject Excel.Application
$excelapp.SheetsInNewWorkbook = $csvs.Count
$xlsx = $excelapp.Workbooks.Add()
for ($i=1;$i -le $y;$i++) {
$worksheet = $xlsx.Worksheets.Item($i)
$worksheet.Name = $csvs[$i-1].Name
$file = (Import-Csv $csvs[$i-1].FullName)
$file | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation | clip
$worksheet.Cells.Item(1).PasteSpecial() | Out-Null
}
$output = "C:\Users\Francesco\Desktop\CSV\Results\Results.xlsx"
$xlsx.SaveAs($output)
$excelapp.Quit()
The problem is that I need to run this on several servers and servers are well known for not having Office installed so I cannot use Excel.Application.
Is there a way to merge multiple CSV into one CSV or XLSX without using Excel.Application and saving each CSV into a different sheet?
#AnsgarWiechers is right, ImportExcel is powerful and not difficult to use. However for your specific case you can use a more limited approach, using OleDb (or ODBC or ADO) to write to an Excel file like a database. Here is some sample code showing how to write to an Excel file using OleDb.
$provider = 'Microsoft.ACE.OLEDB.12.0'
$dataSource = 'C:\users\user\OleDb.xlsb'
$connStr = "Provider=$provider;Data Source=$dataSource;Extended Properties='Excel 12.0;HDR=YES'"
$objConn = [Data.OleDb.OleDbConnection]::new($connStr)
$objConn.Open()
$cmd = $objConn.CreateCommand()
$sheetName = 'Demo'
$cmd.CommandText = 'CREATE TABLE $sheetName (Name TEXT,Age NUMBER)'
$cmd.ExecuteNonQuery()
$cmd.CommandText = "INSERT INTO demo (Name,Age) VALUES ('Adam', 20)"
$cmd.ExecuteNonQuery()
$cmd.CommandText = "INSERT INTO demo (Name,Age) VALUES ('Bob',30)"
$cmd.ExecuteNonQuery()
$cmd.Dispose()
$objConn.Close()
$objConn.Dispose()
You didn't say much about the CSV files you'll be processing. If column data varies, to create the table you'll have to get the attribute (column) names from the CSV header (either by reading the first line of the CSV file, or by enumerating the properties of the first item returned by Import-CSV).
If your CSV files have a large number of lines, writing one line at a time may be slow. In that case using a DataSet and OleDbDataAdapter might improve performance (but I haven't tested). But at that point you might as well use OleDb to read the .csv directly into a DataSet, create a OleDbDataAdapter, set the adapter's InsertCommand property, and finally call the adapters Update method. I don't have time to write and test all that.
This is not intended as a full solution, just a demo of how to use OleDb to write to an Excel file.
Note: I tested this on a server that didn't have Office or Excel installed. The Office data providers pre-installed on that machine were 32-bit, but I was using 64-bit PowerShell. To get 64-bit drivers I installed the Microsoft Access Database Engine 2016 Redistributable and that's what I used for testing.
Time has passed and I have found a new solution: Install-Module -Name ImportExcel
This way the module takes care of the job like in this script.

Performance increase for PowerShell dataset to Excel

I have a PowerShell script which pulls data from DB and pushes it to a excel sheet. I am facing slowness (45 mins approx) while copying the records in the dataset to the excel sheet as the number of records exceed 200K. And I am trying to loop them one by one using the below snippet, which takes more time. Is there a way in which I can transfer the data from dataset to excel more efficiently?
$cells=$Worksheet.Cells
$row=1
foreach ($rec in $dataset.Tables[0].Rows)
{
$row++
$col=1
$cells.item($Row,$col)=$USR.ID
$col++
$cells.item($Row,$col)=$USR.Name
$col++
$cells.item($Row,$col)=$USR.Age
$col++
}
You shoud try PSExcel module. There's no need to create COM object and even have Excel installed. Your example would look like this and be lightning fast:
$dataset.Tables[0] |
Select-Object ID,Name,Age |
Export-XLSX -Path $FullName -AutoFit -WorksheetName 'MyData'
A nice little workaround I saw sometime ago was to format the rows as a CSV string and simply paste them in. For the environment I was using, this proved to be more efficient than creating a file using Export-CSV, then loading it in Excel.
#Row data joined with tabs
$data = #("[A1]", "[A2]", "[A3]", "[A4]", "[A5]", "[A6]") -join "`t"
#Multiple rows joined with new lines
$dataToPaste = "{0}`n{1}`n{2}" -f $data, $data.replace("A", "B"), $data.replace("A", "C")
$excel = New-Object -ComObject Excel.Application
$book = $excel.Workbooks.Add()
$sheet = $book.Worksheets.Add()
#Activate where to put data
$sheet.Range("B2").Activate() | Out-Null
#Copy data to clipboard and paste into sheet.
$dataToPaste | Clip
$sheet.Paste()
$excel.Visible = $true
#Cleanup
[Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null
$excel = $null
I did find that, very rarely, the Paste method throws an error, which was fixed by retrying a second time if it failed:
try{
$sheet.Paste()
}catch{
$sheet.Paste()
}
This may not be a preferred option if you are running something on a PC being used by someone, as the user could copy something to the clipboard after the script does (but before $sheet.Paste()) and invalidate your data.

Faster way of writing out to excel in powershell

I have a powershell script that reads in a csv and then appends to an excel worksheet.
It runs quite painfully slow. I have searched and it seems this is a limitation of using com to write to excel. Some suggestions I have found to speed this up are to write out entire ranges instead of cell by cell. However I need to format the cells and it doesn't seem to be possible to do this when writing out ranges. Any suggestions on how to optimize the below code would be welcome.
I do not have the option to use a DB.
$csvPath = "Z:\script_test\"
$outputFile = "Z:\script_test\exceltest.xlsx"
foreach($csvFile in Get-ChildItem $csvPath -Filter "STATS*.txt" ){
$csvFilePath = [io.path]::combine($csvPath, $csvFile)
$rawcsvData = Import-Csv -Delimiter ";" -Path $csvFilePath
$Excel = New-Object -ComObject excel.application
$Excel.visible = $false
$workbook = $Excel.workbooks.Open($outputFile)
$ExcelWorkSheet = $Excel.WorkSheets.item("2016")
$ExcelWorkSheet.activate()
$excel.cells.item(1,1) = “PEX”
$excel.cells.item(1,2) = “RUN DATE”
$excel.cells.item(1,3) = “EXECS”
$excel.cells.item(1,4) = “CPU AV.”
$excel.cells.item(1,5) = “CPU HI.”
$excel.cells.item(1,6) = “CPU TOT.”
$excel.cells.item(1,7) = “#VALUE!”
$excel.cells.item(1,8) = “ELAPS AV.”
$excel.cells.item(1,9) = “ELAPSE HI.”
$excel.cells.item(1,10) = “ELAPSE TOT”
$i = $ExcelWorkSheet.UsedRange.rows.count + 1
foreach($rawcsv in $rawcsvData)
{
$RUNDATE = $rawcsv.“RUN DATE ”.replace("--1","")
$EXECS = $rawcsv."EXECS ".replace("?","")
$CPUAV = $rawcsv.“CPU AV. ”.replace("-",":")
$CPUHI = $rawcsv.“CPU HI. ”.replace("-",":")
$CPUTOT = $rawcsv.“CPU TOT. ”.replace("-",":")
$ELAPSEAV = $rawcsv.“ELAPSE AV.”.replace("-",":")
$ELAPSEHI = $rawcsv.“ELAPSE HI.”.replace("-",":")
$ELPASETOT = $rawcsv.“ELPASE TOT”.replace("-",":")
Write-Output("working" + $i)
$excel.cells.item($i,1) = $rawcsv."PEX "
$excel.cells.item($i,2) = $RUNDATE
$excel.cells.item($i,2).NumberFormat = “yyyy/mm/dd”
$excel.cells.item($i,3) = $EXECS
$excel.cells.item($i,4) = $CPUAV
$excel.cells.item($i,4).NumberFormat = “hh:mm:ss.00”
$excel.cells.item($i,5) = $CPUHI
$excel.cells.item($i,5).NumberFormat = “hh:mm:ss.00”
$excel.cells.item($i,6) = $CPUTOT
$excel.cells.item($i,6).NumberFormat = “hh:mm:ss.00”
$excel.cells.item($i,7) = “=((HOUR(F"+$i+")*3600)+(MINUTE(F"+$i+")*60)+SECOND(F"+$i+"))*21”
$excel.cells.item($i,8) = $ELAPSEAV
$excel.cells.item($i,8).NumberFormat = “hh:mm:ss.00”
$excel.cells.item($i,9) = $ELAPSEHI
$excel.cells.item($i,9).NumberFormat = “hh:mm:ss.00”
$excel.cells.item($i,10) = $ELPASETOT
$excel.cells.item($i,10).NumberFormat = “hh:mm:ss.00”
$i++
}
$ExcelWorkSheet.UsedRange.RemoveDuplicates()
#$workbook.saveas($outputFile)
$workbook.save()
$Excel.Quit()
Remove-Variable -Name excel
[gc]::collect()
[gc]::WaitForPendingFinalizers()
Move-Item -Path $csvFilePath -Destination "Z:\script_test\used files"
}
The slow part is all about COM object performance. You won't be able to speed this up good enough if you will keep working with COM object, sadly.
Back in days I had some project related to Excel and I found some great module that uses external DLL, you can take a look on it: PSExcel
Best part is that you will not need to have Excel installed, like you do with COM object.
There is a Powershell cmdlet you can install called Export-XLSX that works very similarly to the native Export-CSV: https://gallery.technet.microsoft.com/office/Export-XLSX-PowerShell-f2f0c035
The documentation there is pretty good, but here's an example of how you would use it:
# 1. Define path of Export-XLSX.ps1 script:
$ExportXLSX = "C:\YourFilePath\Export-XLSX\Export-XLSX.ps1"
# 2. Call script same as any other function by preceding filename with a period (.$ExportXLSX) and following it with parameters:
Get-ChildItem $env:windir | Select-Object Mode,LastWriteTime,Length,Name | .$ExportXLSX -Path 'c:\temp\PSExcel.xlsx' -WorkSheetName 'Files'
UPDATE: Having compared this option with the full PSExcel module presented in the other answer, I actually prefer the PSExcel module. The performance speed-wise is pretty much the same in my testing, but the PSExcel module appears to create much smaller files.
For example, using the above list of the windows directory outputs a 53KB file using the Export-XLSX.ps1 on my machine. However, the PSExcel module outputs a 7KB file. Given its ease of use, I would go with it.

How to convert .xls to .csv using Powershell without Excel installed

Is there a way to convert .xls to .csv without Excel being installed using Powershell?
I don't have access to Excel on a particular machine so I get an error when I try:
New-Object -ComObject excel.application
New-Object : Retrieving the COM class factory for component with CLSID
{00000000-0000-0000-0000-000000000000} failed due to the following
error: 80040154 Class not registered (Exception from HRESULT:
0x80040154 (REGDB_E_CLASSNOTREG)).
Forward
Depending on what you already have installed on your system you might need the Microsoft Access Database Engine 2010 Redistributable for this solution to work. That will give you access to the provider: "Microsoft.ACE.OLEDB.12.0"
Disclaimer: Not super impressed with the result and someone with more background could make this answer better but here it goes.
Code
$strFileName = "C:\temp\Book1.xls"
$strSheetName = 'Sheet1$'
$strProvider = "Provider=Microsoft.ACE.OLEDB.12.0"
$strDataSource = "Data Source = $strFileName"
$strExtend = "Extended Properties='Excel 8.0;HDR=Yes;IMEX=1';"
$strQuery = "Select * from [$strSheetName]"
$objConn = New-Object System.Data.OleDb.OleDbConnection("$strProvider;$strDataSource;$strExtend")
$sqlCommand = New-Object System.Data.OleDb.OleDbCommand($strQuery)
$sqlCommand.Connection = $objConn
$objConn.open()
$da = New-Object system.Data.OleDb.OleDbDataAdapter($sqlCommand)
$dt = New-Object system.Data.datatable
[void]$da.fill($dt)
$dataReader.close()
$objConn.close()
$dt
Create an ODBC connection to the excel file $strFileName. You need to know your sheet name and populate $strSheetName which helps build $strQuery. When then use several objects to create a connection and extract the data from the sheet as a System.Data.DataTable. In my test file, with one populated sheet, I had two columns of data. After running the code the output of $dt is:
letter number
------ ------
a 2
d 34
b 0
e 4
You could then take that table and then ExportTo-CSV
$dt | Export-Csv c:\temp\data.csv -NoTypeInformation
This was built based on information gathered from:
Scripting Guy
PowerShell Code Repository

Resources