Perl Text::CSV
last modified November 7, 2026
CSV, comma-separated values, is the most common format for exchanging tabular
data, and it looks deceptively simple. Real CSV files contain fields with
commas, quotes, and line breaks, which are protected by double quotes as
described in RFC 4180. Text::CSV parses and produces such files
correctly. It is a front end that uses the fast C implementation
Text::CSV_XS when it is installed and falls back to the pure-Perl
Text::CSV_PP otherwise; both have the same interface.
The modules are installed with cpanm Text::CSV Text::CSV_XS. The
examples were run with Perl 5.44, Text::CSV 2.06, and
Text::CSV_XS 1.65 on Linux. Most of them read the following
file.
id,name,city,note,amount 1,Jana Kovac,Bratislava,regular customer,120.50 2,"Novak, Peter",Kosice,"says ""hi"" every time",75.00 3,Eva Horvath,Zilina,"two-line note",310.25 4,Tomas Balaz,Nitra,,0
The second record has a name with a comma and a note with quotes, which are doubled inside the quoted field. The note of the third record spans two lines, and the fourth record has an empty note.
Why split is not enough
The first idea is often to read the file line by line and split each line on commas.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
open my $fh, '<', 'customers.csv' or die "customers.csv: $!";
while (my $line = <$fh>) {
chomp $line;
my @fields = split /,/, $line;
say scalar(@fields), ' fields: ', join ' | ', @fields;
}
The simple records are fine, but the second record falls apart into six fields with the quotes left in, and the multi-line record is split into two broken lines. Handling quoting with regular expressions quickly becomes complicated and is easy to get wrong; a real parser is the safer choice.
$ perl split.pl 5 fields: id | name | city | note | amount 5 fields: 1 | Jana Kovac | Bratislava | regular customer | 120.50 6 fields: 2 | "Novak | Peter" | Kosice | "says ""hi"" every time" | 75.00 4 fields: 3 | Eva Horvath | Zilina | "two-line 2 fields: note" | 310.25 5 fields: 4 | Tomas Balaz | Nitra | | 0
Reading records with getline
The getline method reads one record from a file handle and returns
a reference to an array of fields, or undef at the end of the
file.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV;
my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
open my $fh, '<:encoding(UTF-8)', 'customers.csv' or die "customers.csv: $!";
while (my $row = $csv->getline($fh)) {
say scalar(@$row), ' fields: ', join ' | ', map { s/\n/\\n/gr } @$row;
}
close $fh;
The binary option allows non-ASCII characters and line breaks
inside quoted fields; it should always be set. With auto_diag,
the parser reports errors as warnings, so problems do not go unnoticed. Every
record now has five fields, the quotes are removed, doubled quotes are
restored to single ones, and the two-line note is a single field; we print its
newline as \n. The file is opened with an encoding layer, so the
fields are decoded text.
$ perl read.pl 5 fields: id | name | city | note | amount 5 fields: 1 | Jana Kovac | Bratislava | regular customer | 120.50 5 fields: 2 | Novak, Peter | Kosice | says "hi" every time | 75.00 5 fields: 3 | Eva Horvath | Zilina | two-line\nnote | 310.25 5 fields: 4 | Tomas Balaz | Nitra | | 0
Headers and getline_hr
Most CSV files start with a header line. The header method reads
it and stores the column names, after which getline_hr returns each
record as a hash reference.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV;
my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
open my $fh, '<:encoding(UTF-8)', 'customers.csv' or die "customers.csv: $!";
my @cols = $csv->header($fh);
say "columns: @cols";
my $total = 0;
while (my $r = $csv->getline_hr($fh)) {
printf "%-12s %-11s %8.2f\n", $r->{name}, $r->{city}, $r->{amount};
$total += $r->{amount};
}
printf "%-24s %8.2f\n", 'total', $total;
The header method returns the column names; by default it
lowercases them. It also detects the separator from a set of candidates and
skips a byte order mark at the start of the file. Accessing fields by name
makes the code independent of the column order and much easier to read than
numeric indexes.
$ perl header.pl columns: id name city note amount Jana Kovac Bratislava 120.50 Novak, Peter Kosice 75.00 Eva Horvath Zilina 310.25 Tomas Balaz Nitra 0.00 total 505.75
Writing CSV
The print method writes an array reference as one record. It
quotes and escapes fields only where necessary.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use utf8;
use open qw(:std :encoding(UTF-8));
use Text::CSV;
my @rows = (
['product', 'size', 'price', 'comment'],
['T-shirt', 'M', 12.9, 'cotton, organic'],
['Mug', '0.3 l', 6.5, 'the "classic" one'],
['Poster', 'A2', 9, ''],
['Café sign', undef, 24, "hand-made\nin Brno"],
);
my $csv = Text::CSV->new({ binary => 1, auto_diag => 1, eol => "\n" });
open my $fh, '>:encoding(UTF-8)', 'products.csv' or die "products.csv: $!";
$csv->print($fh, $_) for @rows;
close $fh or die "products.csv: $!";
open $fh, '<:encoding(UTF-8)', 'products.csv' or die "products.csv: $!";
print while <$fh>;
The eol option adds a line ending after each record; without it,
print writes no newline. The field with a comma is quoted, and the
quotes in the "classic" one are doubled. By default
Text::CSV_XS also quotes fields that contain spaces, such as
0.3 l and Café sign, which is valid and harmless; the
quote_space option turns this off. Both the empty string and
undef are written as empty fields, and the field with a newline is
quoted and keeps its line break. The source contains the non-ASCII character
é, so the program declares use utf8, and the open
pragma sets UTF-8 on the standard streams. Closing the handle is checked,
because a full disk is often reported only at that point.
$ perl write.pl product,size,price,comment T-shirt,M,12.9,"cotton, organic" Mug,"0.3 l",6.5,"the ""classic"" one" Poster,A2,9, "Café sign",,24,"hand-made in Brno"
Separators and quoting options
Not every CSV file uses commas. Spreadsheets in many European locales write
semicolons, and tab-separated files are common too. The combine
method builds a single record in memory, which is handy for comparing the
options.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV;
my @row = ('Kosice', 3.14, 'a;b', 'say "hi"', '', undef);
my %variants = (
'1 default' => {},
'2 semicolon' => { sep_char => ';' },
'3 tab' => { sep_char => "\t" },
'4 always_quote' => { always_quote => 1 },
'5 quote_empty' => { quote_empty => 1 },
'6 escape' => { escape_char => '\\' },
);
for my $name (sort keys %variants) {
my $csv = Text::CSV->new({ binary => 1, %{ $variants{$name} } });
$csv->combine(@row) or die $csv->error_diag;
printf "%-15s %s\n", $name, $csv->string =~ s/\t/<TAB>/gr;
}
The combine method joins the fields and string
returns the result; we show tabs as <TAB>. The
sep_char option sets the separator, and a field containing it is
quoted, so a;b is quoted only in the semicolon variant.
always_quote quotes every defined field, and
quote_empty quotes empty strings, which makes it possible to tell
an empty string from undef in the output. The
escape_char option switches from doubled quotes to backslash
escapes, a style that some databases produce.
$ perl options.pl 1 default Kosice,3.14,a;b,"say ""hi""",, 2 semicolon Kosice;3.14;"a;b";"say ""hi""";; 3 tab Kosice<TAB>3.14<TAB>a;b<TAB>"say ""hi"""<TAB><TAB> 4 always_quote "Kosice","3.14","a;b","say ""hi""","", 5 quote_empty Kosice,3.14,a;b,"say ""hi""","", 6 escape Kosice,3.14,a;b,"say \"hi\"",,
The csv function
For small and medium files, the csv function reads or writes a
whole file in a single call.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV qw(csv);
my $customers = csv(in => 'customers.csv', headers => 'auto', encoding => 'UTF-8');
say scalar(@$customers), ' records';
say "$_->{id}: $_->{name} ($_->{city})" for @$customers;
my $rich = csv(
in => 'customers.csv',
headers => 'auto',
encoding => 'UTF-8',
filter => { amount => sub { $_ > 100 } },
);
csv(in => $rich, out => 'rich.csv', headers => [qw(id name amount)], encoding => 'UTF-8');
open my $fh, '<', 'rich.csv' or die "rich.csv: $!";
print while <$fh>;
With headers => 'auto', csv returns a reference
to an array of hashes keyed by the header line. The filter option
keeps only the records for which the callback returns true; inside it,
$_ is the value of the named column. Given an array reference as
in and a file name as out, the function writes CSV;
the headers list selects the columns and their order.
$ perl func.pl 4 records 1: Jana Kovac (Bratislava) 2: Novak, Peter (Kosice) 3: Eva Horvath (Zilina) 4: Tomas Balaz (Nitra) id,name,amount 1,"Jana Kovac",120.50 3,"Eva Horvath",310.25
Handling errors
A parse error stops getline, which then returns
undef just as at the end of the file. The eof method
tells the two cases apart. The sample file has a stray quote in an unquoted
field.
id,name,score 1,Anna,90 2,Boris "Bo" Black,85 3,Clara,77
The program reads the file twice: once checking the error by hand, once letting the parser die.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV;
my $csv = Text::CSV->new({ binary => 1 });
open my $fh, '<', 'bad.csv' or die "bad.csv: $!";
while (my $row = $csv->getline($fh)) {
say join ' | ', @$row;
}
unless ($csv->eof) {
my ($code, $message, $pos, undef, $field) = $csv->error_diag;
say "error $code in field $field at position $pos: $message";
}
my $strict = Text::CSV->new({ binary => 1, auto_diag => 2 });
seek $fh, 0, 0;
eval { 1 while $strict->getline($fh) };
print "died: $@";
The first loop ends at the bad record. Because eof is false, the
program asks error_diag for the details: in list context it returns
the error code, the message, the position in the record, the record number, and
the field number. With auto_diag => 2, the parser dies instead
of warning, which suits batch jobs that must not continue with partial data. The
message mentions CSV_XS, the backend doing the work. Loose quotes
can be accepted with the allow_loose_quotes option, but it is usually
better to fix the producer of the file.
$ perl errors.pl id | name | score 1 | Anna | 90 error 2034 in field 2 at position 9: EIF - Loose unescaped quote died: # CSV_XS ERROR: 2034 - EIF - Loose unescaped quote @ rec 2 pos 9 field 2
Generating a large file
Because getline reads one record at a time, a file of any size can
be processed with constant memory. To test this, we first generate a file with
a million sales records.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV;
my $rows = shift // 1_000_000;
my @cities = ('Bratislava', 'Kosice', 'Zilina', 'Nitra', 'Presov', 'Banska Bystrica');
srand 1;
my $csv = Text::CSV->new({ binary => 1, auto_diag => 1, eol => "\n" });
open my $fh, '>', 'sales.csv' or die "sales.csv: $!";
$csv->print($fh, [qw(id date city item amount)]);
for my $id (1 .. $rows) {
$csv->print($fh, [
$id,
sprintf('2026-%02d-%02d', 1 + int rand 12, 1 + int rand 28),
$cities[rand @cities],
"item, size " . (1 + int rand 5),
sprintf('%.2f', rand 500),
]);
}
close $fh or die "sales.csv: $!";
printf "wrote %d rows, %.1f MB\n", $rows, (-s 'sales.csv') / 1e6;
The generator writes random dates, cities, items, and amounts. The item field contains a comma, so every record needs quoting.
$ perl gen.pl wrote 1000000 rows, 49.0 MB
Streaming large files
The streaming version reads the records one by one and keeps only the running totals per city.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV;
use Time::HiRes qw(time);
sub peak_mb {
open my $st, '<', '/proc/self/status' or return 0;
while (<$st>) { return $1 / 1024 if /^VmHWM:\s+(\d+)/ }
return 0;
}
my $start = time;
my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
open my $fh, '<', 'sales.csv' or die "sales.csv: $!";
$csv->header($fh);
my %by_city;
my $count = 0;
while (my $r = $csv->getline_hr($fh)) {
$by_city{ $r->{city} } += $r->{amount};
$count++;
}
close $fh;
printf "%-16s %14.2f\n", $_, $by_city{$_}
for sort { $by_city{$b} <=> $by_city{$a} } keys %by_city;
printf "%d rows in %.1f s, peak memory %.0f MB\n", $count, time - $start, peak_mb();
The peak_mb function reads the peak resident memory of the process,
VmHWM, from the Linux /proc file system. The million
records are processed in about two seconds with a peak of a few megabytes,
the memory of the interpreter itself.
$ perl stream.pl Banska Bystrica 41798989.04 Nitra 41718723.80 Presov 41608325.96 Zilina 41605567.63 Bratislava 41586261.99 Kosice 41554585.74 1000000 rows in 2.3 s, peak memory 8 MB
Loading the whole file
For comparison, the same computation with the csv function, which
loads the whole file before processing it.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV qw(csv);
use Time::HiRes qw(time);
sub peak_mb {
open my $st, '<', '/proc/self/status' or return 0;
while (<$st>) { return $1 / 1024 if /^VmHWM:\s+(\d+)/ }
return 0;
}
my $start = time;
my $rows = csv(in => 'sales.csv', headers => 'auto');
my %by_city;
$by_city{ $_->{city} } += $_->{amount} for @$rows;
printf "%d rows in %.1f s, peak memory %.0f MB\n", scalar @$rows, time - $start, peak_mb();
The run takes about the same time, but the peak memory exceeds a gigabyte for a 49 MB file, because every record becomes a Perl hash with five keys. For large inputs, streaming is the only approach that scales.
$ perl slurp.pl 1000000 rows in 2.5 s, peak memory 1247 MB
Faster parsing with bind_columns
Creating a new hash or array for every record has its own cost. The
bind_columns method makes the parser store the fields directly in
existing variables.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Text::CSV;
use Time::HiRes qw(time);
my $start = time;
my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
open my $fh, '<', 'sales.csv' or die "sales.csv: $!";
$csv->getline($fh);
my ($id, $date, $city, $item, $amount);
$csv->bind_columns(\($id, $date, $city, $item, $amount));
my %by_city;
my $count = 0;
while ($csv->getline($fh)) {
$by_city{$city} += $amount;
$count++;
}
printf "%d rows in %.1f s using %s\n", $count, time - $start, Text::CSV->backend;
The method takes one reference per column, and the number of references must
match the number of fields. After binding, getline fills the
variables and returns a true value, so the loop tests it directly. The first
call reads and discards the header line. Skipping the per-record data structures
makes the run more than twice as fast as the getline_hr version.
The backend class method confirms that the XS implementation is
in use.
$ perl fast.pl 1000000 rows in 1.0 s using Text::CSV_XS
Best practices
- Never parse CSV with split: quoted fields with commas, quotes, or newlines break it.
- Always set binary: it is needed for non-ASCII text and for newlines inside fields.
- Turn on auto_diag: errors should be reported, not
silently end the loop; check
eofwhen handling them by hand. - Use encoding layers: open files with
:encoding(UTF-8)or passencodingtocsv. - Read by header names:
headerandgetline_hrmake code independent of the column order. - Stream large files: process records one at a time with
getline, and usebind_columnswhen speed matters; reservecsvfor files that fit comfortably in memory. - Install Text::CSV_XS:
Text::CSVuses it automatically, and it is many times faster than the pure-Perl backend.
In this article we have read CSV files with getline and
getline_hr, written them with print, changed
separators and quoting, used the csv function, handled parse
errors, and streamed a file of a million records with constant memory.
Source
Text::CSV on MetaCPAN · Text::CSV_XS · RFC 4180
Author
List all Perl tutorials.