news 2026/10/26 7:30:16

原始EEG直入1D CNN:癫痫检测跳过特征工程全流程拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
原始EEG直入1D CNN:癫痫检测跳过特征工程全流程拆解

简介:一套基于一维卷积神经网络(1DCNN)的脑电信号(EEG)处理与癫痫检测项目,适合从事脑电分类、癫痫发作检测的算法学习者和研究人员。资源压缩包内共1个文件,为Python脚本,整体包体仅2KB,轻量精简,可快速下载并直接阅读。代码围绕原始时间序列脑电信号展开,涵盖噪声去除与标准化等预处理、一维卷积与池化层的模型构建、损失函数与优化器设定、训练及评估流程,利用卷积核沿时间轴滑动提取局部特征,用于识别癫痫发作的异常波动模式。这份脚本可作为深度学习应用于神经信号分析的入门实现,也可作为后续扩展与对比的基线。目前已有720人学习下载,适合需要以最小代码量理解1DCNN核心流程的读者。

1. 一维卷积网络啃原始 EEG:为什么癫痫检测不绕道特征工程

做过脑电信号处理的人都知道,癫痫发作检测最磨人的地方不是模型选型,而是特征工程那套流程——小波变换、功率谱密度、Hjorth 参数,每一层都要调参,每一层都在消耗时间。1DCNNforRawTimeSeries 这个项目反着来:把原始脑电时间序列直接喂给一维卷积神经网络,让卷积核沿着时间轴自己学特征,从 Delta、Theta 波段到棘波、尖慢复合波这些癫痫发作的特异性模式,全部交给网络自动提取。这个思路对从事脑电分类、癫痫发作检测的算法工程师和科研人员很有价值,它省掉了手工特征这套最容易翻车的中间环节,让我能从原始采样点一路训到端到端的分类器。本文就是这个项目的完整拆解笔记,从数据预处理、网络结构、训练评估到避坑和验证全流程展开。

2. 原始时间序列进网络前的三刀:去噪、标准化与滑窗分段

2.1 为什么 1D CNN 可以直接吃原始信号

很多人在接触 1D CNN 时会问一个很实际的问题:原始 EEG 信号采样率动辄 256Hz、512Hz,直接喂给网络,卷积层能学到东西吗?答案是可以,但前提是数据本身要干净。1D CNN 之所以能跳过手工特征,是因为它的卷积核天然就是一个可学习的特征提取器——一个长度为 32 的卷积核在时间轴上滑动,本质上等价于一个有限冲激响应滤波器,不同卷积核可以学习到不同频段的响应模式。更关键的是,多层卷积叠加之后,浅层学到的是瞬态波形,深层学到的是时序组合模式,这正好匹配癫痫发作的层次化特征。

但这里有个边界条件:卷积核能学特征,不代表它有能力对抗坏数据。基线漂移、工频干扰、肌电伪迹这些噪声如果原样进网络,卷积核要花很大一部分容量去拟合噪声模式。所以,原始信号直接进模型,指的是不做复杂的特征工程,而不是不做基础的信号预处理。

2.2 预处理流水线的三步操作

项目中处理 EEG 的常见做法是先走一个三步骤的预处理流水线:带通滤波、标准化、滑窗分段。带通滤波用 scipy 的 butterworth 设计一个 0.5Hz 到 40Hz 的带通滤波器,这个频段覆盖了 EEG 的主要节律波和癫痫特征波;标准化用 z-score 逐通道处理,把不同受试者、不同电极的幅值差异拉齐;分段则用固定长度的滑窗把长序列切成样本。

import numpy as np from scipy.signal import butter, filtfilt def preprocess_eeg(raw, fs, lowcut=0.5, highcut=40.0, window_sec=2.0): # 带通滤波:去掉基线漂移和部分高频肌电噪声 b, a = butter(4, [lowcut, highcut], btype='bandpass', fs=fs) filtered = filtfilt(b, a, raw, axis=-1) # 逐通道 z-score 标准化:消除通道间的幅值差异 mean = np.mean(filtered, axis=-1, keepdims=True) std = np.std(filtered, axis=-1, keepdims=True) normalized = (filtered - mean) / (std + 1e-6) # 滑窗分段:切成 (样本数, 通道数, 窗口长度) 的张量 window_len = int(window_sec * fs) n_windows = normalized.shape[-1] // window_len segments = normalized[:, :n_windows * window_len] segments = segments.reshape(segments.shape[0], n_windows, window_len) segments = segments.transpose(1, 0, 2) return segments

