0

0

[数据可视化]RumorLens: 社交媒体谣言的互动分析和验证

P粉084495128

P粉084495128

发布时间:2025-07-28 10:03:12

|

938人浏览过

|

来源于php中文网

原创

RumorLens是发表于CHIEA'22的交互式可视化分析系统,旨在助力社交媒体管理员高效分析验证谣言。其整合NLP与可视化技术,通过三个层次视图呈现信息:概述显示谣言时空分布,投影视图体现特征与相似性,传播视图以圆形设计展示动态传播细节,还介绍了数据处理及视图设计思路。

☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜

[数据可视化]rumorlens: 社交媒体谣言的互动分析和验证 - php中文网

[数据可视化]RumorLens: 社交媒体谣言的互动分析和验证

RumorLens 论文分享,发表于CHIEA'22(CCF-A)

论文网址

1. 视频展示

       

2. 视频讲解

       

3. 摘要

随着社会化媒体的发展,各种谣言很容易在互联网上传播,对社会造成严重的负面影响。因此,处理可疑谣言已经成为社交媒体平台的关键任务。但是,由于缺乏有效的工具,平台管理员往往难以有效地分析和验证来自社交媒体平台上大量信息的谣言。我们与社交媒体平台管理员紧密合作了四个月,总结了他们对谣言识别和分析的要求,并进一步提出了一个交互式可视化分析系统RumorLens,帮助他们高效应对谣言,深入了解谣言传播模式。RumorLens将自然语言处理(NLP)和其他数据处理技术与可视化技术相结合,以促进对可疑谣言的交互式分析和验证。

我们提出了协调良好的可视化方法,为用户提供可疑谣言的三个层次的细节:概述显示可疑谣言的空间分布和时间演化;投影视图利用基于隐喻的图示符来表示每个可疑谣言,并进一步使用户能够快速了解它们的总体特征和彼此之间的相似性;传播视图通过新颖的圆形可视化设计可视化可疑谣言的动态传播细节,并促进谣言的交互式分析和验证。

4. 简介

社交媒体在我们的日常生活中得到了广泛的应用,使得信息共享和交流变得非常方便。但是,它也提供了一种简单快捷的方法来产生和传播各种谣言。社交媒体服务提供商在过去几年中一直试图识别社交媒体平台上流传的谣言。

传统的谣言识别方法依赖于内容专家的个人经验,但处理海量信息却相当耗时费力。自动识别谣言的方法比手工方法更有效,但是大多数现有的自动化方法无法保证准确度,在实际应用中往往不够稳健。与这些自动方法相比,人类的经验可以提供更丰富的输出,包括决策的原因。

但是对于社交媒体管理者而言,他们更需要从内容、用户、话题、传播等方面深入了解可疑谣言的特点,使谣言验证更加扎实、可信。其中,十分重要的需求是追踪社交媒体上可疑谣言的动态传播细节。

为此,我们提出了一个交互式可视化分析系统RumorLens,帮助社交媒体平台的管理者有效地分析和验证可疑谣言,并深入洞察谣言的传播。我们开发了协调良好的可视化视图,为用户提供不同层次的可疑谣言细节:

  • Overview: 快速过滤可疑谣言,可视化谣言的空间分布和时间演化
  • Projection View: 基于它们的整体特征和相似性上进行选择
  • Propagation View: 本文提出了一种新的圆形可视化设计,以紧凑的方式可视化所选可疑谣言的动态传播细节

[数据可视化]RumorLens: 社交媒体谣言的互动分析和验证 - php中文网        

Figure1: RumorLens,一个多层次的可视化分析系统,帮助用户以交互方式分析和验证社交媒体上的可疑谣言。A) 位置分布视图提供可疑谣言的空间分布摘要;B) 话题演化视图显示了不同话题的可疑谣言随着时间的推移而发生的变化;C) 特征投影图揭示了可疑谣言的总体特征和相互之间的相似性;D) 传播视图采用新颖的圆形可视化设计,可视化可疑谣言的动态传播细节;E) 帖子详细信息视图显示用户信息和帖子内容的详细信息。

5. 数据提取和处理

