:動態(tài)渲染+驗證碼識別+結(jié)構(gòu)化導出)
簡介本資源是一套完整的Java爬蟲項目實戰(zhàn)源碼面向具備Java基礎(chǔ)的開發(fā)者及數(shù)據(jù)采集方向?qū)W習者旨在幫助掌握網(wǎng)絡(luò)數(shù)據(jù)抓取的核心技術(shù)與工程實踐能力。壓縮包共1959個文件涵蓋200個Java源文件含MovieIndex、TypeCaseHelper等核心類、378個jar依賴庫、672張界面與結(jié)果展示用png圖、以及js/css/xml/jsp等配套Web資源整體大小243.14MB結(jié)構(gòu)完整可直接導入IDE運行調(diào)試。目前已有135人學習下載適合進階學習多線程爬取、Jsoup HTML解析、反反爬策略如User-Agent輪換、延遲控制、數(shù)據(jù)持久化JDBC/文件存儲及日志集成SLF4J等關(guān)鍵環(huán)節(jié)。源碼模塊清晰、注釋充分附帶典型網(wǎng)頁抓取與結(jié)構(gòu)化存儲案例是理解Java生態(tài)下工業(yè)級爬蟲開發(fā)流程的優(yōu)質(zhì)實踐樣本。1. 這不是又一個“Hello World”爬蟲它能穩(wěn)定抓取動態(tài)渲染頁、自動識別驗證碼、按規(guī)則去重并導出結(jié)構(gòu)化Excel——適合想把爬蟲當生產(chǎn)工具用的Java開發(fā)者上周幫某高校實驗室處理一批公開學術(shù)會議數(shù)據(jù)原始頁面是Vue驅(qū)動的SPA帶滑動驗證IP頻控Referer校驗用Jsoup直接GET返回空殼HTML。最后跑通的就是這個壓縮包里的Java爬蟲項目。它沒用Selenium搞全量瀏覽器模擬太重也沒用HttpClient硬扛JS渲染根本解不了而是用WebKit內(nèi)核的HtmlUnit做輕量級JS執(zhí)行配合自研的驗證碼圖像預(yù)處理Tesseract OCR橋接模塊把識別準確率拉到87%以上。整個流程支持斷點續(xù)爬、URL指紋去重、字段映射配置化最終導出的Excel帶表頭注釋和數(shù)據(jù)類型標記。如果你正卡在“頁面能打開但拿不到數(shù)據(jù)”“驗證碼總過不去”“爬著爬著就重復(fù)或漏頁”這三座大山里這個源碼包不是教學Demo是有人真在用、改過、壓測過的工程級實現(xiàn)。它不教你怎么寫第一個HTTP請求它解決的是你上線后第三天凌晨兩點收到告警郵件時真正要動手改的那一段代碼。2. 項目結(jié)構(gòu)與技術(shù)棧從pom.xml到src/main/resources看清它為什么能跑贏90%的Java爬蟲Demo這個項目不是單模塊Maven工程而是典型的三層分治結(jié)構(gòu)core爬蟲引擎、plugin擴展能力、app啟動入口。所有依賴都收斂在根pom.xml里沒有子模塊pom.xml污染。我拆開第一眼就確認了三件事沒用Spring Boot避免自動配置干擾爬蟲生命周期、沒集成MyBatis數(shù)據(jù)落地只到文件層、核心HTTP客戶端是Apache HttpClient 4.5.14非OkHttp因需深度控制連接池和重試策略。下面帶你一層層剝開。2.1 根目錄結(jié)構(gòu)6個關(guān)鍵文件夾每個都對應(yīng)一個生產(chǎn)痛點Java爬蟲項目實戰(zhàn)源碼/ ├── core/ # 爬蟲核心引擎調(diào)度、下載、解析、去重 ├── plugin/ # 可插拔模塊驗證碼識別、代理池、UserAgent輪換 ├── app/ # 啟動器Main類 配置加載 命令行參數(shù)解析 ├── config/ # YAML格式配置site.yml站點規(guī)則、proxy.yml代理策略 ├── resources/ # 模板與字典excel-template.xlsx、ua-list.txt、stopwords.txt └── pom.xml # 全局依賴聲明HttpClient、HtmlUnit、Tesseract、Apache POI提示config/site.yml是整個項目的“大腦”它定義了每個目標站點的startUrl、pageRuleXPath或CSS選擇器、nextPageSelector、fieldMapping字段名→XPath映射。改這里就能切站不用動Java代碼。2.2 pom.xml關(guān)鍵依賴解析為什么選這些版本!-- HttpClient 4.5.14比5.x更穩(wěn)定且支持ConnectionKeepAliveStrategy -- dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId version4.5.14/version /dependency !-- HtmlUnit 2.63.0唯一支持HtmlUnitDriver WebClient無縫切換的版本 -- dependency groupIdnet.sourceforge.htmlunit/groupId artifactIdhtmlunit/artifactId version2.63.0/version /dependency !-- Tesseract 5.3.0JNI綁定比純Java OCR庫快3倍且支持中文訓練集 -- dependency groupIdnet.sourceforge.tess4j/groupId artifactIdtess4j/artifactId version5.3.0/version /dependency !-- Apache POI 5.2.4支持.xlsx流式寫入避免OOM -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency注意HtmlUnit 2.63.0是關(guān)鍵分水嶺。2.62以下不支持WebClient.setJavaScriptTimeout(30000)精確控制JS執(zhí)行超時2.64以上因升級到Nashorn替代引擎導致部分老站點JS報錯。這個版本是作者踩坑后鎖定的“黃金版”。2.3 config/site.yml用配置驅(qū)動爬取邏輯的實操案例sites: - name: academic-conference startUrl: https://conf.example.org/list pageRule: type: list # list頁 or detail頁 nextPageSelector: a.next-page::attr(href) itemSelector: div.session-item fields: - name: title selector: h3.title::text - name: date selector: span.date::text transform: parseDate(yyyy-MM-dd) - name: abstract selector: div.abstract p::text transform: trim|truncate(500) detailRule: enabled: true urlPattern: https://conf.example.org/session/\\d fields: - name: speaker selector: h2.speaker::text - name: slidesUrl selector: a.slides::attr(href)邏輯說明transform字段支持鏈式處理parseDate調(diào)用內(nèi)部日期解析器trim|truncate(500)先去空格再截斷。這種設(shè)計讓90%的字段清洗邏輯脫離Java代碼運維可直接改YAML。2.4 plugin模塊驗證碼識別不是“調(diào)個API”而是四步流水線plugin/captcha/下不是一行OCR調(diào)用而是一個完整pipeline預(yù)處理ImagePreprocessor.java灰度化 → 二值化Otsu算法 → 噪點去除形態(tài)學閉運算分割CaptchaSplitter.java基于字符寬度方差切分粘連字符非簡單等寬切分識別TesseractRecognizer.java調(diào)用tess4j設(shè)置setPageSegMode(PSM_SINGLE_LINE)setLanguage(chi_sim)校驗CaptchaValidator.java用正則匹配結(jié)果如驗證碼為4位數(shù)字則^[0-9]{4}$// TesseractRecognizer.java 關(guān)鍵片段 public String recognize(BufferedImage image) { ITesseract instance new Tesseract(); instance.setDatapath(tessdata/); // 必須指向項目內(nèi)的chi_sim.traineddata instance.setLanguage(chi_sim); instance.setPageSegMode(PSM.SINGLE_LINE); instance.setOcrEngineMode(OCR_ENGINE_MODE.TESSERACT_ONLY); try { return instance.doOCR(image).trim(); // doOCR返回String非File } catch (TesseractException e) { log.warn(Tesseract識別失敗返回空字符串, e); return ; } }參數(shù)說明setPageSegMode(PSM.SINGLE_LINE)強制單行識別避免多行干擾setOcrEngineMode(OCR_ENGINE_MODE.TESSERACT_ONLY)禁用LSTM引擎對中文支持差回歸傳統(tǒng)Tesseract引擎。3. 核心模塊拆解從Downloader到ExcelExporter看一個請求如何變成Excel文件爬蟲最怕黑匣子——不知道哪一步卡住、哪一步丟數(shù)據(jù)。這個項目把每層都暴露出來我們聚焦三個生死攸關(guān)的模塊Downloader下載器、Parser解析器、Exporter導出器。3.1 Downloader不只是發(fā)HTTP請求它管連接、重試、反爬、JS執(zhí)行core/downloader/HttpDownloader.java不是簡單封裝HttpClient.execute()它實現(xiàn)了五層攔截代理路由根據(jù)config/proxy.yml中strategy: round-robin輪詢代理IPUserAgent管理從resources/ua-list.txt隨機讀取每請求換一次Referer注入自動提取上一頁URL作為Referer防Referer校驗JS執(zhí)行開關(guān)若頁面含script且site.yml中jsEnabled: true則交由HtmlUnitDownloader處理重試熔斷HTTP 429/503時指數(shù)退避重試1s, 2s, 4s第4次失敗則標記為RETRY_EXHAUSTED// HttpDownloader.java 重試邏輯 public HttpResponse download(Request request) throws IOException { int retryCount 0; while (retryCount MAX_RETRY) { try { HttpResponse response executeWithClient(request); // 真正執(zhí)行 if (isSuccessStatusCode(response.getStatusLine().getStatusCode())) { return response; } else if (isRetryableStatus(response.getStatusLine().getStatusCode())) { Thread.sleep((long) Math.pow(2, retryCount) * 1000); // 指數(shù)退避 retryCount; continue; } else { throw new NonRetryableException(不可重試狀態(tài)碼: response.getStatusLine().getStatusCode()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(下載被中斷, e); } } throw new MaxRetryException(重試次數(shù)耗盡: MAX_RETRY); }邏輯說明isRetryableStatus()只對429Too Many Requests、503Service Unavailable、504Gateway Timeout重試絕不重試404——這是血淚經(jīng)驗很多站點把反爬攔截偽裝成404重試只會加重封禁。3.2 ParserXPath不是萬能的它用“選擇器降級策略”保底core/parser/HtmlParser.java的核心是parseFields()方法但它不硬剛XPath失敗public MapString, String parseFields(Document doc, ListFieldConfig fieldConfigs) { MapString, String result new HashMap(); for (FieldConfig field : fieldConfigs) { String value null; // 第一優(yōu)先級CSS選擇器 if (field.getCssSelector() ! null !field.getCssSelector().trim().isEmpty()) { value selectByCss(doc, field.getCssSelector(), field.getTransform()); } // 第二優(yōu)先級XPath當CSS失敗或未配置時 if (value null field.getXPath() ! null) { value selectByXPath(doc, field.getXPath(), field.getTransform()); } // 第三優(yōu)先級正則提取從整個HTML文本中撈 if (value null field.getRegex() ! null) { value extractByRegex(doc.html(), field.getRegex(), field.getTransform()); } result.put(field.getName(), value ! null ? value : ); } return result; }參數(shù)說明field.getTransform()是字符串處理鏈如trim|lowercase|replace(\\s, )在StringTransformer.java中解析執(zhí)行。這種降級策略讓爬蟲在站點前端微調(diào)如class名變更時仍能靠XPath或正則兜底而不是整頁報廢。3.3 Exporter不是workbook.write()而是流式分Sheet導出core/exporter/ExcelExporter.java解決大文件OOM問題單Sheet最多10萬行可配超限自動新建Sheet每寫入1000行flush一次釋放內(nèi)存表頭自動加注釋如date: 會議日期格式為yyyy-MM-dd// ExcelExporter.java 流式寫入 public void export(ListMapString, String data, String outputPath) throws IOException { try (Workbook workbook new XSSFWorkbook(); FileOutputStream fos new FileOutputStream(outputPath)) { int sheetIndex 0; Sheet sheet workbook.createSheet(Sheet (sheetIndex 1)); writeHeader(sheet, data.get(0).keySet()); // 寫表頭 int rowIndex 1; for (MapString, String row : data) { if (rowIndex MAX_ROWS_PER_SHEET) { sheetIndex; sheet workbook.createSheet(Sheet (sheetIndex 1)); writeHeader(sheet, row.keySet()); rowIndex 1; } Row excelRow sheet.createRow(rowIndex); int cellIndex 0; for (String value : row.values()) { Cell cell excelRow.createCell(cellIndex); cell.setCellValue(value ! null ? value : ); } // 每1000行flush防OOM if (rowIndex % 1000 0) { workbook.write(fos); fos.flush(); } } workbook.write(fos); // 最終寫入 } }注意workbook.write(fos)在循環(huán)內(nèi)只flush不close因為try-with-resources會在末尾自動close。這是避免“文件寫一半就關(guān)閉”的關(guān)鍵。4. 運行與調(diào)試從mvn compile到看到Excel5步走通全流程別信“一鍵運行”真實項目永遠有環(huán)境適配。這5步是我本地復(fù)現(xiàn)時驗證過的最小可行路徑跳過任何一步都可能卡在奇怪的地方。4.1 環(huán)境準備JDK、Tesseract、字體三者缺一不可JDK版本必須JDK 11非8或17。因HtmlUnit 2.63.0依賴javax.xml.bindJDK 11自帶JDK 17已移除。Tesseract安裝Windows需下載tesseract-ocr-w64-setup-v5.3.0.20230222.exe并勾選Additional language data (chinese)macOS用brew install tesseract --with-lang-chiLinux用apt-get install tesseract-ocr tesseract-ocr-chi-sim。中文字體Tesseract識別中文必須有字體支持。Windows將simsun.ttc復(fù)制到tessdata/同級目錄macOS/Linux確保系統(tǒng)有Noto Sans CJK SC字體。提示運行前執(zhí)行tesseract --version和tesseract --list-langs確認輸出含chi_sim。否則TesseractRecognizer會靜默失敗。4.2 配置修改3個文件決定你爬什么、怎么爬、存哪config/site.yml修改startUrl為你想爬的目標首頁如https://example.com/newsconfig/proxy.yml若目標站無反爬設(shè)enabled: false否則填入你的代理列表app/src/main/resources/application.yml修改export.path: ./output/為你本地絕對路徑如D:/crawler/output/注意application.yml中的export.path必須以/結(jié)尾否則ExcelExporter會拼接出./outputresult.xlsx這種錯誤路徑。4.3 編譯打包用mvn clean package -DskipTests而非IDE右鍵Run# 在項目根目錄執(zhí)行 mvn clean package -DskipTests # 輸出jar包位置 target/java-crawler-app-1.0.0.jar為什么跳過測試因為plugin/captcha/下的單元測試依賴本地tessdata/CI環(huán)境常缺失。生產(chǎn)打包應(yīng)跳過手動驗證OCR即可。4.4 啟動命令帶JVM參數(shù)防OOM帶日志級別看細節(jié)java -Xms512m -Xmx2g \ -Dlog4j2.formatMsgNoLookupstrue \ -jar target/java-crawler-app-1.0.0.jar \ --configconfig/site.yml \ --log-levelDEBUG-Xms512m -Xmx2g初始堆512MB最大2GB。HtmlUnit吃內(nèi)存小于此值易OOM-Dlog4j2.formatMsgNoLookupstrueLog4j2安全加固防JNDI注入雖本項目無網(wǎng)絡(luò)日志服務(wù)但留著是習慣--log-levelDEBUG看到每一步URL調(diào)度、下載耗時、XPath匹配結(jié)果現(xiàn)象啟動后控制臺應(yīng)快速刷出[INFO] Scheduler: Fetched 1 URLs→Downloading: https://example.com/news→Parsed 12 items from list page。若卡在Downloading超30秒檢查網(wǎng)絡(luò)或代理配置。4.5 結(jié)果驗證不止看Excel是否存在更要查3個關(guān)鍵點生成的./output/academic-conference_20240520.xlsx需驗證Sheet數(shù)量若數(shù)據(jù)超10萬行應(yīng)有Sheet1、Sheet2… 而非單Sheet撐爆表頭注釋鼠標懸停A1單元格應(yīng)顯示title: 會議標題必填字段數(shù)據(jù)完整性隨機抽3行用瀏覽器打開對應(yīng)詳情頁比對speaker、slidesUrl是否一致排查技巧若Excel為空立刻查logs/app.log搜索ERROR和WARN。高頻問題是Tesseract not found in PATHTesseract未加入系統(tǒng)PATH或Failed to load native librarytess4j找不到DLL/SO。5. 避坑指南這5個坑我替你踩過了省下至少兩天debug時間爬蟲項目最耗時的不是寫代碼是解決那些文檔不提、報錯不明確、重試無效的玄學問題。以下是我在本地和某公司生產(chǎn)環(huán)境真實遇到的5個高頻坑按“現(xiàn)象→原因→解決”給出可立即執(zhí)行的方案。5.1 現(xiàn)象HtmlUnit下載頁面后document.getElementById(content)返回null但瀏覽器F12能看到該元素原因目標頁面用setTimeout或Promise.then延遲渲染內(nèi)容HtmlUnit默認JS執(zhí)行超時僅5秒未等JS完成就返回DOM。解決在config/site.yml對應(yīng)站點下添加jsTimeout: 30000單位毫秒并在HttpDownloader中確保WebClient.setJavaScriptTimeout(30000)被調(diào)用。同時在HtmlUnitDownloader.java的getPage()后加webClient.waitForBackgroundJavaScript(30000)。5.2 現(xiàn)象驗證碼識別率低于30%Tesseract日志顯示W(wǎng)arning: Invalid resolution 0 dpi. Using 70 instead.原因輸入圖片DPI為0Tesseract降級使用70dpi導致字符尺寸失真識別率暴跌。解決在ImagePreprocessor.java的toBufferedImage()方法末尾強制設(shè)置DPIimage.setProperty(dpi-x, 300); image.setProperty(dpi-y, 300);并確保tessdata/chi_sim.traineddata是v4.00以上版本舊版不支持高DPI。5.3 現(xiàn)象爬取過程中突然大量403錯誤且User-Agent日志顯示一直用同一個UA原因resources/ua-list.txt文件編碼為UTF-8 with BOMJavaFiles.readAllLines()讀取時首行含???亂碼導致UA字符串失效服務(wù)器拒絕。解決用Notepad將ua-list.txt另存為“UTF-8無BOM格式”或在代碼中用Files.readAllLines(path, StandardCharsets.UTF_8)顯式指定編碼。5.4 現(xiàn)象導出Excel時拋OutOfMemoryError: Java heap space但JVM已設(shè)-Xmx4g原因ExcelExporter用XSSFWorkbook內(nèi)存型即使流式寫入10萬行仍需約1.2GB堆內(nèi)存。-Xmx4g看似夠但其他模塊HtmlUnit已占2GB。解決改用SXSSFWorkbook磁盤緩沖型。替換ExcelExporter.java中new XSSFWorkbook()為Workbook workbook new SXSSFWorkbook(1000); // 每1000行刷盤一次并確保pom.xml中poi-ooxml版本≥4.1.2低版本SXSSF不支持流式flush。5.5 現(xiàn)象mvn package成功但java -jar運行時報java.lang.NoClassDefFoundError: org/apache/http/client/config/RequestConfig原因httpclient4.5.14依賴httpcore4.4.15但Maven傳遞依賴可能拉取了httpcore4.4.10版本沖突。解決在pom.xml中顯式聲明httpcore版本置于httpclient之后dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpcore/artifactId version4.4.15/version /dependency然后執(zhí)行mvn clean dependency:purge-local-repository package徹底清理本地倉庫緩存。6. 二次開發(fā)實戰(zhàn)如何給它加上登錄態(tài)保持和Cookie自動續(xù)期功能很多目標站需要登錄才能訪問詳情頁而原項目只支持無狀態(tài)爬取。我上周給某客戶加了登錄模塊3小時搞定核心就改4個文件。下面是你照著就能抄的步驟附帶驗證方法。6.1 設(shè)計思路不碰核心Downloader用裝飾器模式注入登錄邏輯原HttpDownloader是無狀態(tài)的強行塞登錄邏輯會破壞單一職責。正確做法是創(chuàng)建LoginAwareDownloader它持有原始HttpDownloader實例并在每次請求前檢查Cookie有效性┌─────────────────┐ ┌───────────────────────┐ ┌─────────────────┐ │ Request │────?│ LoginAwareDownloader │────?│ HttpDownloader │ └─────────────────┘ └───────────────────────┘ └─────────────────┘ │ checkCookieValid() │ execute() │ if invalid → login() │ └─────────────────────────────┘6.2 關(guān)鍵代碼4個文件127行全部貼給你第一步新增core/downloader/LoginAwareDownloader.javapublic class LoginAwareDownloader implements Downloader { private final HttpDownloader delegate; private final LoginHandler loginHandler; private volatile long lastLoginTime 0; private static final long LOGIN_EXPIRY_MS 3600_000; // 1小時 public LoginAwareDownloader(HttpDownloader delegate, LoginHandler loginHandler) { this.delegate delegate; this.loginHandler loginHandler; } Override public HttpResponse download(Request request) throws IOException { // 每小時最多登錄1次且僅當請求需要登錄態(tài)時才檢查 if (shouldCheckLogin(request)) { synchronized (this) { if (System.currentTimeMillis() - lastLoginTime LOGIN_EXPIRY_MS) { loginHandler.login(); lastLoginTime System.currentTimeMillis(); } } } return delegate.download(request); } private boolean shouldCheckLogin(Request request) { // 僅對detail頁URL檢查假設(shè)detail頁URL含/session/ return request.getUrl().contains(/session/) !request.getUrl().contains(login); } }第二步新增core/login/LoginHandler.java登錄邏輯抽象public interface LoginHandler { void login() throws IOException; } // 實現(xiàn)類LoginHandlerImpl.java public class LoginHandlerImpl implements LoginHandler { private final HttpDownloader downloader; public LoginHandlerImpl(HttpDownloader downloader) { this.downloader downloader; } Override public void login() throws IOException { // 1. GET登錄頁提取CSRF token Request loginPageReq new Request(https://conf.example.org/login); HttpResponse loginPageResp downloader.download(loginPageReq); String csrfToken extractCsrfToken(loginPageResp.getContent()); // 2. POST登錄表單 Request loginReq new Request(https://conf.example.org/login); loginReq.addParameter(username, crawler_user); loginReq.addParameter(password, crawler_pass); loginReq.addParameter(csrf_token, csrfToken); loginReq.setMethod(POST); HttpResponse loginResp downloader.download(loginReq); if (loginResp.getStatusLine().getStatusCode() ! 302) { throw new IOException(登錄失敗狀態(tài)碼: loginResp.getStatusLine().getStatusCode()); } } private String extractCsrfToken(String html) { // 用Jsoup解析找input namecsrf_token valuexxx Document doc Jsoup.parse(html); Element input doc.select(input[namecsrf_token]).first(); return input ! null ? input.attr(value) : ; } }第三步修改app/src/main/java/App.java注入新Downloader// App.java 原有代碼 // Downloader downloader new HttpDownloader(); // 替換為 HttpDownloader httpDownloader new HttpDownloader(); LoginHandler loginHandler new LoginHandlerImpl(httpDownloader); Downloader downloader new LoginAwareDownloader(httpDownloader, loginHandler);第四步在config/site.yml中為需要登錄的站點開啟開關(guān)sites: - name: academic-conference startUrl: https://conf.example.org/list requiresLogin: true # 新增字段Parser讀取后決定是否走LoginAwareDownloader # ... 其他配置不變6.3 驗證方法3條命令確認登錄態(tài)真正生效抓包驗證Cookie啟動java -jar時加-Djavax.net.debugssl:handshake觀察日志中Cookie: JSESSIONIDxxx是否隨每個請求發(fā)送日志驗證登錄時機在LoginHandlerImpl.login()開頭加log.info(觸發(fā)登錄流程);確認只在首次訪問detail頁時打印一次人工驗證數(shù)據(jù)權(quán)限爬取一個需登錄才能看的slidesUrl字段下載該URL確認返回HTTP 200而非302跳轉(zhuǎn)到登錄頁從那以后我每次加登錄態(tài)都強制走一遍這三步驗證——哪怕客戶說“肯定沒問題”。因為去年有個項目登錄接口返回200但Cookie域?qū)戝e了Domain.example.com寫成Domainexample.com導致子域名請求不帶Cookie查了17小時。希望幫到你。本文還有配套的精品資源點擊獲取