这段代码是 1DCNNforRawTimeSeries.py 里最核心的预处理逻辑的通用实现。三个参数值得留意:滤波阶数设成 4 是为了平衡过渡带陡峭度和相位失真,filtfilt 做零相位滤波避免波形偏移;z-score 中的 1e-6 是防除零的保护项,在处理某些该通道信号全为零的坏导时尤其重要;window_sec 设为 2 秒是经验值,既能覆盖数次棘波放电的完整形态,又不会让单样本过长导致训练时内存爆炸。如果你的任务是在线实时检测,滑窗需要改成步进式滑窗——每次向前移动 0.5 秒,而不是不重叠地切段,否则会有信息断层。

2.3 数据划分必须按受试者切

预处理做完之后,有一个比模型结构更容易让结果虚高的步骤——数据划分。很多初学者把同一受试者的脑电随机切成样本,然后按 8:2 划分训练集和测试集,这样做的结果是测试集里混着训练集中同一时段的信号片段,模型记住的是样本本身而不是癫痫模式,测试准确率能冲到 99% 以上,但一遇到新患者立刻崩盘。

正确做法是按受试者分组划分:训练集包含一拨患者的全部样本,验证集和测试集分别包含完全独立的患者。这样才能真实模拟临床场景——模型在没见过的人身上做发作检测。对癫痫检测,数据泄漏是最致命的错误,后面避坑章会展开讲。预处理到这一步,数据已经是可以直接进 1D CNN 的形状了:每个样本是(通道数,时间步长)的二维矩阵,多个通道相当于多个输入通道。

3. 1D CNN 结构设计与关键参数:从感受野到类别权重

3.1 网络骨架:卷积块堆叠加一个分类头

1D CNN 处理脑电的经典结构是三到四个 Conv1D 块加一个全局池化加全连接分类头。每个 Conv1D 块由一维卷积、批归一化、激活函数和最大池化组成。第一层的卷积核长度通常设大一点(比如 kernel_size=32),因为原始采样点上单个时间点没有语义信息,相邻 32 个采样点(约 125ms)才能构成一个有意义的波形片段;后续层卷积核长度逐步缩小到 16、8,对应提取更大时间跨度的组合特征。

通道数则从 64 开始逐层翻倍到 128、256,这个设计哲学是:浅层学到的低频特征空间不需要太多滤波器,深层需要更多滤波器来捕捉复杂的时序组合模式。池化层用 MaxPooling1D 且池化大小为 2,每经过一层池化,时间分辨率减半,感受野翻倍。项目里如果序列长度输入是 512(2 秒乘 256Hz),经过三层池化后长度变为 64,再经过一层全局平均池化变成 256 维向量,送入分类头。

from tensorflow.keras import layers, models def build_1dcnn(num_channels, input_len, num_classes=2): inputs = layers.Input(shape=(num_channels, input_len)) x = layers.Permute((2, 1))(inputs) # 转成 (时间步, 通道数),Conv1D 默认在最后维做卷积 x = layers.Conv1D(filters=64, kernel_size=32, strides=1, padding='same')(x) x = layers.BatchNormalization()(x) x = layers.ReLU()(x) x = layers.MaxPooling1D(pool_size=2)(x) x = layers.Conv1D(filters=128, kernel_size=16, padding='same')(x) x = layers.BatchNormalization()(x) x = layers.ReLU()(x) x = layers.MaxPooling1D(pool_size=2)(x) x = layers.Conv1D(filters=256, kernel_size=8, padding='same')(x) x = layers.BatchNormalization()(x) x = layers.ReLU()(x) x = layers.GlobalAveragePooling1D()(x) # 全局平均池化替代 Flatten,降低过拟合风险 x = layers.Dropout(0.5)(x) outputs = layers.Dense(num_classes, activation='softmax')(x) model = models.Model(inputs, outputs) return model

