数据挖掘(九)

news2024/11/13 15:07:47

数据挖掘(九)

文章目录

    • 数据挖掘(九)
      • 获取新闻文章
        • 使用Web API获取数据
      • 从任意网站抽取文本
        • 寻找任意网站网页中的主要内容
      • 示例代码1
      • 新闻语料聚类
        • k-means算法
        • 评估结果
        • 从簇中抽取主题信息
      • 聚类融合
        • 证据累积
        • 工作原理
      • 线上学习

已知目标类别的学习任务,叫做监督学习。不知道类别的情况下进行数据挖掘,这叫做无监督学习,偏重于探索、发现隐藏在数据里的信息,而不是用模型来分类。

获取新闻文章

本文讨论构建一个按照主题为最新的新闻报道分组的系统。

使用Web API获取数据
  1. 使用Web API采集数据,我们需要注意授权方法、请求频率限制和API端点。
  • 授权方法时数据提供方用来管理数据采集方的。数据提供方以此来了解谁正在采集数据,确保采集方抓取数据的频率没有超出上限,同时对采集方都采集了哪些数据可以做到监视。对于多数网站来说,普通个人账号就能用来采集数据,但也有部分网站要求采集方使用正式的开发者账号。
  • 采集频率限制规定了采集方在约定时间内的最大请求次数,特别是对于免费提供的服务。在使用数据获取接口时,一定要了解不同的网站可能有着不同的规定。即使同一个网站,不同的API调用也有不同的采集频率限制。
  • API端点是指用来抽取信息的实际网址。不同网站提供不同的接口,大部分Web API提供Restful接口。

从任意网站抽取文本

我们从Hao123收集的网址所指向的网站分属不同的网站组织。当我们尝试用程序获取里面的实际内容时,可能遇到各种困难,比如有很多逻辑是在后台运行的,调用JS库,应用样式表,用AJAX加载广告,在侧边栏增加很多内容等,这些功能增加了网站的复杂程度,增加了自动采集信息的难度。

寻找任意网站网页中的主要内容
  1. 首先访问每个链接,下载各个网页,把它们保存到数据文件夹中事先建好的用于存放原始网页的文件夹raw,然后我们从这些原始网页中获取有用的信息。我们使用MD5散列算法为每篇报道创建一个唯一的文件名。散列函数将输入转换为一个看似随机产生的字符串。对于相同的输入,散列函数返回相同的结果,并且散列函数是单向函数,根据散列值无法得到原来的值。
import os
import hashlib
import requests

stories = [['“旅行者”来做客,“美食”多样!唯美生态画卷颜值不断被“刷新”', 'http://baijiahao.baidu.com/s?id=1815151290164989110', '12'], 
           ['网友称镇卫生院有人“吃空饷”,相关人员应被追责;珠海金湾区卫健局:重复领取', 'http://baijiahao.baidu.com/s?id=1815068325137550619', '14'],
           ['泼天流量说来就来,这次大学生“夜袭开封”', 'http://baijiahao.baidu.com/s?id=1815025933758571293', '18']]
data_folder = os.path.join(os.getcwd(), 'data_mining', 'news', 'raws')
# 对于网页下载失败的网站,直接跳过,维护计数器统计下载失败的次数,如果是系统本身的问题阻止下载,我们需要解决问题。如果失败次数过多,我们就需要找出到底是什么问题并解决
number_errors = 0
# 遍历每篇新闻报道
for title, url, score in stories:
    # 对标题进行散列操作
    output_filename = hashlib.md5(url.encode()).hexdigest()
    fullpath = os.path.join(data_folder, output_filename + '.txt')
    try: 
	# 下载网页保存到输出文件夹
        response = requests.get(url)
        data = response.text
        with open(fullpath, 'w') as outf:
            outf.write(data)
    except Exception as e:
	number_errors += 1
	# 如果异常过多,调用raise处理异常中断机制
	print(e)
  1. 获得原始网页后,我们需要找出每个网页中的新闻报道内容,有些在线资源使用数据挖掘方法来解决这个问题。从网页中抽取文本代码可以使用lxml包解析HTML文件,lxml的HTML解析器容错能力强,可以处理不规范的HTML代码。文本抽取首先遍历HTML文件的每个节点,抽取其中的文本内容;其次跳过JS、样式和注释节点,这些系欸但不太可能包含对我有有价值的信息。最后确保文本内容长度至少为100字符。文本抽取函数在任何子节点上调用自己来抽取子节点中的文本内容,最后返回拼接在一起的所有子节点的文本。如果一个节点没有任何子节点,文本抽取函数返回该节点的文本内容,如果该节点不包含任何内容,就返回空字符串。
