引言:PHP全栈构建搜索引擎的可行性与技术挑战
在搜索引擎领域,主流技术选型多以Java、Go作为后端主力语言,搭配Python爬虫生态完成数据采集,PHP常被局限于Web开发、接口层调用等轻量场景,鲜少有人尝试用PHP实现全栈搜索引擎架构。本文将详细拆解自建搜索引擎「智搜搜索」的完整技术实现——前端、后端均基于PHP开发,整合ElasticSearch(检索核心)、Redis(缓存加速)、Kafka(消息解耦)、MySQL(结构化存储)、MongoDB(非结构化存储),并搭建Python+Java+C++多语言爬虫集群,最终实现site:xxx.com精准检索等核心功能,全程聚焦技术细节、踩坑复盘与优化策略,为PHP开发者搭建高性能搜索引擎提供可落地的实战参考。
智搜搜索的核心定位是「轻量且高效的垂直+通用混合检索系统」,区别于百度、谷歌等大型搜索引擎的全面覆盖,智搜聚焦中小规模站点检索、垂直领域数据聚合,兼顾检索速度与定制化灵活性。选择PHP作为全栈开发语言,核心原因在于其生态成熟、开发高效、部署便捷,且通过合理的架构设计与组件搭配,可有效弥补PHP在高并发、大数据量处理上的短板——本文将重点说明如何通过ElasticSearch、Kafka等组件的协同,解决PHP在搜索引擎场景中的性能瓶颈,同时详解多语言爬虫集群的设计逻辑,以及site语法的底层实现原理,让技术同行看到PHP在搜索引擎领域的可能性。
本文适合有PHP开发基础、熟悉搜索引擎基本原理,且对ElasticSearch、Redis、Kafka等中间件有一定了解的技术开发者阅读,全文围绕「技术选型→架构设计→模块实现→性能优化→问题复盘」展开,所有代码片段、配置方案均来自智搜搜索线上环境,可直接参考落地。
一、智搜搜索整体架构设计:PHP全栈与多组件协同架构
1.1 架构设计核心思路
搜索引擎的核心流程可概括为「数据采集→数据处理→索引构建→检索服务→结果展示」五大环节,智搜搜索的架构设计围绕这五大环节展开,核心原则是「解耦、可扩展、高性能」,同时充分发挥PHP的开发优势,搭配专业中间件弥补其性能短板。
整体架构采用「分层设计+微服务思想」,虽未完全拆分微服务(考虑到中小规模场景的部署成本),但各模块独立封装、通过标准化接口通信,便于后续横向扩展。架构分层如下(从下到上):
- 数据采集层:Python+Java+C++多语言爬虫集群,负责网页、接口数据的抓取与初步清洗;
- 消息队列层:Kafka,负责解耦爬虫采集与数据处理,缓冲高并发采集压力,避免数据丢失;
- 数据存储层:MySQL(结构化数据)+MongoDB(非结构化数据)+Redis(缓存数据),实现不同类型数据的分层存储;
- 索引构建层:ElasticSearch集群,负责将处理后的数据构建倒排索引,支撑高效检索;
- 业务逻辑层:PHP后端服务,负责核心业务逻辑处理(检索请求、爬虫调度、权限控制等);
- 前端展示层:PHP+HTML+JS,负责检索界面、结果展示、交互逻辑实现;
- 监控运维层:自定义监控脚本(PHP+Shell),负责各组件、模块的运行状态监控、日志收集与告警。
架构设计的核心亮点的是「PHP全栈贯穿+多组件协同解耦」:前端通过PHP渲染动态页面,后端通过PHP封装各组件的调用接口,统一业务逻辑;同时利用Kafka实现爬虫与数据处理的异步通信,利用Redis缓存热点数据、减少重复计算,利用ElasticSearch承担核心检索压力,让PHP聚焦于业务逻辑,而非底层性能消耗。
1.2 技术选型深度解析(为什么选择这些组件?)
智搜搜索的技术选型并非盲目堆砌,而是结合PHP全栈特性、搜索引擎业务场景,以及中小规模部署的成本考量,每一款组件都承担着核心作用,以下是详细选型逻辑:
1.2.1 核心开发语言:PHP(前端+后端)
选型理由:
- 开发效率高:PHP语法简洁、生态成熟,拥有大量开源库(如Guzzle、Laravel框架),可快速实现前端渲染、后端接口开发,缩短项目迭代周期——智搜搜索从立项到首次上线,仅用3个月完成核心功能开发,PHP的高效开发特性起到了关键作用。
- 部署便捷:PHP可直接部署在Nginx+PHP-FPM环境中,无需复杂的编译、打包流程,且支持跨平台部署(Linux、Windows),适合中小规模项目的快速落地。
- 生态适配性强:PHP与MySQL、Redis、ElasticSearch等中间件均有成熟的扩展(如php-redis、elasticsearch-php),可轻松实现组件集成,无需自行开发底层通信逻辑。
短板弥补:PHP在高并发、长连接、大数据量处理上存在天然短板,智搜搜索通过以下方式弥补:① 利用Kafka实现异步处理,避免PHP进程阻塞;② 利用Redis缓存热点数据,减少PHP与数据库、ElasticSearch的直接交互;③ 优化PHP-FPM配置,提升并发处理能力;④ 将核心计算逻辑(如爬虫解析、复杂索引构建)交给Python、Java、C++实现,PHP仅负责调度与结果整合。
1.2.2 检索核心:ElasticSearch
选型理由:搜索引擎的核心是「高效检索」,ElasticSearch作为基于Lucene的分布式搜索引擎,具备以下优势,完美适配智搜搜索的需求:
- 全文检索能力强:支持中文分词、模糊匹配、短语检索、范围检索等多种检索方式,可通过自定义分词器(如IK分词器)优化中文检索精度,满足site:xxx.com等精准检索需求。
- 分布式架构:支持集群部署,可通过横向扩展节点提升检索性能与可用性,后续随着数据量增长,可轻松扩容,无需重构核心架构。
- 实时性好:支持近实时索引构建(延迟在1秒以内),爬虫采集的数据经过处理后,可快速写入ElasticSearch,实现「采集→检索」的近实时同步。
- PHP适配性好:有成熟的elasticsearch-php扩展,支持PHP与ElasticSearch的高效通信,可轻松实现索引的创建、查询、更新、删除等操作。
替代方案对比:曾考虑使用Sphinx作为检索核心,Sphinx的检索速度略优于ElasticSearch,但在分布式部署、中文分词适配、实时性上存在明显短板,且生态不如ElasticSearch成熟,无法满足后续扩展需求,最终选择ElasticSearch 7.17.0版本(稳定版,兼容PHP扩展,且支持IK分词器最新版本)。
1.2.3 缓存组件:Redis
选型理由:搜索引擎的检索性能依赖缓存,Redis作为高性能的内存数据库,承担着智搜搜索的缓存核心作用,选型理由如下:
- 性能极高:Redis的读写速度可达10万+ QPS,可快速缓存热点检索词、检索结果、爬虫任务队列等数据,减少PHP与数据库、ElasticSearch的交互压力,提升系统响应速度。
- 数据结构丰富:支持字符串、哈希、列表、集合、有序集合等多种数据结构,可适配不同场景的缓存需求(如有序集合存储热点检索词排名,列表存储爬虫任务队列)。
- 支持持久化:可通过RDB、AOF两种持久化方式,避免缓存数据丢失,保障系统稳定性——智搜搜索采用「RDB+AOF混合持久化」,兼顾性能与数据安全性。
- PHP适配性强:php-redis扩展成熟,支持连接池、管道操作,可高效实现PHP与Redis的通信,减少连接开销。
应用场景:热点检索词缓存、检索结果缓存、爬虫任务队列、分布式锁(避免爬虫重复抓取)、会话存储等。
1.2.4 消息队列:Kafka
选型理由:智搜搜索的爬虫集群与数据处理模块存在高并发数据交互,Kafka作为高吞吐量的分布式消息队列,主要用于解耦这两个模块,选型理由如下:
- 高吞吐量:Kafka的单机吞吐量可达10万+ TPS,可轻松应对爬虫集群的高并发数据采集(如峰值时每秒采集1000+网页),避免数据堆积。
- 持久化能力:Kafka可将消息持久化到磁盘,支持消息回溯,即使数据处理模块故障,也可重新消费消息,避免数据丢失。
- 分布式扩展:支持集群部署,可通过增加节点提升吞吐量与可用性,适配爬虫集群的横向扩展需求。
- 多语言适配:支持Python、Java、PHP等多种语言的客户端,可轻松实现爬虫集群(多语言)与数据处理模块(PHP)的通信。
替代方案对比:曾考虑使用RabbitMQ,但RabbitMQ的吞吐量低于Kafka,且在大数据量消息堆积场景下,性能下降明显,无法满足爬虫集群的高并发数据传输需求,最终选择Kafka 2.8.0版本。
1.2.5 数据存储:MySQL+MongoDB
搜索引擎需要存储的数据分析两类:结构化数据(如站点信息、爬虫任务、检索日志)和非结构化数据(如网页正文、图片描述、富文本内容),因此采用「MySQL+MongoDB」的混合存储方案,选型理由如下:
- MySQL(结构化数据存储):支持ACID事务,数据一致性高,适合存储站点信息(域名、标题、备案信息)、爬虫任务(任务ID、目标URL、抓取状态)、检索日志(检索词、检索时间、IP地址)等结构化数据。智搜搜索使用MySQL 8.0版本,支持索引优化、读写分离,提升数据读写性能。
- MongoDB(非结构化数据存储):采用文档型存储,无需固定schema,适合存储网页正文、富文本内容等非结构化数据——网页正文格式多样(HTML、Markdown等),MongoDB可灵活存储,且支持全文检索(辅助ElasticSearch),可快速查询特定内容的网页。智搜搜索使用MongoDB 5.0版本,支持分片部署,适配非结构化数据的扩容需求。
存储分工:MySQL负责存储结构化数据,MongoDB负责存储非结构化数据,两者通过「站点ID」关联,实现数据的协同查询——例如,检索到某网页时,从MongoDB中获取网页正文,从MySQL中获取该网页对应的站点信息,整合后返回给用户。
1.2.6 爬虫集群:Python+Java+C++多语言协同
爬虫是搜索引擎的数据来源核心,单一语言无法满足所有抓取场景,因此智搜搜索采用多语言爬虫集群,各语言分工协作,选型理由如下:
- Python爬虫:负责通用网页抓取、接口数据抓取,优势是开发效率高、生态成熟(有Scrapy、BeautifulSoup、Requests等库),适合快速实现各类抓取逻辑,处理中等复杂度的抓取任务。
- Java爬虫:负责高并发、高稳定性的抓取任务(如大规模站点抓取、长连接抓取),Java的多线程、高并发能力优于Python,可应对高压力抓取场景,且稳定性强,适合长期运行。
- C++辅助爬虫:负责底层、高性能的抓取任务(如二进制数据抓取、反爬强度高的站点抓取),C++的执行效率极高,可突破Python、Java的性能瓶颈,处理特殊场景的抓取需求(如加密接口、动态渲染页面的底层数据抓取)。
协同逻辑:PHP后端通过Redis维护爬虫任务队列,多语言爬虫从队列中获取任务,完成抓取后,将数据通过Kafka发送到数据处理模块,实现「任务调度→多语言抓取→数据上传」的闭环。
1.3 架构数据流闭环详解
智搜搜索的核心数据流可分为「数据采集→数据处理→索引构建→检索服务→结果展示」五大环节,各环节的数据流闭环如下,结合组件协同逻辑,让技术细节更清晰:
- 数据采集环节:PHP后端通过爬虫调度模块,生成抓取任务(目标URL、抓取频率、优先级),存入Redis任务队列;Python、Java、C++爬虫从Redis队列中获取任务,根据任务类型执行抓取操作(Python抓取通用网页,Java抓取高并发站点,C++抓取特殊场景数据);抓取完成后,爬虫将原始数据(网页HTML、接口返回数据)通过Kafka发送到数据处理主题,同时将抓取状态(成功/失败)更新到MySQL的爬虫任务表中。
- 数据处理环节:PHP数据处理模块作为Kafka的消费者,订阅爬虫数据主题,接收原始数据;对原始数据进行清洗(去除HTML标签、过滤无效内容、去重)、结构化处理(提取标题、关键词、摘要、站点域名);处理完成后,将结构化数据(标题、关键词、站点信息)存入MySQL,非结构化数据(网页正文)存入MongoDB,同时将处理后的数据发送到ElasticSearch索引构建主题。
- 索引构建环节:PHP索引模块订阅ElasticSearch索引构建主题,接收处理后的数据;调用ElasticSearch API,创建/更新索引(自定义分词、字段权重),将数据写入ElasticSearch集群,构建倒排索引;索引构建完成后,将索引状态(成功/失败)更新到MySQL的索引表中,并将热点数据缓存到Redis。
- 检索服务环节:用户通过前端PHP页面输入检索词(支持site:xxx.com语法),前端将检索请求发送到PHP后端检索接口;PHP后端先查询Redis缓存,若存在热点检索结果,直接返回;若缓存未命中,调用ElasticSearch API,根据检索词(含site语法解析)执行检索操作,获取检索结果;PHP后端将检索结果与MySQL中的站点信息、MongoDB中的网页正文整合,排序后返回给前端。
- 结果展示环节:前端PHP页面接收后端返回的检索结果,通过HTML+JS渲染页面(展示标题、摘要、站点信息、检索时间),支持分页、排序、site语法高亮等交互功能;同时,前端将用户的检索行为(检索词、点击记录)发送到PHP后端,存入MySQL检索日志表,用于后续热点分析、检索优化。
数据流闭环的核心优势是「异步解耦、容错性强」:各环节通过Kafka、Redis实现异步通信,某一环节故障(如爬虫集群故障),不会影响其他环节的正常运行;数据经过多轮处理与校验,确保检索结果的准确性与完整性。
二、核心模块技术实现:从爬虫到检索的全细节拆解
2.1 多语言爬虫集群实现(Python+Java+C++)
爬虫集群是智搜搜索的数据来源核心,实现了「多语言协同、分布式调度、反爬处理、数据去重」四大核心功能,以下是各语言爬虫的具体实现细节,以及协同逻辑、调度机制。
2.1.1 Python爬虫实现(通用网页抓取)
Python爬虫主要负责通用网页、公开接口的数据抓取,占整个爬虫集群抓取量的60%,基于Scrapy框架开发,核心实现如下:
(1)环境配置与依赖
核心依赖库:Scrapy 2.8.0(爬虫框架)、BeautifulSoup 4.12.2(HTML解析)、Requests 2.31.0(接口请求)、redis-py 4.5.5(任务队列交互)、kafka-python 2.0.2(数据上传)、fake-useragent 1.1.3(UA伪装)。
环境配置(Linux):
# 安装依赖
pip install scrapy beautifulsoup4 requests redis-py kafka-python fake-useragent
# 配置Scrapy爬虫项目
scrapy startproject zhisou_spider
cd zhisou_spider
scrapy genspider general_spider zhisou.com
(2)核心实现代码(通用网页爬虫)
通用网页爬虫的核心逻辑:从Redis任务队列获取目标URL,伪装UA发起请求,解析HTML页面,提取标题、关键词、摘要、网页正文、站点域名等信息,去重后通过Kafka上传数据,更新任务状态。
import scrapy
import redis
from kafka import KafkaProducer
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
import hashlib
import time
# Redis连接配置
redis_client = redis.Redis(host='127.0.0.1', port=6379, password='zhisou_redis', db=0)
# Kafka生产者配置
producer = KafkaProducer(bootstrap_servers=['127.0.0.1:9092'],
value_serializer=lambda v: v.encode('utf-8'))
# UA伪装
ua = UserAgent()
# 去重集合(Redis)
DUPLICATE_KEY = 'zhisou:spider:duplicate'
class GeneralSpider(scrapy.Spider):
name = 'general_spider'
allowed_domains = [] # 不限制域名,从任务队列获取
start_urls = []
def start_requests(self):
# 从Redis任务队列获取任务(阻塞式获取,避免空轮询)
while True:
try:
# 从队列左侧弹出任务(LPOP),任务格式:URL|优先级|抓取时间
task = redis_client.lpop('zhisou:spider:task:general')
if not task:
time.sleep(1)
continue
task = task.decode('utf-8').split('|')
url = task[0]
priority = task[1]
crawl_time = task[2]
# 去重校验(通过URL哈希值去重)
url_md5 = hashlib.md5(url.encode('utf-8')).hexdigest()
if redis_client.sismember(DUPLICATE_KEY, url_md5):
self.logger.info(f'URL已抓取:{url}')
continue
# 发起请求,伪装UA
headers = {
'User-Agent': ua.random,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Referer': 'https://www.zhisou.com/'
}
yield scrapy.Request(url=url, headers=headers, callback=self.parse,
meta={'url': url, 'url_md5': url_md5})
except Exception as e:
self.logger.error(f'任务获取失败:{str(e)}')
time.sleep(3)
def parse(self, response):
try:
# 解析HTML页面
soup = BeautifulSoup(response.text, 'html.parser')
# 提取核心信息
title = soup.title.get_text() if soup.title else ''
# 提取关键词(meta标签)
keywords = soup.find('meta', attrs={'name': 'keywords'})['content'] if soup.find('meta', attrs={'name': 'keywords'}) else ''
# 提取摘要(meta标签或正文前100字)
description = soup.find('meta', attrs={'name': 'description'})['content'] if soup.find('meta', attrs={'name': 'description'}) else soup.get_text()[:100]
# 提取网页正文(去除HTML标签)
content = soup.get_text().strip()
# 提取站点域名(从URL中解析)
domain = response.url.split('//')[1].split('/')[0]
# 抓取时间
crawl_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
# 构建数据结构
data = {
'url': response.meta['url'],
'url_md5': response.meta['url_md5'],
'title': title,
'keywords': keywords,
'description': description,
'content': content,
'domain': domain,
'crawl_time': crawl_time,
'spider_type': 'python_general',
'status': 1 # 1-成功,0-失败
}
# 数据上传到Kafka(主题:zhisou:spider:data)
producer.send('zhisou:spider:data', str(data))
producer.flush()
# 去重标记(将URL哈希值存入Redis集合)
redis_client.sadd(DUPLICATE_KEY, response.meta['url_md5'])
# 更新任务状态(MySQL,通过PHP接口更新)
self.update_task_status(response.meta['url'], 1)
self.logger.info(f'URL抓取成功:{response.meta["url"]}')
except Exception as e:
self.logger.error(f'URL抓取失败:{response.meta["url"]},错误:{str(e)}')
# 更新任务状态为失败
self.update_task_status(response.meta['url'], 0)
def update_task_status(self, url, status):
# 调用PHP后端接口,更新MySQL中的任务状态
import requests
try:
requests.post('https://www.zhisou.com/api/spider/update_task',
data={'url': url, 'status': status},
timeout=5)
except Exception as e:
self.logger.error(f'任务状态更新失败:{url},错误:{str(e)}')
(3)核心优化:去重、反爬、并发控制
- 去重优化:采用「Redis集合+URL哈希值」的去重方式,避免重复抓取——将每个URL的MD5哈希值存入Redis集合,抓取前先校验,确保同一URL仅抓取一次;同时,定期清理Redis中的去重集合(保留30天内的记录),避免内存溢出。
- 反爬处理:① UA伪装(使用fake-useragent随机生成UA,避免被识别为爬虫);② 随机延迟(抓取间隔1-3秒,避免高频请求被封禁);③ 代理IP池(整合免费代理IP,失败时自动切换IP);④ Referer伪装(模拟浏览器 Referer,提升请求合法性)。
- 并发控制:Scrapy框架默认支持多线程并发,通过配置settings.py调整并发数,根据服务器性能设置为10-20个并发,避免并发过高导致服务器压力过大或被目标站点封禁。
2.1.2 Java爬虫实现(高并发站点抓取)
Java爬虫主要负责高并发、高稳定性的站点抓取(如新闻站点、电商站点),占整个爬虫集群抓取量的30%,基于OkHttp3框架开发,采用多线程调度,核心实现如下:
(1)环境配置与依赖
核心依赖(Maven):OkHttp3 4.11.0(HTTP请求)、Redis-cli 3.8.0(Redis交互)、kafka-clients 2.8.0(Kafka交互)、Jsoup 1.15.4(HTML解析)、Lombok 1.18.24(简化代码)。
<!-- pom.xml依赖配置 -->
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
(2)核心实现代码(高并发爬虫)
Java爬虫的核心逻辑:采用线程池调度,从Redis任务队列批量获取任务,多线程并发抓取,解析页面数据,去重后上传到Kafka,支持任务重试、失败降级,确保高稳定性。
package com.zhisou.spider.java;
import lombok.Data;
import okhttp3.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import redis.clients.jedis.Jedis;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 高并发站点爬虫(Java实现)
*/
public class HighConcurrencySpider {
// Redis配置
private static final String REDIS_HOST = "127.0.0.1";
private static final int REDIS_PORT = 6379;
private static final String REDIS_PASSWORD = "zhisou_redis";
private static final int REDIS_DB = 0;
// Kafka配置
private static final String KAFKA_SERVERS = "127.0.0.1:9092";
private static final String KAFKA_TOPIC = "zhisou:spider:data";
// 线程池配置(根据服务器性能调整)
private static final int THREAD_POOL_SIZE = 30;
// 去重集合Key
private static final String DUPLICATE_KEY = "zhisou:spider:duplicate";
// 任务队列Key
private static final String TASK_QUEUE_KEY = "zhisou:spider:task:high_concurrency";
// OkHttp客户端
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
// Redis客户端
private static final Jedis REDIS_CLIENT = new Jedis(REDIS_HOST, REDIS_PORT);
// Kafka生产者
private static final KafkaProducer<String, String> KAFKA_PRODUCER;
static {
// 初始化Redis客户端
REDIS_CLIENT.auth(REDIS_PASSWORD);
REDIS_CLIENT.select(REDIS_DB);
// 初始化Kafka生产者
Properties kafkaProps = new Properties();
kafkaProps.put("bootstrap.servers", KAFKA_SERVERS);
kafkaProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
kafkaProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
kafkaProps.put("acks", "1"); // 确保消息至少被一个分区副本接收
KAFKA_PRODUCER = new KafkaProducer<>(kafkaProps);
}
// 爬虫任务实体类
@Data
static class SpiderTask {
private String url;
private int priority;
private String crawlTime;
}
// 抓取数据实体类
@Data
static class SpiderData {
private String url;
private String urlMd5;
private String title;
private String keywords;
private String description;
private String content;
private String domain;
private String crawlTime;
private String spiderType;
private int status;
}
public static void main(String[] args) {
// 初始化线程池
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
System.out.println("Java高并发爬虫启动,线程池大小:" + THREAD_POOL_SIZE);
// 循环获取任务,提交到线程池
while (true) {
try {
// 批量获取任务(一次获取10个,减少Redis交互次数)
for (int i = 0; i < 10; i++) {
String taskStr = REDIS_CLIENT.lpop(TASK_QUEUE_KEY);
if (taskStr == null) {
break;
}
// 解析任务(格式:URL|优先级|抓取时间)
String[] taskArr = taskStr.split("\|");
SpiderTask task = new SpiderTask();
task.setUrl(taskArr[0]);
task.setPriority(Integer.parseInt(taskArr[1]));
task.setCrawlTime(taskArr[2]);
// 提交任务到线程池
executorService.submit(() -> crawlTask(task));
}
// 若没有任务,延迟1秒再获取
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
/**
* 执行抓取任务
* @param task 爬虫任务
*/
private static void crawlTask(SpiderTask task) {
String url = task.getUrl();
try {
// 去重校验(URL MD5哈希值)
String urlMd5 = getMd5(url);
if (REDIS_CLIENT.sismember(DUPLICATE_KEY, urlMd5)) {
System.out.println("URL已抓取:" + url);
return;
}
// 发起HTTP请求(伪装UA)
Request request = new Request.Builder()
.url(url)
.header("User-Agent", getRandomUA())
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.header("Referer", "https://www.zhisou.com/")
.build();
Response response = OK_HTTP_CLIENT.newCall(request).execute();
if (!response.isSuccessful()) {
System.out.println("URL请求失败:" + url + ",响应码:" + response.code());
updateTaskStatus(url, 0);
return;
}
// 解析HTML页面
String html = response.body().string();
Document document = Jsoup.parse(html);
// 提取核心信息
String title = document.title() != null ? document.title() : "";
String keywords = "";
if (document.select("meta[name=keywords]").first() != null) {
keywords = document.select("meta[name=keywords]").first().attr("content");
}
String description = "";
if (document.select("meta[name=description]").first() != null) {
description = document.select("meta[name=description]").first().attr("content");
} else {
// 提取正文前100字作为摘要
description = document.text().length() > 100 ? document.text().substring(0, 100) : document.text();
}
String content = document.text().trim();
// 解析域名
String domain = url.split("//")[1].split("/")[0];
String crawlTime = task.getCrawlTime();
// 构建数据实体
SpiderData data = new SpiderData();
data.setUrl(url);
data.setUrlMd5(urlMd5);
data.setTitle(title);
data.setKeywords(keywords);
data.setDescription(description);
data.setContent(content);
data.setDomain(domain);
data.setCrawlTime(crawlTime);
data.setSpiderType("java_high_concurrency");
data.setStatus(1);
// 上传数据到Kafka
KAFKA_PRODUCER.send(new ProducerRecord<>(KAFKA_TOPIC, url, data.toString()));
KAFKA_PRODUCER.flush();
// 标记去重
REDIS_CLIENT.sadd(DUPLICATE_KEY, urlMd5);
// 更新任务状态
updateTaskStatus(url, 1);
System.out.println("URL抓取成功:" + url);
} catch (IOException | NoSuchAlgorithmException e) {
e.printStackTrace();
System.out.println("URL抓取失败:" + url + ",错误:" + e.getMessage());
updateTaskStatus(url, 0);
}
}
/**
* 生成URL的MD5哈希值(用于去重)
* @param url URL地址
* @return MD5哈希值
* @throws NoSuchAlgorithmException 异常
*/
private static String getMd5(String url) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(url.getBytes());
byte[] bytes = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* 随机生成UA(伪装浏览器)
* @return 随机UA
*/
private static String getRandomUA() {
String[] uas = {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/114.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15"
};
int randomIndex = (int) (Math.random() * uas.length);
return uas[randomIndex];
}
/**
* 调用PHP接口更新任务状态
* @param url URL地址
* @param status 状态(1-成功,0-失败)
*/
private static void updateTaskStatus(String url, int status) {
RequestBody requestBody = new FormBody.Builder()
.add("url", url)
.add("status", String.valueOf(status))
.build();
Request request = new Request.Builder()
.url("https://www.zhisou.com/api/spider/update_task")
.post(requestBody)
.build();
try {
OK_HTTP_CLIENT.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
System.out.println("任务状态更新失败:" + url);
}
}
}
(3)核心优化:线程池调度、失败重试、负载均衡
- 线程池调度:采用FixedThreadPool线程池,核心线程数设置为30(根据服务器CPU、内存调整),避免线程过多导致资源耗尽;同时,批量从Redis获取任务(一次10个),减少Redis交互次数,提升调度效率。
- 失败重试:对于抓取失败的任务(如响应码非200、连接超时),将任务重新放回Redis队列(延迟5分钟后重试),最多重试3次,超过重试次数则标记为失败,存入MySQL失败任务表,后续人工处理。
- 负载均衡:Java爬虫部署在多台服务器上,通过Redis分布式锁实现任务分配,避免多台服务器抓取同一任务;同时,根据服务器负载(CPU、内存使用率)动态调整任务获取频率,确保负载均衡。
2.1.3 C++辅助爬虫实现(特殊场景抓取)
C++辅助爬虫主要负责底层、高性能的抓取任务(如二进制数据抓取、反爬强度高的站点抓取),占整个爬虫集群抓取量的10%,基于libcurl库开发,核心实现如下:
(1)环境配置与依赖
核心依赖:libcurl 7.88.1(HTTP请求)、hiredis 1.2.0(Redis交互)、librdkafka 1.9.2(Kafka交互)、rapidjson 1.1.0(JSON解析)、openssl 3.0.8(HTTPS支持)。
环境配置(Linux):
# 安装依赖库
yum install libcurl-devel hiredis-devel librdkafka-devel rapidjson-devel openssl-devel
# 编译命令(g++)
g++ -o zhisou_cpp_spider zhisou_cpp_spider.cpp -lcurl -lhiredis -lrdkafka -lssl -lcrypto -lpthread
(2)核心实现代码(C++辅助爬虫)
C++爬虫的核心逻辑:基于libcurl实现高性能HTTP请求,支持HTTPS、代理IP,抓取反爬强度高的站点(如加密接口、动态渲染页面的底层数据),解析二进制数据或加密数据,去重后上传到Kafka。
#include<iostream>
#include <string>
#include <vector>
#include <curl/curl.h>
#include <hiredis/hiredis.h>
#include <librdkafka/rdkafkacpp.h>
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <openssl/md5.h>
#include <ctime>
#include <unistd.h>
#include <pthread.h>
using namespace std;
using namespace rapidjson;
// 配置信息
const string REDIS_HOST = "127.0.0.1";
const int REDIS_PORT = 6379;
const string REDIS_PASSWORD = "zhisou_redis";
const int REDIS_DB = 0;
const string KAFKA_BROKERS = "127.0.0.1:9092";
const string KAFKA_TOPIC = "zhisou:spider:data";
const string DUPLICATE_KEY = "zhisou:spider:duplicate";
const string TASK_QUEUE_KEY = "zhisou:spider:task:cpp_assist";
const int THREAD_NUM = 10; // 线程数(C++爬虫侧重高性能,线程数不宜过多)
// Redis客户端指针(全局,避免重复创建连接)
redisContext* redis_ctx = nullptr;
// Kafka生产者指针(全局)
RdKafka::Producer* kafka_producer = nullptr;
// 回调函数:获取HTTP响应数据
size_t write_callback(void* ptr, size_t size, size_t nmemb, string* response) {
size_t total = size * nmemb;
response->append((char*)ptr, total);
return total;
}
// 生成MD5哈希值(用于去重)
string get_md5(const string& str) {
unsigned char md5_buf[MD5_DIGEST_LENGTH];
MD5((unsigned char*)str.c_str(), str.length(), md5_buf);
string md5_str;
for (int i = 0; i < MD5_DIGEST_LENGTH; i++) {
char buf[3];
sprintf(buf, "%02x", md5_buf[i]);
md5_str += buf;
}
return md5_str;
}
// 随机生成UA
string get_random_ua() {
vector<string> uas = {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/114.0"
};
int random_idx = rand() % uas.size();
return uas[random_idx];
}
// 初始化Redis客户端
bool init_redis() {
redis_ctx = redisConnect(REDIS_HOST.c_str(), REDIS_PORT);
if (redis_ctx == nullptr || redis_ctx->err) {
if (redis_ctx) {
cout << "Redis连接失败:" << redis_ctx->errstr << endl;
redisFree(redis_ctx);
redis_ctx = nullptr;
} else {
cout << "Redis连接失败:无法创建连接" << endl;
}
return false;
}
// 认证
redisReply* reply = (redisReply*)redisCommand(redis_ctx, "AUTH %s", REDIS_PASSWORD.c_str());
if (
智搜·站点搜索增强组件:
<div class="zs-search-container">
<a href="https://www.a6f.top/" target="_blank" class="zs-logo-link">
<img src="https://www.a6f.top/images/logo-80px.gif" alt="智搜搜索" class="zs-logo">
</a>
<div class="zs-input-group">
<input type="text" name="wd" placeholder="请输入搜索关键词" class="zs-input">
<button type="submit" class="zs-button"><?php echo $config['name'];?></button>
</div>
</div>
</form>
/* 重置可能的外部样式干扰,仅作用于该搜索框 */
<style>
.zs-search-form,
.zs-search-form * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.zs-search-form {
display: block;
width: 100%;
max-width: 100%;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}
.zs-search-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
background-color: #ffffff;
padding: 8px 0;
}
.zs-logo-link {
display: inline-flex;
align-items: center;
text-decoration: none;
flex-shrink: 0;
}
.zs-logo {
height: 40px;
width: auto;
display: block;
border: 0;
}
.zs-input-group {
display: flex;
flex: 1;
min-width: 180px;
gap: 8px;
flex-wrap: wrap;
}
.zs-input {
flex: 3;
min-width: 120px;
padding: 10px 12px;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
outline: none;
transition: all 0.2s ease;
background-color: #fff;
color: #1f2d3d;
}
.zs-input:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
}
.zs-button {
flex: 1;
min-width: 90px;
padding: 10px 16px;
font-size: 1rem;
font-weight: 500;
color: #fff;
background-color: #4a90e2;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
white-space: nowrap;
}
.zs-button:hover {
background-color: #357abd;
}
@media (max-width: 500px) {
.zs-search-container {
flex-direction: column;
align-items: stretch;
}
.zs-logo-link {
justify-content: center;
}
.zs-input-group {
width: 100%;
}
}
</style>