news 2026/10/19 16:45:07

用 Qt 实现文件搜索工具:QDir 递归遍历、正则匹配与多线程架构解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Qt 实现文件搜索工具:QDir 递归遍历、正则匹配与多线程架构解析

简介:面向初学者的基于Qt框架的文件搜索示例资源,运用改良算法实现文件夹内文件遍历与过滤检索,并将匹配结果以表格形式呈现。支持双击列表中的文件调用系统关联程序打开编辑,覆盖文档、表格、演示文稿等常见办公格式,适合学习文件操作、界面交互与多线程协作的读者参考。压缩包共十七个文件,包含六个源文件与五个头文件,另有工程文件、界面图标、图像资源与资源描述文件,整体结构精简,大小仅十二KB,便于逐行研读。目前已有三千六百四十六人学习下载。通过该项目可以了解搜索线程如何拆分、表格委托如何定制、配置对话框如何维护,以及应用权限清单如何声明;对于理解界面与业务逻辑分层、熟悉文件关联编辑机制都有直接帮助。

1. 拿 Qt 写文件搜索:比 Everything 慢,但比想象中难搞

你有没有遇到过这种场景:某个测试环境里不能装第三方工具,系统自带搜索又慢得让人抓狂,而你手头唯一能用的图形界面框架就是 Qt。这时候自己写一个文件搜索器,就成了最务实的选择。这个项目就是用 Qt Widgets 实现的一个本地文件搜索工具,支持按文件名关键字过滤、递归遍历子目录、正则匹配,以及双击结果直接打开所在目录。它不追求 Everything 那种毫秒级索引速度,胜在逻辑清晰、代码量可控,适合拿来学 Qt 的 QDir 遍历、QFileInfo 属性读取、QThread 后台任务和 QTreeWidget 结果展示。如果你正打算用 Qt 做一个带图形界面的小工具,或者想把文件操作、多线程、界面刷新这三件事串起来跑一遍,这个项目的代码结构值得当做一个范本来看。

我从一个实际开发者的视角把这个项目的源码拆开讲:先看整体模块怎么分层,再逐段讲核心的搜索逻辑怎么写的,最后给你一套完整的编译运行步骤,以及我在改这个项目时踩过的几个坑。

2. 项目模块拆解:界面、搜索核心、线程交互三层架构

整个项目不是一个单文件的玩具,而是按职责拆成了三个层次。界面层负责输入关键字、展示结果树;业务层封装目录遍历和文件匹配规则;控制层通过信号槽把后台线程的搜索结果投递到界面。这个分层方式对后续二次开发非常重要,因为你可以不动界面代码,直接替换匹配引擎来扩展搜索能力。

2.1 工程目录结构与核心类职责

项目打开后你会看到典型的 Qt Widgets 工程布局,头文件和源文件分开存放,UI 文件单独管理界面布局。这是一个我用 Qt Creator 新建 Widgets 项目后手动整理过的标准结构,也符合大多数 Qt 项目的组织习惯:

FileSearch/ ├── FileSearch.pro # qmake 工程文件 ├── main.cpp # 程序入口 ├── mainwindow.h/.cpp # 主窗口类,负责界面布局和结果展示 ├── SearchWorker.h/.cpp # 后台搜索线程类,执行遍历与匹配 ├── search_worker.cpp # 线程函数实现,QDir 递归遍历 └── mainwindow.ui # 界面布局文件,含输入框、按钮、结果树

mainwindow是核心控制类,用户输入的关键字、选中的搜索路径、点击的搜索按钮,全部从它开始流转。SearchWorker是被移动到独立线程里的执行者,它接收路径前缀和关键字,然后开始遍历文件系统,匹配到的结果通过信号发回主线程。两个类之间不直接调用函数,只通过信号槽通信,这是一个值得学习的线程安全设计。

2.2 搜索执行流程:从用户输入到结果回显的完整链路

整个搜索链路可以拆成六个关键步骤:获取界面参数、启动后台线程、递归遍历目录、逐个文件匹配、结果信号回传、主线程刷新树控件。我把核心流程的核心代码片段整理如下,配合注释理解会比直接跑工程更快。