新浪社区管理中心是负责收集、记录和处理新浪微博上发布的可疑谣言等非法信息的在线平台。据新浪微博报道,每年都有数以万计的各领域谣言需要及时处理,否则可能对公众和政府造成严重影响。我们从中收集了2019年12月27日至2020年12月14日期间的可疑谣言数据集。

可疑谣言的用户、内容、话题和传播等多个特性对于谣言分析和验证非常重要。因此,在推特原始数据的基础上,分别采用TF-IDF、情绪识别、话题分类、影响力计算进一步提取可疑谣言的关键词、情绪、话题和影响力。特别地,我们采用t-SNE来减少可疑谣言的维度并投影到2D平面图上以显示它们之间的关系。

  • 这里简单放一下主题分类的代码,其实可以参照我之前的项目有很多类似的例子
In [ ]
import paddleimport paddlenlp as ppnlpfrom paddlenlp.data import Stack, Pad, Tupleimport paddle.nn.functional as Fimport numpy as npimport reimport randomimport pandas as pdfrom functools import partial  # partial()函数可以用来固定某些参数值,并返回一个新的callable对象to_label = {'社会时事': '0', '母婴育儿': '1', '历史文化': '2', '常识': '3', '国际': '4',            '军事': '5', '教育': '6', '娱乐': '7', '科技': '8', '情感': '9'}
content = pd.read_csv("final_all_texts_nonull.csv")["content"].values.tolist()
strlabel = pd.read_csv("final_all_texts_topic_withprob_merge_nonull.csv")["topic"].values.tolist()

int_label = []for i in range(len(content)):
    content[i] = re.sub(u"\\【.*?\\】|\\{.*?\\}|\\<.*?\\>|\\#.*?\\#", "", content[i]).replace(" ", "")
    int_label.append(to_label[strlabel[i]])

all_data = []for i in range(len(content)):
    all_data.append([content[i], int_label[i]])

count = {'0': [0, []], '1': [0, []], '2': [0, []], '3': [0, []], '4': [0, []], '5': [0, []], '6': [0, []], '7': [0, []],         '8': [0, []], '9': [0, []]}for i in range(len(int_label)):
    count[int_label[i]][0] += 1
    count[int_label[i]][1].append(i)

train = []
dev = []for i in count:
    trainlen = count[i][0] // 10 * 7
    for j in range(trainlen):
        train.append(all_data[count[i][1][j]])    for j in range(count[i][0] - trainlen):
        dev.append(all_data[count[i][1][j + trainlen]])

random.shuffle(train)
random.shuffle(dev)

trainls, devls = train[:], dev[:]
testlst = []  # 没有测试数据class SelfDefinedDataset(paddle.io.Dataset):
    def __init__(self, data):
        super(SelfDefinedDataset, self).__init__()
        self.data = data    def __getitem__(self, idx):
        return self.data[idx]    def __len__(self):
        return len(self.data)    def get_labels(self):
        return ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]


train_ds, dev_ds, test_ds = SelfDefinedDataset.get_datasets([trainls, devls, testlst])
label_list = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]print("训练集样本个数:{}".format(len(train_ds)))print("验证集样本个数:{}".format(len(dev_ds)))print("测试集样本个数:{}".format(len(test_ds)))