# 获得raws文件夹中的所有文件名
filenames = [os.path.join(data_folder, filename) for filename in os.listdir(data_folder)]
from lxml import etree, html
# 存放不可能包含新闻报道内容的节点
skip_node_types = ['script', 'head', 'style', etree.Comment]
# 把html文件解析成lxml etree对象
def get_text_from_file(filename):
    with open(filename) as inf:
        html_tree = html.parse(inf)
    # 调用getroot()函数获取树的根节点,这样以这个节点作为入参,能处理包括根节点在内的所有节点
    return get_text_from_node(html_tree.getroot())
# 对每个子节点递归调用文本抽取函数,把返回后的文本内容拼接在一起
def get_text_from_node(node):
    if len(node) == 0:
        if node.text and len(node.text) > 100:
            return node.text
        else:
            return ''
    results = (get_text_from_node(child) for child in node if child.tag not in skip_node_types)
    # 防止返回空行
    return '\n'.join(r for r in results if len(r) > 1)
# 从文件中抽取文本
for filename in os.listdir(data_folder):
    text = get_text_from_file(os.path.join(data_folder, filename))
    with open(os.path.join(data_folder, filename), 'w') as outf:
        outf.write(text)

示例代码1

import os
import hashlib
import requests
from lxml import etree, html

stories = [['“旅行者”来做客,“美食”多样!唯美生态画卷颜值不断被“刷新”', 'http://baijiahao.baidu.com/s?id=1815151290164989110', '12'], 
           ['网友称镇卫生院有人“吃空饷”,相关人员应被追责;珠海金湾区卫健局:重复领取', 'http://baijiahao.baidu.com/s?id=1815068325137550619', '14'],
           ['泼天流量说来就来,这次大学生“夜袭开封”', 'http://baijiahao.baidu.com/s?id=1815025933758571293', '18']]
data_folder = os.path.join(os.getcwd(), 'data_mining', 'news', 'raws')
number_errors = 0
for title, url, score in stories:
    print(title, url, score)
    output_filename = hashlib.md5(url.encode()).hexdigest()
    fullpath = os.path.join(data_folder, output_filename + '.txt')
    try: 
        response = requests.get(url)
        response.encoding = 'utf-8'
        data = response.text  
        with open(fullpath, 'w') as outf:
            outf.write(data)
    except Exception as e:
        number_errors += 1
        print(e)
documents = [open(os.path.join(data_folder, filename), encoding='utf-8').read() for filename in os.listdir(data_folder)]    
skip_node_types = ['script', 'head', 'style', etree.Comment]
def get_text_from_file(filename):
    with open(filename, encoding='utf-8') as inf:
        html_tree = html.parse(inf)
    return get_text_from_node(html_tree.getroot())
def get_text_from_node(node):
    if len(node) == 0:
        if node.text and len(node.text) > 100:
            return node.text
        else:
            return ''
    results = (get_text_from_node(child) for child in node if child.tag not in skip_node_types)
    return '\n'.join(r for r in results if len(r) > 1)
for filename in os.listdir(data_folder):
    text = get_text_from_file(os.path.join(data_folder, filename))
    with open(os.path.join(data_folder, filename), 'w', encoding='utf-8') as outf:
        outf.write(text)

新闻语料聚类

我们可以通过聚类发现新闻语料中潜藏的趋势。我们使用经典的机器学习算法k均值算法k-means。聚类属于无监督学习。假设我们使用500篇新闻报道组成的数据集,人工查看这些文章的主题费时费力,即使使用概括统计方法也不容易。聚类分析可以按照主题把它们分成不同的簇,然后可以按簇研究它们的主题。

