1

Using Powershell and Excel 2016, I'm trying to open a .xlsx file, extract a single page, and save this page as a .csv with a " ; " delimiter. The problem is that while Excel expects " ; " delimiter when opening a csv file, it always saves them with a " , " delimiter.

I'd prefer to not have to change any settings, this is a script i'm writing for a project that needs to work natively on any pc, so having to go and change settings every time I need it to run on another computer would be problematic.

I already checked that the list delimiter settigs in windows was indeed a " ; ", and it is.

I tried every type of CSV saving described in the microsoft doc (https://learn.microsoft.com/fr-fr/office/vba/api/excel.xlfileformat), what's weird is that when saving a file from the GUI version, I only have 3 versions of CSV, instead of 5 listed on the website, and one of them is "CSV with " ; " delimiter", which works as intended, but I can't seem to use this type of file when saving using Excel via Powershell

There's apparently a "local" flag that can be activated for Excel to use the delimiter settings of windows, but I have no idea of how ot activate it in Powershell and I'd prefer not to use this since it means that the program wouldn't work on a Windows with a different delimiter configuration.

# Args[0] : file to open
#     [1] : file to save
# page_to_extract : name of the page I need



# I open an Excel session
$excel_session               = New-Object -Com Excel.Application
$excel_session.displayAlerts = $false

# I open the file I need to extract the page from
$excel_workbook              = $excel_session.workbooks.open($args[0])

# I load in the page 
$excel_worksheet             = $excel_workbook.worksheets($page_to_extract)

# I save the page using a csv type (6,22,24,62,23)
$excel_worksheet.saveAs($args[1], 6)
$excel_session.quit()

This code always saves my csv with a " , " delimiter, I need " ; " instead.

I need to use Powershell and ONLY Powershell for this, no windows settings, no excel settings.

  • `$Data | Export-Csv file.csv -NoType -Delimiter ';'` is a simple way to export a CSV with a special delimiter. – AdminOfThings Aug 07 '19 at 12:26
  • As a sidenote, you should release the com objects from memory after quitting Excel: `[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel_workbook) | Out-Null; [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel_session) | Out-Null; [System.GC]::Collect(); [System.GC]::WaitForPendingFinalizers()` – Theo Aug 07 '19 at 12:29
  • I can't use Export-Csv as the file isn't opened using import-csv but via an Excel session, piping the sheet variable into export csv just outputs a csv that contains informations about the sheet but the data it contains. – Florian Dendoncker Aug 07 '19 at 12:43
  • You "can" use `Export-Csv` if you pipe the proper object into the command. If you want a solution that can use `Export-Csv`, it can be provided. – AdminOfThings Aug 07 '19 at 12:45
  • If it can be used then yes, but I don't know what to pipe into it, piping the sheet variable just gives informations about it in CSV format, and using get-variable, I wasn't able to determine what I should pipe into Export-Csv to get the expected result. – Florian Dendoncker Aug 07 '19 at 12:52
  • Why is the `;` delimiter required? – Bill_Stewart Aug 07 '19 at 13:24
  • I need to feed the file created to other programs that will only work with ``` ; ``` delimiters. I can't change these programs so I have no choice. – Florian Dendoncker Aug 07 '19 at 13:30
  • Just because you didn't pull the data in using `Import-CSV` doesn't mean the data can't be exported using `Export-CSV`. I second @AdminOfThings suggestion, and recommend they post as an answer, given the existing one wasn't clear about the problem and constraints. – gravity Aug 07 '19 at 13:30
  • I'm ok with using Export-Csv. But how do I get the data contained in the sheet so that I can pipe it in Export-Csv ? Knowing this would solve this problem entirely – Florian Dendoncker Aug 07 '19 at 13:33

4 Answers4

2

I had success with the following code with my own data. This uses your COM Object assignment code. I added logic to extract the cells that contain data, add that data to a new custom object on each row iteration, store each custom object in an array, and finally pipe the array into Export-Csv. Your specified delimiter ; is used in the Export-Csv command.

$excel_session               = New-Object -Com Excel.Application
$excel_session.displayAlerts = $false

# I open the file I need to extract the page from
$excel_workbook              = $excel_session.workbooks.open($args[0])

# I load in the page 
$excel_worksheet             = $excel_workbook.worksheets($page_to_extract)

# Get Range of Used Cells in Worksheet
$range = $excel_worksheet.usedrange

# Get First Row Column Text to be Used as Object Properties
$headers = $range.rows.item(1).value2

# Loop through Rows and Columns to Extract Data
# First loop traverses rows
# Second loop traverses columns

$output = for ($i = 2; $i -le $range.rows.count; $i++) {
    $hash = [ordered]@{}
    for ($j = 1; $j -le $range.columns.count; $j++) {
        [void]$hash.Add($headers.GetValue(1,$j),$range.rows.item($i).columns.item($j).Text)
    }
    [pscustomobject]$hash
    }

$output | Export-Csv file.csv -NoType -Delimiter ';'

# Clean Up COM Objects

[void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel_workbook)
[void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel_session)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
AdminOfThings
  • 23,946
  • 4
  • 17
  • 27
  • +1, but please note that the only action that is truly needed to (at least _eventually_) release the Excel process created by `New-Object -Com Excel.Application` is `$excel_session.Quit()`, which is absent from your answer. Use of `[System.Runtime.InteropServices.Marshal]::ReleaseComObject()` and `[System.GC]::WaitForPendingFinalizers()` is very widespread, but, I think, misguided overall - I'm not sure I have all the answers, but I invite you to give feedback at https://stackoverflow.com/a/55423449/45375 – mklement0 Aug 08 '19 at 23:19
  • it is very slow method! 2-3 rows/sec – Vladislav Dec 04 '20 at 17:35
0

The List Separator is a Windows regional setting.

To change it, please see : https://support.office.com/en-us/article/import-or-export-text-txt-or-csv-files-5250ac4c-663c-47ce-937b-339e391393ba

Change the separator in all .csv text files In Microsoft Windows, click the Start button, and then click Control Panel.

Open the dialog box for changing Regional and Language settings.

Type a new separator in the List separator box.

Click OK twice.

Note: After you change the list separator character for your computer, all programs use the new character as a list separator. You can change the character back to the default character by following the same procedure.

You should now be able to change the csv character delimiter.

Please note that you'll need to restart your computer to make the change in effect. You can check your current List Separator value in your Powershell session with (Get-Culture).TextInfo.ListSeparator

You can also check this post, which has a lot of screenshot and different other options on how to do so: https://superuser.com/questions/606272/how-to-get-excel-to-interpret-the-comma-as-a-default-delimiter-in-csv-files

P-L
  • 523
  • 4
  • 14
  • 1
    It already is a " ; " in the regional settings, as stated in the original post. – Florian Dendoncker Aug 07 '19 at 12:35
  • 1
    @FlorianDendoncker There are situations where the program will ignore the regional settings. See https://superuser.com/q/408191/75477 – Bacon Bits Aug 07 '19 at 12:39
  • Sorry I read your post too quickly. You could then use the argument as @AdminOfThings said -Delimiter ';' Can you please run the following command from your powershell session: (Get-Culture).TextInfo.ListSeparator – P-L Aug 07 '19 at 12:45
  • (Get-Culture).TextInfo.ListSeparator gives " ; ", the argument -delimiter works is I use an Export-Csv command, but I don't know how to pipe the correct object into it. – Florian Dendoncker Aug 07 '19 at 12:50
  • Hmm, that is weird Florian. I could see reasons why the TextInfo.ListSeparator would be different from the control panel List Separator but your problem is interesting. As for the -Delimiter with Export-Csv, it isn't pretty, but after saving your file you could load your CSV and then re-export it using the -Delimiter flag. Did you change the ListSeparator character recently without rebooting out of curiosity? I did some tests here and the changes won't be applied without a reboot. – P-L Aug 07 '19 at 12:58
  • Yes, I know that a reboot is needed for a change to be effective, but the option was already set at " ; ". I think I'll just import-export it or just "find and replace" every instance of " , ", it's dirty but at least it works. – Florian Dendoncker Aug 07 '19 at 13:08
0

My recommendation is to avoid Excel and use the database objects instead. Example:

[CmdletBinding()]
param(
  [Parameter(Position = 0,Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  $ExcelFileName,

  [Parameter(Position = 1,Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  $SheetName
)
$queryString = 'SELECT * FROM [{0}$A1:end]' -f $SheetName
$connectionString = ("Provider=Microsoft.ACE.OLEDB.12.0;" +
  "Data Source=$((Get-Item -LiteralPath $ExcelFileName -ErrorAction Stop).FullName);" +
  "Extended Properties=Excel 8.0;")
try {
  $connection = New-Object Data.OleDb.OleDbConnection($connectionString)
  $command = New-Object Data.OleDb.OleDbCommand($queryString)
  $command.Connection = $connection
  $connection.Open()
  $adapter = New-Object Data.OleDb.OleDbDataAdapter($command)
  $dataTable = New-Object Data.DataTable
  [Void] $adapter.Fill($dataTable)
  $dataTable
}
catch [Management.Automation.MethodInvocationException] {
  Write-Error $_
}
finally {
  $connection.Close()
}

If the above script is Import-ExcelSheet.ps1, you could export to a ;-delimited CSV file by running a command such as:

Import-ExcelSheet "C:\Import Files\ExcelFile.xlsx" "Sheet1" |
  Export-Csv C:\Import Files\Test.Csv" --Delimiter ';' -NoTypeInformation

If you have the 32-bit version of Excel installed, you will need to run the above script in the 32-bit version of PowerShell.

If you don't want to license Excel or can't install it on some computer where you want to run the script, you can install the Access database engine instead:

https://www.microsoft.com/en-us/download/details.aspx?id=54920

Bill_Stewart
  • 22,916
  • 4
  • 51
  • 62
  • Can't do, stuck using PC from work using softwraes pre-installed, nothing more is allowed :\ – Florian Dendoncker Aug 08 '19 at 05:57
  • You already have Excel installed, right? You don't need to install anything in that case. (The download link is in case you have a machine where you cannot install Excel.) – Bill_Stewart Aug 08 '19 at 13:17
0

An inefficient, but simple and pragmatic workaround is to:

  • Use your code as-is to let Excel temporarily produce an interim ,-separated CSV file.

  • Import that file with Import-Csv (which uses , by default), and export again with Export-Csv -Delimiter ';'.

In the context of your code:

(Import-Csv $args[1]) | Export-Csv $args[1] -Delimiter ';' -NoTypeInformation

Note:

  • The Import-Csv call is enclosed in (...) to ensure that the input file is read in full up front, which enables writing back to the same file in the same pipeline.

  • Export-Csv, sadly, defaults to ASCII(!) encoding; if your data contains non-ASCII characters, specify an appropriate encoding with -Encoding.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Yeah that's the "solution" I've implemented, I didn't want to do this at first since it's not a very clean solution but since the files aren't too big,it doesn't slow the process down too much. – Florian Dendoncker Aug 08 '19 at 05:57
  • I was going to suggest this also, but using the database objects is likely a more efficient solution. – Bill_Stewart Aug 08 '19 at 13:18
  • Agreed re efficiency, @Bill_Stewart; I tried your solution on a machine with Excel 2019, but `Microsoft.ACE.OLEDB.12.0` wasn't preinstalled for me (I found references to it under "ClickToRun" entries in the registry, however), and seemingly the OP doesn't have it either on his Excel 2016 machine. – mklement0 Aug 08 '19 at 14:32
  • You may have to open the 32-bit version of PowerShell. – Bill_Stewart Aug 08 '19 at 15:03
  • @Bill_Stewart: Unfortunately, that didn't help (and it would be a potentially unacceptable constraint to place on the solution). – mklement0 Aug 08 '19 at 15:07
  • Can't reproduce; sorry. Works fine for me here. – Bill_Stewart Aug 08 '19 at 15:09