GRU 是在 LSTM 基础上的简化,将 LSTM 内部的三个闸门简化成两个,往往 GRU 的计算效果会优于 LSTM

目录

1. 导入工具包

2. 获取数据集

3. 数据预处理

4. 时间序列滑窗

5. 数据集划分

6. 构造网络模型

7. 网络训练

8. 查看训练过程信息

9. 预测阶段

10. 对比 LSTM 和 GRU

11.源代码

1. 导入工具包

如果没有电脑没有GPU的话就把下面那段调用GPU加速计算的代码删了

import tensorflow as tf

from tensorflow import keras

from tensorflow.keras import layers

import matplotlib.pyplot as plt

# 调用GPU加速

gpus = tf.config.experimental.list_physical_devices(device_type='GPU')

for gpu in gpus:

tf.config.experimental.set_memory_growth(gpu, True)

2. 获取数据集

首先,安装一下 pandas_datareader 这个能远程获取金融数据的工具包。接下来使用 web.DataReader() 指定在哪个平台获取哪个公司的股票信息。具体的函数参数请看知乎专栏:【Python搞量化】pandas_datareader 经济和金融数据读取API介绍 - 知乎

本次我们获取谷歌在2000年到2021年的股票信息,把原始数据读入进来后的一定要删除数据中的空缺值,避免对后续数据处理的影响。为了方便循环神经网络的学习,需要将数据按照从旧到新的顺序排列

本节是通过一个序列来预测10天后的股票收盘价格在数据表格中新添 'label' 列保存每个序列的标签值。是对一个时间点的预测。

# pip install pandas_datareader

import pandas_datareader.data as web

import datetime  # datetime是Python处理日期和时间的标准库。

# 设置获取股票的时间范围的数据

start = datetime.datetime(2000,1,1)  # 设置开始时间

end = datetime.datetime(2021,9,1)  # 设置结束时间

# 在stooq数据源上获取googl在2000-2021年的股票数据

df = web.DataReader('GOOGL', 'stooq', start, end)

# 查看股票信息, 时间, 开盘价, 最高价, 最低价, 收盘价, 交易量

print(df)

df.dropna(inplace=True)  # 删除表格中的空值

# 根据数据的索引(时间)从小到大排序

df.sort_index(inplace=True)  # 排序完成后替换原来的df

print(df)

# 获取标签,预测10天后的收盘价

pre_days = 10

# 添加一个新的列存放标签, 相当于通过2004-08-19的特征来预测2004-08-29的收盘价

df['label'] = df['Close'].shift(-pre_days)

print(df)

由于 'label' 列是将 'Close' 列集体向上移动10行,因此,'label' 列最后的10行会出现空缺值nan,这里要注意,后续会处理。

3. 数据预处理

导入 sklearn 标准化方法,对所有的特征数据进行标准化处理,对标签数据 'label' 列不做处理。标准化后能够避免偏差过大的数据对训练结果的影响。

from sklearn.preprocessing import StandardScaler  # 导入数据标准化方法

scaler = StandardScaler()  # 接收数据标准化方法

# 对所有的特征数据进行标准化,最后一列是标签值

sca_x = scaler.fit_transform(df.iloc[:,:-1])

# 查看标准化后的特征数据

print(sca_x)

五个特征列分别对应:开盘价,最高价,最低价,收盘价,交易量

4. 时间序列滑窗

这里使用一个很方便的队列deque,指定这个队列的最大长度maxlen=20,就代表一个时间序列的长度为20,如果队列deq的长度超过20就将第1个特征删除,将第21个特征追加到第20个特征后面,就能一直保持队列的长度是20。那么每一个时间序列的shape=[20,5],代表20行数据5列特征。

完成对所有数据的时间序列分组之后,由于特征数据sca_x最后10行是没有对应的标签值的。因此需要把最后10组时间序列删除。每一个序列对应一个标签,标签和序列的长度是相同的。

import numpy as np

from collections import deque  # 相当于一个列表,可以在两头增删元素

men_his_days = 20  # 用20天的特征数据来预测

# 创建一个队列, 长度等于记忆的天数,时间序列滑窗大小=20

deq = deque(maxlen=men_his_days)

# 创建一个特征列表,保存每个时间序列的特征

x = []

# 遍历每一行特征数据

