How to Extract URLs with Regex
Patterns that work, measured failures from the ones that do not, and a clear account of why no regular expression can fully validate a URL.
A regular expression is a good way to find URLs and a poor way to validate them. This guide shows both halves honestly: a pattern that works well enough, and measured evidence of where the popular ones break.
The pattern
https?://[^\s<>"'`]+That is it. It finds a scheme and then runs until it hits whitespace or a character a URL written in prose cannot contain.
It is intentionally simple because the interesting work happens afterwards. Trying to encode "where does this URL end" into the pattern itself is the mistake.
What the popular patterns actually do
These are real outputs, produced by running each pattern against the same inputs in Python 3.11.
| Input | `https?://\S+` | `https?://[\w.-]+` | Desired |
|---|---|---|---|
| Read https://example.com/pricing. | `…/pricing.` — keeps the full stop | `https://example.com` — loses the path | `…/pricing` |
| See https://en.example.org/wiki/Foo_(bar) now | `…/Foo_(bar)` ✓ | `https://en.example.org` — loses the path | `…/Foo_(bar)` |
| (https://example.com/terms) | `…/terms)` — keeps the bracket | `https://example.com` | `…/terms` |
| [docs](https://example.com/docs) | `…/docs)` — keeps the bracket | `https://example.com` | `…/docs` |
| https://example.com/s?q=a+b&sort=Desc#Top | whole address ✓ | `https://example.com` — loses query and fragment | whole address |
| https://example.com/a,https://example.com/b | both merged into one match | two matches, both truncated | two complete addresses |
Two clear lessons.
\S+ keeps too much. It is right about where the address starts and wrong about where it ends, every time punctuation follows.
[\w.-]+ keeps far too little. It stops at the first /, so you get bare hostnames and lose every path, query and fragment. This pattern is widespread in answers online, and in most of them it is quietly producing the wrong result.
The fix is not a bigger pattern
The natural next move is to bolt trailing-punctuation handling into the regex:
https?://[^\s<>"'`]+?(?=[.,;:!?]*(?:\s|$))That handles a trailing full stop. It still cannot handle brackets, because deciding whether a closing bracket belongs to the URL requires counting how many openers came before it — and counting is precisely what regular expressions cannot do. It is not a limitation of any particular engine; it is what "regular" means.
So: match generously, then post-process.
TRAILING = '.,;:!?\'"'
PAIRS = {')': '(', ']': '[', '}': '{'}
def trim(candidate: str) -> str:
while candidate:
last = candidate[-1]
if last in TRAILING:
candidate = candidate[:-1]
continue
if last in PAIRS and candidate.count(last) > candidate.count(PAIRS[last]):
candidate = candidate[:-1]
continue
break
return candidateRun against Python 3.11.15. Keeps `…/Foo_(bar)` intact while stripping the bracket from `(…/terms)` — the two cases no single pattern gets right.
The same idea in other languages
// JavaScript
const CANDIDATE = /https?:\/\/[^\s<>"'`]+/gi;
const found = text.match(CANDIDATE) ?? [];# Google Sheets (RE2 — no lookbehind, no backreferences)
=REGEXEXTRACT(A2, "https?://[^\s,;""'<>)\]]+")# grep
grep -Eo 'https?://[^[:space:]<>"'"'"']+' notes.txtValidation is a different job
Finding something URL-shaped is not the same as knowing it is a usable address. After matching, hand the candidate to a real parser:
- Python —
urllib.parse.urlparse - JavaScript —
new URL(value), which throws on invalid input - Go —
net/url.Parse - Java —
java.net.URI
The parser tells you the scheme, the host and whether the whole thing is coherent. It is also what keeps javascript: and data: out of a list you are about to render as links.
And no parser tells you the address works. That needs a request.
Frequently asked questions
Is there one correct URL regex?
No. There are patterns derived from RFC 3986 that match the grammar, and they are enormous, unreadable, and still cannot tell you where an address ends inside a sentence. For finding URLs in text, a simple pattern plus post-processing beats a complicated one.
Should I use regex on HTML?
No. Use a parser. An HTML parser handles attribute quoting, entities and malformed markup correctly; a regular expression handles the first two badly and the third not at all.
How do I match bare domains?
You cannot do it reliably with a pattern alone, because a pattern cannot know which suffixes are real. `main.py` and `shop.io` are indistinguishable by shape, and `.py` and `.io` are both real TLDs. Match the shape, then check it against the Public Suffix List.
Why does my pattern miss internationalised domains?
Because `\w` is ASCII-only in most engines by default. Either add a Unicode range, or match generously on "not whitespace" as above and let the parser handle the rest.
Sources
- RFC 3986 — Uniform Resource Identifier syntax — IETF
- WHATWG URL Standard — parsing — WHATWG
- RE2 syntax — Google
- OWASP — Regular expression denial of service — OWASP