全栈PHP逆袭:智搜搜索从爬虫到检索全链路技术拆解(ElasticSearch+Kafka+多语言爬虫实战

0 阅读24分钟

fPaFOQj7e.jpeg

引言: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的开发优势,搭配专业中间件弥补其性能短板。

整体架构采用「分层设计+微服务思想」,虽未完全拆分微服务(考虑到中小规模场景的部署成本),但各模块独立封装、通过标准化接口通信,便于后续横向扩展。架构分层如下(从下到上):

  1. 数据采集层:Python+Java+C++多语言爬虫集群,负责网页、接口数据的抓取与初步清洗;
  2. 消息队列层:Kafka,负责解耦爬虫采集与数据处理,缓冲高并发采集压力,避免数据丢失;
  3. 数据存储层:MySQL(结构化数据)+MongoDB(非结构化数据)+Redis(缓存数据),实现不同类型数据的分层存储;
  4. 索引构建层:ElasticSearch集群,负责将处理后的数据构建倒排索引,支撑高效检索;
  5. 业务逻辑层:PHP后端服务,负责核心业务逻辑处理(检索请求、爬虫调度、权限控制等);
  6. 前端展示层:PHP+HTML+JS,负责检索界面、结果展示、交互逻辑实现;
  7. 监控运维层:自定义监控脚本(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 架构数据流闭环详解

fPaFdJCuc.jpeg

智搜搜索的核心数据流可分为「数据采集→数据处理→索引构建→检索服务→结果展示」五大环节,各环节的数据流闭环如下,结合组件协同逻辑,让技术细节更清晰:

  1. 数据采集环节:PHP后端通过爬虫调度模块,生成抓取任务(目标URL、抓取频率、优先级),存入Redis任务队列;Python、Java、C++爬虫从Redis队列中获取任务,根据任务类型执行抓取操作(Python抓取通用网页,Java抓取高并发站点,C++抓取特殊场景数据);抓取完成后,爬虫将原始数据(网页HTML、接口返回数据)通过Kafka发送到数据处理主题,同时将抓取状态(成功/失败)更新到MySQL的爬虫任务表中。
  2. 数据处理环节:PHP数据处理模块作为Kafka的消费者,订阅爬虫数据主题,接收原始数据;对原始数据进行清洗(去除HTML标签、过滤无效内容、去重)、结构化处理(提取标题、关键词、摘要、站点域名);处理完成后,将结构化数据(标题、关键词、站点信息)存入MySQL,非结构化数据(网页正文)存入MongoDB,同时将处理后的数据发送到ElasticSearch索引构建主题。
  3. 索引构建环节:PHP索引模块订阅ElasticSearch索引构建主题,接收处理后的数据;调用ElasticSearch API,创建/更新索引(自定义分词、字段权重),将数据写入ElasticSearch集群,构建倒排索引;索引构建完成后,将索引状态(成功/失败)更新到MySQL的索引表中,并将热点数据缓存到Redis。
  4. 检索服务环节:用户通过前端PHP页面输入检索词(支持site:xxx.com语法),前端将检索请求发送到PHP后端检索接口;PHP后端先查询Redis缓存,若存在热点检索结果,直接返回;若缓存未命中,调用ElasticSearch API,根据检索词(含site语法解析)执行检索操作,获取检索结果;PHP后端将检索结果与MySQL中的站点信息、MongoDB中的网页正文整合,排序后返回给前端。
  5. 结果展示环节:前端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爬虫实现(高并发站点抓取)

fPaFqvS9d.jpeg

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++辅助爬虫实现(特殊场景抓取)

fPaFhr0kA.jpeg

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>