for i in sca_x:

# 将每行特征保存进队列

deq.append(list(i))  # array类型转为list类型

# 如果队列的长度等于记忆的天数(时间滑窗的的长度)就证明特征组成了一个时间序列

# 如果队列长度大于记忆天数,队列会自动将头端的那个特征删除,将新输入追加到队列尾部

if len(deq) == men_his_days:

# 将这一组序列保存下来

x.append(list(deq))  # array类型转为list类型

# 由于原特征中最后10条数据没有标签值, 在x特征数据中将最后10个序列删除

x = x[:-pre_days]

# 查看有多少个序列

print(len(x))  # 4260

# 数据表格df中最后一列代表标签值, 把所有标签取出来

# 例如使用[0,1,2,3,4]天的特征预测第20天的收盘价, 使用[1,2,3,4,5]天的特征预测第21天的收盘价

# 而表格中索引4对应的标签就是该序列的标签

y = df['label'].values[men_his_days-1: -pre_days]

print(len(y))  # 序列x和标签y的长度应该一样

# 将特征和标签变成numpy类型

x, y = np.array(x), np.array(y)

5. 数据集划分

我们已经获得了处理后的时间序列和对应的标签,接下来就按比例划分训练集、验证集、测试集即可。对于训练集需要使用 .shuffle() 随机打乱数据行排列顺序,避免偶然性。设置迭代器 iter(),结合 next() 函数从训练集中取出一个batch的数据

total_num = len(x)  # 一共多少组序列和标签

train_num = int(total_num*0.8)  # 80%的数据用于训练

val_num = int(total_num*0.9)  # 80-90%的数据用于验证

# 剩余的数据用于测试

x_train, y_train = x[:train_num], y[:train_num]  # 训练集

x_val, y_val = x[train_num:val_num], y[train_num:val_num]  # 验证集

x_test, y_test = x[val_num:], y[val_num:]  # 测试集

# 转为tensor类型

batch_size = 128  # 每个step训练多少组序列数据

# 训练集

train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train))

train_ds = train_ds.batch(batch_size).shuffle(10000)  # 随机打乱

# 验证集

val_ds = tf.data.Dataset.from_tensor_slices((x_val, y_val))

val_ds = val_ds.batch(batch_size)

# 测试集

test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test))

test_ds = test_ds.batch(batch_size)

# 查看数据集信息

sample = next(iter(train_ds))  # 取出一个batch的数据

print('x_train.shape:', sample[0].shape)  # (128, 20, 5)

print('y_train.shape:', sample[1].shape)  # (128,)

6. 构造网络模型

这里以 GRU 网络为例,LSTM 只需要将下面代码中的 layers.GRU() 换成 layers.LSTM() 即可。

要注意参数 return_sequences,代表返回输出序列中的最后一个值,还是所有值。默认False一般是下一层还是 LSTM 的时候才用 return_sequences=True

input_shape = sample[0].shape[-2:]  # [20,5] 输入维度不需要写batch维度

# 构造输入层

inputs = keras.Input(shape=input_shape)  # [None,20,5]

# 第一个GRU层, 如果下一层还是LSTM层就需要return_sequences=True, 否则就是False

x = layers.GRU(8, activation='relu', return_sequences=True, kernel_regularizer=keras.regularizers.l2(0.01))(inputs)

x = layers.Dropout(0.2)(x)  # 随机杀死神经元防止过拟合

# 第二个GRU层

x = layers.GRU(16, activation='relu', return_sequences=True, kernel_regularizer=keras.regularizers.l2(0.01))(x)

x = layers.Dropout(0.2)(x)

# 第三个GRU层

x = layers.GRU(32, activation='relu')(x)

x = layers.Dropout(0.2)(x)

# 全连接层, 随机权重初始化, l2正则化

x = layers.Dense(16, activation='relu', kernel_initializer='random_normal', kernel_regularizer=keras.regularizers.l2(0.01))(x)

x = layers.Dropout(0.2)(x)

# 输出层, 输入序列的10天后的股票,是时间点。保证输出层神经元个数和y_train.shape[-1]相同

outputs = layers.Dense(1)(x)

# 构造网络

model = keras.Model(inputs, outputs)

# 查看网络结构

model.summary()

查看网络结构