k-means算法

k-means聚类算法迭代寻找最能够代表数据的聚类质心点。算法开始时使用从训练数据中随机选取的几个数据点作为质心点。k-means中的k表示寻找多少个质心点,同时也是算法将会找到的簇的数量。

  1. k-means算法分为两步,一是为每个数据点分配簇标签,二是更新各簇的质心点。一个数据点对应数据集中一条数据,把数据集看成样本,一条数据可以看作是一个个体。我们在分簇时为数据集中的个体设置一个标签,把它和最近的质心点联系起来。对于距离质心点1最近的个体,我们为它们分配标签1,以此类推。标签相同的个体属于同一个簇。更新环节计算各簇内所有数据点的均值,更新质心点。k-means算法会重复上述步骤,每次更新质心点时,所有质心点将会小范围移动。这个过程会执行知道条件不再满足位置。
  2. k-means算法只有一个参数,在很多数据挖掘问题上效果很好,所以仍然被频繁使用。scikit-learn实现了k-means算法。创建数据分析流水线,第一步是特征抽取,第二步是调用k-means算法。
from sklearn.cluster import KMeans
# 这个向量化工具根据词语出现在多少篇文档中对词语计数进行加权,出现在较多文档中的词语权重较低。用文档集数量除以词语出现在的文档的数量,然后取对数
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import Pipeline
from collections import Counter
# 参数max_df=0.4表示忽略出现在40%及以上的文档中的词语,可以用来剔除本身不含有主题相关含义的词语
documents = [open(os.path.join(data_folder, filename), encoding='utf-8').read() for filename in os.listdir(data_folder)]   
n_clusters = 2
pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)), ('clusterer', KMeans(n_clusters))])
pipeline.fit(documents)
# labels变量包含每个数据点的簇标签
labels = pipeline.predict(documents)
# 使用Counter类来查看每个簇有多少个数据点
c = Counter(labels)
for cluster_number in range(n_clusters):
    print('Cluster {} contains {} samples'.format(cluster_number, c[cluster_number]))
评估结果
  1. 聚类分析主要是探索性分析,因此很难有效地评估聚类算法结果的好坏。评估算法结果最直接的方式是根据它要学习的标准对其进行评价。对于k均值算法,寻找新质心点的标准是最小化每个数据点到最近质心点的距离,这叫做算法的惯性权重。
# 任何经过训练的KMeans实例都有惯性权重,可以用来确定分多少簇合适
pipeline.named_steps['clusterer'].inertia_
# n_cluster_values依次取2到20之间的值,每取一个值,k-means算法运行10此,每次运行算法都记录惯性权重,每次仅训练X矩阵一次
inertia_scores = []
n_cluster_values = list(range(2, 20))
for n_clusters in n_cluster_values:
    cur_inertia_scores = []
    X = TfidfVectorizer(max_df=0.4).fit_transform(documents)
    for i in range(40):
        km = KMeans(n_clusters).fit(X)
        cur_inertia_scores.append(km.inertia_)
    inertia_scores.append(cur_inertia_scores)
# 变量inertia_scores存储了n_clusters取2到20每个值时所对应的惯性权重,把惯性权重和簇的数量做成图。随着簇数量增加,质心点和其他数据点位置的调整逐渐减少,惯性权重应该逐渐降低
from matplotlib import pyplot as plt
inertia_means = np.mean(inertia_scores, axis=1)
inertia_stderr = np.std(inertia_scores, axis=1)
fig = plt.figure(figsize=(40,20))
plt.errorbar(n_cluster_values, inertia_means, inertia_stderr, color='green')
plt.show()
从簇中抽取主题信息
  1. 我们尝试找到每个簇的主题,首先从特征提取这一步抽取词表。其次k均值算法可以用来简化特征。特征简化的方法有很多种,比如主要成分分析、潜在语义索引等,这些方法还能用来创建新特征。
