首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 正則表達式解析 HTML 實戰:從入門到反爬蟲對抗

     編輯:Java 程式設計入門

前言:正則解析 HTML 的爭議與現實

「不要用正則解析 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 結尾

關鍵技術陷阱與對策

1. 貪婪 vs 非貪婪:* vs *?


// 錯誤:貪婪匹配會吞噬多個 <li> 直到最後一個 </li>
Pattern.compile("<li>.+</li>")

// 正確:非貪婪,逐個匹配
Pattern.compile("<li>.+?</li>")
// 或使用零寬斷言(如示例),更穩健

2. 跨行匹配:Pattern.DOTALL vs [\s\S]

Java . 默認不匹配換行,Pattern.DOTALL 讓 . 匹配任意字符。但 [\s\S](空白字符 ∪ 非空白字符)無需標誌位即可匹配任意字符,且效能略優,推薦使用。

3. 編碼陷阱:HTTP 響應編碼 ≠ 頁面聲明編碼


// 強制指定 UTF-8(或根據 Content-Type / meta charset 動態檢測)
get.getParams().setContentCharset("UTF-8");
// 讀取時顯式指定
new InputStreamReader(is, StandardCharsets.UTF_8)

4. 反爬蟲基礎對抗

  • User-Agent 偽裝:get.setRequestHeader("User-Agent", "Mozilla/5.0 ...");
  • Cookie 維持:使用 HttpState 管理 Session
  • 請求頻率控制:Thread.sleep(random(1000, 3000))
  • 代理 IP 池:HttpClient 配置 HostConfiguration 代理
  • JS 渲染頁面:正則失效,需 Selenium / Playwright / Puppeteer

現代替代方案對比

方案優點缺點適用場景
正則零依賴、極快、內存極低脆弱、難維護、無法處理 JS固定結構片段提取、輕量腳本
JsoupjQuery 風格選擇器、容錯強、支援 CSS Selector需依賴、全量載入 DOM通用網頁解析、結構化數據提取
HtmlUnit無頭瀏覽器、支援 JS/AJAX重、啟動慢、資源佔用大動態渲染頁面、表單提交、E2E 測試
Selenium/Playwright真實瀏覽器環境、DevTools 協議極重、需驅動、維護成本高複雜 SPA、反爬對抗、截圖/PDF 生成

Jsoup 重寫示例(推薦生產使用)


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 片段。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved