Describe the target first
Before writing a pattern, describe the goal in one sentence: "eleven digits starting with 1", "a date shaped like 2026-09-24", "two or more consecutive Chinese characters". Then translate piece by piece: digits become [0-9] or \d, repetition becomes {n}, literal characters stay as they are. Keep that sentence in a comment — the cheapest habit you can build, because in six months you will still know what it matches. Conversely, if the description is vague ("anything that looks like a URL"), the pattern will be wrong too.
The five building blocks
One, character classes: [0-9] is a digit, [a-zA-Z] a letter, [^0-9] "not a digit". Two, quantifiers: {3} exactly three, {2,5} two to five, + one or more, * zero or more, ? zero or one. Three, groups and captures: (abc) groups content so you can bind a quantifier or extract a part — (\d{3})-(\d{4}) separates area code from number. Four, anchors: ^ is line start and $ line end; without them, \d{11} happily matches eleven digits inside a twenty-digit string. Five, alternation: | means "one of these", and parentheses limit its scope — cat|dog and (cat|dog)s are entirely different patterns.
Greedy versus lazy
Quantifiers are greedy by default: <.*> runs from the first angle bracket to the last one, not to the nearest. Add a question mark to take the shortest match instead: <.*?>. This is the most common cause of "my replacement rewrote the whole line". The reverse lesson also applies: when the target genuinely runs to the end of the line, .* or [^\n]* is more accurate, so do not reflexively write .*? everywhere. When a match looks too long, suspect greediness first.
The g / i / m / s flags
g means global: without it a replacement only hits the first occurrence, which is exactly why "there is clearly a match but only one thing changed". i ignores case, handy for tag names and email domains. m makes ^ and $ match the start and end of each line rather than the whole text — essential for multi-line logs. s lets the dot match newlines too; many online testers leave it off, so cross-line patterns fail, and [\s\S] is the safer construction. Defaults differ between languages as well: Python’s ^ and $ behave differently from JavaScript’s in multi-line text.
Ready-made patterns
Mainland China mobile numbers: ^1[3-9]\d{9}$. Emails (good enough in practice): ^[\w.+-]+@[\w-]+\.[\w.-]+$. Dates: ^\d{4}-\d{2}-\d{2}$. URLs: ^https?:\/\/[^\s]+$. Chinese text (two or more characters): [\u4e00-\u9fa5]{2,}. Collapsing blank lines: replace \n{3,} with \n\n. Trimming trailing spaces: replace [ \t]+$ with nothing. Each one is "make it work, then tighten": a fully RFC-compliant email pattern is unreadable, and tightening to the actual requirement is enough.
Debugging and performance
Three debugging steps: confirm matches and groups on a few sample lines, run it against the full dataset, then check that the replacement did not over-reach. On performance, watch out for nested quantifiers — patterns like (a+)+b trigger catastrophic backtracking on long text, showing up as a frozen page or a script timeout; rewriting as (a+)b, adding anchors or narrowing the character range eases it sharply. And remember regex is not a universal tool: HTML, JSON and CSV need real parsers, because hand-rolled patterns break on nested tags and quoted commas.
想马上动手:在 正则测试工具 里贴样本、看高亮与分组,确认后在代码里落地;处理字符串前后差异用 文本对比,需要给正则转义 URL 参数时用 URL 编码。Try it now: paste a sample into the regex tester to check highlights and capture groups, compare results with the text diff, and escape parameters with URL encoding.
FAQ
Why did my replacement only apply once?
Almost always the missing g (global) flag. Without it only the first match is replaced. Also check the replacement string itself: $1 and $2 refer to capture groups, and a slip produces unexpected output.
How do I match Chinese characters?
[\u4e00-\u9fa5] covers one basic-block character and {2,} makes it two or more. It excludes extension blocks and punctuation; for full coverage use Unicode property syntax such as \p{Script=Han}.
Can regex parse HTML?
It should not. Nested markup makes patterns spiral out of control — use a parser instead. Regex suits short, well-formed text such as pulling a timestamp out of a log line.