Model: "model_3"

_________________________________________________________________

Layer (type)                 Output Shape              Param #

=================================================================

input_4 (InputLayer)         [(None, 20, 5)]           0

_________________________________________________________________

gru_3 (GRU)                  (None, 20, 8)             360

_________________________________________________________________

dropout_12 (Dropout)         (None, 20, 8)             0

_________________________________________________________________

gru_4 (GRU)                  (None, 20, 16)            1248

_________________________________________________________________

dropout_13 (Dropout)         (None, 20, 16)            0

_________________________________________________________________

gru_5 (GRU)                  (None, 32)                4800

_________________________________________________________________

dropout_14 (Dropout)         (None, 32)                0

_________________________________________________________________

dense_6 (Dense)              (None, 16)                528

_________________________________________________________________

dropout_15 (Dropout)         (None, 16)                0

_________________________________________________________________

dense_7 (Dense)              (None, 1)                 17

=================================================================

Total params: 6,953

Trainable params: 6,953

Non-trainable params: 0

7. 网络训练

使用预测值和标签值之间的平均绝对误差mae作为损失函数,使用均方对数误差msle作为网络的监控指标。history中保存训练时的每次迭代的mae损失和msle指标

# 网络编译

model.compile(optimizer = keras.optimizers.Adam(0.001),  # adam优化器学习率0.001

loss = tf.keras.losses.MeanAbsoluteError(),  # 标签和预测之间绝对差异的平均值

metrics = tf.keras.losses.MeanSquaredLogarithmicError())  # 计算标签和预测之间的对数误差均方值。

epochs = 10  # 网络迭代次数

# 网络训练

history = model.fit(train_ds, epochs=epochs, validation_data=val_ds)

训练过程如下

Epoch 1/10

27/27 [==============================] - 8s 214ms/step - loss: 395.9859 - mean_squared_logarithmic_error: 32.9226 - val_loss: 1164.5131 - val_mean_squared_logarithmic_error: 46.3883

Epoch 2/10

27/27 [==============================] - 5s 198ms/step - loss: 404.5123 - mean_squared_logarithmic_error: 28.0247 - val_loss: 1153.9086 - val_mean_squared_logarithmic_error: 20.9722

----------------------------------------------------

----------------------------------------------------

Epoch 9/10

27/27 [==============================] - 5s 200ms/step - loss: 111.9984 - mean_squared_logarithmic_error: 0.1729 - val_loss: 174.2481 - val_mean_squared_logarithmic_error: 0.0213

Epoch 10/10

27/27 [==============================] - 5s 199ms/step - loss: 101.5161 - mean_squared_logarithmic_error: 0.1041 - val_loss: 54.0906 - val_mean_squared_logarithmic_error: 0.0028

8. 查看训练过程信息

绘制每次迭代的训练集损失和验证机损失、训练集监控指标和验证集监控指标

#(10)查看训练信息

history_dict = history.history  # 获取训练的数据字典

train_loss = history_dict['loss']  # 训练集损失

val_loss = history_dict['val_loss']  # 验证集损失

train_msle = history_dict['mean_squared_logarithmic_error']  # 训练集的百分比误差

val_msle = history_dict['val_mean_squared_logarithmic_error']  # 验证集的百分比误差

#(11)绘制训练损失和验证损失

plt.figure()

plt.plot(range(epochs), train_loss, label='train_loss')  # 训练集损失

plt.plot(range(epochs), val_loss, label='val_loss')  # 验证集损失

plt.legend()  # 显示标签

plt.xlabel('epochs')

plt.ylabel('loss')

plt.show()

#(12)绘制训练百分比误差和验证百分比误差

plt.figure()

plt.plot(range(epochs), train_msle, label='train_msle')  # 训练集损失

plt.plot(range(epochs), val_msle, label='val_msle')  # 验证集损失

plt.legend()  # 显示标签

plt.xlabel('epochs')

plt.ylabel('msle')

plt.show()

9. 预测阶段

使用 evaluate() 函数对整个测试集计算损失和监控指标,获取每个真实值的时间刻度

#(13)测试集评价, 计算损失和监控指标

model.evaluate(test_ds)

# 预测

y_pred = model.predict(x_test)

# 获取标签值对应的时间

df_time = df.index[-len(y_test):]