# 从流水线的特征提取这一步抽取词条
terms = pipeline.named_steps['feature_extraction'].get_feature_names()
# 计算每簇所包含的个体数量
c = Counter(labels)
# 遍历所有的簇,输出每簇所包含的个体数量
for cluster_number in range(n_clusters):
    print("Cluster {} contains {} samples".format(cluster_number, c[cluster_number]))
    print("  Most important terms")
    # 从质心点找出特征值最大的5个特征
    centroid = pipeline.named_steps['clusterer'].cluster_centers_[cluster_number]
    most_important = centroid.argsort()
    for i in range(5):
        term_index = most_important[-(i+1)]
        print("  {0}) {1} (score: {2:.4f})".format(i+1, terms[term_index], centroid[term_index]))

聚类融合

聚类算法可以进行融合,融合后得到的算法能够平滑算法多次运行所得到的不同结果。多次运行k均值算法得到结果因为最初选择的质心点不同而不同。聚类融合方法可以降低参数选择对最终结果的影响。

证据累积
  1. 基本的融合方法是对数据进行多次聚类,每次都记录各个数据点的簇标签,然后计算每两个数据点被分到同一个簇的次数。这就是证据累积算法的精髓。第一步是使用k-means等低水平的聚类算法对数据集进行多次聚类,记录每次跌倒两个数据点出现在同一簇的频率,将结果保存到共协矩阵中。第二步是是用另一种聚类算法,分级聚类对第一步得到的共协矩阵进行聚类分析。
from scipy.sparse import csr_matrix
# 遍历所有标签,记录具有相同标签的两个数据点的位置,创建共协矩阵
def create_coassociation_matrix(labels):
    rows = []
    cols = []
    unique_labels = set(labels)
    for label in unique_labels:
        indices = np.where(labels == label)[0]
        for index1 in indices:
            for index2 in indices:
                rows.append(index1)
                cols.append(index2)
    data = np.ones((len(rows),))
    return csr_matrix((data, (rows, cols)), dtype='float')
C = create_coassociation_matrix(labels)
  1. 对共协矩阵进行分级聚类。我们需要找到该矩阵的最小生成树MST,删除权重低于阈值的边。
from scipy.sparse.csgraph import minimum_spanning_tree
# 矩阵C中值越高表示一组数据点被分到同一簇的次数越多,这个值表示相似度
mst = minimum_spanning_tree(C)
pipeline.fit(documents)
label2s = pipeline.predict(documents)
C2 = create_coassociation_matrix(labels2)
C_sum = (C + C2)/2
mst = minimum_spanning_tree(-C_sum)
# 删除两个矩阵中都没有出现的边
mst.data[mst.data > -1] = 0
mst.eliminate_zeros()
# 找到所有的连通分支,寻找移除低权重的边以后仍然连接在一起的节点
from scipy.sparse.csgraph import connected_components
number_of_clusters, labels = connected_components(mst)
工作原理
  1. k-means算法不考虑特征的权重,假定所有的特征取值范围相同,寻找的是圆形簇。证据累积算法的工作原理是重新把特征映射到新空间,每次运行k-means算法相当于使用转换器对特征进行一次转换。证据累积算法只关心数据点之间的距离而不是它们在原来特征空间的位置。对于没有规范化过的特征,仍然存在问题。我们使用tf-idf规范特征值,从而使特征具有相同的值域。
from sklearn.base import BaseEstimator, ClusterMixin
# 创建证据累积算法类
class EAC(BaseEstimator, ClusterMixin):
    # k-means算法运行次数,用来删除边的阈值和每次运行算法要找到的簇的数量,指定取值范围
    def __init__(self, n_clusterings=10, cut_threshold=0.5, n_clusters_range=(3, 10)):
        self.n_clusterings = n_clusterings
        self.cut_threshold = cut_threshold
        self.n_clusters_range = n_clusters_range
  
    def fit(self, X, y=None):
	# 使用低水平聚类把每次迭代得到的共协矩阵加起来,为了节省内存,我们使用生成器,仅在需要时创建共协矩阵
        C = sum((create_coassociation_matrix(self._single_clustering(X))
                 for i in range(self.n_clusterings)))
        mst = minimum_spanning_tree(-C)
        mst.data[mst.data > -self.cut_threshold] = 0
        mst.eliminate_zeros()
        self.n_components, self.labels_ = connected_components(mst)
        return self
  
    def _single_clustering(self, X):
        n_clusters = np.random.randint(*self.n_clusters_range)
        km = KMeans(n_clusters=n_clusters)
        return km.fit_predict(X)
  
    def fit_predict(self, X):
        self.fit(X)
        return self.labels_

