☰
headless-chrome-crawler API 全解:从 HCCrawler 启动、队列调度到缓存与导出的完整接口指南
2026/10/12 3:47:48 网站建设 项目流程
  • 网页爬虫
  • 后端

【免费下载链接】headless-chrome-crawler

Distributed crawler powered by Headless Chrome

项目地址:https://gitcode.com/gh_mirrors/he/headless-chrome-crawler
点击查看免费下载

本指南以 headless-chrome-crawler 官方 API 参考文档(docs/API.md)为骨架,系统讲解基于 Headless Chrome 的分布式爬虫的全部公开接口:从HCCrawler.launch()/connect()的启动方式,到crawler.queue()的 20+ 配置项、11 个生命周期事件,再到SessionCache/RedisCache缓存体系与CSVExporter/JSONLineExporter结果导出。读完本文,你将能够仅凭公开 API 组合出可上线的爬虫任务,并具备按需自定义缓存、导出器与抓取逻辑的能力。

一、HCCrawler 类总览与最小可用示例

HCCrawler是 headless-chrome-crawler 的核心类,它封装了 Puppeteer 的浏览器实例,并提供「启动/连接 → 入队 → 空闲等待 → 关闭」的完整爬取生命周期。所有对外方法集中在 lib/hccrawler.js 中实现,包入口为 index.js。

一份最小可用示例(摘自 API 文档)如下:

