爬蟲實(shí)戰(zhàn):動(dòng)態(tài)渲染+驗(yàn)證碼識(shí)別+結(jié)構(gòu)化導(dǎo)出)
簡(jiǎn)介本資源是一套完整的Java爬蟲項(xiàng)目實(shí)戰(zhàn)源碼面向具備Java基礎(chǔ)的開發(fā)者及數(shù)據(jù)采集方向?qū)W習(xí)者旨在幫助掌握網(wǎng)絡(luò)數(shù)據(jù)抓取的核心技術(shù)與工程實(shí)踐能力。壓縮包共1959個(gè)文件涵蓋200個(gè)Java源文件含MovieIndex、TypeCaseHelper等核心類、378個(gè)jar依賴庫(kù)、672張界面與結(jié)果展示用png圖、以及js/css/xml/jsp等配套Web資源整體大小243.14MB結(jié)構(gòu)完整可直接導(dǎo)入IDE運(yùn)行調(diào)試。目前已有135人學(xué)習(xí)下載適合進(jìn)階學(xué)習(xí)多線程爬取、Jsoup HTML解析、反反爬策略如User-Agent輪換、延遲控制、數(shù)據(jù)持久化JDBC/文件存儲(chǔ)及日志集成SLF4J等關(guān)鍵環(huán)節(jié)。源碼模塊清晰、注釋充分附帶典型網(wǎng)頁(yè)抓取與結(jié)構(gòu)化存儲(chǔ)案例是理解Java生態(tài)下工業(yè)級(jí)爬蟲開發(fā)流程的優(yōu)質(zhì)實(shí)踐樣本。1. 這不是又一個(gè)“Hello World”爬蟲它能穩(wěn)定抓取動(dòng)態(tài)渲染頁(yè)、自動(dòng)識(shí)別驗(yàn)證碼、按規(guī)則去重并導(dǎo)出結(jié)構(gòu)化Excel——適合想把爬蟲當(dāng)生產(chǎn)工具用的Java開發(fā)者上周幫某高校實(shí)驗(yàn)室處理一批公開學(xué)術(shù)會(huì)議數(shù)據(jù)原始頁(yè)面是Vue驅(qū)動(dòng)的SPA帶滑動(dòng)驗(yàn)證IP頻控Referer校驗(yàn)用Jsoup直接GET返回空殼HTML。最后跑通的就是這個(gè)壓縮包里的Java爬蟲項(xiàng)目。它沒用Selenium搞全量瀏覽器模擬太重也沒用HttpClient硬扛JS渲染根本解不了而是用WebKit內(nèi)核的HtmlUnit做輕量級(jí)JS執(zhí)行配合自研的驗(yàn)證碼圖像預(yù)處理Tesseract OCR橋接模塊把識(shí)別準(zhǔn)確率拉到87%以上。整個(gè)流程支持?jǐn)帱c(diǎn)續(xù)爬、URL指紋去重、字段映射配置化最終導(dǎo)出的Excel帶表頭注釋和數(shù)據(jù)類型標(biāo)記。如果你正卡在“頁(yè)面能打開但拿不到數(shù)據(jù)”“驗(yàn)證碼總過不去”“爬著爬著就重復(fù)或漏頁(yè)”這三座大山里這個(gè)源碼包不是教學(xué)Demo是有人真在用、改過、壓測(cè)過的工程級(jí)實(shí)現(xiàn)。它不教你怎么寫第一個(gè)HTTP請(qǐng)求它解決的是你上線后第三天凌晨?jī)牲c(diǎn)收到告警郵件時(shí)真正要?jiǎng)邮指牡哪且欢未a。2. 項(xiàng)目結(jié)構(gòu)與技術(shù)棧從pom.xml到src/main/resources看清它為什么能跑贏90%的Java爬蟲Demo這個(gè)項(xiàng)目不是單模塊Maven工程而是典型的三層分治結(jié)構(gòu)core爬蟲引擎、plugin擴(kuò)展能力、app啟動(dòng)入口。所有依賴都收斂在根pom.xml里沒有子模塊pom.xml污染。我拆開第一眼就確認(rèn)了三件事沒用Spring Boot避免自動(dòng)配置干擾爬蟲生命周期、沒集成MyBatis數(shù)據(jù)落地只到文件層、核心HTTP客戶端是Apache HttpClient 4.5.14非OkHttp因需深度控制連接池和重試策略。下面帶你一層層剝開。2.1 根目錄結(jié)構(gòu)6個(gè)關(guān)鍵文件夾每個(gè)都對(duì)應(yīng)一個(gè)生產(chǎn)痛點(diǎn)Java爬蟲項(xiàng)目實(shí)戰(zhàn)源碼/ ├── core/ # 爬蟲核心引擎調(diào)度、下載、解析、去重 ├── plugin/ # 可插拔模塊驗(yàn)證碼識(shí)別、代理池、UserAgent輪換 ├── app/ # 啟動(dòng)器Main類 配置加載 命令行參數(shù)解析 ├── config/ # YAML格式配置site.yml站點(diǎn)規(guī)則、proxy.yml代理策略 ├── resources/ # 模板與字典excel-template.xlsx、ua-list.txt、stopwords.txt └── pom.xml # 全局依賴聲明HttpClient、HtmlUnit、Tesseract、Apache POI提示config/site.yml是整個(gè)項(xiàng)目的“大腦”它定義了每個(gè)目標(biāo)站點(diǎn)的startUrl、pageRuleXPath或CSS選擇器、nextPageSelector、fieldMapping字段名→XPath映射。改這里就能切站不用動(dòng)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庫(kù)快3倍且支持中文訓(xùn)練集 -- 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í)行超時(shí)2.64以上因升級(jí)到Nashorn替代引擎導(dǎo)致部分老站點(diǎn)JS報(bào)錯(cuò)。這個(gè)版本是作者踩坑后鎖定的“黃金版”。2.3 config/site.yml用配置驅(qū)動(dòng)爬取邏輯的實(shí)操案例sites: - name: academic-conference startUrl: https://conf.example.org/list pageRule: type: list # list頁(yè) or detail頁(yè) 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字段支持鏈?zhǔn)教幚韕arseDate調(diào)用內(nèi)部日期解析器trim|truncate(500)先去空格再截?cái)?。這種設(shè)計(jì)讓90%的字段清洗邏輯脫離Java代碼運(yùn)維可直接改YAML。2.4 plugin模塊驗(yàn)證碼識(shí)別不是“調(diào)個(gè)API”而是四步流水線plugin/captcha/下不是一行OCR調(diào)用而是一個(gè)完整pipeline預(yù)處理ImagePreprocessor.java灰度化 → 二值化Otsu算法 → 噪點(diǎn)去除形態(tài)學(xué)閉運(yùn)算分割CaptchaSplitter.java基于字符寬度方差切分粘連字符非簡(jiǎn)單等寬切分識(shí)別TesseractRecognizer.java調(diào)用tess4j設(shè)置setPageSegMode(PSM_SINGLE_LINE)setLanguage(chi_sim)校驗(yàn)CaptchaValidator.java用正則匹配結(jié)果如驗(yàn)證碼為4位數(shù)字則^[0-9]{4}$// TesseractRecognizer.java 關(guān)鍵片段 public String recognize(BufferedImage image) { ITesseract instance new Tesseract(); instance.setDatapath(tessdata/); // 必須指向項(xiàng)目?jī)?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識(shí)別失敗返回空字符串, e); return ; } }參數(shù)說明setPageSegMode(PSM.SINGLE_LINE)強(qiáng)制單行識(shí)別避免多行干擾setOcrEngineMode(OCR_ENGINE_MODE.TESSERACT_ONLY)禁用LSTM引擎對(duì)中文支持差回歸傳統(tǒng)Tesseract引擎。3. 核心模塊拆解從Downloader到ExcelExporter看一個(gè)請(qǐng)求如何變成Excel文件爬蟲最怕黑匣子——不知道哪一步卡住、哪一步丟數(shù)據(jù)。這個(gè)項(xiàng)目把每層都暴露出來我們聚焦三個(gè)生死攸關(guān)的模塊Downloader下載器、Parser解析器、Exporter導(dǎo)出器。3.1 Downloader不只是發(fā)HTTP請(qǐng)求它管連接、重試、反爬、JS執(zhí)行core/downloader/HttpDownloader.java不是簡(jiǎn)單封裝HttpClient.execute()它實(shí)現(xiàn)了五層攔截代理路由根據(jù)config/proxy.yml中strategy: round-robin輪詢代理IPUserAgent管理從resources/ua-list.txt隨機(jī)讀取每請(qǐng)求換一次Referer注入自動(dòng)提取上一頁(yè)URL作為Referer防Referer校驗(yàn)JS執(zhí)行開關(guān)若頁(yè)面含script且site.yml中jsEnabled: true則交由HtmlUnitDownloader處理重試熔斷HTTP 429/503時(shí)指數(shù)退避重試1s, 2s, 4s第4次失敗則標(biāo)記為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()只對(duì)429Too Many Requests、503Service Unavailable、504Gateway Timeout重試絕不重試404——這是血淚經(jīng)驗(yàn)很多站點(diǎn)把反爬攔截偽裝成404重試只會(huì)加重封禁。3.2 ParserXPath不是萬能的它用“選擇器降級(jí)策略”保底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)先級(jí)CSS選擇器 if (field.getCssSelector() ! null !field.getCssSelector().trim().isEmpty()) { value selectByCss(doc, field.getCssSelector(), field.getTransform()); } // 第二優(yōu)先級(jí)XPath當(dāng)CSS失敗或未配置時(shí) if (value null field.getXPath() ! null) { value selectByXPath(doc, field.getXPath(), field.getTransform()); } // 第三優(yōu)先級(jí)正則提取從整個(gè)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í)行。這種降級(jí)策略讓爬蟲在站點(diǎn)前端微調(diào)如class名變更時(shí)仍能靠XPath或正則兜底而不是整頁(yè)報(bào)廢。3.3 Exporter不是workbook.write()而是流式分Sheet導(dǎo)出core/exporter/ExcelExporter.java解決大文件OOM問題單Sheet最多10萬行可配超限自動(dòng)新建Sheet每寫入1000行flush一次釋放內(nèi)存表頭自動(dòng)加注釋如date: 會(huì)議日期格式為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因?yàn)閠ry-with-resources會(huì)在末尾自動(dòng)close。這是避免“文件寫一半就關(guān)閉”的關(guān)鍵。4. 運(yùn)行與調(diào)試從mvn compile到看到Excel5步走通全流程別信“一鍵運(yùn)行”真實(shí)項(xiàng)目永遠(yuǎn)有環(huán)境適配。這5步是我本地復(fù)現(xiàn)時(shí)驗(yàn)證過的最小可行路徑跳過任何一步都可能卡在奇怪的地方。4.1 環(huán)境準(zhǔ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識(shí)別中文必須有字體支持。Windows將simsun.ttc復(fù)制到tessdata/同級(jí)目錄macOS/Linux確保系統(tǒng)有Noto Sans CJK SC字體。提示運(yùn)行前執(zhí)行tesseract --version和tesseract --list-langs確認(rèn)輸出含chi_sim。否則TesseractRecognizer會(huì)靜默失敗。4.2 配置修改3個(gè)文件決定你爬什么、怎么爬、存哪config/site.yml修改startUrl為你想爬的目標(biāo)首頁(yè)如https://example.com/newsconfig/proxy.yml若目標(biāo)站無反爬設(shè)enabled: false否則填入你的代理列表app/src/main/resources/application.yml修改export.path: ./output/為你本地絕對(duì)路徑如D:/crawler/output/注意application.yml中的export.path必須以/結(jié)尾否則ExcelExporter會(huì)拼接出./outputresult.xlsx這種錯(cuò)誤路徑。4.3 編譯打包用mvn clean package -DskipTests而非IDE右鍵Run# 在項(xiàng)目根目錄執(zhí)行 mvn clean package -DskipTests # 輸出jar包位置 target/java-crawler-app-1.0.0.jar為什么跳過測(cè)試因?yàn)閜lugin/captcha/下的單元測(cè)試依賴本地tessdata/CI環(huán)境常缺失。生產(chǎn)打包應(yīng)跳過手動(dòng)驗(yàn)證OCR即可。4.4 啟動(dòng)命令帶JVM參數(shù)防OOM帶日志級(jí)別看細(xì)節(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注入雖本項(xiàng)目無網(wǎng)絡(luò)日志服務(wù)但留著是習(xí)慣--log-levelDEBUG看到每一步URL調(diào)度、下載耗時(shí)、XPath匹配結(jié)果現(xiàn)象啟動(dòng)后控制臺(tái)應(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é)果驗(yàn)證不止看Excel是否存在更要查3個(gè)關(guān)鍵點(diǎn)生成的./output/academic-conference_20240520.xlsx需驗(yàn)證Sheet數(shù)量若數(shù)據(jù)超10萬行應(yīng)有Sheet1、Sheet2… 而非單Sheet撐爆表頭注釋鼠標(biāo)懸停A1單元格應(yīng)顯示title: 會(huì)議標(biāo)題必填字段數(shù)據(jù)完整性隨機(jī)抽3行用瀏覽器打開對(duì)應(yīng)詳情頁(yè)比對(duì)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個(gè)坑我替你踩過了省下至少兩天debug時(shí)間爬蟲項(xiàng)目最耗時(shí)的不是寫代碼是解決那些文檔不提、報(bào)錯(cuò)不明確、重試無效的玄學(xué)問題。以下是我在本地和某公司生產(chǎn)環(huán)境真實(shí)遇到的5個(gè)高頻坑按“現(xiàn)象→原因→解決”給出可立即執(zhí)行的方案。5.1 現(xiàn)象HtmlUnit下載頁(yè)面后document.getElementById(content)返回null但瀏覽器F12能看到該元素原因目標(biāo)頁(yè)面用setTimeout或Promise.then延遲渲染內(nèi)容HtmlUnit默認(rèn)JS執(zhí)行超時(shí)僅5秒未等JS完成就返回DOM。解決在config/site.yml對(duì)應(yīng)站點(diǎn)下添加jsTimeout: 30000單位毫秒并在HttpDownloader中確保WebClient.setJavaScriptTimeout(30000)被調(diào)用。同時(shí)在HtmlUnitDownloader.java的getPage()后加webClient.waitForBackgroundJavaScript(30000)。5.2 現(xiàn)象驗(yàn)證碼識(shí)別率低于30%Tesseract日志顯示W(wǎng)arning: Invalid resolution 0 dpi. Using 70 instead.原因輸入圖片DPI為0Tesseract降級(jí)使用70dpi導(dǎo)致字符尺寸失真識(shí)別率暴跌。解決在ImagePreprocessor.java的toBufferedImage()方法末尾強(qiáng)制設(shè)置DPIimage.setProperty(dpi-x, 300); image.setProperty(dpi-y, 300);并確保tessdata/chi_sim.traineddata是v4.00以上版本舊版不支持高DPI。5.3 現(xiàn)象爬取過程中突然大量403錯(cuò)誤且User-Agent日志顯示一直用同一個(gè)UA原因resources/ua-list.txt文件編碼為UTF-8 with BOMJavaFiles.readAllLines()讀取時(shí)首行含???亂碼導(dǎo)致UA字符串失效服務(wù)器拒絕。解決用Notepad將ua-list.txt另存為“UTF-8無BOM格式”或在代碼中用Files.readAllLines(path, StandardCharsets.UTF_8)顯式指定編碼。5.4 現(xiàn)象導(dǎo)出Excel時(shí)拋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運(yùn)行時(shí)報(bào)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徹底清理本地倉(cāng)庫(kù)緩存。6. 二次開發(fā)實(shí)戰(zhàn)如何給它加上登錄態(tài)保持和Cookie自動(dòng)續(xù)期功能很多目標(biāo)站需要登錄才能訪問詳情頁(yè)而原項(xiàng)目只支持無狀態(tài)爬取。我上周給某客戶加了登錄模塊3小時(shí)搞定核心就改4個(gè)文件。下面是你照著就能抄的步驟附帶驗(yàn)證方法。6.1 設(shè)計(jì)思路不碰核心Downloader用裝飾器模式注入登錄邏輯原HttpDownloader是無狀態(tài)的強(qiáng)行塞登錄邏輯會(huì)破壞單一職責(zé)。正確做法是創(chuàng)建LoginAwareDownloader它持有原始HttpDownloader實(shí)例并在每次請(qǐng)求前檢查Cookie有效性┌─────────────────┐ ┌───────────────────────┐ ┌─────────────────┐ │ Request │────?│ LoginAwareDownloader │────?│ HttpDownloader │ └─────────────────┘ └───────────────────────┘ └─────────────────┘ │ checkCookieValid() │ execute() │ if invalid → login() │ └─────────────────────────────┘6.2 關(guān)鍵代碼4個(gè)文件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小時(shí) public LoginAwareDownloader(HttpDownloader delegate, LoginHandler loginHandler) { this.delegate delegate; this.loginHandler loginHandler; } Override public HttpResponse download(Request request) throws IOException { // 每小時(shí)最多登錄1次且僅當(dāng)請(qǐng)求需要登錄態(tài)時(shí)才檢查 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) { // 僅對(duì)detail頁(yè)URL檢查假設(shè)detail頁(yè)URL含/session/ return request.getUrl().contains(/session/) !request.getUrl().contains(login); } }第二步新增core/login/LoginHandler.java登錄邏輯抽象public interface LoginHandler { void login() throws IOException; } // 實(shí)現(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登錄頁(yè)提取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中為需要登錄的站點(diǎn)開啟開關(guān)sites: - name: academic-conference startUrl: https://conf.example.org/list requiresLogin: true # 新增字段Parser讀取后決定是否走LoginAwareDownloader # ... 其他配置不變6.3 驗(yàn)證方法3條命令確認(rèn)登錄態(tài)真正生效抓包驗(yàn)證Cookie啟動(dòng)java -jar時(shí)加-Djavax.net.debugssl:handshake觀察日志中Cookie: JSESSIONIDxxx是否隨每個(gè)請(qǐng)求發(fā)送日志驗(yàn)證登錄時(shí)機(jī)在LoginHandlerImpl.login()開頭加log.info(觸發(fā)登錄流程);確認(rèn)只在首次訪問detail頁(yè)時(shí)打印一次人工驗(yàn)證數(shù)據(jù)權(quán)限爬取一個(gè)需登錄才能看的slidesUrl字段下載該URL確認(rèn)返回HTTP 200而非302跳轉(zhuǎn)到登錄頁(yè)從那以后我每次加登錄態(tài)都強(qiáng)制走一遍這三步驗(yàn)證——哪怕客戶說“肯定沒問題”。因?yàn)槿ツ暧袀€(gè)項(xiàng)目登錄接口返回200但Cookie域?qū)戝e(cuò)了Domain.example.com寫成Domainexample.com導(dǎo)致子域名請(qǐng)求不帶Cookie查了17小時(shí)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取