线上学习

线上学习是指用新数据增量地改进模型。支持线上学习的算法可以先用一条或少量数据进行训练,随着更多新数据的添加,更新模型。标准的k-means算法不支持线上学习。线上学习算法在只有几条新数据的情况下就能做到部分更新已有模型。神经网络算法使支持线上学习的标准例子。随着一条心数据插入到神经网络后,网络中的权重根据学习速率进行更新,学习速率通常使一个很小的值。神经网络还可以按照批模式来训练,每次只用一组数据进行训练。我们可以用一个数据点或少量数据点来轻微更新k-means中的质心点。

  1. scikit-learn提供了MiniBatchKBeans算法用来实现线上学习功能。
from sklearn.cluster import MiniBatchKMeans
# 从数据集中抽取特征,创建矩阵X
vec = TfidfVectorizer(max_df=0.4)
X = vec.fit_transform(documents)
mbkm = MiniBatchKBeans(random_state=14, n_clusters=3)
# 随机从X矩阵中选择数据,模拟来自外部的新数据
batch_size = 10
for iteration in range(int(X.shape[0] / batch_size)):
    start = batch_size * iteration
    end = batch_size * (iteration + 1)
    mbkm.partial_fit(X[start:end])
# 获得原始数据集的聚类结果
labels_mbkm = mbkm.predict(X)
mbkm.inertia_

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2237806.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

详解基于C#开发Windows API的SendMessage方法的鼠标键盘消息发送

在C#中,SendMessage方法是一个强大的工具,它允许我们与Windows API交互,模拟键盘和鼠标事件。本文将详细介绍如何使用SendMessage方法来发送鼠标和键盘消息。 1. SendMessage方法概述 SendMessage是Windows API中的一个函数,它用…

CSS教程(三)- CSS 三大特性

1. 层叠性 介绍 多组CSS样式共同作用于一个元素,就会出现 覆盖(层叠) 另一个冲突的样式。 层叠原则 样式冲突:遵循就近原则(哪个样式离结构近,就执行哪个样式) 样式不冲突,就不会重…

CyclicBarrier使用详解及遇到的坑

上一篇文章讲的是关于是使用CountDownLatch实现生成年底报告遇到的问题,这个计数器和CyclicBarrier也有类似功能,但是应用场景不同。 一、应用场景 CountDownLatch: 有ABCD四个任务,ABC是并行执行,等ABC三个任务都执行完…

Java-I/O框架14:Properties集合及使用

视频链接:16.32 Properties使用(2)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Tz4y1X7H7?spm_id_from333.788.player.switch&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5&p32 1.Properties集合 特性: 存储…

Windows下mysql数据库备份策略

Windows下mysql的增量备份和全量备份,并利用schtasks设置定时任务执行bat脚本。 一、备份要求 序号 备份类型 备份频次 备份时间 1 增量备份 每周一-每周六各一次 18:00:00 2 全量备份 每周日一次 18:00:00 二、备份方法 2.1增量备份 2.1.1准备工作…

架构师备考-概念背诵(软件工程)

软件工程 软件开发生命周期: 软件定义时期:包括可行性研究和详细需求分析过程,任务是确定软件开发工程必须完成的总目标,具体可分成问题定义、可行性研究、需求分析等。软件开发时期:就是软件的设计与实现,可分成概要设计、详细设计、编码、测试等。软件运行和维护:就是…

【Linux】Linux入门实操——vim、目录结构、远程登录、重启注销

一、Linux 概述 1. 应用领域 服务器领域 linux在服务器领域是最强的,因为它免费、开源、稳定。 嵌入式领域 它的内核最小可以达到几百KB, 可根据需求对软件剪裁,近些年在嵌入式领域得到了很大的应用。 主要应用:机顶盒、数字电视、网络…

【Java项目】基于SpringBoot的【生鲜交易系统】