tokenizer = ppnlp.transformers.BertTokenizer.from_pretrained("bert-base-chinese")def convert_example(example, tokenizer, label_list, max_seq_length=256, is_test=False):
    if is_test:
        text = example    else:
        text, label = example    # tokenizer.encode方法能够完成切分token,映射token ID以及拼接特殊token
    encoded_inputs = tokenizer.encode(text=text, max_seq_len=max_seq_length)
    input_ids = encoded_inputs["input_ids"]
    segment_ids = encoded_inputs["segment_ids"]    if not is_test:
        label_map = {}        for (i, l) in enumerate(label_list):
            label_map[l] = i

        label = label_map[label]
        label = np.array([label], dtype="int64")        return input_ids, segment_ids, label    else:        return input_ids, segment_ids# 数据迭代器构造方法def create_dataloader(dataset, trans_fn=None, mode='train', batch_size=1, use_gpu=True, pad_token_id=0,
                      batchify_fn=None):
    if trans_fn:
        dataset = dataset.apply(trans_fn, lazy=True)    if mode == 'train' and use_gpu:
        sampler = paddle.io.DistributedBatchSampler(dataset=dataset, batch_size=batch_size, shuffle=True)    else:
        shuffle = True if mode == 'train' else False  # 如果不是训练集,则不打乱顺序
        sampler = paddle.io.BatchSampler(dataset=dataset, batch_size=batch_size, shuffle=shuffle)  # 生成一个取样器
    dataloader = paddle.io.DataLoader(dataset, batch_sampler=sampler, return_list=True, collate_fn=batchify_fn)    return dataloader# 使用partial()来固定convert_example函数的tokenizer, label_list, max_seq_length, is_test等参数值trans_fn = partial(convert_example, tokenizer=tokenizer, label_list=label_list, max_seq_length=128, is_test=False)
batchify_fn = lambda samples, fn=Tuple(Pad(axis=0, pad_val=tokenizer.pad_token_id),
                                       Pad(axis=0, pad_val=tokenizer.pad_token_id),
                                       Stack(dtype="int64")): [data for data in fn(samples)]# 训练集迭代器train_loader = create_dataloader(train_ds, mode='train', batch_size=64, batchify_fn=batchify_fn, trans_fn=trans_fn)# #验证集迭代器dev_loader = create_dataloader(dev_ds, mode='dev', batch_size=64, batchify_fn=batchify_fn, trans_fn=trans_fn)

model = ppnlp.transformers.BertForSequenceClassification.from_pretrained("bert-base-chinese", num_classes=10)#学习率learning_rate = 1e-5#训练轮次epochs = 100#学习率预热比率warmup_proption = 0.1#权重衰减系数weight_decay = 0.01num_training_steps = len(train_loader) * epochs
num_warmup_steps = int(warmup_proption * num_training_steps)def get_lr_factor(current_step):
    if current_step < num_warmup_steps:        return float(current_step) / float(max(1, num_warmup_steps))    else:        return max(0.0,                    float(num_training_steps - current_step) /                    float(max(1, num_training_steps - num_warmup_steps)))#学习率调度器lr_scheduler = paddle.optimizer.lr.LambdaDecay(learning_rate, lr_lambda=lambda current_step: get_lr_factor(current_step))#优化器optimizer = paddle.optimizer.AdamW(
    learning_rate=lr_scheduler,
    parameters=model.parameters(),
    weight_decay=weight_decay,
    apply_decay_param_fun=lambda x: x in [
        p.name for n, p in model.named_parameters()        if not any(nd in n for nd in ["bias", "norm"])
    ])#损失函数criterion = paddle.nn.loss.CrossEntropyLoss()#评估函数metric = paddle.metric.Accuracy()#评估函数def evaluate(model, criterion, metric, data_loader):
    model.eval()
    metric.reset()
    losses = []    for batch in data_loader:
        input_ids, segment_ids, labels = batch
        logits = model(input_ids, segment_ids)
        loss = criterion(logits, labels)
        losses.append(loss.numpy())
        correct = metric.compute(logits, labels)
        metric.update(correct)
        accu = metric.accumulate()    print("eval loss: %.5f, accu: %.5f" % (np.mean(losses), accu))
    model.train()
    metric.reset()#开始训练global_step = 0for epoch in range(1, epochs + 1):    for step, batch in enumerate(train_loader, start=1): #从训练数据迭代器中取数据
        input_ids, segment_ids, labels = batch
        logits = model(input_ids, segment_ids)
        loss = criterion(logits, labels) #计算损失
        probs = F.softmax(logits, axis=1)
        correct = metric.compute(probs, labels)
        metric.update(correct)
        acc = metric.accumulate()

        global_step += 1
        if global_step % 50 == 0 :            print("global step %d, epoch: %d, batch: %d, loss: %.5f, acc: %.5f" % (global_step, epoch, step, loss, acc))
        loss.backward()
        optimizer.step()
        lr_scheduler.step()
        optimizer.clear_gradients()
    evaluate(model, criterion, metric, dev_loader)