// SearchWorker.cpp 中的核心遍历函数 void SearchWorker::doSearch(const QString &path, const QString &keyword) { QDir dir(path); // 获取当前目录下所有条目,不含符号链接,避免循环引用 QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); foreach (const QFileInfo &info, entries) { if (m_stopRequested) { return; // 检查停止标志位,支持用户取消 } if (info.isDir()) { // 目录先递归进去继续找 doSearch(info.absoluteFilePath(), keyword); } else if (info.isFile()) { // 文件就做文件名匹配 if (info.fileName().contains(keyword, Qt::CaseInsensitive)) { emit fileFound(info.absoluteFilePath()); } } } }

这段代码的核心逻辑集中在QDir::entryInfoList的参数上。QDir::Dirs | QDir::Files表示同时列出目录和文件,NoSymLinks防止软链接导致无限递归,NoDotAndDotDot排除.和..这两个特殊目录。Qt::CaseInsensitive让搜索对大小写不敏感,Windows 用户会更习惯这种表现。当匹配到文件时通过emit fileFound()把绝对路径发射出去,这是 Qt 信号槽机制的典型用法,信号在主线程被槽函数接收,从而实现线程安全的 UI 更新。

2.3 界面交互设计:输入、触发、展示三板斧

主界面的逻辑是把 QLineEdit 当作关键字输入框,QPushButton 作为搜索触发按钮,QTreeWidget 作为结果展示容器。整个交互设计里最具讨论价值的是结果树的动态加载策略,我把相关代码简化后罗列如下。

// mainwindow.cpp 中处理搜索结果的槽函数 void MainWindow::onFileFound(const QString &filePath) { QFileInfo info(filePath); QTreeWidgetItem *item = new QTreeWidgetItem(ui->treeWidget); item->setText(0, info.fileName()); // 第一列显示文件名 item->setText(1, info.absolutePath()); // 第二列显示父目录 item->setText(2, QString::number(info.size())); // 第三列显示文件大小 item->setData(0, Qt::UserRole, filePath); // 完整路径存到 UserRole,双击时用 item->setToolTip(0, filePath); // 悬停提示完整路径 ui->treeWidget->addTopLevelItem(item); ui->labelCount->setText(QString::number(ui->treeWidget->topLevelItemCount())); }

这里比较重要的设计细节是Qt::UserRole的用法。界面显示需要文件名和路径,但双击打开文件时需要完整路径,把路径存在UserRole里可以避免用文件名去拼接路径的麻烦,尤其在包含空格和特殊字符的路径中,这种存储方式更可靠。逐条添加而不是一次性组装整棵树的策略,能换来搜索过程的渐进式反馈,文件很多时不会让界面卡死等待。

表格可以在这里列举三类节点的交互参数,方便快速查阅:

界面控件关键属性作用
QLineEditsetPlaceholderText提示用户输入关键字
QPushButtonsetShortcut绑定回车触发搜索
QTreeWidgetsetColumnCount控制列数,按需展示文件名、路径、大小

这三层结构把搜索工具最常见的逻辑组织了起来,核心的遍历代码不过几十行,多数工作量反而在结构设计和用户交互上。到这里这个项目能跑起来的闭环已经清楚了,下一步深入看它是怎么处理更复杂的搜索场景的。

3. 核心代码逐段精讲:QDir 递归、大小写与正则匹配的实现

搜索工具最核心的部分就是目录遍历和文件名匹配。前一章把整体结构梳理清楚了,这一章把每一段关键代码从文件系统调用的角度掰开揉碎,看它到底做了什么,以及哪些部分是优化空间最大的地方。

3.1 目录递归的边界条件与隐藏问题

递归遍历最害怕的就是目录嵌套层级太深和循环引用。常见文件系统里硬链接和挂载点会造成目录环路,一个目录通过硬链接指向自身祖先,递归就会无限展开,最终栈溢出崩溃。这个项目的写法里,NoSymLinks已经拦住了符号链接构成的环路,但挂载点(比如 Windows 的 junction 目录)仍然可能造成问题。稳妥的做法是在递归函数里维护一个访问过的目录集合,或者限制递归深度。实际生产环境中,我会在递归函数里增加一个层级参数,超过 32 层直接跳过,这比无限递归安全得多。修改方式很简单,把函数签名加一个int depth参数,进入时if (depth > 32) return;即可。

