爬虫技术分享
网络爬虫技术分享
作者:技术分享
日期:2026年3月
适用语言:Python / Java
一、什么是网络爬虫?
网络爬虫(Web Crawler),又称网络蜘蛛或网络机器人,是一种按照一定规则自动抓取网页信息的自动化程序。其本质是模拟人类浏览器访问网页的行为,通过发送 HTTP 请求获取页面内容,再从中提取有价值的结构化数据并加以存储,最终服务于数据分析、业务监控、信息聚合等场景。
爬虫的工作流程可以简单概括为四个核心步骤:
获取网页 → 提取信息 → 保存数据 → 自动化调度 这四个步骤形成了一个完整的数据采集闭环。理解这个闭环,是学习爬虫技术的基础。
为什么要学爬虫?
在数据驱动的时代,数据就是生产力。无论是金融分析、市场调研还是风控建模,高质量、及时准确的数据都是前提。爬虫技术提供了一种低成本、高效率的数据获取手段,在银行、证券、保险等金融机构中有着广泛的实际应用价值。
二、爬虫在银行相关技术中的应用
爬虫技术在银行及金融领域的应用远比想象中广泛,以下是几个典型场景:
1. 舆情监控与品牌风控
银行需要实时关注网络上与自身品牌、产品相关的舆论动态。通过爬虫自动采集新闻门户、社交媒体、论坛等渠道的信息,可以及时发现负面舆情,为公关和风控部门提供预警。
2. 金融数据采集
对公开的利率信息、汇率数据、债券发行公告、同业产品定价等进行定时采集和结构化存储,为量化分析和市场研究提供数据基础。
3. 信用信息辅助核查
在贷前审核环节,爬虫可以辅助采集企业工商信息、司法裁判文书、行政处罚记录等公开数据,补充征信报告的覆盖面,提升风险评估的全面性。
4. 监管政策跟踪
自动抓取银保监会、央行等监管机构网站的政策法规和公告通知,第一时间推送给合规团队,确保政策响应的时效性。
5. 竞品分析与产品研究
定期采集同业银行的产品信息(如理财产品收益率、信用卡权益等),形成对比分析报告,为产品设计和定价策略提供参考。
注意: 银行场景下的爬虫开发需格外注意合规性,必须遵守 robots.txt 协议,不采集非公开数据,避免对目标网站造成过大访问压力,并确保数据使用符合隐私保护相关法规。三、获取静态网页
3.1 什么是静态网页?
静态网页是指服务器直接将完整的 HTML 文档返回给客户端的页面,所有数据都嵌入在 HTML 响应体中,无需额外的 JavaScript 渲染。这是最简单的爬取场景,也是学习爬虫的起点。
对于静态页面,我们只需要发送一个普通的 HTTP GET 请求,就能获取到包含所有目标数据的 HTML 文本。
3.2 Python 实现
Python 的 requests 库是目前最流行的 HTTP 客户端库之一,语法简洁直观,非常适合快速开发爬虫原型。
import requests url ="https://cc.cmbchina.com/notice/1/" headers ={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",# 模拟浏览器} response = requests.get(url, headers=headers)if response.status_code ==200: html = response.text # 获取 HTML 内容print(html)else:print("请求失败,状态码:", response.status_code)这里有一个关键细节:为什么要设置 User-Agent? 很多网站会识别请求头中的 User-Agent 字段,如果发现是非浏览器的请求(例如 Python 的默认 python-requests/x.x.x),可能会直接拒绝访问或返回反爬验证页面。通过伪装成浏览器的 User-Agent,可以绕过这类基础的反爬措施。
3.3 Java 实现
在 Java 生态中,Apache HttpClient 是使用最广泛的 HTTP 客户端库,提供了完善的连接池管理、重试机制和 HTTPS 支持,更适合生产级别的爬虫项目。
importorg.apache.http.HttpEntity;importorg.apache.http.client.methods.CloseableHttpResponse;importorg.apache.http.client.methods.HttpGet;importorg.apache.http.impl.client.CloseableHttpClient;importorg.apache.http.impl.client.HttpClients;importorg.apache.http.util.EntityUtils;publicclassSpider{publicstaticvoidmain(String[] args){try(CloseableHttpClient httpClient =HttpClients.createDefault()){// 1. 创建 GET 请求HttpGet request =newHttpGet("https://ssr1.scrape.center/");// 2. 设置请求头 request.setHeader("User-Agent","Mozilla/5.0");// 3. 发送请求并获取响应try(CloseableHttpResponse response = httpClient.execute(request)){// 4. 获取响应实体(网页内容)HttpEntity entity = response.getEntity();if(entity !=null){String html =EntityUtils.toString(entity);System.out.println(html);}}}catch(Exception e){ e.printStackTrace();}}}小贴士: 上面的 Java 代码使用了 try-with-resources 语法,确保 HTTP 连接在使用完毕后自动关闭,避免连接泄漏——这在长时间运行的爬虫任务中尤为重要。四、提取信息
拿到 HTML 文本之后,我们需要从中提取出目标数据。这是爬虫的核心环节,也是技巧最多的部分。下面介绍几种主流方案,并对比它们各自的优缺点。
4.1 正则表达式
正则表达式是最原始的解析方式,直接对 HTML 字符串进行模式匹配,无需任何额外依赖。
Python 示例:提取电影标题
import re import requests url ="https://ssr1.scrape.center/" headers ={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",} response = requests.get(url, headers=headers)if response.status_code ==200: html = response.text # 使用正则表达式提取电影标题 pattern = re.compile(r'<h2[^>]*class=[\'"]m-b-sm[\'"][^>]*>\s*(.*?)\s*</h2>', re.DOTALL) movies = pattern.findall(html)for i, movie inenumerate(movies,1):print(f"{i}. {movie.strip()}")else:print("请求失败,状态码:", response.status_code)正则含义解析:
| 模式片段 | 含义 |
|---|---|
<h2[^>]* | 匹配 h2 标签的开始部分(含任意属性) |
class=['"]m-b-sm['"] | 精确定位 class 为 m-b-sm 的 h2 元素 |
>\s*(.*?)\s*</h2> | 提取标签内的文本内容,忽略首尾空白 |
(.*?) | 非贪婪捕获组,即我们想要的电影名 |
Java 示例:
Pattern pattern =Pattern.compile("<h2 data-v-7f856186=\"\" class=\"m-b-sm\">(.+?)</h2>");Matcher matcher = pattern.matcher(html);int count =0;while(matcher.find()){ count++;String fullName = matcher.group(1).trim();System.out.println(count +". "+ fullName);}正则表达式的局限性: HTML 结构复杂且多变,正则表达式难以处理嵌套标签、属性顺序变化、换行等情况,一旦页面结构调整,正则极易失效,维护成本极高。实际项目中不推荐作为主要解析手段。
4.2 Python 主流解析库对比
为了解决正则表达式的痛点,Python 生态提供了多种专业的 HTML 解析库。
4.2.1 XPath(lxml)
XPath 是 W3C 标准的 XML/HTML 路径语言,基于文档树结构进行节点定位,语义清晰,性能优秀。
from lxml import etree tree = etree.HTML(html) movies = tree.xpath('//h2[@class="m-b-sm"]/text()')XPath 语法说明:
//h2:查找文档中所有<h2>标签[@class="m-b-sm"]:筛选class属性为m-b-sm的节点/text():提取该节点的文本内容
4.2.2 BeautifulSoup
BeautifulSoup 将 HTML 解析为 DOM 树,提供了直观的 Python API,对新手非常友好,容错性强(能处理不规范的 HTML)。
from bs4 import BeautifulSoup soup = BeautifulSoup(html,"lxml")# 推荐使用 lxml 解析器,速度更快 movies = soup.find_all("h2", class_="m-b-sm")for movie in movies:print(movie.get_text())4.2.3 pyquery
pyquery 借鉴了前端 jQuery 的 CSS 选择器语法,对于熟悉前端开发的工程师来说上手极快。
from pyquery import PyQuery as pq doc = pq(html) movies =[item.text()for item in doc('h2.m-b-sm').items()]4.2.4 parsel(推荐)
parsel 是 Scrapy 框架内置的解析库,同时支持 XPath、CSS 选择器和正则表达式,功能最为全面,也是目前生产项目中使用最广泛的方案。
from parsel import Selector selector = Selector(text=html)# 使用 CSS 选择器 movies = selector.css("h2.m-b-sm::text").getall()# 使用 XPath movies = selector.xpath('//h2[@class="m-b-sm"]/text()').getall() movies =[movie.strip()for movie in movies]各解析方案横向对比:
| 解析方式 | 语法风格 | 容错性 | 性能 | 推荐场景 |
|---|---|---|---|---|
| 正则表达式 | 模式匹配 | 差 | 高 | 简单字符串提取 |
| XPath (lxml) | 路径语言 | 一般 | 高 | 复杂层级结构 |
| BeautifulSoup | Python API | 强 | 中 | 快速原型、脏数据 |
| pyquery | CSS 选择器 | 一般 | 中 | 前端熟悉的开发者 |
| parsel | XPath + CSS | 一般 | 高 | 生产项目(推荐) |
4.3 Java:使用 Jsoup
Java 生态中,Jsoup 是解析 HTML 的首选库,支持 CSS 选择器语法,API 设计简洁,同时内置了 HTTP 请求功能,可以一步完成获取和解析。
importorg.jsoup.Jsoup;importorg.jsoup.nodes.Document;importorg.jsoup.nodes.Element;importorg.jsoup.select.Elements;publicclassHttpClientCrawler{publicstaticvoidmain(String[] args)throwsException{String url ="https://ssr1.scrape.center/";// 1. 获取并解析 HTML 文档(Jsoup 内置 HTTP 请求)Document doc =Jsoup.connect(url).userAgent("Mozilla/5.0").get();// 2. 使用 CSS 选择器定位元素Elements movieElements = doc.select("div.el-card.item h2.m-b-sm");int count =0;for(Element element : movieElements){String fullName = element.text().trim();// 拆分中英文名String[] nameParts = fullName.split(" - ",2);String chineseName = nameParts[0];String englishName =(nameParts.length >1)? nameParts[1]:"无英文名";System.out.printf("%d. %s - %s%n",++count, chineseName, englishName);}}}五、获取动态页面
随着前端技术的发展,越来越多的页面采用了 JavaScript 动态渲染技术(如 Vue.js、React 等),数据不再直接包含在 HTML 源码中,而是由浏览器在加载后通过 JavaScript 动态生成。这类页面需要特殊处理。
5.1 方案一:Ajax 接口爬取
原理: 很多"动态页面"本质上是通过 Ajax 请求从后端 API 获取 JSON 数据,再由前端渲染成页面。我们只需找到这个 API 接口,直接请求它,就能绕过前端渲染,直接拿到干净的结构化数据。
如何找到 Ajax 接口?
打开浏览器开发者工具(F12),切换到「网络」标签页,筛选 XHR 或 Fetch 类型的请求,刷新页面后即可看到所有的 Ajax 请求,找到返回目标数据的接口 URL 即可。
Python 示例:
import requests url ="https://cc.cmbchina.com/api/content/list-paged" headers ={"Accept":"application/json","Content-Type":"application/json;charset=UTF-8","Origin":"https://cc.cmbchina.com","Referer":"https://cc.cmbchina.com/notice/1/","User-Agent":"Mozilla/5.0"}for page inrange(1,3): payload ={"url":"cusservice/news/","type":"notice","pageIndex": page,"pageSize":15} resp = requests.post(url, headers=headers, json=payload, timeout=15) data = resp.json()print(f"\n===== 第 {page} 页 =====")for item in data["body"]["pageList"]:print(item["title"],"|", item["timeEffective"])Java 示例(使用 Java 11 内置 HttpClient + Jackson):
importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.nio.charset.StandardCharsets;importjava.time.Duration;publicclassHttpClientCrawler{privatestaticfinalStringAPI_URL="https://spa1.scrape.center/api/movie/?limit=10&offset=0";publicstaticvoidmain(String[] args)throwsException{HttpClient client =HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();HttpRequest request =HttpRequest.newBuilder().uri(URI.create(API_URL)).header("User-Agent","Mozilla/5.0").header("Accept","application/json").GET().build();HttpResponse<String> response = client.send( request,HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));if(response.statusCode()!=200){System.out.println("请求失败,状态码:"+ response.statusCode());return;}ObjectMapper mapper =newObjectMapper();JsonNode rootNode = mapper.readTree(response.body());JsonNode results = rootNode.get("results");if(results !=null&& results.isArray()){for(JsonNode movie : results){int id = movie.has("id")? movie.get("id").asInt():0;String name = movie.has("name")? movie.get("name").asText():"";double score = movie.has("score")? movie.get("score").asDouble():0.0;System.out.println(id +". "+ name +" 评分:"+ score);}}}}Ajax 爬取的优点: 无需渲染 HTML,数据直接是结构化的 JSON,解析简单,运行速度快,资源消耗少。这是处理动态页面的首选方案。
5.2 方案二:异步爬虫(高性能并发采集与存储)
在实际项目中,待采集的数据量往往很大(几十页甚至上百页),逐条同步请求效率极低。异步爬虫通过非阻塞 I/O + 协程/多线程并发,可以大幅提升采集效率。
5.2.1 异步爬虫的核心优势
- 高吞吐量:利用异步 I/O,在等待网络响应的间隙发起其他请求,极大减少空闲等待时间
- 资源利用率高:相比多线程/多进程方案,协程的内存开销更小,可以轻松支撑数百个并发连接
- 可控的并发度:通过信号量(Semaphore)精确控制并发数量,避免对目标服务器造成过大压力
- 与数据库异步集成:配合异步数据库驱动(如
aiomysql),实现采集与存储的全链路异步化
5.2.2 Python 异步爬虫实现(aiohttp + aiomysql)
以下是一个完整的生产级异步爬虫示例,实现了分页列表采集 → 详情页正文抓取 → MySQL 异步入库的全流程:
import asyncio import aiomysql import aiohttp import time from bs4 import BeautifulSoup start_time = time.time() list_url ="https://cc.cmbchina.com/api/content/list-paged" detail_api_url ="https://cc.cmbchina.com/api/content/list/detail" detail_page_prefix ="https://cc.cmbchina.com/notice/" headers ={"Accept":"application/json","Content-Type":"application/json;charset=UTF-8","Origin":"https://cc.cmbchina.com","Referer":"https://cc.cmbchina.com/notice/1/","User-Agent":"Mozilla/5.0"} MYSQL_CONFIG ={"host":"127.0.0.1","port":3306,"user":"root","password":"123456","db":"zhaohang","charset":"utf8mb4"}# 并发控制:最多同时发起的详情请求数量 MAX_CONCURRENCY =5# ────────────────────────────────────────────────# 数据库相关# ────────────────────────────────────────────────asyncdefcreate_pool():"""创建 aiomysql 连接池"""returnawait aiomysql.create_pool( host=MYSQL_CONFIG["host"], port=MYSQL_CONFIG["port"], user=MYSQL_CONFIG["user"], password=MYSQL_CONFIG["password"], db=MYSQL_CONFIG["db"], charset=MYSQL_CONFIG["charset"], autocommit=False, minsize=1, maxsize=10)asyncdefcreate_table(pool):"""创建数据表(如不存在)""" create_sql =""" CREATE TABLE IF NOT EXISTS cmb_notice ( id BIGINT PRIMARY KEY AUTO_INCREMENT, page_index INT NOT NULL, notice_id BIGINT NULL, guid VARCHAR(64) NULL, name VARCHAR(64) NOT NULL, title VARCHAR(500) NOT NULL, time_effective DATETIME NULL, detail_page_url VARCHAR(255) NULL, content LONGTEXT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """asyncwith pool.acquire()as conn:asyncwith conn.cursor()as cursor:await cursor.execute(create_sql)await conn.commit()asyncdefsave_to_mysql(pool, record):"""使用 UPSERT 语法写入数据库,避免重复插入""" insert_sql =""" INSERT INTO cmb_notice ( page_index, notice_id, guid, name, title, time_effective, detail_page_url, content ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE page_index = VALUES(page_index), notice_id = VALUES(notice_id), guid = VALUES(guid), title = VALUES(title), time_effective = VALUES(time_effective), detail_page_url = VALUES(detail_page_url), content = VALUES(content), updated_at = CURRENT_TIMESTAMP """asyncwith pool.acquire()as conn:try:asyncwith conn.cursor()as cursor:await cursor.execute(insert_sql,( record["page_index"], record["notice_id"], record["guid"], record["name"], record["title"], record["time_effective"], record["detail_page_url"], record["content"]))await conn.commit()print(f"已写入数据库: {record['name']}")except Exception as e:await conn.rollback()print(f"写入数据库失败 {record['name']}: {e}")# ────────────────────────────────────────────────# 网络请求相关# ────────────────────────────────────────────────asyncdefget_notice_content(session: aiohttp.ClientSession, name:str)->str:"""异步获取单条公告的正文内容"""ifnot name:return"name 为空,无法获取正文" payload ={"name": name,"parent":"news","type":"notice"}try:asyncwith session.post( detail_api_url, headers=headers, json=payload, timeout=aiohttp.ClientTimeout(total=15))as resp: resp.raise_for_status() data =await resp.json(content_type=None)except Exception as e:returnf"详情请求异常: {e}"if data.get("returnCode")!="SUC0000":returnf"详情接口返回失败: {data}" html_content = data.get("body",{}).get("contentInfo","")ifnot html_content:return"未获取到正文内容" soup = BeautifulSoup(html_content,"html.parser") content_list =[ p.get_text(" ", strip=True).replace("\xa0"," ")for p in soup.find_all("p")if p.get_text(" ", strip=True).replace("\xa0"," ")]return"\n".join(content_list)if content_list else"正文解析后为空"asyncdeffetch_page_list(session: aiohttp.ClientSession, page:int):"""异步获取列表页数据""" payload ={"url":"cusservice/news/","type":"notice","pageIndex": page,"pageSize":15}try:asyncwith session.post( list_url, headers=headers, json=payload, timeout=aiohttp.ClientTimeout(total=15))as resp: resp.raise_for_status()returnawait resp.json(content_type=None)except Exception as e:print(f"第 {page} 页列表请求失败: {e}")returnNoneasyncdefprocess_item(session, pool, item, page, idx, semaphore):"""处理单条公告:获取正文并写入数据库""" title = item.get("title","") time_effective = item.get("timeEffective",None) name = item.get("name","") guid = item.get("guid","") notice_id = item.get("id",None) detail_page_url =f"{detail_page_prefix}{name}.htm"if name elseNoneprint(f"正在抓取:第 {page} 页 第 {idx} 条 -> {title}")asyncwith semaphore:# 信号量控制并发try: content =await get_notice_content(session, name)except Exception as e: content =f"抓取正文失败: {e}" record ={"page_index": page,"notice_id": notice_id,"guid": guid,"name": name,"title": title,"time_effective": time_effective,"detail_page_url": detail_page_url,"content": content }await save_to_mysql(pool, record)asyncdefprocess_page(session, pool, page, semaphore):"""处理单页:拉取列表,并发抓取所有条目""" data =await fetch_page_list(session, page)if data isNone:returnif data.get("returnCode")!="SUC0000":print(f"第 {page} 页列表接口失败: {data}")return page_list = data.get("body",{}).get("pageList",[])print(f"\n===== 正在抓取第 {page} 页,共 {len(page_list)} 条 =====") tasks =[ process_item(session, pool, item, page, idx, semaphore)for idx, item inenumerate(page_list,1)]await asyncio.gather(*tasks)print(f"第 {page} 页抓取完成")# ────────────────────────────────────────────────# 入口# ────────────────────────────────────────────────asyncdefcrawl_notice_pages(start_page:int, end_page:int): pool =await create_pool()await create_table(pool) semaphore = asyncio.Semaphore(MAX_CONCURRENCY) connector = aiohttp.TCPConnector(limit=20, ssl=False)asyncwith aiohttp.ClientSession(connector=connector)as session: page_tasks =[ process_page(session, pool, page, semaphore)for page inrange(start_page, end_page +1)]await asyncio.gather(*page_tasks) pool.close()await pool.wait_closed()if __name__ =="__main__": start_page =1 end_page =7 asyncio.run(crawl_notice_pages(start_page, end_page)) end_time = time.time()print("爬取时间为:", end_time - start_time)print("抓取完成并已写入 MySQL。")5.2.3 Java 异步爬虫实现(HttpClient + CompletableFuture + HikariCP)
Java 11 内置的 HttpClient 天生支持异步请求(sendAsync),配合 CompletableFuture 可以实现类似 Python asyncio 的并发效果,同时使用 Semaphore 控制并发度、HikariCP 管理数据库连接池:
importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.node.ObjectNode;importcom.zaxxer.hikari.HikariConfig;importcom.zaxxer.hikari.HikariDataSource;importorg.jsoup.Jsoup;importorg.jsoup.nodes.Document;importorg.jsoup.nodes.Element;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.sql.Connection;importjava.sql.PreparedStatement;importjava.sql.SQLException;importjava.time.Duration;importjava.time.Instant;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.*;/** * Maven 依赖: * com.fasterxml.jackson.core:jackson-databind:2.17.1 * com.zaxxer:HikariCP:5.1.0 * org.jsoup:jsoup:1.17.2 * com.mysql:mysql-connector-j:8.3.0 * * 要求:JDK 11+ */publicclassHttpClientCrawler{// ── URL 常量 ──privatestaticfinalStringLIST_URL="https://cc.cmbchina.com/api/content/list-paged";privatestaticfinalStringDETAIL_API_URL="https://cc.cmbchina.com/api/content/list/detail";privatestaticfinalStringDETAIL_PAGE_PREFIX="https://cc.cmbchina.com/notice/";// ── HTTP 请求头 ──privatestaticfinalStringH_ACCEPT="application/json";privatestaticfinalStringH_CONTENT_TYPE="application/json;charset=UTF-8";privatestaticfinalStringH_ORIGIN="https://cc.cmbchina.com";privatestaticfinalStringH_REFERER="https://cc.cmbchina.com/notice/1/";privatestaticfinalStringH_USER_AGENT="Mozilla/5.0";// ── MySQL 配置 ──privatestaticfinalStringDB_HOST="127.0.0.1";privatestaticfinalintDB_PORT=3306;privatestaticfinalStringDB_USER="root";privatestaticfinalStringDB_PASSWORD="123456";privatestaticfinalStringDB_NAME="zhaohang";// ── 并发参数 ──privatestaticfinalintMAX_CONCURRENCY=5;// ── SQL ──privatestaticfinalStringCREATE_TABLE_SQL="CREATE TABLE IF NOT EXISTS cmb_notice (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";privatestaticfinalStringUPSERT_SQL="INSERT INTO cmb_notice (...) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE ...";// ── 内部数据记录类 ──staticrecordNoticeRecord(int pageIndex,Long noticeId,String guid,String name,String title,String timeEffective,String detailPageUrl,String content ){}privatefinalHttpClient httpClient;privatefinalHikariDataSource dataSource;privatefinalObjectMapper objectMapper;privatefinalSemaphore semaphore;privatefinalExecutorService dbExecutor;publicHttpClientCrawler(){this.objectMapper =newObjectMapper();this.semaphore =newSemaphore(MAX_CONCURRENCY);this.dbExecutor =Executors.newFixedThreadPool(10);this.httpClient =HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).connectTimeout(Duration.ofSeconds(15)).executor(Executors.newCachedThreadPool()).build();this.dataSource =buildDataSource();}privatestaticHikariDataSourcebuildDataSource(){HikariConfig config =newHikariConfig();String url ="jdbc:mysql://"+DB_HOST+":"+DB_PORT+"/"+DB_NAME+"?connectionCollation=utf8mb4_unicode_ci"+"&useSSL=false&allowPublicKeyRetrieval=true"+"&serverTimezone=Asia/Shanghai&autoReconnect=true"; config.setJdbcUrl(url); config.setUsername(DB_USER); config.setPassword(DB_PASSWORD); config.setAutoCommit(true); config.setMaximumPoolSize(10);returnnewHikariDataSource(config);}// ── 异步 HTTP POST ──privateCompletableFuture<String>postJson(String url,String jsonBody){HttpRequest request =HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(15)).header("Accept",H_ACCEPT).header("Content-Type",H_CONTENT_TYPE).header("Origin",H_ORIGIN).header("Referer",H_REFERER).header("User-Agent",H_USER_AGENT).POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();return httpClient.sendAsync(request,HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body);}// ── 异步获取正文(受信号量限速)──privateCompletableFuture<String>getNoticeContent(String name){if(name ==null|| name.isBlank())returnCompletableFuture.completedFuture("name 为空");ObjectNode payload = objectMapper.createObjectNode(); payload.put("name", name).put("parent","news").put("type","notice");returnCompletableFuture.supplyAsync(()->{try{ semaphore.acquire();returnnull;}catch(InterruptedException e){thrownewCompletionException(e);}}, dbExecutor).thenCompose(ignored ->postJson(DETAIL_API_URL, payload.toString()).thenApply(this::parseContent).exceptionally(e ->"详情请求异常: "+ e.getMessage()).whenComplete((r, e)-> semaphore.release()));}// ── 异步写入数据库 ──privateCompletableFuture<Void>saveToMysqlAsync(NoticeRecord record){returnCompletableFuture.runAsync(()->{try(Connection conn = dataSource.getConnection();PreparedStatement ps = conn.prepareStatement(UPSERT_SQL)){// 设置参数并执行... ps.executeUpdate();System.out.printf("已写入数据库: %s%n", record.name());}catch(SQLException e){System.err.printf("写入失败 %s: %s%n", record.name(), e.getMessage());}}, dbExecutor);}// ── 入口 ──publicvoidcrawl(int startPage,int endPage)throwsException{// 建表、逐页并发处理、等待完成、关闭资源List<CompletableFuture<Void>> tasks =newArrayList<>();for(int page = startPage; page <= endPage; page++){ tasks.add(processPage(page));}CompletableFuture.allOf(tasks.toArray(newCompletableFuture[0])).join(); dbExecutor.shutdown(); dataSource.close();}publicstaticvoidmain(String[] args)throwsException{Instant startTime =Instant.now();newHttpClientCrawler().crawl(1,10);Duration elapsed =Duration.between(startTime,Instant.now());System.out.printf("总耗时:%d 秒 %d 毫秒%n", elapsed.toSecondsPart(), elapsed.toMillisPart());}}Python vs Java 异步爬虫对比:
5.3 方案三:Selenium 自动化浏览器
当页面加密了 Ajax 接口、接口参数复杂难以复现,或者页面确实依赖 JavaScript 渲染时,我们需要驱动真实浏览器执行页面逻辑,再从渲染后的 DOM 中提取数据。
Selenium 是最成熟的浏览器自动化框架,支持 Chrome、Edge、Firefox 等主流浏览器。
使用前提: 需要下载与浏览器版本匹配的 WebDriver(如 EdgeDriver),并配置到系统路径中。
Python 示例(携带 Cookie 登录博客园):
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 启动 Edge 浏览器 driver = webdriver.Edge() driver.get("https://i.cnblogs.com/") driver.delete_all_cookies()# 解析并注入 Cookie(实现免密登录) cookie_str ="your_cookie_string_here" cookies = cookie_str.split("; ")for cookie in cookies: name, value = cookie.split("=",1) driver.add_cookie({"name": name,"value": value,"domain":".cnblogs.com"})# 注入 Cookie 后重新加载页面 driver.get("https://i.cnblogs.com/articles")# 等待页面元素加载完成(显式等待,避免直接 sleep) wait = WebDriverWait(driver,10) wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR,"tr.post-list-item")))# 提取文章标题 titles = driver.find_elements(By.CSS_SELECTOR,"a.entry.post-title-appendix")for t in titles:print(t.text)Java 示例(Selenium + EdgeDriver):
importorg.openqa.selenium.*;importorg.openqa.selenium.edge.EdgeDriver;importorg.openqa.selenium.support.ui.ExpectedConditions;importorg.openqa.selenium.support.ui.WebDriverWait;importjava.time.Duration;importjava.util.List;publicclassSeleniumCrawler{publicstaticvoidmain(String[] args){WebDriver driver =newEdgeDriver();try{ driver.get("https://i.cnblogs.com/"); driver.manage().deleteAllCookies();// 注入 CookieString cookieStr ="your_cookie_string_here";for(String item : cookieStr.split("; ")){String[] parts = item.split("=",2);if(parts.length ==2){ driver.manage().addCookie(newCookie.Builder(parts[0], parts[1]).domain(".cnblogs.com").path("/").build());}} driver.get("https://i.cnblogs.com/articles");// 显式等待WebDriverWait wait =newWebDriverWait(driver,Duration.ofSeconds(10)); wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(By.cssSelector("tr.post-list-item")));List<WebElement> titles = driver.findElements(By.cssSelector("a.entry.post-title-appendix"));System.out.println("文章标题:");for(WebElement title : titles){System.out.println(title.getText());}}finally{ driver.quit();}}}Selenium 的注意事项:显式等待(WebDriverWait)优于隐式等待(implicitly_wait),更可控浏览器实例资源消耗较大,不适合高并发爬取,适合低频、需要登录状态的场景可结合无头模式(--headless)在服务器环境中运行,不弹出浏览器窗口
六、Java 逆向爬虫技术
6.1 什么是逆向爬虫?
在很多实际场景中,网站不仅仅是简单地通过 Ajax 返回数据,还会对请求参数进行加密、签名或动态生成 Token,以防止接口被直接调用。此时单纯抓取 Ajax 接口将无法直接使用,需要逆向分析前端 JavaScript 代码的加密逻辑,在爬虫端复现签名算法后才能成功请求。
逆向爬虫的核心思路:
分析网络请求 → 定位加密参数 → 阅读/调试 JS 代码 → 在 Java 中复现加密逻辑 → 构造合法请求 6.2 常见的反爬加密手段
| 加密手段 | 说明 | 逆向难度 |
|---|---|---|
| 时间戳 + 签名 | 请求参数中包含 timestamp 和基于密钥的 sign 值 | 中等 |
| Token 动态生成 | 页面加载时 JS 动态计算 Token,每次请求携带 | 中等 |
| 请求参数加密 | 请求体或 URL 参数经过 Base64、AES、RSA 等加密 | 较高 |
| Cookie 反爬 | 通过 JS 动态设置 Cookie,服务器校验 Cookie 合法性 | 中等 |
| 字体反爬 | 使用自定义字体映射,页面显示正常但源码中是乱码 | 较高 |
6.3 逆向实战:复现签名算法
以下以一个典型的「时间戳 + HMAC-SHA256 签名」场景为例,演示如何在 Java 中复现前端的签名逻辑。
场景描述: 通过浏览器 DevTools 分析发现,接口请求头中包含以下自定义字段:
X-Timestamp:当前时间戳(毫秒)X-Sign:基于请求路径、时间戳和密钥的 HMAC-SHA256 签名
前端 JavaScript 签名逻辑(简化版):
// 前端代码中定位到的签名函数functiongenerateSign(path, timestamp, secretKey){const message = path +"|"+ timestamp;return CryptoJS.HmacSHA256(message, secretKey).toString();}Java 复现:
importjavax.crypto.Mac;importjavax.crypto.spec.SecretKeySpec;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.nio.charset.StandardCharsets;importjava.time.Duration;publicclassReverseCrawler{// 通过逆向 JS 代码获取到的密钥(通常硬编码在前端或由特定接口返回)privatestaticfinalStringSECRET_KEY="your_secret_key_from_js";/** * 复现前端 HMAC-SHA256 签名算法 */publicstaticStringgenerateSign(String path,long timestamp,String secretKey)throwsException{String message = path +"|"+ timestamp;Mac mac =Mac.getInstance("HmacSHA256");SecretKeySpec keySpec =newSecretKeySpec( secretKey.getBytes(StandardCharsets.UTF_8),"HmacSHA256"); mac.init(keySpec);byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));// 转为十六进制字符串StringBuilder hexString =newStringBuilder();for(byte b : hash){String hex =Integer.toHexString(0xff& b);if(hex.length()==1) hexString.append('0'); hexString.append(hex);}return hexString.toString();}publicstaticvoidmain(String[] args)throwsException{String apiPath ="/api/data/list";String baseUrl ="https://example.com";long timestamp =System.currentTimeMillis();// 1. 生成签名String sign =generateSign(apiPath, timestamp,SECRET_KEY);// 2. 构造带签名的请求HttpClient client =HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();HttpRequest request =HttpRequest.newBuilder().uri(URI.create(baseUrl + apiPath)).header("User-Agent","Mozilla/5.0").header("X-Timestamp",String.valueOf(timestamp)).header("X-Sign", sign).header("Accept","application/json").GET().build();// 3. 发送请求HttpResponse<String> response = client.send( request,HttpResponse.BodyHandlers.ofString());System.out.println("状态码:"+ response.statusCode());System.out.println("响应:"+ response.body());}}6.4 逆向爬虫的常用工具与技巧
调试工具:
- 浏览器 DevTools:Network 面板查看请求参数,Sources 面板断点调试 JS 代码
- Charles / Fiddler:抓包工具,可以查看 HTTPS 请求的完整内容(需要安装证书)
- 浏览器 Override 功能:可以替换线上 JS 文件进行调试,在关键位置插入
console.log输出中间值
Java 中常用的加密工具库:
| 加密算法 | Java 实现方式 |
|---|---|
| MD5 / SHA-256 | java.security.MessageDigest |
| HMAC-SHA256 | javax.crypto.Mac |
| AES | javax.crypto.Cipher + AES/CBC/PKCS5Padding |
| RSA | javax.crypto.Cipher + RSA/ECB/PKCS1Padding |
| Base64 | java.util.Base64 |
6.5 逆向爬虫的注意事项
- 优先尝试 Ajax 接口:逆向是最后的手段,很多看似加密的接口实际上可以通过简单的参数分析直接调用
- 关注 JS 混淆:生产环境的 JS 通常经过 Webpack 打包和混淆,需要使用美化工具(如
js-beautify)还原可读性 - 密钥可能动态变化:部分网站的签名密钥会定期更换或从服务端动态获取,需要建立自动化的密钥更新机制
- 合规性:逆向他人的加密算法可能涉及法律风险,仅应在合法合规的前提下用于公开数据的采集
七、反爬机制与应对策略总结
在实际爬虫开发中,目标网站通常会部署多种反爬手段来限制自动化访问。了解这些机制并掌握合理的应对策略,是爬虫工程师的必备技能。
| 反爬手段 | 表现形式 | 应对策略 |
|---|---|---|
| User-Agent 检测 | 拒绝非浏览器 UA 的请求 | 设置随机 User-Agent 池 |
| IP 频率限制 | 同一 IP 短时间大量请求被封禁 | 使用代理 IP 池,控制请求频率 |
| 验证码 | 弹出图形/滑块验证码 | 接入验证码识别服务或降低访问频率 |
| Cookie / Session 校验 | 要求携带有效 Cookie | Selenium 获取 Cookie 后注入 |
| 请求参数加密 | URL 或 Body 参数经过加密 | 逆向 JS 代码复现加密逻辑 |
| 动态渲染 | 数据由 JS 动态生成 | Selenium / Playwright 渲染 |
| 字体反爬 | 自定义字体导致源码乱码 | 解析字体文件建立映射表 |
| Honeypot 陷阱 | 隐藏链接诱导爬虫进入 | 过滤 display:none 等隐藏元素 |
核心原则: 合理控制请求频率、遵守 robots.txt、不采集敏感非公开数据。技术上可以突破的限制,法律上不一定允许。始终在合法合规的前提下进行数据采集。八、总结
本次技术分享从基础概念到实战应用,系统性地介绍了网络爬虫的核心技术栈:
- 基础篇:HTTP 请求与静态页面获取(requests / Apache HttpClient)
- 解析篇:正则、XPath、BeautifulSoup、pyquery、parsel、Jsoup 多方案对比
- 进阶篇:Ajax 接口爬取、异步高并发爬虫(Python asyncio / Java CompletableFuture)
- 自动化篇:Selenium 浏览器自动化
- 逆向篇:Java 逆向爬虫技术,复现前端签名算法
- 框架篇:Scrapy 工业级爬虫框架快速上手
- 防御篇:反爬机制识别与应对策略
在技术选型上,建议遵循以下优先级:Ajax 接口直接调用 > 异步批量爬取 > Scrapy 框架 > Selenium 自动化 > 逆向分析。优先选择轻量高效的方案,只在必要时才引入更重的工具。
本文仅供技术学习交流,请在合法合规的前提下使用爬虫技术。