Skip to Content
RowSlice

Guide

How to Split a Large CSV File with PowerShell, Bash or Python

Published

Your export has 2.5 million rows and Excel stops at 1,048,576. The usual advice is to split the file into parts on the command line. That works, with two catches the one-liners skip: the header row only ends up in the first part, and most commands count lines, which is not the same as counting rows.

Every script below was run on a 99.4 MB test file with 2,500,000 rows and six columns, including an id and a zip column with leading zeros. Like many real exports, one row in every 97 had a line break inside a quoted notes field. We then parsed every part to check the rows, the headers and the column counts.

Lines Are Not Rows

A CSV row normally sits on one line, but a quoted value is allowed to contain a line break, and that row then spans two lines. Tools such as split, and PowerShell’s ReadLine(), cut wherever the line count says, including in the middle of such a row. Here is a small file split every two lines:

id,note
1,ok
2,"first line
second line"
3,ok
== part_00.csv
1,ok
2,"first line
== part_01.csv
second line"
3,ok

Row 2 is now half in one file and half in the other. Both halves are broken: a CSV reader treats the first part’s opening quote as running to the end of the file, and it reads second line" in the second part as a one-column row.

On the 2.5-million-row file, splitting every 1,000,000 lines gave parts with 989,795, 989,796 and 520,409 rows, because about one line in a hundred belonged to a row that had already started. None of the cuts happened to land inside a value, so every part parsed correctly. That was luck: with different line breaks in the data, the same command would have cut a row. If your file has free-text fields, use the Python script further down, which counts rows instead of lines.

macOS and Linux: split

This version uses only options that the macOS and Linux versions of split share:

tail -n +2 big.csv | split -l 1000000 - part_
for f in part_*; do
  { head -n 1 big.csv; cat "$f"; } > "$f.csv" && rm "$f"
done

tail -n +2 sends everything after the header to split, which writes 1,000,000 lines at a time into part_aa, part_ab and so on. The loop then puts the header back on top of each part and adds .csv to the name. Run it in a folder with nothing else named part_, or the loop will process those files too. It took about a second on the test file.

On Linux, GNU split can also split by size without cutting a line in half, which suits an upload limit:

tail -n +2 big.csv | split -C 40m -d --additional-suffix=.csv - part_
for f in part_*.csv; do
  { head -n 1 big.csv; cat "$f"; } > "with-header-$f" && rm "$f"
done

-C 40m puts as many whole lines as fit into 40 MB, -d numbers the parts from 00, and --additional-suffix adds the extension. The test file became parts of 40.0, 40.0 and 19.4 MB. -C and --additional-suffix do not exist in the macOS version. Both of these commands count lines, so the warning above applies to them.

Windows: PowerShell

Windows has no split command. The obvious PowerShell route, Get-Content, sends every line through the pipeline as a separate object, which is slow on large files — more than twice as slow as .NET when we counted lines. Reading the file with .NET instead still reads one line at a time:

$source = 'C:\data\big.csv'
$folder = 'C:\data'
$rowsPerFile = 1000000

$reader = [System.IO.StreamReader]::new($source)
$header = $reader.ReadLine()
$utf8WithBom = [System.Text.UTF8Encoding]::new($true)
$part = 0; $count = 0; $writer = $null
while ($null -ne ($line = $reader.ReadLine())) {
  if ($count % $rowsPerFile -eq 0) {
    if ($writer) { $writer.Close() }
    $part++
    $writer = [System.IO.StreamWriter]::new("$folder\part-$part.csv", $false, $utf8WithBom)
    $writer.WriteLine($header)
  }
  $writer.WriteLine($line)
  $count++
}
if ($writer) { $writer.Close() }
$reader.Close()

Change the first three lines, and use full paths: .NET resolves a relative path against the folder PowerShell started in, which is not always the one you are looking at. Each part gets the header and a UTF-8 byte order mark, so accented letters display correctly when the part is double-clicked open in Excel. On the test file it wrote three parts in anywhere from 7 seconds to 2 minutes, depending on what else the laptop was doing. It reads lines, so the warning above applies here too.

