String manipulation
Substring extraction
Programming
Code duplication
Text processing

Find string between two substrings

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Extracting text between two markers is a small task that shows up everywhere: log parsing, protocol decoding, templating, and scraping. The right solution depends on whether you need one match or many matches, and on how predictable the input format really is.

Start With Index-Based Extraction

If the input is simple and you need exactly one result, plain index lookup is usually the clearest option. It avoids regex complexity and makes missing-boundary handling explicit.

python
1def between(text: str, left: str, right: str) -> str | None:
2    start = text.find(left)
3    if start == -1:
4        return None
5
6    start += len(left)
7    end = text.find(right, start)
8    if end == -1:
9        return None
10
11    return text[start:end]
12
13message = "user=[alice];role=admin"
14print(between(message, "user=[", "]"))

This is often the best choice for configuration strings and other flat formats where the markers are fixed and non-nested.

Handle Missing Markers Deliberately

A lot of substring bugs come from slicing first and checking later. When either marker is missing, code that assumes both exist can silently produce the wrong substring or an empty value that looks valid.

A safer version returns None or raises a custom error:

python
1def between_or_fail(text: str, left: str, right: str) -> str:
2    value = between(text, left, right)
3    if value is None:
4        raise ValueError(f"Could not find text between {left!r} and {right!r}")
5    return value

That is especially useful when the parsed value drives downstream behavior such as authorization rules, object IDs, or file names.

Use Regex for Repeated Flat Patterns

When the same pair of delimiters occurs multiple times, regular expressions are often more convenient. The key detail is to use a non-greedy match so each capture stops at the nearest closing marker.

python
1import re
2
3text = "<id>12</id><id>99</id><id>42</id>"
4ids = re.findall(r"<id>(.*?)</id>", text)
5print(ids)

Without the ?, a greedy pattern could match from the first opening tag to the last closing tag, which is almost never what you want.

If the delimiters are dynamic, escape them before building the pattern:

python
def regex_between_all(text: str, left: str, right: str) -> list[str]:
    pattern = re.escape(left) + r"(.*?)" + re.escape(right)
    return re.findall(pattern, text)

This prevents punctuation in the delimiters from being misinterpreted as regex syntax.

JavaScript Follows the Same Tradeoff

In JavaScript, indexOf plus slice is the normal one-match solution.

javascript
1function between(text, left, right) {
2  const leftIndex = text.indexOf(left);
3  if (leftIndex === -1) return null;
4
5  const start = leftIndex + left.length;
6  const rightIndex = text.indexOf(right, start);
7  if (rightIndex === -1) return null;
8
9  return text.slice(start, rightIndex);
10}
11
12console.log(between("token=[abc123];ok=true", "token=[", "]"));

For many matches, use matchAll with a capture group:

javascript
const html = "<id>12</id><id>99</id>";
const values = [...html.matchAll(/<id>(.*?)<\/id>/g)].map(match => match[1]);
console.log(values);

The core logic is the same in both languages: simple index search for one segment, regex for repeated flat segments.

Know When Substring Logic Stops Being Enough

Delimiter-based extraction breaks down when the input can nest, escape, or overlap. HTML, JSON, XML with nested tags, or quoted text with escaped delimiters are classic examples. In those cases, a real parser is safer than repeated substring operations.

For example, this is not a good regex problem:

  • nested markup
  • balanced parentheses
  • CSV fields with embedded separators
  • JSON embedded in a larger payload

Once grammar matters, use a parser for that grammar instead of forcing everything through find and regex.

Choosing the Right Approach

A practical rule is:

  • one simple match: use index-based slicing
  • multiple simple matches: use non-greedy regex
  • nested or escaped structure: use a real parser

That keeps the solution proportional to the input complexity and reduces maintenance later.

Common Pitfalls

The most common mistake is using a greedy regex and capturing far more text than intended. Non-greedy matching is usually the right default for repeated delimiter pairs.

Another problem is not checking whether the left or right marker exists before slicing. That can produce misleading output instead of an obvious failure.

Developers also often use regex on nested formats because it works on a tiny sample. As soon as the structure becomes more realistic, the extraction logic becomes brittle.

Finally, if the delimiters come from user input, escape them before building regex patterns. Unescaped regex metacharacters can completely change the match behavior.

Summary

  • Use index lookup and slicing for one simple extraction between fixed markers.
  • Use non-greedy regex for repeated flat patterns.
  • Check for missing delimiters explicitly instead of slicing blindly.
  • Escape dynamic delimiters before turning them into regex patterns.
  • Switch to a proper parser when the input can nest or escape delimiters.

Course illustration
Course illustration

All Rights Reserved.