Java爬虫实战:HttpClient模拟登录绕过验证,Cookie与Token会话管理详解

发布时间:2026/8/28 10:23:03
Java爬虫实战:HttpClient模拟登录绕过验证,Cookie与Token会话管理详解 1. 项目缘起当爬虫遇到登录墙做数据采集的朋友十有八九都遇到过这个坎儿目标网站的数据就在那里清晰可见但中间横着一道登录墙。浏览器里手动登录一下一切畅通无阻可一旦换成程序去抓取返回的不是401 Unauthorized就是403 Forbidden再不然就是一堆让你验证身份的Cookie和Session。这堵“墙”的本质是网站为了保护用户数据和服务器资源在特定页面尤其是用户个人中心、后台管理、付费内容等前设置的身份验证关卡。用Java写爬虫绕过登录听起来像是个“灰色”操作但其实在合法合规的范围内比如爬取自己公司内部需要登录才能查看的报表、自动化测试需要登录状态的页面、或者研究公开API的调用流程时这是一个非常实际且高频的技术需求。它考验的不是“黑科技”而是你对HTTP协议、会话管理以及目标网站验证逻辑的透彻理解。很多人一上来就找Selenium模拟浏览器虽然省事但资源消耗大、速度慢容易被反爬策略识别。而更优雅、更接近本质的做法是直接使用HttpClient、OkHttp这类库去模拟一次完整的登录过程拿到那个代表“已登录”状态的令牌通常是Cookie或Token然后用这个令牌去请求后续的数据接口。今天我就以一个资深Java开发者的视角抛开那些花哨的框架和复杂的封装带你从HTTP协议最底层开始一步步拆解如何用Java实现绕过登录界面直接获取目标数据。我们会聚焦于最核心、最通用的技术方案并分享我在实际项目中踩过的坑和总结的经验让你不仅能写出能跑的代码更能理解其背后的每一个“为什么”。2. 核心原理会话、Cookie与Token的攻防战在动手写代码之前我们必须把“登录”这件事在HTTP层面的运作机制彻底搞明白。HTTP协议本身是无状态的这意味着服务器无法自动区分两次请求是否来自同一个用户。登录机制就是为了在无状态的HTTP之上构建出一个“有状态”的会话。2.1 经典会话管理Cookie-Session机制这是最传统、也最普遍的方案。其流程可以概括为“一问一答一凭证”客户端发起登录请求用户在前端页面输入用户名和密码点击登录。浏览器会向服务器的登录接口例如/login发送一个POST请求请求体中携带账号密码等凭证。服务器验证并创建会话服务器验证凭证通过后会在内存或数据库中创建一个Session对象用于存储该用户的登录状态和信息如用户ID、权限。这个Session有一个全局唯一的ID称为Session ID。服务器下发凭证服务器在返回的HTTP响应头中通过Set-Cookie字段将这个Session ID发送给客户端。最常见的Cookie名就是JSESSIONIDJava EE标准或PHPSESSIDPHP等。客户端携带凭证浏览器收到这个Cookie后会将其保存起来。之后在向同一域名下的任何页面或接口发起请求时浏览器都会自动在请求头中通过Cookie字段将这个Session ID回传给服务器。服务器识别用户服务器收到请求后从Cookie中取出Session ID去查找对应的Session对象。如果找到且未过期就认为该请求来自已登录的用户允许访问受保护资源。注意这里有一个关键点爬虫程序要模拟的就是第4步。我们不需要关心Session在服务器端如何存储只需要在代码中像浏览器一样妥善保存登录成功后服务器下发的Cookie并在后续请求中主动、正确地携带它。2.2 现代认证方案Token机制如JWT随着前后端分离和分布式架构的流行基于Token尤其是JWT的认证方式越来越普遍。它与Cookie-Session的核心区别在于状态存储的位置。客户端发起登录请求同样发送账号密码到登录接口。服务器验证并签发Token服务器验证通过后不再创建服务器端的Session而是使用密钥生成一个Token通常是一个加密的字符串如JWT其中直接编码了用户身份信息Payload和签名。然后将这个Token放在响应体Response Body中返回给客户端。客户端保存Token客户端前端或我们的爬虫程序需要自己保存这个Token通常放在内存或本地存储。客户端携带Token在后续请求中客户端需要手动将Token添加到请求头中最常见的字段是Authorization: Bearer your_token。服务器验证Token服务器收到请求后从Authorization头中取出Token验证其签名和有效期。验证通过即认为用户已登录。实操心得对于爬虫开发者识别目标网站使用哪种机制至关重要。最直接的方法是使用浏览器的开发者工具F12观察登录请求的响应头如果有Set-Cookie且后续请求的请求头中有对应的Cookie那就是Cookie-Session。观察登录请求的响应体如果返回了一个长长的、由点号分隔的三段式字符串xxxxx.yyyyy.zzzzz那很可能是JWT。后续请求的请求头中会看到Authorization: Bearer ...。有些网站会混合使用比如用Cookie做会话保持用Token做API授权。我们的策略是响应里给什么我们就存什么请求里要什么我们就带什么。3. 工具选型为什么是HttpClientJava生态中有很多HTTP客户端库比如原生的HttpURLConnection、Apache HttpClient、OkHttp、Spring的RestTemplate/WebClient。对于爬虫这种需要精细控制请求头、自动管理Cookie、处理重定向和连接池的场景Apache HttpClient是经过时间检验的经典选择。HttpURLConnectionJDK自带但API较为底层和繁琐缺少连接池、自动重试、Cookie管理等高级功能不适合复杂的爬虫任务。OkHttp非常优秀、现代的HTTP客户端API设计优雅性能出色。如果你是新项目OkHttp是绝佳选择。但其在Cookie的自动管理上默认策略可能不如HttpClient的CookieStore直观。RestTemplate/WebClient属于Spring生态更适合在Spring项目中做服务间调用。用于爬虫时封装层次较高有时反而不如直接使用底层库灵活。Apache HttpClient功能全面且稳定对HTTP协议的各种特性支持得非常细致如各种认证方式、重定向策略、连接管理。其内置的BasicCookieStore可以完美模拟浏览器的Cookie管理行为这是实现“一次登录多次访问”的关键。社区资料和解决方案也极其丰富。因此我们选择Apache HttpClient 4.x目前最新是4.5.x系列作为本次实战的核心工具。它的核心对象包括CloseableHttpClient: 可关闭的HTTP客户端实例代表一个连接池。HttpPost/HttpGet: 代表具体的POST/GET请求。BasicCookieStore: 用于存储和管理Cookie的容器。RequestConfig: 用于配置请求超时、重定向等行为。4. 实战演练五步攻克登录关卡下面我们以一个假设的网站https://example.com为例它使用经典的Cookie-Session机制。我们将用HttpClient完成从登录到获取数据的过程。4.1 第一步环境准备与依赖引入首先在你的Maven项目的pom.xml中添加HttpClient依赖。我们使用较新的HttpComponents Client 5系列它模块化更清晰。dependency groupIdorg.apache.httpcomponents.client5/groupId artifactIdhttpclient5/artifactId version5.2.1/version /dependency !-- 如果需要使用经典的 Fluent API可以添加此模块 -- dependency groupIdorg.apache.httpcomponents.client5/groupId artifactIdhttpclient5-fluent/artifactId version5.2.1/version /dependency如果你仍在使用HttpComponents 4.x依赖如下dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId version4.5.13/version /dependency本文示例将基于5.x版本编写其API与4.x有较大变化但核心逻辑一致。4.x的用户可以参考思路调整具体类名和方法。4.2 第二步分析目标登录请求这是最关键的一步决定了后续代码的成败。打开浏览器开发者工具F12切换到Network网络选项卡并勾选Preserve log保留日志。在登录页面输入账号密码点击登录按钮。在网络列表中找到类型为document或xhr/fetch的登录请求。通常它的URL包含login、signin、auth等关键词。点击这个请求查看其详细信息Headers请求头:Request URL: 登录接口的完整地址。最重要Request Method: 通常是POST。Content-Type: 通常是application/x-www-form-urlencoded或application/json。这决定了我们提交参数的方式。其他可能需要的头如User-Agent、Referer、Origin。Payload/Form Data/Request Body请求体:查看提交了哪些参数。除了明文的username和password务必注意是否有隐藏字段如csrf_token、lt、execution等。这些是服务器用于防止跨站请求伪造CSRF或维持登录流程状态的必须原样获取并提交。Response Headers响应头:重点关注Set-Cookie字段。这里会包含登录成功后服务器下发的会话Cookie。Response响应体:登录成功后的跳转信息或者返回的Token。假设我们分析得到如下信息登录URL:https://example.com/api/login方法:POSTContent-Type:application/x-www-form-urlencoded请求体参数:usernameyour_namepasswordyour_passcsrf_tokenabc123响应头:Set-Cookie: JSESSIONIDxxxxxx; Path/; HttpOnly4.3 第三步构建HttpClient与Cookie管理器我们需要一个能自动保存和发送Cookie的HttpClient实例。import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.impl.cookie.BasicClientCookie; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.io.entity.EntityUtils; public class LoginCrawler { public static void main(String[] args) throws Exception { // 1. 创建Cookie存储仓库 BasicCookieStore cookieStore new BasicCookieStore(); // 2. 创建HttpClient并绑定CookieStore CloseableHttpClient httpClient HttpClients.custom() .setDefaultCookieStore(cookieStore) // 关键配置 .build(); // 3. 创建HTTP上下文用于关联本次会话的Cookie等状态 HttpClientContext context HttpClientContext.create(); // 上下文会自动使用上面创建的cookieStore // ... 后续登录和数据请求都使用这个httpClient和context } }重要提示BasicCookieStore是一个内存中的Cookie容器。这意味着如果你重启程序Cookie就会丢失。对于需要长期维持会话的爬虫可以考虑将其序列化到文件或数据库中。此外确保使用同一个HttpClient实例和关联的CookieStore/Context进行登录和后续请求否则Cookie无法共享。4.4 第四步模拟登录请求根据第二步分析的结果构造登录请求。这里以application/x-www-form-urlencoded格式为例。import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; // 接上面的main方法 try { // 4. 构建登录请求参数列表 ListNameValuePair params new ArrayList(); params.add(new BasicNameValuePair(username, your_username)); params.add(new BasicNameValuePair(password, your_password)); // 注意csrf_token需要先从登录页面获取这里假设我们已经通过另一个GET请求拿到了值“abc123” params.add(new BasicNameValuePair(csrf_token, abc123)); // 5. 创建POST请求并设置表单实体 ClassicHttpRequest loginPost ClassicRequestBuilder .post(https://example.com/api/login) .setEntity(new UrlEncodedFormEntity(params)) .build(); // 6. 可选但重要设置请求头模仿浏览器 loginPost.setHeader(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...); loginPost.setHeader(Referer, https://example.com/login); // 登录页地址 loginPost.setHeader(Content-Type, application/x-www-form-urlencoded; charsetUTF-8); System.out.println(Executing login request: loginPost.getUri()); // 7. 执行登录请求并将响应上下文关联到我们创建的context try (ClassicHttpResponse response httpClient.execute(loginPost, context)) { HttpEntity entity response.getEntity(); int statusCode response.getCode(); System.out.println(Login Status: statusCode); // 打印响应内容有助于调试 if (entity ! null) { String responseBody EntityUtils.toString(entity); System.out.println(Login Response: responseBody); EntityUtils.consume(entity); // 确保实体内容被完全消费以便连接可复用 } // 8. 关键检查登录是否成功 // 成功不一定是200。可能是302重定向到首页也可能是200但返回了JSON {“code”: 0}。 // 需要根据你分析的实际响应来判断。 if (statusCode 200 || statusCode 302) { // 检查CookieStore里是否已经有了服务器下发的Cookie System.out.println(Cookies after login:); cookieStore.getCookies().forEach(cookie - System.out.println( - cookie.getName() cookie.getValue()) ); // 如果cookieStore里有关键的JSESSIONID说明登录状态已被记录 boolean hasSession cookieStore.getCookies().stream() .anyMatch(c - JSESSIONID.equalsIgnoreCase(c.getName())); if (hasSession) { System.out.println(Login appears successful (Session Cookie found).); } else { System.out.println(Warning: No session cookie found. Login might have failed.); } } else { System.out.println(Login likely failed with status: statusCode); } } } catch (Exception e) { e.printStackTrace(); } finally { httpClient.close(); }4.5 第五步携带会话状态访问目标数据登录成功后cookieStore中已经保存了服务器颁发的Cookie。现在使用同一个httpClient和context去请求需要登录后才能访问的页面。// 接在登录成功的代码块之后 if (/* 判断登录成功的条件 */) { try { // 9. 构建访问目标数据页面的请求 ClassicHttpRequest dataGet ClassicRequestBuilder .get(https://example.com/user/profile) // 假设这是个人资料页 .build(); // 可以继续设置必要的请求头如User-Agent dataGet.setHeader(User-Agent, Mozilla/5.0 ...); System.out.println(\nExecuting data request: dataGet.getUri()); // 10. 执行请求注意这里依然传入了同一个context try (ClassicHttpResponse dataResponse httpClient.execute(dataGet, context)) { HttpEntity dataEntity dataResponse.getEntity(); int dataStatusCode dataResponse.getCode(); System.out.println(Data Request Status: dataStatusCode); if (dataEntity ! null) { String dataHtml EntityUtils.toString(dataEntity); // 现在dataHtml里应该就是登录后才能看到的页面内容了 System.out.println(Data Response Length: dataHtml.length()); // 这里可以开始你的数据解析工作如用Jsoup解析HTML // Document doc Jsoup.parse(dataHtml); // String userName doc.select(.user-name).first().text(); // System.out.println(Extracted User Name: userName); EntityUtils.consume(dataEntity); } // 判断是否真的拿到了数据 // 如果返回302重定向到登录页或者返回401/403说明Cookie无效或已过期 if (dataStatusCode 200 dataHtml ! null dataHtml.contains(个人中心)) { // 根据实际页面特征判断 System.out.println(Successfully accessed protected data!); } else { System.out.println(Failed to access protected data. Might be redirected to login.); } } } catch (Exception e) { e.printStackTrace(); } }5. 进阶挑战与深度避坑指南上面的五步是理想情况下的流程。现实中的网站防御机制要复杂得多。下面是我在多年爬虫实践中总结的几个关键挑战和应对策略。5.1 动态Token如CSRF Token的获取很多网站在登录表单中会嵌入一个一次性的Token用于防止CSRF攻击。这个Token通常在打开登录页时隐藏在HTML的表单里如input typehidden namecsrf_token value动态值或者通过某个API接口返回。应对策略在发起登录POST请求前先发起一个GET请求到登录页面用HTML解析库如Jsoup把这个Token值提取出来然后再用它构造登录参数。// 1. 先GET登录页面获取csrf_token ClassicHttpRequest getLoginPage ClassicRequestBuilder.get(https://example.com/login).build(); String csrfToken null; try (ClassicHttpResponse pageResponse httpClient.execute(getLoginPage, context)) { String html EntityUtils.toString(pageResponse.getEntity()); // 使用Jsoup解析 Document doc Jsoup.parse(html); Element tokenInput doc.select(input[namecsrf_token]).first(); if (tokenInput ! null) { csrfToken tokenInput.attr(value); System.out.println(Fetched CSRF Token: csrfToken); } } // 2. 将获取到的csrfToken填入登录参数列表 params.add(new BasicNameValuePair(csrf_token, csrfToken));5.2 验证码识别这是绕不过去的坎。如果登录需要验证码程序必须能“看到”并“识别”图片。获取验证码图片找到验证码图片的URL用HttpClient下载到本地或内存。识别简单验证码可以使用开源库如Tesseract OCR需训练字库进行识别但成功率有限。复杂验证码考虑使用第三方打码平台如超级鹰、图鉴等的API这是最稳定高效但需要付费的方案。机器学习针对特定网站可以训练一个简单的CNN模型但成本较高。填入并提交将识别出的验证码文本作为参数与其他登录信息一同提交。核心技巧对于需要验证码的网站务必在代码中实现验证码图片的保存和人工查看的接口便于调试。例如将下载的验证码图片保存为文件并暂停程序等待你在控制台输入识别结果。5.3 登录后的重定向处理HttpClient默认会自动处理重定向302,301等。这通常是好事因为它模拟了浏览器的行为。但在爬虫中有时我们需要检查重定向的最终地址以确认登录是否成功例如成功会重定向到/home失败则留在/login?error。可以通过自定义HttpClient的配置来控制重定向行为或者在执行请求后从context中获取重定向的轨迹。// 创建自定义配置允许重定向但我们可以追踪 RequestConfig config RequestConfig.custom() .setRedirectsEnabled(true) // 启用自动重定向 .build(); CloseableHttpClient httpClient HttpClients.custom() .setDefaultCookieStore(cookieStore) .setDefaultRequestConfig(config) .build(); // 执行请求后可以从context中获取重定向链 ListURI redirectLocations context.getRedirectLocations(); if (redirectLocations ! null) { System.out.println(Redirect path:); for (URI uri : redirectLocations) { System.out.println( - uri); } }5.4 会话过期与心跳维持服务器端的Session有存活时间如30分钟。如果爬虫长时间不活动Cookie会失效。解决方案是定时心跳定期比如每15分钟访问网站的一个轻量级接口如/api/keepalive或首页让服务器刷新Session的过期时间。异常重登在抓取数据的主循环中捕获401/403状态码或发现被重定向到登录页。一旦检测到立即触发重新登录流程获取新的Cookie然后重试刚才失败的请求。5.5 应对反爬虫策略绕过登录后你依然可能触发网站的反爬虫机制。User-Agent务必设置一个常见的浏览器UA字符串。请求频率在请求间添加随机延时Thread.sleep(randomInterval)模拟人类操作。请求头完整性复制浏览器请求中的所有常见头如Accept,Accept-Language,Accept-Encoding,Connection,Host等。IP限制这是最棘手的。如果IP被封锁需要考虑使用代理IP池。HttpClient可以很方便地配置代理。HttpHost proxy new HttpHost(proxy-host, 8080); RequestConfig config RequestConfig.custom() .setProxy(proxy) .build(); // 然后将config设置到HttpClient或单个Request上JavaScript渲染如果目标数据由前端JavaScript动态加载上述方法只能拿到初始HTML框架。这时就需要用到Selenium、Puppeteer通过Java驱动或HtmlUnit这类无头浏览器工具来模拟完整的浏览器环境。但这已属于另一个技术范畴资源消耗会大很多。6. 完整代码示例与关键调试技巧将上述所有步骤整合一个健壮的、带有基础错误处理和调试输出的爬虫骨架代码如下import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.cookie.BasicCookieStore; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.message.BasicNameValuePair; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import java.util.ArrayList; import java.util.List; public class RobustLoginCrawler { private static final String LOGIN_URL https://example.com/api/login; private static final String LOGIN_PAGE_URL https://example.com/login; private static final String TARGET_DATA_URL https://example.com/user/profile; public static void main(String[] args) { BasicCookieStore cookieStore new BasicCookieStore(); HttpClientContext context HttpClientContext.create(); try (CloseableHttpClient httpClient HttpClients.custom() .setDefaultCookieStore(cookieStore) .build()) { // --- 阶段一获取动态Token --- String csrfToken fetchCsrfToken(httpClient, context, LOGIN_PAGE_URL); if (csrfToken null || csrfToken.isEmpty()) { System.err.println(Failed to fetch CSRF token. Exiting.); return; } // --- 阶段二执行登录 --- boolean loginSuccess performLogin(httpClient, context, LOGIN_URL, your_user, your_pass, csrfToken); if (!loginSuccess) { System.err.println(Login failed. Exiting.); return; } System.out.println(Login successful. Current cookies:); cookieStore.getCookies().forEach(c - System.out.println( c)); // 可选短暂暂停观察 Thread.sleep(1000); // --- 阶段三访问受保护数据 --- String protectedData fetchProtectedData(httpClient, context, TARGET_DATA_URL); if (protectedData ! null) { System.out.println(\n Successfully Fetched Protected Data ); System.out.println(Data length: protectedData.length()); // 这里进行你的数据解析逻辑 // parseData(protectedData); } else { System.err.println(Failed to fetch protected data.); } } catch (Exception e) { e.printStackTrace(); } } private static String fetchCsrfToken(CloseableHttpClient client, HttpClientContext context, String url) throws Exception { HttpGet request new HttpGet(url); // 模仿浏览器头 request.setHeader(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); try (CloseableHttpResponse response client.execute(request, context)) { HttpEntity entity response.getEntity(); if (entity ! null) { String html EntityUtils.toString(entity); Document doc Jsoup.parse(html); // 假设token在name为‘csrf_token’的input标签里 Element tokenElem doc.select(input[namecsrf_token]).first(); EntityUtils.consume(entity); return tokenElem ! null ? tokenElem.attr(value) : null; } } return null; } private static boolean performLogin(CloseableHttpClient client, HttpClientContext context, String loginUrl, String user, String pass, String token) throws Exception { HttpPost request new HttpPost(loginUrl); ListNameValuePair params new ArrayList(); params.add(new BasicNameValuePair(username, user)); params.add(new BasicNameValuePair(password, pass)); params.add(new BasicNameValuePair(csrf_token, token)); // 可能还有其他隐藏字段根据实际分析添加 request.setEntity(new UrlEncodedFormEntity(params)); request.setHeader(User-Agent, Mozilla/5.0 ...); request.setHeader(Referer, LOGIN_PAGE_URL); request.setHeader(Content-Type, application/x-www-form-urlencoded); System.out.println(Posting to login URL...); try (CloseableHttpResponse response client.execute(request, context)) { int status response.getCode(); HttpEntity entity response.getEntity(); String body entity ! null ? EntityUtils.toString(entity) : ; EntityUtils.consume(entity); System.out.println(Login HTTP Status: status); // 根据实际情况判断成功条件可能是302重定向也可能是200返回成功JSON boolean successByStatus status 302 || status 200; // 进一步检查响应体或Cookie boolean successByCookie context.getCookieStore().getCookies().stream() .anyMatch(c - JSESSIONID.equalsIgnoreCase(c.getName())); return successByStatus successByCookie; } } private static String fetchProtectedData(CloseableHttpClient client, HttpClientContext context, String url) throws Exception { HttpGet request new HttpGet(url); request.setHeader(User-Agent, Mozilla/5.0 ...); try (CloseableHttpResponse response client.execute(request, context)) { int status response.getCode(); HttpEntity entity response.getEntity(); if (status 200 entity ! null) { String data EntityUtils.toString(entity); EntityUtils.consume(entity); // 简单检查内容是否包含登录后才有的特征避免拿到的是登录页 if (data.contains(logout) || data.contains(个人中心)) { return data; } else { System.err.println(Got 200 but content seems not to be protected page.); return null; } } else { System.err.println(Failed to fetch data. Status: status); return null; } } } }关键调试技巧日志输出像上面代码一样打印关键步骤的URL、状态码、响应体片段和Cookie信息。使用抓包工具将Fiddler或Charles设置为系统代理让你的Java程序通过代理发送请求。这样你可以在图形化界面中清晰地看到程序发出的每一个请求和收到的每一个响应方便与浏览器行为对比。保存中间结果将登录请求的响应HTML、获取到的Cookie等保存到文件方便离线分析。逐步验证不要一次性写完所有代码。先写GET登录页面取Token的部分验证能否拿到Token。再写登录部分验证能否拿到Cookie。最后写数据抓取部分。