这个网络骨架有两个容易被忽略的细节。第一个是 Permute 层:Conv1D 在 Keras 中默认卷积维度是最后一个维度,输入原始数据形状是(通道数, 时间步),如果不转置,卷积核会在通道维度上滑动,这是初学时最典型的翻车点。把数据变成(时间步, 通道数)后,每个卷积核处理的是「同一时刻的所有通道」,这类似于多导联 EEG 的同步视图,符合脑电信号的空间-时间联合分析需求。第二个是 Dropout 放在全局池化之后而不是每个卷积块里,因为卷积层自身有参数共享的正则化效果,Dropout 主要用来压制全连接层的过拟合。

3.2 损失函数与类别权重:样本不平衡的第一次博弈

癫痫发作检测天然面临着样本严重不平衡的问题:一个患者 24 小时的脑电记录中,发作期可能只有几分钟,发作片段占比不到 2%。直接用标准的交叉熵损失函数训练,模型会把所有样本都预测为非发作,因为这样已经能达到 98% 的准确率。

常见的解决方案有两种,一种是在损失函数中给少数类加权重,另一种是过采样发作片段。实践中我会选择加权交叉熵——过采样会让模型反复看同样的发作片段,容易过拟合到个别患者的特定发作模式上,而类别权重则是从梯度层面直接改变模型对少数类的关注度。类别权重在 Keras 中通过 class_weight 参数实现:给发作类一个较高的权重(通常是多数类与少数类样本数的比值),让模型在预测发作类错误时承受更大的损失惩罚。

from sklearn.utils.class_weight import compute_class_weight labels = np.argmax(y_train, axis=1) weights = compute_class_weight('balanced', classes=np.unique(labels), y=labels) class_weight = dict(zip(np.unique(labels), weights)) print('类别权重:', class_weight) model.compile( optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'] ) model.fit(x_train, y_train, validation_data=(x_val, y_val), batch_size=64, epochs=50, class_weight=class_weight)

compute_class_weight 的 'balanced' 模式会自动计算权重:权重 = 总样本数 /(类别数 × 该类别样本数)。比如非发作样本 980 个,发作样本 20 个,那么非发作权重约 0.51,发作权重约 25.5。这个 50 倍的差距会让模型在训练过程中把梯度重心放在发作样本上。但权重也不是越大越好——权重过大,模型会走向另一个极端,把大量非发作误报为发作,临床检测中误报过多同样不可接受。一般我会在 'balanced' 基础上再乘一个 0.5~1.0 的缩放系数,用验证集的表现来微调。

3.3 先跑通再调优:小模型的实用主义路线

很多人在搭 1D CNN 时容易一上来就堆层数,觉得网络越深效果越好。在脑电这类样本量通常只有几千到几万条的任务上,这种做法会很快过拟合。我的习惯是先按上面这个结构跑通全流程,确认数据管道、训练逻辑、评估流程都没有问题,然后才根据验证集的表现逐步调整:如果欠拟合就加深卷积层数或者增大滤波器数量;如果过拟合就加大 Dropout 到 0.6,或者减少卷积块到两层。先通后优,用验证集指标说话,而不是凭直觉堆参数。

4. 训练与评估:癫痫检测不看准确率,看敏感度和特异性

4.1 训练策略:早停、学习率衰减和模型保存

1D CNN 在这个任务上训练目标相对单一,重点是控制好两个行为:过拟合和训练震荡。过拟合用早停机制控制——监控验证集 loss,连续 10 个 epoch 不下降就停止训练,并恢复最佳模型权重;训练震荡用学习率衰减控制——初始学习率设 0.001,每 5 个 epoch 乘以 0.6,让模型在训练后期做精细收敛。

from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks = [ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.6, patience=5, min_lr=1e-6), ModelCheckpoint('best_model.keras', monitor='val_loss', save_best_only=True) ] history = model.fit( x_train, y_train, validation_data=(x_val, y_val), batch_size=64, epochs=100, callbacks=callbacks, class_weight=class_weight, verbose=1 )

batch_size=64 是折中值:太小梯度震荡剧烈,56Hz 采样率这种维度下、batch 太大又容易跳过一些稀疏的发作样本的最优点,64 在绝大多数脑电数据集上表现稳定。epochs 设 100 但实际基本跑不到——早停会把训练截断在 30 到 50 轮之间。restore_best_weights=True 很重要,它保证保存的是验证集上表现最优的一版权重,而不是最后一轮可能已经过拟合的权重。癫痫检测这种任务上,模型权重的一点点差异会直接体现在后续的敏感度指标上,这个小细节能让模型的泛化表现稳定不少。