技术简介: 系统软件架构选择B/S模式、java技术和MySQL数据库等,总体功能模块运用自顶向下的分层思想。 系统简介: 考虑到实际生活中在生鲜交易方面的需要以及对该系统认真的分析,将系统权限按管理员,用户这两类涉及用户划分。 (…

AI Weekly『11月4-10日』: Anthropic发布Claude 3.5 Haiku,腾讯开源混元-Large模型!

大家好,我是木易,一个持续关注AI领域的互联网技术产品经理,国内Top2本科,美国Top10 CS研究生,MBA。我坚信AI是普通人变强的“外挂”,专注于分享AI全维度知识,包括但不限于AI科普,AI工…

贪心算法day3(最长递增序列问题)

目录 1.最长递增三元子序列 2.最长连续递增序列 1.最长递增三元子序列 题目链接:. - 力扣(LeetCode) 思路:我们只需要设置两个数进行比较就好。设a为nums[0],b 为一个无穷大的数,只要有比a小的数字就赋值…

vue实现图片无限滚动播放

本人vue新手菜鸡,文章为自己在项目中遇到问题的记录,如有不足还请大佬指正 文章目录 实现效果代码展示总结 因为刚接触vue,本想着看看能不能用一些element的组件实现图片的轮播效果,尝试使用过element-UI里的走马灯Carouse&#x…

[ 内网渗透实战篇-2 ] 父域子域架构的搭建与安装域环境判断域控定位组策略域森林架构配置信任关系

🍬 博主介绍 👨‍🎓 博主介绍:大家好,我是 _PowerShell ,很高兴认识大家~ ✨主攻领域:【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 🎉点赞➕评论➕收藏 养成习…

Follow软件的使用入门教程

开篇 看到很多兄弟还不知道怎么用这个当下爆火的浏览器!在这里简单给需要入门的小伙伴一些建议: 介绍 简单解释一下,RSS 意思是简易信息聚合,用户可以通过 RSS 阅读器或聚合工具自主订阅并浏览各个平台的内容源,不用…

esp32学习:用虫洞ESP32开发板,快速实现无线图传

我们的虫洞ESP32-S3-EYE开发板,能够完美运行esp who AI代码,所以实现无线图传那是非常容易的,我们先看看esp who代码库中examples目录: 里面有比较多的web例程,在这些例程下,稍作修改,就可以快速…

最新三维视觉下的扩散模型综述——Diffusion Models in 3D Vision: A Survey

目录 摘要 一、引言 二、扩散模型简介 A.扩散模型的介绍 B.扩散模型的数学基础 C.扩散模型的变体 D.三维视觉中的生成过程 三、三维视觉基础 A.三维表示 B.三维视觉中的深度学习方法 C.3D视觉中的挑战 四、三维扩散生成任务 A.无条件生成 B.图像到三维 C.文本到…

JavaSE:运算符 (学习笔记)

目录 一,算术运算符 【1】 共同点: 【2】 不同点: 二,关系运算符 三,逻辑运算符 2,&和&&的区别和联系 { |和||的区别和联系 }---两题类似 四,赋值运算符 五,拓展…

strtok函数详解

strtok函数 strtok 函数是一个字符串分割函数,用于将字符串分割成一系列的标记。这个函数通过一组分隔符字符来确定标记的边界,每次调用都会返回字符串中的下一个标记,并且将原始字符串中的分隔符替换为空字符‘\0’,从而实际上是…

题目练习之二叉树那些事儿(续集)

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ ✨✨✨✨✨✨个人…

【入门篇】确定字符串是否包含唯一字符——多语言版本

题目跳转:确定字符串是否包含唯一字符 题目解析 这个问题要求我们判断一个字符串中的字符是否唯一(忽略字母的大小写),并输出相应的结果。如果字符串中所有的字符都是唯一的,输出 YES;否则,输…

ConcurrentModificationException:检测到并发修改完美解决方法

🚦 ConcurrentModificationException:检测到并发修改完美解决方法 💡 🚦 ConcurrentModificationException:检测到并发修改完美解决方法 💡摘要1. 什么是ConcurrentModificationException?&…