Extract from a CSV Column

Write a regular expression over one column and every capture group in it becomes a column of its own. /api/(?<version>v\d+)/(?<resource>[a-z-]+) turns a log path into a version column and a resource column in one pass. Rows that do not match get blank cells and are counted. It runs in your browser, so nothing is uploaded.

Cutting on a delimiter rather than a pattern? Split a column instead.

A regex tester that writes columns

Most regular expression tools answer one question: does this pattern match this string? That is useful while you are writing the pattern and useless afterwards, because what you actually wanted was the pieces, arranged as columns, for every row in the file. Getting from one to the other normally means a script, a dataframe and half an hour.

This page collapses that. The pattern is the interface, the brackets say what you want, and the output is a table you can download. A reference like INV-2024-03-0091 yields a year and a month and a sequence. A URL yields a path segment and a query parameter. A free-text note yields the order number somebody buried inside it. A log line yields a status and a duration.

What makes it more than a convenience is the arithmetic it does while it works. It counts the rows that matched and the rows that did not, and it puts the miss rate in front of you before you can download anything, which is the number people skip when they do this by hand.

Worked example: two columns out of a request log

Three log rows, one of which is deliberately not an API call:

request_id,path
r-8891,/api/v2/orders?id=1204
r-8892,/api/v1/reports/monthly.csv
r-8893,/healthz

Set Column to read to path and type this into Pattern:

/api/(?<version>v\d+)/(?<resource>[a-z-]+)

The result:

request_id,path,path_version,path_resource
r-8891,/api/v2/orders?id=1204,v2,orders
r-8892,/api/v1/reports/monthly.csv,v1,reports
r-8893,/healthz,,

With this above it:

3 rows · extracted from "path" · 2 new columns
2 rows matched · 1 row did not match · original kept

1 row (33%) did not match the pattern, so their new cells are
blank. Check the pattern against one of those values before you
rely on this file.

Four things are worth noticing. The columns are called path_version and path_resource, taken from the group names in the pattern and prefixed with the source column so two extractions from two columns cannot collide. They are inserted directly to the right of path, in pattern order, not appended at the end. The /healthz row is still there with two empty cells, because dropping rows silently would be a worse answer than keeping them. And that 33% is the whole point of the exercise: on three rows it is obvious, on thirty thousand it is the difference between a correct file and one that quietly lost a third of its data.

Counting the groups without counting the brackets

The obvious way to work out how many columns a pattern produces is to count opening brackets. It is also wrong, in two ways that show up in ordinary patterns rather than in contrived ones.

(?:INV|ORD)-(\d{4})     brackets: 2   capture groups: 1
([(\[]\w+[)\]])         brackets: 3   capture groups: 1