4.2 评估指标:为什么准确率在这里没有意义

在癫痫检测这种极不平衡分类任务上,只用准确率评估模型是一个极其容易误导决策的错误。一个把所有样本都预测为非发作的模型,准确率可以高达 98%,但敏感度(Se,也叫召回率)是 0——没有检测到任何一次发作。所以评估必须同时看敏感度、特异性(Sp)和它们的综合指标。

对临床检测场景,敏感度的优先级高于特异性——漏掉一次发作的代价远高于一次误报。项目评估代码里最值得注意的部分是用预测概率计算 AUC 和画 ROC 曲线:AUC 不依赖阈值选择,衡量的是模型把发作样本排到非发作样本前面的能力;而敏感度和特异性则基于默认阈值 0.5,反映的是在固定决策点上的表现。

from sklearn.metrics import classification_report, roc_auc_score, roc_curve y_pred_proba = model.predict(x_test) y_pred = np.argmax(y_pred_proba, axis=1) y_true = np.argmax(y_test, axis=1) # 多分类时取发作类别的概率作为正类概率 pos_idx = 1 auc = roc_auc_score(y_true, y_pred_proba[:, pos_idx]) # 在多个阈值下观察敏感度与特异性 fpr, tpr, thresholds = roc_curve(y_true, y_pred_proba[:, pos_idx]) print(f'AUC: {auc:.4f}') print(classification_report(y_true, y_pred, target_names=['non_seizure', 'seizure']))

用 classification_report 输出每类别的精确率、召回率和 F1,结合 AUC 一起看。如果 AUC 很高(0.95 以上)但默认阈值下的召回率很低,说明模型输出的概率分布整体偏移——可以通过阈值搜索找到更合适的决策点,而不是直接认为模型不行。有一种常见做法是计算约登指数(Youden's J = 敏感度 + 特异性 - 1)自动选择最优阈值,在临床检测场景很实用。用这个阈值替代默认的 0.5,可以在保证特异性不过低的前提下最大化敏感度。

4.3 混淆矩阵逐类诊断错误模式

评估环节我有一个固定动作:把测试集所有预测错误的样本按错误类型分组,逐段回看原始脑电波形。这个动作排查掉了很多模型本身没问题、但数据或标签有问题的「隐性 bug」。比如把发作前的脑电片段错分成发作,先别急着调模型——先看这段信号的原始波形和标签:如果医生标注时把发作起始点标晚了,而模型在发作起始前已经捕捉到了异常放电,这实际上是模型比标签更灵敏,不该被当作错误来优化。

from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt import seaborn as sns cm = confusion_matrix(y_true, y_pred) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['non_seizure', 'seizure'], yticklabels=['non_seizure', 'seizure']) plt.xlabel('Predicted') plt.ylabel('True') plt.show()

混淆矩阵的四个格子,我最关注的是假阴性(真实发作被预测为非发作)这一格——这一格是模型在真实场景里的致命弱点。每个假阴性样本都要单独提取出来看:是发作波形太短,卷积层没能捕捉到完整的棘波序列?还是发作波形幅值太弱,被标准化后淹没在背景信号里?这两种情况的解决办法截然不同,前者需要缩小卷积核长度或减小池化步长,后者需要调整预处理标准化策略。这个环节的复查习惯,帮我避开了不下十个「指标看着不错但落地即翻车」的模型版本。

5. 癫痫检测避坑记录:数据泄漏、瞬时波形丢失与标签错位

5.1 数据泄漏:按受试者切分被随机切分替代

现象:模型在测试集上准确率高达 99.5%,AUC 0.99,看起来一切完美。但把模型部署到新患者的脑电数据上时,检测效果大幅下滑,敏感度只有 60% 左右,完全不可用。

原因:训练时把同一个受试者的脑电数据随机切分成样本,然后按比例划分训练集、验证集和测试集。由于同一位受试者的相邻脑电片段高度相似,测试集中混有和训练样本几乎同源的片段,模型实际上是在「背题」而不是在学癫痫发作的通用模式。这是脑电深度学习中最常见、也最具欺骗性的一个错误,因为训练指标异常的漂亮,让后续诊断变得更加困难。