3.2 文件过滤:把 contains 扩展成正则表达式匹配

项目基础版本用的是QString::contains做子串匹配,功能够用,但谈不上精准。改造方案是引入 QRegularExpression,让用户输入正则表达式来精确控制匹配规则,比如输入\.cpp$只匹配以.cpp结尾的文件,输入^report_202[0-9]匹配特定日期格式的文件。这段代码展示升级后的匹配策略:

// 使用 QRegularExpression 替换 contains void SearchWorker::doSearchWithRegex(const QString &path, const QRegularExpression &regex) { QDir dir(path); QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); foreach (const QFileInfo &info, entries) { if (m_stopRequested) { return; } if (info.isDir()) { doSearchWithRegex(info.absoluteFilePath(), regex); } else if (info.isFile()) { // 正则匹配文件名,match 返回 true 表示命中 if (regex.match(info.fileName()).hasMatch()) { emit fileFound(info.absoluteFilePath()); } } } }

调用方构造正则的时候要特别留意转义问题。QRegularExpression的构造函数接收的是正则模式字符串,如果用户直接输入\.cpp$,反斜杠在 C++ 字符串里要写成"\\.cpp$"。这块容易踩坑,细节我放在后面避坑章节专门讲。

3.3 搜索结果排序与重复项消除

多个子目录下可能包含同名文件,如果搜索根目录重叠(比如同时搜了C:/和C:/Windows),同一条结果会被多次传给界面层。解决方式是在回调函数里先检查QTreeWidget是否已存在相同路径的条目。更高效的做法是在SearchWorker里维护一个QSet<QString>记录已发送的路径,命中即丢弃重复项。排序则有两种策略:界面层拿到结果后排序,适合小规模;重写QTreeWidgetItem::operator<自定义排序规则,更适合大规模结果集。这个项目用的逐条插入方式,排序放到了遍历结束后的后处理里,如果追求实时排序体验,可以考虑插入时按文件名二分定位到指定索引位置。

3.4 结果数据成本:路径字符串拷贝与信号发射开销

每次emit fileFound()都把绝对路径以值方式传递,文件数多到几万时,QString 的深拷贝和信号调度开销相当可观。改进思路有两条:一条是把结果批量打包成QStringList,每攒满 200 条发射一次信号,大幅减少跨线程事件循环的次数;另一条是改用QFileInfoList直接传递结构体列表,界面层按需提取属性,减少 QFileInfo 重复构造。实际测试下来,搜索十万个文件时批量信号比逐条信号快约三成,代码改动也不大,这是性价比很高的一次优化。

正则匹配和批量发送属于这个项目可以独立使用的两个核心技巧。到这里项目的代码逻辑已经讲透,但很多人卡在这一步看懂了却跑不起来,下一章把编译、运行、调试的完整流程梳理清楚。

4. 编译运行与开发环境配置:Windows 环境从零到跑通

拿到这个项目源码后,最现实的问题是怎么把它编译起来。Qt 的构建系统有 qmake 和 CMake 两套,这套源码用的是 qmake,所以环境配置上有一点特有步骤,完整走一遍大概需要十分钟。

4.1 环境准备:编译器版本与 Qt 套件选择

Windows 上首选 Qt Creator 配 MSVC 编译器,版本对应关系我列成表,方便对照选择:

Qt 版本配套编译器备注
Qt 5.15 LTSMSVC 2019稳定省心,适合老项目迁移
Qt 6.5 LTSMSVC 2019 / 2022新特性多,字符串处理性能更好
Qt 6.8 LTSMSVC 2022长期支持版,新项目建议直接用

下载时建议把套件选全,Debug 和 Release 都勾上,否则后面调试的时候会缺符号文件。有人用 MinGW 版本编译同一套代码也没有问题,但我在 Qt 6 上遇到过一次 QRegularExpression 在 MinGW 下的兼容性异常,换回 MSVC 就正常了,所以我个人更推荐 MSVC 组合。