The first is a non-capturing group, written (?:, which is what you use to say "one of these alternatives" without wanting a column for it. The second has brackets inside a character class, where they are literal characters rather than grouping. A bracket counter produces two extra empty columns in the first case and two in the second, and neither is easy to debug from the output.

So the count comes from the engine instead. The pattern is compiled, an alternation with an empty branch is appended so it is guaranteed to match, and it is run once against an empty string. The length of the resulting match array is the group count, straight from the same regular expression engine that will do the real work. It cannot disagree with itself.

Naming, and taking more than one match

Column names come from three places, in order. A comma separated list typed into New column names wins outright and is applied left to right. Failing that, a named group supplies the name, so (?<year>\d{4}) gives ref_year when the source column is ref. Failing both, the group number is used, giving ref_1 and ref_2. A name that would clash with a column already in the file gets a numeric suffix, so nothing is overwritten.

Take is set to The first match, which is what you want when the pattern describes the whole cell. Switch it to Every match when one cell holds several things: a note listing three order numbers, a field holding several email addresses, a path with repeating segments. In that mode the occurrence number is added to each name, so a single unnamed group over a note column produces note_1_1 and note_1_2, meaning group 1 of occurrence 1 and group 1 of occurrence 2.

The result is rectangular, so the number of columns is set by whichever cell had the most matches and shorter cells are padded with blanks on the right. That is where Max new columns earns its place. Left uncapped, one pathological cell in a large file could widen the table to hundreds of columns; the cap stops matching once it is reached, defaults to 12, and accepts anything from 1 to 50.

Patterns worth borrowing

Each of these is a starting point rather than a finished answer. Run it, look at the miss count, then tighten it.

Year and month from a reference
  (?<year>\d{4})-(?<month>\d{2})

An email address, as one column
  [\w.+-]+@[\w-]+\.[\w.]+

Area code and rest of a phone number
  \((?<area>\d{3})\)\s*(?<rest>[\d -]+)

A query parameter's value
  [?&]id=(?<id>[^&]+)

Amount and currency out of free text
  (?<amount>[\d,.]+)\s*(?<currency>[A-Z]{3})

The email pattern has no brackets in it at all, which is fine: with no capture groups the whole match becomes the single new column and it is named after the source column with _match on the end. Note also that Letter case defaults to Case-sensitive, so the currency pattern above will miss a lowercase usd until you switch it to Ignore case.

Practical notes

  • An invalid pattern says so. A stray bracket produces a plain message naming the problem rather than a file with something wrong in it.
  • Nothing matching at all gets its own warning. It reminds you that this is a regular expression and that a dot matches any character, which is the most common reason a pattern that looks right returns nothing.
  • Row order and row count never change. This tool only adds columns and optionally removes the one you read from. No filtering, no sorting, no deduplication.
  • Extracted values keep their exact text. A captured 007 is written as 007, because cells are strings from read to write and nothing here reinterprets them as numbers.
  • Leave the pattern empty to see the columns. With no pattern typed the file passes through unchanged and the column picker fills with the real names from your header row.
  • The download is a new file. requests.csv comes back as requests-extracted.csv.

Frequently Asked Questions

How do I decide how many new columns I get?

One per capture group in the pattern. A pattern with no groups at all still gives you one column holding whatever the pattern matched, named after the source column with _match on the end. The group count is worked out by running the pattern once as a probe rather than by counting brackets in the text, which means a non-capturing (?: ) group and a bracket inside a character class are both handled correctly.

Can I name the new columns?

Two ways. Use named groups in the pattern, so (?<year>\d{4}) produces a column called path_year, or type a comma separated list in New column names and those names are used in order. A typed name wins over a group name. Anything left over falls back to the source column name plus the group number.

What happens to rows the pattern does not match?

The row stays, the new cells are blank, and the row is counted. The count appears in the summary and again as a warning with the percentage, so a pattern that matched 60% of the column tells you it needs work before you download anything. Nothing is dropped and no row order changes.

What does Take Every match do?

It keeps looking after the first hit instead of stopping. A cell containing two four-digit numbers gives you two columns rather than one, with an occurrence number added to each name. The width of the result is set by whichever cell had the most matches, so cells with fewer end up with blanks on the right.

Why is there a Max new columns box?

Because a loose pattern in Every match mode can produce an absurd number of columns from one long cell, and a table three thousand columns wide is not a useful result. The cap is 12 by default and can be set anywhere from 1 to 50. Matching stops once the cap is reached.

Do I need to escape anything?

Yes, this is a real regular expression, not a wildcard search. A dot matches any character, and the characters ( ) [ ] + ? * { } | ^ $ need a backslash in front of them to be taken literally. If the pattern is not valid, an error appears saying so instead of a half-finished file.

Is this the same as splitting a column?

No, and the difference is worth knowing. Splitting cuts on a delimiter and keeps every piece, which is right for a comma separated list. Extracting names the pieces you want and ignores everything else, which is right for pulling a version and a resource out of a URL where the delimiter appears in places you do not care about.

Does the original column survive?

Yes by default. Original column is set to Keep it, and the new columns are inserted immediately to its right so you can compare them against the source. Set it to Remove it once the pattern is right and you no longer need the raw value.

Brackets in, columns out

Free, no account, no upload. Write the pattern, watch the miss count, take the CSV.

Back to the extractor