model.save_pretrained("model")
tokenizer.save_pretrained("tokenzier")def predict(model, data, tokenizer, label_map, batch_size=1):
    examples = []    for text in data:
        input_ids, segment_ids = convert_example(text, tokenizer, label_list=label_map.values(),  max_seq_length=128, is_test=True)
        examples.append((input_ids, segment_ids))

    batchify_fn = lambda samples, fn=Tuple(Pad(axis=0, pad_val=tokenizer.pad_token_id), Pad(axis=0, pad_val=tokenizer.pad_token_id)): fn(samples)
    batches = []
    one_batch = []    for example in examples:
        one_batch.append(example)        if len(one_batch) == batch_size:
            batches.append(one_batch)
            one_batch = []    if one_batch:
        batches.append(one_batch)

    results = []
    model.eval()    for batch in batches:
        input_ids, segment_ids = batchify_fn(batch)
        input_ids = paddle.to_tensor(input_ids)
        segment_ids = paddle.to_tensor(segment_ids)
        logits = model(input_ids, segment_ids)
        probs = F.softmax(logits, axis=1)
        idx = paddle.argmax(probs, axis=1).numpy()
        idx = idx.tolist()
        labels = [label_map[i] for i in idx]
        results.extend(labels)    return results


data = [i[0] for i in dev]
label_map = {0: '0', 1: '1', 2:"2", 3:"3", 4:"4", 5:"5", 6:"6", 7:"7", 8:"8", 9:"9"}

predictions = predict(model, data, tokenizer, label_map, batch_size=32)
   
  • TF-IDF的实现
In [ ]
from sklearn.feature_extraction.text import CountVectorizerfrom sklearn.feature_extraction.text import TfidfTransformerimport jiebaimport reimport jieba.analysedef participle(sentence):
    stopwords = [line.strip() for line in open("cn_stopwords.txt", encoding='UTF-8').readlines()]    for j in range(len(sentence)):
        sentence[j] = re.sub(r"| |\t|\r", "", sentence[j])
        sentence[j] = re.sub(r"\n", "", sentence[j])
        sentence[j] = re.sub('[a-zA-Z0-9]', "", sentence[j])
        sentence[j] = re.sub(            "[\001\002\003\004\005\006\007\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a]"
            , "", sentence[j])#         seg = jieba.lcut(sentence[j], cut_all=False)#         print(sentence[j])
        seg = jieba.analyse.textrank(sentence[j], topK=None, withWeight=False, allowPOS=('ns', 'n', 'vn', 'nt', 'nz'), withFlag=False)
        sentence[j] = ''
        for word in seg:            if word not in stopwords:
                sentence[j] = sentence[j] + word + " "

    for _ in range(sentence.count("")):
        sentence.remove("")    return sentencedef tfidf(sentence):
    words = []    # step 1
    vectoerizer = CountVectorizer(min_df=1, max_df=1.0, token_pattern='\\b\\w+\\b')    # step 2
    vectoerizer.fit(sentence)    # step 3
    bag_of_words = vectoerizer.get_feature_names()    # step 4
    X = vectoerizer.transform(sentence)    # step 1
    tfidf_transformer = TfidfTransformer()    # step 2
    tfidf_transformer.fit(X.toarray())    # step 3
    for idx, word in enumerate(vectoerizer.get_feature_names()):
        words.append((word, tfidf_transformer.idf_[idx]))    # step 4
    # tfidf = tfidf_transformer.transform(X)
    # print(tfidf.toarray())

    words.sort(key=lambda word: word[1], reverse=True)
    words = [i[0] for i in words]    return words
   

5.1 处理微博数据生成传播链的思路

微博的转发文本中会附带有这一整条传播链上用户的名称,并且使用"//@"进行连接,但是每个人转发的时候会由于父级别转发用户的不一样导致从同一条微博产生的转发链会有分支,这时候分支节点就十分关键,节点用户信息的丢失会使得分支丢失,导致最终得到的传播链变小。而由于很多用户会被微博封禁或注销,这就导致很多用户数据消失了,此时传播链中的很多节点用户会出现丢失。