const HCCrawler = require('headless-chrome-crawler'); (async () => { const crawler = await HCCrawler.launch({ // 在浏览器中执行的取数函数,返回可序列化对象 evaluatePage: (() => ({ title: $('title').text(), })), // 每次请求成功后回调 onSuccess: (result => { console.log(result); }), }); crawler.queue('https://example.com/'); // 入队一个请求 await crawler.onIdle(); // 等待队列清空 await crawler.close(); // 关闭浏览器 })();

整个流程可以概括为四条链路:启动(launch/connect)→ 调度(queue + PriorityQueue)→ 抓取(Crawler 单页处理)→ 收尾(onIdle/close),每条链路在本文后续章节都有对应的源码级拆解。

二、启动与连接:HCCrawler.launch / connect / executablePath / defaultArgs

2.1 HCCrawler.launch([options])

launch()负责全新启动一个 Chromium 实例,返回 Promise 并 resolve 出HCCrawler实例。源码实现为:

static async launch(options) { const browser = await Puppeteer.launch(pick(options, LAUNCH_OPTIONS)); const crawler = new HCCrawler(browser, omit(options, LAUNCH_OPTIONS)); await crawler.init(); return crawler; }

参考 lib/hccrawler.js,它通过pick将以下选项透传给puppeteer.launch()(对应源码常量 LAUNCH_OPTIONS):

ignoreHTTPSErrors, headless, executablePath, slowMo, args, ignoreDefaultArgs, handleSIGINT, handleSIGTERM, handleSIGHUP, dumpio, userDataDir, env, devtools

这些参数的含义与 Puppeteer 官方一致:例如headless: false可关闭无头模式便于调试,args: ['--no-sandbox']常用于容器环境(测试用例 test/hccrawler/index.test.js 就默认携带该参数)。

2.2 HCCrawler.connect([options])

connect()用于连接到一个已经运行的 Chromium 实例(例如通过wsEndpoint()拿到的 WebSocket 端点),返回同样结构的HCCrawler实例。透传给puppeteer.connect()的选项只有三个(源码 CONNECT_OPTIONS):

browserWSEndpoint, ignoreHTTPSErrors, slowMo

其中browserWSEndpoint是连接已运行浏览器所必需的。测试 test/hccrawler/index.test.js 验证了「同一浏览器多次连接」与「断线后重连」两种场景:先launch拿到wsEndpoint(),再用HCCrawler.connect({ browserWSEndpoint })接入。

注意:connect()与launch()都允许把url、allowedDomains、deniedDomains、timeout、priority、depthPriority、delay、retryCount、retryDelay、jQuery、browserCache、device、username、password、evaluatePage、cookies、extraHeaders作为默认值传入,这样每次queue()时只需覆盖差异项,避免重复设置(API 文档原话:每次入队都写全套配置是冗余的)。

2.3 两个静态工具方法

  • HCCrawler.executablePath():返回随包下载的 Chromium 可执行文件的预期路径(string),测试断言该路径真实存在。
  • HCCrawler.defaultArgs():返回 Chromium 启动时的默认参数数组,测试断言其中包含--no-first-run。

两者都是对 Puppeteer 同名静态方法的直接转发,参考 lib/hccrawler.js。

2.4 构造器公共选项

无论是launch还是connect,都会接收下列公共选项:

选项类型默认值说明
maxConcurrencynumber10同时打开的最大页面数(并发数)
maxRequestnumber0最大请求数上限,传0表示不限制
exporterExporternull继承 BaseExporter 接口的导出器对象
cacheCacheSessionCache继承 BaseCache 接口的缓存对象,用于记忆并跳过重复请求
persistCachebooleanfalse关闭或断开连接时是否清空缓存;false表示关闭时清空
preRequest(options)Function—每次请求前执行,可修改options;返回false可跳过该请求
customCrawl(page, crawl)Function—自定义抓取流程,暴露 Puppeteer 原始page对象
onSuccess(result)Function—evaluatePage()成功时回调
onError(error)Function—请求失败时回调

对应的默认值合并逻辑见源码 lib/hccrawler.js:maxDepth: 1、maxConcurrency: 10、retryCount: 3、retryDelay: 10000、timeout: 30000、jQuery: true、browserCache: true等都在此处统一初始化。

三、队列控制:crawler.queue([options]) 全参数详解

queue()是入队入口,接受字符串、对象或数组三种形态:

  • 传字符串:等价于{ url: 字符串 };
  • 传对象:按对象中的字段覆盖默认配置;
  • 传数组:数组内每个元素依次入队,元素可以是字符串或对象(示例见 examples/multiple-queue.js)。

测试 test/hccrawler/index.test.js 覆盖了三种形态的组合用法。注意queue()返回的 Promise 在入队完成时 resolve,而非请求完成时。

3.1 queue 选项速查表

选项类型/取值默认值说明
urlstring—要导航的地址,必须带 scheme,如https://;缺失会抛错
maxDepthnumber1自动跟随链接的最大深度;保持默认1即不跟随链接
prioritynumber1队列基础优先级,数值越大越先被处理
depthPrioritybooleantrue是否按深度调整优先级;保持默认会提高更深层级的优先级,即深度优先搜索(DFS)
skipDuplicatesbooleantrue是否跳过重复请求;url、userAgent、device、extraHeaders完全相同即视为重复
skipRequestedRedirectbooleanfalse是否跳过已出现在重定向链中的请求;skipDuplicates=false时忽略
obeyRobotsTxtbooleantrue是否遵守 robots.txt
followSitemapXmlbooleanfalse是否用 sitemap.xml 发现更多地址
allowedDomainsArray<string|RegExp>—允许请求的域名列表,null表示不校验
deniedDomainsArray<string|RegExp>—禁止请求的域名列表,null表示不校验
delaynumber0每次请求后的等待毫秒数;设置后maxConcurrency必须为1
timeoutnumber30000导航超时(毫秒),传0禁用超时
waitUntilstring|Array<string>load判定导航成功的时机,透传给 Puppeteerpage.goto()的waitUntil
waitForObject—等待某个元素/谓词/超时后再取值,透传给 Puppeteerpage.waitFor();含selectorOrFunctionOrTimeout、options、args三个字段
retryCountnumber3失败重试上限次数
retryDelaynumber10000每次重试失败后的等待毫秒数
jQuerybooleantrue是否自动向页面注入 jQuery
browserCachebooleantrue是否开启浏览器缓存
devicestring—要模拟的设备名(如Nexus 7),支持列表以 Puppeteer 的 DeviceDescriptors 为准
username/passwordstring—HTTP Basic 认证凭证,不需要时传null
screenshotObjectnull截图选项,透传给 Puppeteerpage.screenshot();null表示不截图
viewportObject—视口设置,透传给page.setViewport();含width、height
userAgentstring—覆盖页面 UA
extraHeadersObject—每次请求附加的请求头,所有值必须是字符串
cookiesArray<Object>—每次请求携带的 Cookie 列表,每个 Cookie 必须指定url或domain;name、value必填,其余字段与 Cookie 标准字段一致(expires为 Unix 秒、sameSite取值"Strict"或"Lax")
evaluatePage()Function—在浏览器中求值的函数,须返回可序列化对象;若不可序列化则结果为undefined

3.2 queue 的源码级行为约束

queue()在 lib/hccrawler.js 中做了四件事,理解这些有助于避开常见报错:

  1. 禁止覆盖构造器选项:若在queue()的对象里出现maxConcurrency、cache、exporter、onSuccess等构造器专属选项,会直接抛出Overriding xxx is not allowed!(测试 test/hccrawler/index.test.js 验证了这一点)。
  2. evaluatePage 会被包装为立即执行函数:源码mergedOptions.evaluatePage =(${mergedOptions.evaluatePage})()`` 说明该函数在浏览器里以 IIFE 形式运行。
  3. 强校验:url缺失抛Url must be defined!;device不在支持列表抛Specified device is not supported!;delay > 0且maxConcurrency !== 1抛Max concurrency must be 1 when delay is set!(三条均有对应测试)。
  4. URL 规范化:通过url.parse()统一为规范 href 后再入队。

3.3 优先级与遍历策略的实现

优先级队列由 lib/priority-queue.js 实现:入队时把priority作为最后一个参数传给缓存层,SessionCache内部用二分插入(lowerBound)保持队列按优先级降序(cache/session.js),出队时取队首。Redis 版本则用有序集合ZADD按分数排序、ZREVRANGE取出最高分项(cache/redis.js)。

深度优先的逻辑在 lib/hccrawler.js:当未显式指定priority且depthPriority为true时,直接用depth作为优先级,深度越大的链接优先级越高,从而形成 DFS;若想改为广度优先(BFS),将depthPriority置为false即可。完整的优先级用法可参考 examples/priority-queue.js。

3.4 链接跟随、robots.txt 与 sitemap.xml

  • 链接跟随:单页抓取完成后,lib/crawler.js 会通过exposeFunction收集页面中所有a[href]以及iframe/frame内的链接(含相对路径解析、锚点剔除、协议过滤与去重);随后 lib/hccrawler.js 在depth < maxDepth时把子链接以depth + 1入队,否则发出maxdepthreached事件。
  • robots.txt:默认开启。抓取前 lib/hccrawler.js 会用request-promise拉取并缓存robots.txt(失败则按空内容处理并发出robotstxtrequestfailed事件),再用robots-parser判断当前 URL 是否被允许;被禁止则发出requestdisallowed并跳过。
  • sitemap.xml:默认关闭。开启后从 robots.txt 的Sitemap:条目读取 sitemap 地址,解析其中所有<loc>节点(见 lib/helper.js)并逐个入队。测试 test/hccrawler/index.test.js 验证了开启前后行为差异。

3.5 域白名单 / 黑名单

allowedDomains与deniedDomains均支持字符串与正则两种元素,匹配逻辑见 lib/helper.js:正则用test匹配 hostname,字符串要求严格相等。判定顺序为「先黑名单后白名单」,见 lib/hccrawler.js。测试 test/hccrawler/index.test.js 覆盖了精确匹配、正则匹配、未命中跳过三种场景。

3.6 重试、延迟与去重

  • 重试:_request()(lib/hccrawler.js)在失败后若retryCount < options.retryCount则等待retryDelay后递归重试并发出requestretried,超过上限才发出requestfailed并回调onError。
  • 去重键:skipDuplicates为true时,lib/helper.js 取url、device、userAgent、extraHeaders四个字段做稳定 JSON 序列化后计算 MD5 取前 10 位作为缓存键;命中缓存即跳过(requestskipped事件)。
  • 重定向去重:skipRequestedRedirect开启后,会把重定向链中出现的每个 URL 都标记为已请求,避免重复抓取(测试 test/hccrawler/index.test.js)。

四、生命周期与流量控制:pause / resume / setMaxRequest / onIdle / close / disconnect

方法返回值说明
crawler.setMaxRequest(maxRequest)—动态修改构造时传入的maxRequest
crawler.pause()—暂停处理队列,配合resume()恢复
crawler.resume()—恢复处理队列;常用于pause()之后或达到maxRequest上限被自动暂停之后
crawler.clearCache()Promise清空当前使用的缓存
crawler.close()Promise关闭浏览器,等价于终止整个爬虫
crawler.disconnect()Promise断开与浏览器的连接(不关闭浏览器本身)
crawler.onIdle()Promise队列清空或暂停时 resolve

关键的自动暂停逻辑在 lib/hccrawler.js:每完成一次请求requestedCount自增,一旦达到maxRequest就发出maxrequestreached事件并自动调用pause()。因此经典用法是「达到上限 →setMaxRequest调高 →resume()继续」,见 examples/pause-resume.js。

onIdle()的底层实现是 lib/priority-queue.js:它注册一个 resolve 函数,当_pull()发现队列为空或处于暂停状态时触发 resolve。close()/disconnect()都会依次执行「结束队列 → 关闭/断开浏览器 → 结束导出器 → 按persistCache决定是否清空缓存 → 关闭缓存」(lib/hccrawler.js)。

五、信息查询方法

方法返回值说明
crawler.version()Promise<string>Chromium 版本号(测试断言含HeadlessChrome)
crawler.userAgent()Promise<string>默认 User-Agent(测试断言含HeadlessChrome)
crawler.wsEndpoint()string浏览器 WebSocket 端点,供connect()复用(测试断言以ws://开头)
crawler.isPaused()boolean队列是否处于暂停状态
crawler.queueSize()Promise<number>队列中的任务总数
crawler.pendingQueueSize()number正在处理中的任务数
crawler.requestedCount()number已完成的请求总数

这些方法大多是对队列或浏览器的薄封装(lib/hccrawler.js),常用于监控与调试。对应断言见 test/hccrawler/index.test.js。

六、事件系统:11 个公开事件

HCCrawler继承自EventEmitter,公开事件名定义在 lib/hccrawler.js,全部为小写字符串:

事件回调参数触发时机(源码位置)
requestdisallowedoptions请求被 robots.txt 拒绝(lib/hccrawler.js)
requeststartedoptions请求开始(lib/hccrawler.js)
requestskippedoptions请求被跳过(去重/域过滤/preRequest 拦截)
requestfinishedoptions请求成功完成(lib/hccrawler.js)
requestretriedoptions请求失败并进入重试(lib/hccrawler.js)
requestfailederror重试耗尽后请求失败;error含options、depth、previousUrl
robotstxtrequestfailederrorrobots.txt 拉取失败(lib/hccrawler.js)
sitemapxmlrequestfailederrorsitemap.xml 拉取失败(lib/hccrawler.js)
maxdepthreachedoptions队列达到maxDepth上限(lib/hccrawler.js)
maxrequestreached—达到maxRequest上限并自动暂停(lib/hccrawler.js)
disconnected—浏览器实例断开连接(lib/hccrawler.js)

使用方式与 Node.js 标准 EventEmitter 一致:crawler.on('requeststarted', options => { ... })。测试中大量用事件计数验证行为,例如 test/hccrawler/index.test.js 通过requeststarted/requestfinished各触发 1 次来断言一次完整请求。error类事件携带的error对象额外附加了options、depth、previousUrl三个字段,便于定位失败来源。

七、回调扩展点:preRequest、onSuccess、onError、customCrawl

7.1 preRequest(options)

每个请求真正执行前调用(lib/hccrawler.js),接收「已合并默认值的 queue options」,可就地修改options,返回false则跳过该请求。典型用途是条件化截图:examples/conditional-screenshot.js 中自定义saveAs字段,preRequest据此决定是否注入screenshot选项,未携带saveAs的请求直接返回false被跳过。

7.2 onSuccess(result) 的完整 result 结构

result对象由 lib/crawler.js 组装,包含 9 个字段:

  • options:queue 选项合并默认值后的完整对象;
  • depth:当前链接深度;
  • previousUrl:上一请求的 URL,初始请求为null;
  • response:最终响应对象,含ok(状态码是否在 200–299)、status(状态码)、url(最终请求的 URL,重定向后可能与options.url不同)、headers;
  • redirectChain:重定向链数组,每项含url与headers;
  • result:evaluatePage()的返回值(可序列化);
  • screenshot:截图 Buffer,未传screenshot选项时为null;
  • cookies:Cookie 数组,每项含name、value、domain、path、expires(Unix 秒)、httpOnly、secure、session、sameSite;
  • links:页面中找到的链接列表(已解析为绝对地址并去重)。

测试对response.ok、response.status、redirectChain长度与每跳 URL 都有断言(test/hccrawler/index.test.js)。

7.3 onError(error)

请求最终失败时回调。error为 Error 对象,并附加options、depth、previousUrl字段(lib/hccrawler.js)。

7.4 customCrawl(page, crawl) 深入底层

这是扩展性最强的入口:回调接收 Puppeteer 原始page对象与内置的crawl()函数(lib/hccrawler.js)。你可以:

  • 在crawl()前操作页面:如开启请求拦截;
  • 在crawl()后补充字段:如追加result.content = await page.content();
  • 必须把扩展后的 result 返回给onSuccess。

完整示例见 examples/custom-crawl.js:它用page.setRequestInterception(true)只放行以/结尾的请求,其余全部abort(),并给结果追加页面 HTML。

八、缓存体系:SessionCache、RedisCache 与自定义 BaseCache

8.1 SessionCache(默认)

HCCrawler默认使用SessionCache(lib/hccrawler.js),它在内存Map中记录已请求的 URL 与 robots.txt 内容(cache/session.js)。传入cache: null可关闭去重:

const crawler = await HCCrawler.launch({ cache: null }); // 禁用缓存

8.2 RedisCache:分布式去重

RedisCache(cache/redis.js)把已请求 URL 与 robots.txt 持久化到 Redis,适用于多台服务器组成的分布式爬虫,避免重复请求。构造参数除expire(每个值写入后的过期秒数,默认null不过期)外,其余全部透传给 NodeRedis 的redis.createClient()(如host、port):

const HCCrawler = require('headless-chrome-crawler'); const RedisCache = require('headless-chrome-crawler/cache/redis'); const cache = new RedisCache({ host: '127.0.0.1', port: 6379 }); (async () => { const crawler = await HCCrawler.launch({ persistCache: true, // 关闭爬虫时不清空缓存 cache, }); // ... })();

persistCache: true是关键配合项:只有置为true,close()时才不会执行flushdb清空缓存。RedisCache 内部用有序集合承载优先级队列,并借助 Lua 脚本原子完成「取出最高优先级任务并删除」(cache/redis.js)。跨进程复用缓存的完整演示见 examples/redis-cache.js:第一次启动请求example.com、example.net并保留缓存,第二次启动后example.net直接跳过、只新增请求example.org。

8.3 自定义 BaseCache

任何缓存实现都必须继承 cache/base.js 的BaseCache接口,并实现 8 个方法:init()、close()、clear()、get(key)、set(key, value)、enqueue(key, value, priority)、dequeue(key)、size(key)、remove(key)。仓库提供了基于文件系统的完整参考实现 examples/custom-cache.js(FsCache),其中enqueue按priority降序维护队列、dequeue弹出队首,行为与内置缓存保持一致。

九、结果导出:CSVExporter、JSONLineExporter 与自定义 BaseExporter

9.1 CSVExporter

构造选项:

  • file:输出文件路径(必填);
  • fields:作为列(同时用作表头)的字段列表(必填,用点号路径取值,如response.url);
  • separator:列分隔字符,默认,。
const HCCrawler = require('headless-chrome-crawler'); const CSVExporter = require('headless-chrome-crawler/exporter/csv'); const FILE = './tmp/result.csv'; const exporter = new CSVExporter({ file: FILE, fields: ['response.url', 'response.status', 'links.length'], separator: '\t', }); (async () => { const crawler = await HCCrawler.launch({ exporter }); // ... })();

实现见 exporter/csv.js:writeHeader()写出表头行,writeLine()通过get(result, field)按点号路径取值,并用escapeQuotes(lib/helper.js)对含分隔符、引号或换行的值做 RFC4180 风格转义。落地示例见 examples/csv-exporter.js。

9.2 JSONLineExporter

构造选项:

  • file:输出文件路径(必填);
  • fields:要过滤保留的字段列表,默认null(不过滤);
  • jsonReplacer:自定义序列化 replacer 函数,默认null,可用于固定键的排序顺序。
const HCCrawler = require('headless-chrome-crawler'); const JSONLineExporter = require('headless-chrome-crawler/exporter/json-line'); const FILE = './tmp/result.json'; const exporter = new JSONLineExporter({ file: FILE, fields: ['options', 'response'], }); (async () => { const crawler = await HCCrawler.launch({ exporter }); // ... })();

实现见 exporter/json-line.js:每行输出一个 JSON 对象(JSON Lines 格式),可搭配fields裁剪体积。

9.3 自定义 BaseExporter

自定义导出器需继承 exporter/base.js 的BaseExporter:基类负责创建写入流(默认utf8编码)与end()/onEnd(),子类必须实现writeLine(result)、writeHeader()、writeFooter()。仓库提供了用util.inspect输出调试结果的参考实现 examples/custom-exporter.js。导出器的生命周期由爬虫自动驱动:构造时writeHeader(),每次请求成功后writeLine(result),close()/disconnect()时writeFooter()+end()(lib/hccrawler.js)。

十、抓取单页的完整内部流程

为了理解所有选项的生效顺序,这里串讲单次请求的内部调用链(实现见 lib/crawler.js):

  1. _prepare()(lib/crawler.js)并行执行:拦截window.open防止新开标签页、Basic 认证(username/password)、设备模拟(device)、视口设置(viewport)、CSP 绕过(jQuery开启时)、浏览器缓存开关(browserCache=false时关闭)、UA 覆盖、附加请求头、注入 Cookie、监听页面 console/dialog/pageerror;
  2. _request():以timeout/waitUntil调用page.goto(url);
  3. _waitFor():若配置了waitFor则等待元素/谓词/超时——测试用 200ms 延迟渲染的页面验证了该选项的价值(test/hccrawler/index.test.js);
  4. 并行执行_scrape()(注入 jQuery 后执行evaluatePage)、_screenshot()、_getCookies()、_collectLinks();
  5. 组装 result 对象返回给上层。

十一、综合实战:一个可复制的完整爬虫

将以上 API 组合起来,可以得到一个兼顾「深度跟随、重试、截图、CSV 导出」的完整任务(各文件均可直接运行,示例需在仓库根目录执行NODE_PATH=../ node examples/xxx.js):

const HCCrawler = require('headless-chrome-crawler'); const CSVExporter = require('headless-chrome-crawler/exporter/csv'); const exporter = new CSVExporter({ file: './tmp/result.csv', fields: ['response.url', 'response.status', 'result.title', 'links.length'], }); (async () => { const crawler = await HCCrawler.launch({ maxConcurrency: 4, // 并发 4 个页面 maxRequest: 100, // 最多 100 个请求 maxDepth: 2, // 跟随两层链接 retryCount: 3, retryDelay: 5000, exporter, evaluatePage: () => ({ title: $('title').text() }), onSuccess: result => { console.log(`[${result.response.status}] ${result.options.url}`); }, onError: error => { console.error(`Failed: ${error.options.url}`); }, }); crawler.on('requestskipped', () => console.log('Skipped a duplicate.')); crawler.on('maxrequestreached', () => { crawler.setMaxRequest(200); // 动态扩容后继续 crawler.resume(); }); await crawler.queue({ url: 'https://example.com/', allowedDomains: ['example.com'], obeyRobotsTxt: true, }); await crawler.onIdle(); await crawler.close(); // persistCache 默认 false,缓存随之清空 })();

十二、环境与依赖说明

  • headless-chrome-crawler 内部打包了 Puppeteer,安装(yarn add headless-chrome-crawler或npm i headless-chrome-crawler)时会自动下载对应版本的 Chromium;
  • 本仓库package.json声明puppeteer: 1.20.0、jquery: 3.5.1、redis为 peerDependency(仅使用RedisCache时才需要安装),Node 引擎要求>= 8.10.0;
  • RedisCache与test/hccrawler/redis.test.js依赖本机 Redis 服务,仓库默认测试命令会排除 Redis 相关用例(见 package.json 的jest-exclude-redis-cache脚本);
  • 更多调试手段可阅读 docs/TIPS.md,更多运行示例可浏览 examples/ 目录。
  • 网页爬虫
  • 后端

【免费下载链接】headless-chrome-crawler

Distributed crawler powered by Headless Chrome

项目地址:https://gitcode.com/gh_mirrors/he/headless-chrome-crawler
点击查看免费下载
上一篇:web.py 快速入门:8 行代码构建你的第一个 Python Web 应用
下一篇:Pinpoint Undertow Servlet 插件深度解析:Servlet 3.0 异步链路追踪与 WildFly 场景支持

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询