「不要用正則解析 HTML」是程式設計界的金句,因為 HTML 結構複雜、巢狀層級深、屬性順序不固定、存在註釋/CDATA/腳本干擾。但實際工程中,針對固定結構、特定片段、輕量提取場景,正則依然是零依賴、啟動快、記憶體佔用極低的最佳選擇。本文將以抓取脚本之家(jb51.net)首頁列表為例,實戰演示正則提取鏈接、標題、圖片,並探討邊界處理與反爬應對。
<dependencies>
<!-- HttpClient 4.5.x(舊版,示例兼容) -->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<!-- 現代項目建議用 HttpClient 5 / OkHttp / WebClient -->
</dependencies>
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.regex.*;
import java.util.*;
public class HtmlRegexParser {
private static final String TARGET_URL = "https://www.jb51.net";
private static final Pattern FORM_PATTERN = Pattern.compile(
"<form name=\"compareForm\"[\\s\\S]+?</form>.*?<script",
Pattern.CASE_INSENSITIVE
);
private static final Pattern LI_PATTERN = Pattern.compile(
"(?<=<li>)[\\s\\S]+?(?=</li>)",
Pattern.CASE_INSENSITIVE
);
private static final Pattern URL_PATTERN = Pattern.compile("[a-z]+-[0-9]+\\.html");
private static final Pattern TITLE_PATTERN = Pattern.compile("(?<=title=\")[^\"]+(?=\")");
private static final Pattern IMG_PATTERN = Pattern.compile("images[^\"]*\\.jpg");
public static void main(String[] args) {
String html = fetchHtml(TARGET_URL);
if (html == null) return;
// 1. 定位目標區塊(比對原頁面結構調整)
Matcher formMatcher = FORM_PATTERN.matcher(html);
StringBuilder targetBlock = new StringBuilder();
while (formMatcher.find()) {
targetBlock.append(formMatcher.group());
}
// 2. 提取每個 <li> 內容
Matcher liMatcher = LI_PATTERN.matcher(targetBlock);
int count = 0;
while (liMatcher.find()) {
String liContent = liMatcher.group();
extractAndPrint(liContent);
count++;
}
System.out.println("共提取 " + count + " 條數據");
}
private static String fetchHtml(String url) {
HttpClient client = new HttpClient();
GetMethod get = new GetMethod(url);
get.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
get.getParams().setContentCharset("UTF-8");
try {
int status = client.executeMethod(get);
if (status != HttpStatus.SC_OK) {
System.err.println("HTTP 錯誤: " + get.getStatusLine());
return null;
}
try (InputStream is = get.getResponseBodyAsStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
return sb.toString();
}
} catch (HttpException | IOException e) {
System.err.println("網絡異常: " + e.getMessage());
e.printStackTrace();
return null;
} finally {
get.releaseConnection();
}
}
private static void extractAndPrint(String liHtml) {
Matcher mUrl = URL_PATTERN.matcher(liHtml);
Matcher mTitle = TITLE_PATTERN.matcher(liHtml);
Matcher mImg = IMG_PATTERN.matcher(liHtml);
String url = mUrl.find() ? mUrl.group() : "N/A";
String title = mTitle.find() ? mTitle.group() : "N/A";
String img = mImg.find() ? mImg.group() : "N/A";
System.out.printf("標題: %s | 鏈接: %s | 圖片: %s%n", title, url, img);
}
}
| 正則 | 用途 | 關鍵語法 | 說明 |
|---|---|---|---|
(?<=<li>)[\s\S]+?(?=</li>) | 提取 <li> 內容 | 正向前瞻/後顧、非貪婪 | 不消耗 <li> 標籤本身,[\s\S] 匹配含換行任意字符 |
[a-z]+-[0-9]+\.html | 匹配類似 article-123.html 的鏈接 | 字符類、量詞、轉義 | 針對該站點 URL 規律,通用性弱 |
(?<=title=\")[^\"]+(?=\") | 提取 title 屬性值 | 零寬斷言、排除字符類 | 避免匹配跨越雙引號,比 title=\"(.+?)\" 更精準 |
images[^\"]*\.jpg | 匹配圖片相對路徑 | 前綴錨定、排除引號 | 假設圖片路徑含 images 且以 .jpg 結尾 |
* vs *?
// 錯誤:貪婪匹配會吞噬多個 <li> 直到最後一個 </li>
Pattern.compile("<li>.+</li>")
// 正確:非貪婪,逐個匹配
Pattern.compile("<li>.+?</li>")
// 或使用零寬斷言(如示例),更穩健
Pattern.DOTALL vs [\s\S]Java . 默認不匹配換行,Pattern.DOTALL 讓 . 匹配任意字符。但 [\s\S](空白字符 ∪ 非空白字符)無需標誌位即可匹配任意字符,且效能略優,推薦使用。
// 強制指定 UTF-8(或根據 Content-Type / meta charset 動態檢測)
get.getParams().setContentCharset("UTF-8");
// 讀取時顯式指定
new InputStreamReader(is, StandardCharsets.UTF_8)
get.setRequestHeader("User-Agent", "Mozilla/5.0 ...");HttpState 管理 SessionThread.sleep(random(1000, 3000))HostConfiguration 代理| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 正則 | 零依賴、極快、內存極低 | 脆弱、難維護、無法處理 JS | 固定結構片段提取、輕量腳本 |
| Jsoup | jQuery 風格選擇器、容錯強、支援 CSS Selector | 需依賴、全量載入 DOM | 通用網頁解析、結構化數據提取 |
| HtmlUnit | 無頭瀏覽器、支援 JS/AJAX | 重、啟動慢、資源佔用大 | 動態渲染頁面、表單提交、E2E 測試 |
| Selenium/Playwright | 真實瀏覽器環境、DevTools 協議 | 極重、需驅動、維護成本高 | 複雜 SPA、反爬對抗、截圖/PDF 生成 |
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
public class JsoupParser {
public static void main(String[] args) throws IOException {
Document doc = Jsoup.connect("https://www.jb51.net")
.userAgent("Mozilla/5.0")
.timeout(10000)
.get();
Elements lis = doc.select("form[name=compareForm] ~ script").first()
.previousElementSibling() // 視具體結構調整
.select("li");
for (Element li : lis) {
String title = li.select("[title]").attr("title");
String url = li.select("a[href$=.html]").attr("href");
String img = li.select("img[src*=images]").attr("src");
System.out.printf("標題: %s | 鏈接: %s | 圖片: %s%n", title, url, img);
}
}
}
正則解析 HTML 是把雙刃劍:
工程建議:新項目首選 Jsoup,僅在受限環境(無法引入第三方庫、極簡 Shell 腳本、Android 無網絡權限解析本地 HTML)下使用正則,並嚴格編寫單元測試覆蓋各種邊界 HTML 片段。