解决:数据划分必须在采集层面按受试者分组。把所有患者的 ID 先切成训练患者集合和测试患者集合,再在这些集合内部做滑窗分段。代码上用一个简单的逻辑强制隔离:按患者 ID 分组后用 GroupShuffleSplit 或者在拼接数据之前就先切分索引。从那以后我每次做数据集划分,都会先打印训练集和测试集的受试者 ID 列表确认交集为空,再开始训练。

5.2 类不平衡导致准确率虚假繁荣

现象:训练过程中准确率稳步上升最终停在 98%,但打印出分类报告后发现,发作类别的召回率是 0,模型把所有样本都预测成了非发作。

原因:发作样本占比太小,常规交叉熵损失函数下非发作样本的梯度贡献完全淹没了发作样本。模型只需要把所有样本都预测成多数类,就能把损失降到足够低,根本没有动力去学习发作模式。

解决:用类权重或过采样重新平衡训练。推荐 class_weight='balanced' 自动计算权重,简单有效。权重设置时注意验证集的评估标尺要保持真实分布,不能把验证集也改成平衡的——因为真实场景中发作就是少数,评估必须反映实际部署条件。

5.3 卷积核长度过大把棘波瞬态波形磨平

现象:模型在验证集上的 AUC 只有 0.82,而且反复调试网络深度、滤波器数量都无法提升。回看波形时发现,模型把整个发作过程都「糊」成了一段平滑的曲线。

原因:第一层卷积核设了 128,对应 0.5 秒的时间跨度,而典型的癫痫棘波持续时间只有 20~70ms,单个尖慢复合波约 200~500ms。卷积核太大,等价于在时间轴上做了强低通滤波,瞬态特征在第一个卷积层就被抹掉了,深层网络想学也学不到。

解决:把第一层卷积核长度降到 16~32 之间,对应 60~125ms,能保留棘波和尖波的瞬态形态。从第一层开始就捕捉这些短时特征,后续层再逐渐拉大卷积核长度去学习发作的连续演变模式。检查方法也很简单:把第一层卷积核的权重可视化,如果看到的是类似平滑滤波的响应,说明卷积核偏大;如果看到陡峭的差分响应,说明卷积核工作在瞬态特征的频率区间。

5.4 标签时间段标注粗糙导致样本错位

现象:模型在发作起始时间附近的预测概率出现异常摆动,有时提前很久就输出高概率,有时发作已经开始却依然没有反应。人工对比标注数据和网络预测时,发现模型的预测边界和标注边界存在稳定偏移。

原因:脑电的癫痫发作标注是按时间段标注的,标注的起止时间通常依赖标注医师的肉眼判断,存在主观误差。更常见的情况是发作起始前的 1~2 秒内已有异常放电,但标注里没有计入,模型提前预测实际上不是错误;反过来也有标注包含了一段发作后抑制期,而这段信号的形态和发作期完全不同,导致模型学习到了混乱的目标模式。

解决:对标注做容忍窗口处理,把发作起始前 2 秒也视为正样本区间;同时对标签边界做腐蚀/膨胀处理——以手工标注为中心向两侧各扩展或收缩 1 秒,然后观察模型预测对标注修正的响应。如果模型在扩展后的区间内预测准确率更高,说明原始标注边界确实偏窄了。这个问题的核心启示是:在医疗时序任务里,不要无条件相信标签是金标准,标签噪声是误差的一部分,需要可视化复查而不是盲目调模型。

6. 验证模型到底学没学会:逐样本可视化与容忍窗口评估

6.1 类激活图:定位模型注意到的波形片段

一个在测试集上指标合格的模型,在真实场景中能不能用,我的判断依据是逐样本可视化。1D CNN 同样可以用类激活图(CAM)来回答这个问题:模型做决策时,到底在输入脑电信号的哪个时间段上下了结论?具体做法是用全局平均池化层的输出特征图,和全连接层的权重做加权求和,得到每个时间步对「发作」类别的贡献分数。

from tensorflow.keras import models as km def grad_cam_1d(model, sample, layer_name='global_average_pooling1d'): grad_model = km.Model( inputs=model.input, outputs=[model.get_layer(layer_name).output, model.output] ) with tf.GradientTape() as tape: features, pred = grad_model(sample) target = pred[0, 1] # 发作类别的预测分数 grads = tape.gradient(target, features) pooled_grads = tf.reduce_mean(grads, axis=1) cam = tf.reduce_sum(features * pooled_grads, axis=1) cam = tf.nn.relu(cam) cam /= (tf.reduce_max(cam) + 1e-8) return cam.numpy()