一开始,我以为简单的根据"//@"进行分割就可以得到传播链。但是,当我初步完成以后,对这一部分传播链进行深入分析的时候,我就发现由于节点用户数据的丢失使得传播链变短了,但是后续的数据又没有用到;因此,我摒弃了根据"//@"从根节点到子节点进行简单分割的方法,选择了从子节点往根节点反推,再从根节点向子节点验证的方法,最大程度地恢复了完整的传播链,减少了数据的误差,最好的效果是使得传播链从五到六级恢复到十五六级。

6. 视图设计

6.1 Suspected Rumors Overview

可疑谣言概述(图1(A)和(B))旨在为平台管理员提供所有可疑谣言的时空分布概述(R1)。可疑谣言概述由两个主要部分组成,一个是choropleth map,一个是line map。choropleth map(图1(A))显示了中国不同地区和海外的可疑谣言数量。line map(图1(B))显示了不同主题的可疑谣言随时间变化的数量。

Timely
Timely

一款AI时间跟踪管理工具!

下载

在顶部的choropleth map中,有更多可疑谣言的位置以较暗的颜色显示,准确的数字可以通过悬停在上面观察到。单击地图时,会高亮显示并选择相应的位置。下面的line map显示了在choropleth map上选定的可疑谣言的主题演变,不同颜色的线代表不同的主题。一旦选择了某个主题,该主题下的可疑谣言关键词就会显示在相应的行上。还提供了时间轴,通过选择开始和结束时间进一步过滤可疑谣言。

6.2 Projection View

Projection View(图1(C))旨在帮助平台管理员快速检查和定位最可疑的功能,这些功能可能属于谣言,以便进一步验证(R2)。 Projection View由表示投影到2D特征图上的过滤可疑谣言的图示符组成,图示符之间的距离表示可疑谣言消息之间的相似性。

为了便于目视检查和比较可疑特征,我们将每个可疑谣言编码为一个圆形符号,该符号由两部分组成:内圈和外圈,如图2(a)所示。内圈的颜色代表可疑谣言的主题,大小则表示其影响力。外部四个弧线分别显示了粉丝、关注者、发帖和用户信息完整性的数量,图示符如图2(B)所示。特别是,由于不同用户的粉丝和粉丝数量差异很大,为了便于比较,采用对数法进行计算。与thermograph shape(图2(C))相比,我们的图示符设计可以提供更简洁、紧凑的方式来显示每个可疑谣言。此外,在左侧缩略图的引导下,可以通过拖动和缩放来调整显示视图的位置和大小。一旦选中,图示符将被放大以突出显示与其他图示符的差异,以便近距离观察。

[数据可视化]RumorLens: 社交媒体谣言的互动分析和验证 - php中文网        

Figure 2: Glyph designs for features of each suspected rumor. (a) round glyph design; (b) arc glyph design; (c) thermography shape glyph design.

6.3 Propagation View

Propagation View(图1(D)和(E))提供了对可疑谣言消息如何在社交媒体上传播的详细理解,从而使平台管理员能够做出最终决策(R3)。这一点非常重要,因为专家经验和以往研究所提到的丰富的传播信息可以被展示和探索,以验证可疑谣言。该视图包含两个部分,一个新颖的圆形设计用于在顶部可视化可疑谣言传播,另一个表格在底部显示相应的内容详细信息。

我们提出了一种新颖的圆形设计,它由中心的原始tweet和周围的多层次转发组成。如图3所示,由圆形节点表示的原始tweet位于中心,大小表示其影响。在它的周围有几个宽度不同的同心圆环,每个圆环都表示同一层次上的转发。每个同心圆环都包含扇形指针,扇形指针由径向白线隔开,白线按日期顺时针排列。每个扇区都充满了由许多单元格编码的转发,这些单元格的大小表示包含的字数。此外,单元格的颜色表示转发的情绪是中性(淡黄色)、积极(绿色)还是消极(红色)。特别是,能够提供转发的粗略含义的关键字显示在一些大单元格中。