Python: Split by Rows, Not Lines

When the file has free-text fields, count rows with a real CSV parser. This script uses only Python’s standard library:

import csv

ROWS_PER_FILE = 1_000_000

with open("big.csv", encoding="utf-8-sig", newline="") as source:
    reader = csv.reader(source)
    header = next(reader)
    out = None
    for i, row in enumerate(reader):
        if i % ROWS_PER_FILE == 0:
            if out:
                out.close()
            part = i // ROWS_PER_FILE + 1
            out = open(f"part-{part}.csv", "w", encoding="utf-8-sig", newline="")
            writer = csv.writer(out)
            writer.writerow(header)
        writer.writerow(row)
    if out:
        out.close()

It gave exactly 1,000,000, 1,000,000 and 500,000 rows, every one with six columns and every line break inside a value intact, in 6 to 42 seconds over our runs. newline="" is what makes the reader treat a line break inside quotes as part of the value. utf-8-sig strips a byte order mark from the source if it has one and writes one into each part for Excel. The values themselves do not change, including leading zeros, though quotes are added only where a value needs them, so a value that was quoted without needing it comes out unquoted.

Which Method to Use

MethodHeader in every partCan cut a row in halfTest file
split with the header loopYesYesAbout 1 s
GNU split -C by sizeYesYesAbout 1 s
PowerShell scriptYesYes7 s to 2 min
Python scriptYesNo6 to 42 s

Times are the fastest and slowest we saw on a Windows 11 laptop that was busy with other work; yours will differ. Choose by the middle column first: if nobody can promise the file has no line breaks inside values, the speed of the line-based commands is not worth the risk.

Without the Command Line

The CSV splitter parses rows the way the Python script does, so a line break inside a value never ends a part, and it puts the header in every part. It can split by rows, by maximum file size or into a set number of files. It runs in your browser and reads the file from your disk, so nothing is uploaded. To choose the numbers, see Excel’s row limit and splitting a CSV to fit an import limit. To put parts back together, see merging CSV files from the command line.

FAQ

Frequently Asked Questions

How many rows should each part have for Excel?

Excel shows at most 1,048,576 rows on a sheet, and the header takes one of them, so any number up to 1,048,575 fits. We use 1,000,000 in every example: it leaves room to add rows later and makes it obvious which part holds which rows. The line-based commands put fewer rows than that in each part, never more, so they are safe for Excel too.

Why does each part have fewer rows than I asked for?

Because you split by lines, and some of your rows take up more than one line. A value containing a line break — an address, a comment, a product description — makes its row two or more lines long. In our test file, 1,000,000 lines held 989,795 rows. It is a sign that the file contains line breaks inside values, which is also the situation in which a line-based split can cut a row in half.

Does the header row appear in every part?

With the scripts in this guide, yes. Plain split does not know what a header is: it copies the first line into the first part only, and the other parts start with data. The tail -n +2 and head -n 1 steps in the shell versions take the header off and put it back on each part.

Why do accented letters look wrong when I open a part in Excel?

The part is UTF-8 without a byte order mark, and Excel guesses a different character set when you double-click it. The shell versions copy bytes as they are, so parts inherit whatever the original file had. The PowerShell and Python scripts here write a BOM into every part. To repair parts you have already made, run them through the encoding fixer.

Can I split a CSV by the values in a column instead of by row count?

Yes, and it is often the more useful split: one file per region, store or customer. That needs a program that parses each row, so it is not a one-line shell job. The split by column tool does it in the browser, writing one file per distinct value with the header in each.

Does split work the same on macOS as on Linux?

The basic options do. macOS uses the BSD version, which has -l, -b and, on current versions, -d for numbered parts, but not the GNU-only --additional-suffix or -C. The first shell example in this guide uses only options both versions share.

Guides

See All 20 CSV Guides