# 绘制对比曲线

fig = plt.figure(figsize=(10,5))  # 画板大小

axes = fig.add_subplot(111)  # 画板上添加一张图

# 真实值, date_test是对应的时间

axes.plot(df_time, y_test, 'b-', label='actual')

# 预测值,红色散点

axes.plot(df_time, y_pred, 'r--', label='predict')

# 设置横坐标刻度

axes.set_xticks(df_time[::50])

axes.set_xticklabels(df_time[::50], rotation=45)

plt.legend()  # 注释

plt.grid()  # 网格

plt.show()

绘制真实值和预测值之间的对比曲线

10. 对比 LSTM 和 GRU

使用相同方法训练的LSTM和GRU的预测曲线图如下,两种方法差别不大,如果有实际需要时可对比使用。

训练过程比较图

11.源代码

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import matplotlib.pyplot as plt# 调用GPU加速
gpus = tf.config.experimental.list_physical_devices(device_type='GPU')
for gpu in gpus:tf.config.experimental.set_memory_growth(gpu, True)# pip install pandas_datareader
import pandas_datareader.data as web
import datetime  # datetime是Python处理日期和时间的标准库。# 设置获取股票的时间范围的数据
start = datetime.datetime(2000,1,1)  # 设置开始时间
end = datetime.datetime(2021,9,1)  # 设置结束时间# 在stooq数据源上获取googl在2000-2021年的股票数据
df = web.DataReader('GOOGL', 'stooq', start, end)
# 查看股票信息, 时间, 开盘价, 最高价, 最低价, 收盘价, 交易量
print(df)df.dropna(inplace=True)  # 删除表格中的空值# 根据数据的索引(时间)从小到大排序
df.sort_index(inplace=True)  # 排序完成后替换原来的df
print(df)# 获取标签,预测10天后的收盘价
pre_days = 10
# 添加一个新的列存放标签, 相当于通过2004-08-19的特征来预测2004-08-29的收盘价
df['label'] = df['Close'].shift(-pre_days)
print(df)from sklearn.preprocessing import StandardScaler  # 导入数据标准化方法scaler = StandardScaler()  # 接收数据标准化方法
# 对所有的特征数据进行标准化,最后一列是标签值
sca_x = scaler.fit_transform(df.iloc[:,:-1])
# 查看标准化后的特征数据
print(sca_x)import numpy as np
from collections import deque  # 相当于一个列表,可以在两头增删元素men_his_days = 20  # 用20天的特征数据来预测
# 创建一个队列, 长度等于记忆的天数,时间序列滑窗大小=20
deq = deque(maxlen=men_his_days)# 创建一个特征列表,保存每个时间序列的特征
x = []
# 遍历每一行特征数据
for i in sca_x:# 将每行特征保存进队列deq.append(list(i))  # array类型转为list类型# 如果队列的长度等于记忆的天数(时间滑窗的的长度)就证明特征组成了一个时间序列# 如果队列长度大于记忆天数,队列会自动将头端的那个特征删除,将新输入追加到队列尾部if len(deq) == men_his_days:# 将这一组序列保存下来x.append(list(deq))  # array类型转为list类型# 由于原特征中最后10条数据没有标签值, 在x特征数据中将最后10个序列删除
x = x[:-pre_days]
# 查看有多少个序列
print(len(x))  # 4260# 数据表格df中最后一列代表标签值, 把所有标签取出来
# 例如使用[0,1,2,3,4]天的特征预测第20天的收盘价, 使用[1,2,3,4,5]天的特征预测第21天的收盘价
# 而表格中索引4对应的标签就是该序列的标签
y = df['label'].values[men_his_days-1: -pre_days]
print(len(y))  # 序列x和标签y的长度应该一样# 将特征和标签变成numpy类型
x, y = np.array(x), np.array(y)total_num = len(x)  # 一共多少组序列和标签
train_num = int(total_num*0.8)  # 80%的数据用于训练
val_num = int(total_num*0.9)  # 80-90%的数据用于验证
# 剩余的数据用于测试x_train, y_train = x[:train_num], y[:train_num]  # 训练集
x_val, y_val = x[train_num:val_num], y[train_num:val_num]  # 验证集
x_test, y_test = x[val_num:], y[val_num:]  # 测试集# 转为tensor类型
batch_size = 128  # 每个step训练多少组序列数据
# 训练集
train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_ds = train_ds.batch(batch_size).shuffle(10000)  # 随机打乱
# 验证集
val_ds = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_ds = val_ds.batch(batch_size)
# 测试集
test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_ds = test_ds.batch(batch_size)# 查看数据集信息
sample = next(iter(train_ds))  # 取出一个batch的数据
print('x_train.shape:', sample[0].shape)  # (128, 20, 5)
print('y_train.shape:', sample[1].shape)  # (128,)input_shape = sample[0].shape[-2:]  # [20,5] 输入维度不需要写batch维度# 构造输入层
inputs = keras.Input(shape=input_shape)  # [None,20,5]# 第一个GRU层, 如果下一层还是LSTM层就需要return_sequences=True, 否则就是False
x = layers.GRU(8, activation='relu', return_sequences=True, kernel_regularizer=keras.regularizers.l2(0.01))(inputs)
x = layers.Dropout(0.2)(x)  # 随机杀死神经元防止过拟合# 第二个GRU层
x = layers.GRU(16, activation='relu', return_sequences=True, kernel_regularizer=keras.regularizers.l2(0.01))(x)
x = layers.Dropout(0.2)(x)  # 第三个GRU层
x = layers.GRU(32, activation='relu')(x)
x = layers.Dropout(0.2)(x)  # 全连接层, 随机权重初始化, l2正则化
x = layers.Dense(16, activation='relu', kernel_initializer='random_normal', kernel_regularizer=keras.regularizers.l2(0.01))(x)
x = layers.Dropout(0.2)(x)  # 输出层, 输入序列的10天后的股票,是时间点。保证输出层神经元个数和y_train.shape[-1]相同
outputs = layers.Dense(1)(x)# 构造网络
model = keras.Model(inputs, outputs)# 查看网络结构
model.summary()# 网络编译
model.compile(optimizer = keras.optimizers.Adam(0.001),  # adam优化器学习率0.001loss = tf.keras.losses.MeanAbsoluteError(),  # 标签和预测之间绝对差异的平均值metrics = tf.keras.losses.MeanSquaredLogarithmicError())  # 计算标签和预测之间的对数误差均方值。epochs = 10  # 网络迭代次数# 网络训练
history = model.fit(train_ds, epochs=epochs, validation_data=val_ds)#(10)查看训练信息
history_dict = history.history  # 获取训练的数据字典
train_loss = history_dict['loss']  # 训练集损失
val_loss = history_dict['val_loss']  # 验证集损失
train_msle = history_dict['mean_squared_logarithmic_error']  # 训练集的百分比误差
val_msle = history_dict['val_mean_squared_logarithmic_error']  # 验证集的百分比误差#(11)绘制训练损失和验证损失
plt.figure()
plt.plot(range(epochs), train_loss, label='train_loss')  # 训练集损失
plt.plot(range(epochs), val_loss, label='val_loss')  # 验证集损失
plt.legend()  # 显示标签
plt.xlabel('epochs')
plt.ylabel('loss')
plt.show()#(12)绘制训练百分比误差和验证百分比误差
plt.figure()
plt.plot(range(epochs), train_msle, label='train_msle')  # 训练集损失
plt.plot(range(epochs), val_msle, label='val_msle')  # 验证集损失
plt.legend()  # 显示标签
plt.xlabel('epochs')
plt.ylabel('msle')
plt.show()#(13)测试集评价, 计算损失和监控指标
model.evaluate(test_ds)# 预测
y_pred = model.predict(x_test)# 获取标签值对应的时间
df_time = df.index[-len(y_test):]# 绘制对比曲线
fig = plt.figure(figsize=(10,5))  # 画板大小
axes = fig.add_subplot(111)  # 画板上添加一张图
# 真实值, date_test是对应的时间
axes.plot(df_time, y_test, 'b-', label='actual')
# 预测值,红色散点
axes.plot(df_time, y_pred, 'r--', label='predict')
# 设置横坐标刻度
axes.set_xticks(df_time[::50])
axes.set_xticklabels(df_time[::50], rotation=45)plt.legend()  # 注释
plt.grid()  # 网格
plt.show()

Python——LSTM、GRU 时间序列股票数据预测(文末完整代码)相关推荐

  1. 【数值预测案例】(6) LSTM、GRU 时间序列股票数据预测,附TensorFlow完整代码

    大家好,今天和各位分享一下如何使用循环神经网络 LSTM 和 GRU 完成对股票数据的预测.GRU 是在 LSTM 基础上的简化,将 LSTM 内部的三个闸门简化成两个,往往 GRU 的计算效果会优于 ...

  2. TensorFlow 实战案例:利用 LSTM、GRU 进行股票数据预测(附 Python 完整代码)

    大家好,今天和各位分享一下如何使用循环神经网络 LSTM 和 GRU 完成对股票数据的预测.GRU 是在 LSTM 基础上的简化,将 LSTM 内部的三个闸门简化成两个,往往 GRU 的计算效果会优于 ...

  3. 手把手教你用Python直观查看贵州茅台股票交易数据,文末送书!

  4. 【Python】面向对象版学生管理系统(文末有源代码)

  5. python采集往期股票数据进行分析预测

    前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! 准备工作 既然要去赚马内,咱们首先要获取往期的数据来进行分析, 通过往期的规律来对当前进行预测,准不准我不知道,反正比人预测的准, 不准也不要喷我,咱们是 ...

  6. 深度学习(7):基于LSTM算法的股票走势预测

    目标:基于LSTM网络实现对股票走势分析,将股票指数输入LSTM模型训练和推理,最后将判断结果进行输出. 一.原理 先了解RNN,参考博客 ​​​​​​​好好学习第三天:RNN与股票预测_流萤数点的博 ...

  7. Get了!用Python制作数据预测集成工具 | 附代码

    作者 | 李秋键 责编 | 晋兆雨 大数据预测是大数据最核心的应用,是它将传统意义的预测拓展到"现测".大数据预测的优势体现在,它把一个非常困难的预测问题,转化为一个相对简单的描述 ...

  8. 雅虎财经数据python 网络爬虫stock股票 用 Python 通过雅虎财经获取股票数据

    yfinance Python Tutorial (2022) - Analyzing Alpha https://www.youtube.com/watch?v=SxIwqdedomg   讲的很详 ...

  9. 用Python爬取最新股票数据含完整源代码

    用Python爬取最新股票数据含完整源代码 抓取目标: url:http://webapi.cninfo.com.cn/#/marketDataDate 数据目标: 获取 证券代码 证券简称 交易日期 ...

最新文章

  1. php页面是什么原因,PHP空白页面常见原因及解决方法
  2. matlab降低视频帧数,如何更改视频的帧速率
  3. Caffe学习系列(8):solver优化方法
  4. SaltStack实战之配置管理-Jinja2模板
  5. 人工智障学习笔记——深度学习(4)生成对抗网络
  6. 设计模式笔记八:过滤器模式
  7. Star Schema完全参考手册读书笔记二
  8. Hadoop与Spark的集群复制
  9. 微型计算机三级项目,微机原理三级项目.doc
  10. Win32 Application和Win32 Console Application
  11. 试议软件开发与硬件开发的异同。
  12. 修改整个app的字体
  13. 问题:加入导航条后webView的UIWebBrowserView位置偏移
  14. office转换pdf [doc,docx,xls,xlsx]
  15. 基于SpringBoot+Vue开发的一个知识社区分享平台
  16. 关于汽油动力汽车和混合动力汽车的环保问题。
  17. rockchip的调试手段
  18. hive使用,及语法
  19. java contions_十一届蓝桥模拟赛 元辅音字母 JAVA
  20. 【论文解读】关于基于视觉无人机自主降落平台的论文梳理

热门文章

  1. 在线UTF16编码/解码
  2. 小程序手动设置缓存时间
  3. pngQuant图片压缩命令行工具
  4. java多线程基础篇(二)java线程常见问题Thread Dump日志分析
  5. loadrunner-录制脚本
  6. VxWorks驱动程序开发指南--驱动程序的组织结构
  7. 金蝶软件遭遇.locked勒索病毒攻击:如何保护与解救您的数据?
  8. 浅谈如何app增加用户粘性
  9. pbootcms自定义标签开发_替换标题中的特殊符号
  10. PHP表单提交的方式有哪两种,form表单提交数据的几种方式