安装完成后打开工程文件有两种方式:直接在 Qt Creator 里点击FileSearch.pro,或者通过命令行qmake && mingw32-make(MSVC 下的nmake)构建。如果是新手,用 Qt Creator 图形界面打开.pro文件是零门槛的方式,工具会自动读取项目配置、完成 qmake 步骤并生成构建目录。

4.2 编译步骤与常见链接错误排查

按 F5 直接跑大概率会报错,主要问题集中在几个方面。最常见的链接期错误是LNK2019: unresolved external symbol,通常是因为.pro文件里漏掉了模块声明。这个项目依赖的是QT += core gui widgets,如果你拉取的分支里这句话被注释掉了,搜索相关的QTreeWidget和QLineEdit类就会链接失败。打开.pro文件检查就明白:

QT += core gui widgets TARGET = FileSearch TEMPLATE = app SOURCES += main.cpp \ mainwindow.cpp \ SearchWorker.cpp HEADERS += mainwindow.h \ SearchWorker.h FORMS += mainwindow.ui

TARGET决定生成 exe 的名字,TEMPLATE = app表示这是独立应用程序而不是库。这两行作为工程核心项一般没人动,但是如果你复制别人的.pro文件改了个名字,记得TARGET也要同步更新,否则生成的程序名和你预期不一致。

另一个高频错误是编译期找不到QtWidgets/QTreeWidget头文件。检查QT += widgets是否丢失是第一步,第二步是在项目构建步骤里强制重新执行 qmake(右键项目名,点击 Run qmake)。这个操作会重新生成Makefile,新增的模块依赖才能真正生效。

4.3 调试技巧:qDebug 在搜索逻辑里的实际用法

跑起来之后想确认遍历逻辑有没有问题,最直接的手段是在 SearchWorker 的搜索函数里加临时输出。qDebug()打印路径和文件名,就能清晰看到每一层递归的展开顺序。但要注意,你把 qDebug 放在信号发射处会得到大量输出,窗口控制台会被瞬间刷爆。更好用的排查手段是利用 Qt Creator 的断点功能,在onFileFound槽函数开头下断点,然后逐次跳过,观察 QTreeWidget 的条目增长是否符合预期。

我不太建议一上来就在 Worker 里加各种输出,先跑通最小流程,确认目录递归正常,再逐步加过滤条件,这样定位问题明显更高效。这一段故意写得比较琐碎,因为这些坑几乎所有人都会碰上。

5. 避坑与常见问题排查:递归死循环、中文乱码与信号丢失

项目跑通是一回事,项目跑得稳是另一回事。这章专门聊我在使用和修改这套代码时实际遇到的四个问题,按现象、原因、解决的顺序拆解,便于你在自己调试时快速定位。

5.1 搜索到一半程序崩溃,原因不是递归深而是符号链接

现象:搜索某个大目录时程序栈溢出直接崩溃,排查完逻辑觉得递归没问题。

原因:Windows 的某个目录存在符号链接形式的循环引用,entryInfoList里虽然写了NoSymLinks过滤,但这个过滤只对当前层生效,如果符号链接触发了一个隐藏的挂载点,递归依然可能绕进去。

解决:把QDir::NoSymLinks换成组合参数QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks的同时加上深度限制。在递归函数入口处写if (depth > 24) return;,循环引用最多绕 24 层就断掉,既不影响正常目录的深度,又杜绝了环路问题。

5.2 中文路径搜出乱码,界面显示文件名一堆问号

现象:搜索包含中文名的文件时,界面显示的路径和文件名变成问号。

原因:Qt 5 默认在 Windows 下使用 UTF-8,而源码文件如果保存成 GBK 编码,中文字符串字面量在运行期会解析错位。

解决:Head 文件统一转成 UTF-8 编码(Qt Creator 菜单里 Edit -> Select Encoding -> UTF-8),同时确认.pro文件里加上QMAKE_CXXFLAGS += /utf-8(MSVC 下必要)。这两处处理完,中文路径的显示就正常了。

5.3 执行搜索时界面假死,鼠标转圈无法操作

现象:输入关键字点搜索,界面立马卡住,等搜索结果全部出来才有响应。