这段代码把所有模型不可解释的黑匣子透明化了。对癫痫检测,正常的 CAM 应该集中在发作间期出现棘波的几个时间段,而不是均匀分布在整段信号上。如果 CAM 高亮区间和标注的发作区间完全错位,那不管指标多漂亮,这个模型都不可信——它学到的可能是某个特定的伪迹模式。我在实践中遇到过 CAM 集中在某单个导联上的情况,排查后是数据标准化时混进了坏导信号,这也是可视化带来的意外收获。

6.2 容忍窗口评估:贴合临床的评分方式

除了逐样本可视化,我还会用容忍窗口评估来校准模型输出。发作检测在临床上有一个特点:允许几秒的延迟或提前。传统逐样本评估把每次样本的预测独立看待,发作起始样本被判错就记错一次,但真实场景中,一次在发作后 3 秒内报警的检测完全可用。评估方式的调整能让模型真正满足临床场景的使用价值。

容忍窗口评估的做法:将连续的爆发样本合并为事件,以标注窗口为中心、定义前后各 5 秒的容忍区间,模型输出概率超过阈值进入区间,只要在该区间内检测到就记为命中一次,一个连续发作事件算一次,避免以秒为粒度的误报被高估。这个评估方法能直接反映临床监测设备的核心诉求:是否能在允许的延迟内可靠报警、误报率是否低到不干扰正常工作。项目代码中 1DCNNforRawTimeSeries.py 的主要流程跑通之后,建议固定加上这两步验证,模型距离真实应用场景的一步之遥才算真正落地。

我是一个在脑电信号处理上踩过不少坑的工程师,现在的习惯是每次训练完成之后,强制走一遍「数据划分审查 → 混淆矩阵假阴性复查 → CAM 可视化 → 容忍窗口评估」这个流程,哪怕指标再好也雷打不动。这些步骤让我避免了太多次被漂亮指标误导的教训,希望帮到你。

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

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

基于Python溯源图的APT攻击检测:从StreamSpot到RGAT+GRU实践

简介:一份面向毕业设计与安全方向初学者的APT攻击检测项目,以Python实现基于溯源图的检测思路,核心代码涉及RGAT与GRU等模型,适合计算机、软件工程、人工智能等专业用于毕设、课设或项目演示。包内共26个文件,源码以11…

作者头像 李华
网站建设 2026/10/26 7:29:10

Docker本地开发环境搭建与优化实践

1. 为什么需要Docker搭建本地开发环境?作为一名经历过无数次"环境地狱"的开发者,我深刻理解传统开发环境配置的痛苦。还记得刚入行时,为了在本地跑通一个Java项目,花了整整两天时间折腾JDK版本、Maven依赖和环境变量。更…

作者头像 李华
网站建设 2026/10/26 7:28:06

域名注册中的商标风险与规避策略

1. 从真实案例看域名注册的商标风险去年我帮一家初创科技公司注册品牌域名时,差点酿成大错。当时团队已经确定了"QuickFlow"作为产品名称,我在未做商标检索的情况下,直接注册了quickflow-tech.com。结果三个月后,收到了…

作者头像 李华
网站建设 2026/10/26 7:28:06

JSP+MySQL网上商城系统部署指南:从环境配置到避坑实践

简介:面向Java Web初学者与毕业设计选题学生,这份网上商城系统源码以JSPServletMySQL构建了一个可直接运行的电商项目,完整覆盖用户注册登录、商品分类搜索、购物车管理、订单状态追踪等核心流程,采用MVC分层架构将页面显示、业务…

作者头像 李华
网站建设 2026/10/26 7:27:44

链家二手房Python爬虫实战:从数据采集到深圳楼市分析

简介:面向数据分析与爬虫初学者的Python实战资源,以深圳链家二手房为对象,完整实现从二级域名抓取、二手房链接组装、筛选条件获取到数据入库及分析的闭环流程,设计贴合真实项目场景。代码已测试通过,支持将爬取结果保…

作者头像 李华