[数据可视化]RumorLens: 社交媒体谣言的互动分析和验证 - php中文网Figure 3: 传播视图的图示符设计。该图用于显示特定的编码和交互演示

[数据可视化]RumorLens: 社交媒体谣言的互动分析和验证 - php中文网Figure 4: 传播视图的交互设计。它允许用户通过左键和右键单击选择两个转发。详细视图将显示所选两个转发的详细信息,以进行详细比较

在传播视图中支持丰富的交互。首先,当鼠标悬停在相应的单元格上时,会出现一个面板来显示一些简短的用户信息,通过单击可以在图1(E)中显示更详细的信息,如用户属性、创建时间和完整内容。其次,还支持比较分析以便于仔细检查。通过连续单击两个转发单元格,可以并排查看和检查这两个单元格的详细信息,如图4所示。第三,在左上角有一个时间直方图,显示转发数量随时间的变化。平台管理员可以单击它并高亮显示当天在不同层次结构中转发的单元格。

在设计过程中,我们最初考虑使用节点链接图、树图、螺旋时间线或sunburst图来可视化社交媒体上的推特传播。但是,它们都不能从传播路径同时呈现转发层次结构和时间序列。此外,当大量数据需要可视化时,节点链接图的空间效率不够。相比之下,我们所提出的圆形设计能够显示可疑谣言的动态传播细节,以简洁的方式促进谣言的互动分析和验证。

7. 结论和未来工作

我们提出了RumorLens,这是一个交互式可视分析系统,可以帮助社交媒体平台的管理员有效地处理可疑的谣言。此外,本文还提出了一种新颖的圆形 glyph 设计,以显示可疑谣言的动态传播细节,从而简化谣言的交互分析和验证。一位领域专家进行的案例研究为谣言在帮助用户分析和验证可疑谣言方面的有效性提供了支持。

但是,针对可疑谣言的互动分析和验证的谣言仍然需要进一步改进。首先,通过与领域专家合作,我们认识到用户信息对于谣言识别的重要性。例如,如果用户的帐户未定义,并且之前有几次已识别的传闻在推特上发布,则消息具有成为传闻的高风险。因此,有必要提供与用户相关的历史投诉的更多信息。其次,谣言可以通过各种特征进行识别,因此如何选择和评价其对谣言验证的影响仍然是一个有待解决的问题。

相关专题

更多
golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

73

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

25

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

37

2025.11.17

java判断map相关教程
java判断map相关教程

本专题整合了java判断map相关教程,阅读专题下面的文章了解更多详细内容。

32

2025.11.27

PHP 命令行脚本与自动化任务开发
PHP 命令行脚本与自动化任务开发

本专题系统讲解 PHP 在命令行环境(CLI)下的开发与应用,内容涵盖 PHP CLI 基础、参数解析、文件与目录操作、日志输出、异常处理,以及与 Linux 定时任务(Cron)的结合使用。通过实战示例,帮助开发者掌握使用 PHP 构建 自动化脚本、批处理工具与后台任务程序 的能力。

22

2025.12.13

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

150

2025.12.31

php网站源码教程大全
php网站源码教程大全

本专题整合了php网站源码相关教程,阅读专题下面的文章了解更多详细内容。

88

2025.12.31

视频文件格式
视频文件格式

本专题整合了视频文件格式相关内容,阅读专题下面的文章了解更多详细内容。

90

2025.12.31

不受国内限制的浏览器大全
不受国内限制的浏览器大全

想找真正自由、无限制的上网体验?本合集精选2025年最开放、隐私强、访问无阻的浏览器App,涵盖Tor、Brave、Via、X浏览器、Mullvad等高自由度工具。支持自定义搜索引擎、广告拦截、隐身模式及全球网站无障碍访问,部分更具备防追踪、去谷歌化、双内核切换等高级功能。无论日常浏览、隐私保护还是突破地域限制,总有一款适合你!

61

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Git 教程
Git 教程

共21课时 | 2.4万人学习

Git版本控制工具
Git版本控制工具

共8课时 | 1.5万人学习

Git中文开发手册
Git中文开发手册

共0课时 | 0人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号