原因:doSearch是在主线程里直接调用的,虽然SearchWorker写了独立线程,但搜索函数本身还是同步执行的。看代码时容易忽略一个问题:线程里执行的是run()函数,而这里的doSearch是被信号直接调用的普通函数。

解决:用QtConcurrent::run替代手写线程管理,这个 API 可以直接把函数抛到线程池执行。具体写法是QtConcurrent::run(this, &SearchWorker::doSearch, path, keyword),注意把doSearch改成私有槽函数,然后通过信号接收结果。

5.4 文件数量大时界面卡顿明显,滚动结果树像拖泥带水

现象:搜索结果超过几千条以后,滚动 QTreeWidget、点击条目时明显一顿一顿的。

原因:每条结果都立即插入QTreeWidget,插入过程会触发一次布局重算和刷新,高频操作下重算成本被放大。

解决:先用批量信号把结果收集到QStringList,攒满一批再统一插入ui->treeWidget,同时用setUpdatesEnabled(false)暂时冻结重绘,等插入完再恢复。几千条这种规模差距可能不明显,到几万条时感知非常强烈。我一般会在SearchWorker里加一个m_resultBuffer成员变量,addResult函数里先追加到 buffer,不满 100 条不发射信号。

这四条坑涵盖了大多数人第一次跑这个项目时会撞到的典型问题,记录下来比到时候到处找资料舒服多了。踩坑归踩坑,代码本身设计空间不小,最后一章直接给出几个具体的二次开发方向。

6. 二次开发实战:从文件搜索升级为文件管理工具的三种改法

拿到一套能跑的代码,真正的乐趣在于按自己的需求不断往外扩。这个项目的边界清晰,尤其在搜索逻辑和界面展示这一层,扩展起来非常顺手。下面三个改造方向是我自己实践过的路径,由浅入深,你也可以按需选择。

6.1 改造一:扩展名过滤器,做成代码文件专用搜索

搜索的关键字匹配目前是针对完整文件名的,但实际开发中经常需要“只找 .cpp/.h/.py,排除 .png/.obj”这类场景。在界面上加一个QComboBox下拉框,预置几个扩展名组合选项,在doSearch的循环里加一行判断:

// 扩展名过滤判断,配合界面下拉框使用 QString ext = info.suffix().toLower(); // 取出文件后缀并转小写 if (ext == "cpp" || ext == "h" || ext == "py") { emit fileFound(info.absoluteFilePath()); }

界面那侧需要做的就是把下拉框的当前文本作为参数传给doSearch,在mainwindow.cpp里取ui->comboBox->currentText()然后在启动线程时带过去。这个功能做出来后,项目的实用价值会明显提升,大家平时搜索代码文件基本都是按扩展名过滤的。

6.2 改造二:双击结果后用系统默认程序打开文件

目前的逻辑是双击结果只定位到文件路径,实际场景里更需要的是一键打开这个文件。Qt 里打开外部文件用QDesktopServices::openUrl最方便:

void MainWindow::onTreeDoubleClicked(QTreeWidgetItem *item, int column) { QString filePath = item->data(0, Qt::UserRole).toString(); QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); }

关键是把之前存入Qt::UserRole的完整路径取出来,转成QUrl交给系统关联程序处理。代码只增加一个槽函数再连接到treeWidget的itemDoubleClicked信号上就完成,改动量极小。这段代码不好加很多注释,它的本质就是路径转 URL 的一层薄封装,很容易理解。下一步如果想增强体验,还可以加“打开所在目录并选中文件”的辅助功能,网上搜一下大多数 Qt 版本都提供了showInFolder的现成写法。

6.3 改造三:按修改时间排序,把结果树变成文件台账

当搜索内容定位到一批文件后,按修改时间排序能更快找到最新改过的文件。QTreeWidget 默认不自持排序,开启setSortingEnabled(true)后,用户可以直接点击表头排序。但要注意列类型,时间不能直接用字符串比较,否则会出现“2024-09-05”排在“2024-09-01”前面这种尴尬。正确做法是在QTreeWidgetItem里把时间存成自定义数据类型,再实现排序重载。更省事的方案是把时间格式改成时间戳数字填到 UserRole 里,界面显示格式化后的字符串,排序时比较数值。配合Qt::UserRole存时间戳,排序直接就是数字排序,零额外代码成本。这个技巧在需要把搜索结果做轻量文件台账时非常好用。

6.4 关于架构取舍的个人经验

做完三种改造后,这个项目的价值已经远超一个简单的文件搜索 demo,它可以扩展成代码工程管理工具、批量文件重命名器乃至日志分析器。但有一点我想特别说明:不要试图把搜索扩展成实时索引工具。Everything 那种级别需要 NTFS USN Journal 级别的底层支持,QDir 遍历撑死每秒几个 GB 的读取速度,应付日常小规模搜索已经足够,想实时监控全盘文件变化不是这个项目该做的事。

每回我拿到这类 Tool 型的 Qt 项目,都会先强制自己走一遍“拆三层、跑一遍、改一处”的流程:看懂界面层和逻辑层怎么解耦,把项目跑起来确认链路通畅,最后改一小块逻辑验证理解对不对。这套流程走完,代码里的玄学就基本消失了,剩下的都是可以直接上手的技巧。希望这些内容对你有帮助。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 16:44:37

ISATAP隧道处理全解析:原理、配置与排障实战

1. 从一个让人抓狂的排障现场说起第一次接触ISATAP隧道&#xff0c;是在一个混合网络环境的联调现场。当时的情况大概是这样&#xff1a;办公网是纯IPv4的&#xff0c;但实验室那边新上了一批只跑IPv6的测试设备&#xff0c;两边要互通&#xff0c;中间又不可能为了这个需求把整…

作者头像 李华
网站建设 2026/10/19 16:44:06

开源惯性导航项目拆解:NaveGo实现GPS与IMU融合的Matlab指南

简介&#xff1a;面向惯性导航与组合导航开发者的MATLAB开源实现&#xff0c;以NaveGo开源项目为完整范例&#xff0c;聚焦GPS与IMU数据融合&#xff0c;系统呈现扩展卡尔曼滤波&#xff08;EKF&#xff09;在惯导解算中的完整应用流程。包体共66个文件&#xff0c;以56个.m源码…

作者头像 李华
网站建设 2026/10/19 16:42:07

从零搭建个人大数据集群:Spark安装与Standalone实战

从零开始搭建个人大数据集群&#xff08;6&#xff09;——SPARK安装个人大数据集群搭建系列走到第6篇&#xff0c;终于轮到Spark了。前面我们搞定了存储和资源调度&#xff0c;HDFS里能存数据&#xff0c;YARN上能跑MapReduce任务&#xff0c;但说实话&#xff0c;MapReduce那…

作者头像 李华
网站建设 2026/10/19 16:42:06

从人治到工具化:构建高效代码审查工作流的实践指南

开源社区的代码审查&#xff0c;表面上是个流程问题&#xff0c;实际上是个工程效率问题。我见过太多团队把 Code Review 挂在嘴边&#xff0c;最后却沦为一个“点个通过按钮”的形式主义动作。真正跑通一套代码审查工作流&#xff0c;需要的不只是纪律&#xff0c;还涉及工具链…

作者头像 李华
网站建设 2026/10/19 16:40:13

基于DeepSeek与RAG的岩土工程AI问答助手搭建指南

搞岩土的人应该都有过这种体验&#xff1a;一个简单的参数要翻三本规范&#xff0c;一份勘察报告翻到最后一页才发现地层描述前后对不上&#xff0c;新来的工程师请教老师傅某个地区经验取值&#xff0c;老师傅翻出十年前的内部报告才找到依据。我把这个场景写进了方案里&#…

作者头像 李华
网站建设 2026/10/19 16:37:09

零基础转岗网络安全:10个实操教程从日志分析到应急响应

这些年我被问得最多的一个问题就是&#xff1a;“我完全没有网络安全基础&#xff0c;能转岗网安吗&#xff1f;”问这种问题的人里有刚毕业的&#xff0c;也有做了几年运维、开发甚至客服想换赛道的。每次看到这类问题&#xff0c;我都会先反问一句&#xff1a;你知不知道网安…

作者头像 李华