使用 MinIO、Langchain 和 Ray Data 构建分布式嵌入式子系统

嵌入子系统是实现检索增强生成所需的四个子系统之一。它将您的自定义语料库转换为可以搜索语义含义的向量数据库。其他子系统是用于创建自定义语料库的数据管道,用于查询向量数据库以向用户查询添加更多上下文的检索器,最后是托管大型语言模型 (LLM) 的服务子系统,并将根据用户的查询和在向量数据库中找到的上下文生成答案。下图显示了这四个子系统在检索增强生成过程中如何协同工作。

在这篇文章中,我想重点介绍嵌入子系统。在此子系统中,构成组织自定义语料库的文档从其原始格式转换为文本,分解为较小的块,然后为每个块创建一个嵌入(这是一个向量,通常具有数百个维度)。创建嵌入后,原始块和向量都将存储在向量数据库中。 嵌入子系统在概念上易于理解,并且实现嵌入简单文本文件的简单脚本非常简单。但是,如果您必须为您的组织实施嵌入子系统,那么您如何为您的组织做出正确的设计决策,以及您如何应对不断增长的需求带来的复杂性?下面列出了一些设计决策和实际复杂性:

  • 如何高效地运行多个实验来测试不同的配置选项?

  • 如何处理文档中的表格和图像?

  • 如何将嵌入子系统部署到生产环境中?

  • 如何处理需要嵌入的大量文档?

  • 什么是最好的载体数据库?

  • 文档、嵌入模型和LLMs的最佳存储选项是什么?

解决这些问题的第一步是使用能够在工程工作站以及生产环境中运行的现代工具。具体来说,我们将使用 MinIO 进行所有存储,使用 Langchain 作为低代码解决方案来进行文档解析(我还将提供一些比 Langchain 更好地处理图像和表的选项),并使用 Ray Data 将分块和嵌入函数分发到集群中。毫不奇怪,分布式技术是我们解决方案的基础。您不仅可以使用商用硬件设置进行并行处理来获得高吞吐量,而且该解决方案是云原生的,使其可以跨云移植,并且还能够在本地运行。让我们从为我们的实验设置一个自定义语料库开始。

在 MinIO 中设置自定义语料库

如上所述,自定义语料库由数据管道创建,该数据管道将可能位于组织中多个门户的文档聚合到 MinIO 中。创建文档管道是另一篇文章的主题 - 所以现在,我们将手动将一些文档暂存到 MinIO 桶中。我在这里也只会使用文本文档来保持简单。但是,这里有一些处理文档中多种文件格式和非文本的提示。首先,查看 Unstructured 的库,用于分区、清理和提取。其次,如果您专门处理 PDF,请查看 Open-Parse 库。我们在之前的博客文章《使用 Open-Parse 智能分块提高 RAG 性能》中介绍了 Open-Parse。下面的屏幕截图显示了我们的自定义语料库。我从古腾堡计划中下载了四本被认为是经典的流行书籍的文本版本。

  • 人性论——大卫·休谟

  • 孙子兵法 - 孙子

  • 杰基尔博士和海德先生的奇案 - 罗伯特·路易斯·史蒂文森

  • 《海底两万里》——儒勒·凡尔纳

现在我们有了一个自定义语料库,我们可以设置一个向量数据库来保存嵌入。

设置 MinIO 和矢量数据库

我将使用的向量数据库是 Pgvector。Pgvector 是 PostgreSQL 的开源扩展,允许用户在数据库中存储、搜索和分析矢量数据。这篇文章的代码下载有一个 docker-compose 文件,其中包含 MinIO、Pgvector 和 pgAdmin。在与 docker-compose fill 相同的目录中运行以下命令会将这三个服务作为容器显示出来。

docker-compose up -d

还有一个init.sql文件(如下所示)。docker-compose 文件将此文件映射到容器的启动目录。这会导致文件中的 SQL 运行,从而在 Postgres 中创建向量扩展和一个“嵌入”表,其中包含下面 SQL 文件中显示的字段。

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE IF NOT EXISTS embeddings (
 id SERIAL PRIMARY KEY,
 embedding vector,
 text text,
 created_at timestamptz DEFAULT now()
);

将嵌入模型保存到 MinIO

我们将使用的嵌入模型是 Hugging Face 的开源模型。详细信息如下所示。在运行实验时指定特定版本始终是一个好主意。模型名称:intfloat/multilingual-e5-small修订版:ffdcc22a9a5c973ef0470385cef91e1ecb461d9f

不要被模型的名字所迷惑。它一点也不小。它是 1.4GB。我们需要下载这个模型并上传到 MinIO。这是一项一次性设置任务,用于在分布式环境中暂存此模型以进行实验。遗憾的是,我们需要的 Hugging Face 函数(snapshot_download)没有 S3 接口,所以我们会使用 MinIO Python SDK 将模型上传到 MinIO。更复杂的是,Hugging Face 模型不是单个文件。它是下载到指定目录中的文件集合。我们必须将整个目录上传到 MinIO,并使用 MinIO 路径保留文件夹结构。这是使用如下所示的“upload_model_to_minio”函数完成的。

from huggingface_hub import snapshot_download

def upload_model_to_minio(bucket_name: str, full_model_name: str, revision: str) -> None:
   '''
   Download a model from Hugging Face and upload it to MinIO. This function will use
   the current systems temp directory to temporarily save the model.
   '
''

   # Create a local directory for the model.
   #home = str(Path.home())
   temp_dir = tempfile.gettempdir()
   base_path = f'{temp_dir}{os.sep}hf-models'
   os.makedirs(base_path, exist_ok=True)

   # Get the user name and the model name.
   tmp = full_model_name.split('/')
   user_name = tmp[0]
   model_name = tmp[1]

   # The snapshot_download will use this pattern for the path name.
   model_path_name=f'models--{user_name}--{model_name}'
   # The full path on the local drive.
   full_model_local_path = base_path + os.sep + model_path_name + os.sep + 'snapshots' + 
                           os.sep + revision
   # The path used by MinIO.
   full_model_object_path = model_path_name + '/snapshots/' + revision

   print(f'Starting download from HF to {full_model_local_path}.')
   snapshot_download(repo_id=full_model_name, revision=revision, cache_dir=base_path)

   print('Uploading to MinIO.')
   upload_local_directory_to_minio(full_model_local_path, bucket_name, 
                                   full_model_object_path)
   shutil.rmtree(full_model_local_path)

运行以下命令将使用此函数将我们的模型上传到名为“hf-models”的存储桶中。

MODELS_BUCKET = 'hf-models'
EMBEDDING_MODEL = 'intfloat/multilingual-e5-small'
EMBEDDING_MODEL_REVISION = 'ffdcc22a9a5c973ef0470385cef91e1ecb461d9f'

upload_model_to_minio(MODELS_BUCKET, EMBEDDING_MODEL, EMBEDDING_MODEL_REVISION)

嵌入函数库

当你使用像 Ray Data 这样的库来分发数据处理时——在本例中是文本的分块和每个块的嵌入生成——你真正要做的就是编排简单的函数调用,这些函数调用在此过程中执行一项任务。下面列出了从 MinIO 存储桶中的文档列表创建嵌入所需的所有函数,以及它们的参数和返回值。如您所见,我们拥有嵌入文档集合所需的一切。

create_logger() -> logging.Logger

创建一个 Python 记录器,用于将调试、信息、错误、警告和关键消息发送到日志记录存储库。

download_model_from_minio(bucket_name: str, full_model_name: str, revision: str) -> str

将模型从 MinIO 下载到当前系统临时目录。一旦它被加载到内存中,它将删除它。

get_document_from_minio(bucket_name: str, object_name: str) -> str:

从 MinIO 下载单个文档,并将其保存到当前系统临时目录中。

get_object_list(bucket_name: str) -> List[str]:

返回指定存储桶中的对象列表。此列表将发送到 Ray Data,后者将其均匀分布在集群中的所有 Ray actor 中。

save_embeddings_to_vectordb(chunks, embeddings) -> None:

将嵌入和文本块一起保存到向量数据库中。

upload_local_directory_to_minio(local_path:str, bucket_name:str , minio_path:str) -> None

将指定本地目录的内容上传到 MinIO,将文件夹结构保留为指定存储桶内的路径。

upload_model_to_minio(bucket_name: str, full_model_name: str, revision: str) -> None:

从Hugging Face下载模型到当前系统临时目录,然后将模型上传到指定的Bucket,同时保留文件夹结构作为指定Bucket内的路径。

一个简单的嵌入子系统

让我们使用上述函数并创建一个简单的非分布式脚本。下面的代码将为 Robert Louis Stevenson 的“The Strange Case of Dr Jekyll and Mr Hyde”创建嵌入。首先,我们需要下载我们希望使用的嵌入模型,并将其保存到 MinIO 中。这是一项一次性任务;您无需在每次想要嵌入新一批模型或运行实验时都这样做。

MODELS_BUCKET = 'hf-models'
EMBEDDING_MODEL = 'intfloat/multilingual-e5-small' # Embedding model to use for converting text chunks to vector embeddings.
EMBEDDING_MODEL_REVISION = 'ffdcc22a9a5c973ef0470385cef91e1ecb461d9f'

eu.upload_model_to_minio(MODELS_BUCKET, EMBEDDING_MODEL, EMBEDDING_MODEL_REVISION)

接下来,我们需要从 MinIO 下载我们的模型,实例化它,创建一个分块器(或拆分器),创建嵌入并将它们保存到我们的 pgvector 数据库中。

CHUNK_SIZE = 1000         # Text chunk sizes which will be converted to vector embeddings
CHUNK_OVERLAP = 10
DIMENSION = 384           # Embeddings size

model_path = eu.download_model_from_minio(MODELS_BUCKET, EMBEDDING_MODEL, 

                                          EMBEDDING_MODEL_REVISION)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
embedding_model = SentenceTransformer(model_path, device=device)
chunker = RecursiveCharacterTextSplitter(chunk_size=CHUNK_SIZE, 

                                         chunk_overlap=CHUNK_OVERLAP, length_function=len)

temp_file = eu.get_document_from_minio(BUCKET_NAME, 

                                       'The Strange Case of Dr Jekyll and Mr Hyde.txt')
file = open(temp_file, 'r')
data = file.read()
chunks = chunker.split_text(data)
print('Number of chunks:', len(chunks))
print('Length of the first chunk:', len(chunks[0]))

embeddings = embedding_model.encode(chunks, batch_size=BATCH_SIZE).tolist()
print('Number of embeddings:', len(embeddings))
print('Length of the first embedding:', len(embeddings[0]))

eu.save_embeddings_to_vectordb(chunks, embeddings)

请注意,如果我们可以访问 GPU,则我们正在使用 GPU。此外,一切都是配置驱动的,因此运行不同的实验就是更改配置以反映您希望运行的实验的问题。这包括根据需要更改嵌入模型。

下面是 pgAdmin 的屏幕截图,显示了我们新创建的嵌入。

现在我们有了一个简单的脚本,可以为单个文档创建嵌入,下一步是将此代码迁移到在集群中运行的框架。这将允许并行嵌入整个文档语料库。我们将使用 Ray Data 来执行此操作。

分发嵌入子系统

分发嵌入子系统的第一步是将所有工作放入一个行为类似于函数的类中。这是使用 Python 的“__call__”内置方法完成的。(这是光线数据的要求。我们的班级如下所示。

class Embed:

   def __init__(self):
       device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
      
       model_path = eu.download_model_from_minio(MODELS_BUCKET, EMBEDDING_MODEL, 

                                                 EMBEDDING_MODEL_REVISION)
       self.embedding_model = SentenceTransformer(model_path, device=device)
       self.splitter = RecursiveCharacterTextSplitter(chunk_size=CHUNK_SIZE, 

                                                      chunk_overlap=CHUNK_OVERLAP, 

                                                      length_function=len)

   def __call__(self, batch_list: List[str]) -> None:
       document_list = batch_list["item"]

       timings = []
       documents = []
       for document_data in document_list:
           start_time = time()
           bucket_name = document_data[0]
           object_name = document_data[1]
           temp_file = eu.get_document_from_minio(bucket_name, object_name)
           file = open(temp_file, 'r')
           data = file.read()

           chunks = self.splitter.split_text(data)
           embeddings = self.embedding_model.encode(chunks, batch_size=BATCH_SIZE).tolist()
           eu.save_embeddings_to_vectordb(chunks, embeddings)
          
           total_time_sec = time() - start_time
           documents.append(object_name)
           timings.append(total_time_sec)

       return {'timings': timings, 'documents': documents}

Ray Data 将为我们将很快创建的 Ray 集群中的每个 actor 实例化一次此类。此对象将保持活动状态,并接收多个批次进行处理。请注意,“__init__”函数正在下载我们的嵌入模型,并使用它创建一个 SentenceTransformer。SentenceTransformer 类使使用嵌入模型变得容易。此外,我们还使用 LangChain 的 RecursiveCharacterTextSplitter 来分割或分块我们的文档。它根据字符列表(我们使用其默认列表)递归拆分文本,从列表中的第一个字符开始,如果第一个拆分太大,则继续到下一个字符。目标是将相关的文本片段保持在一起,保留它们的语义关系。所有这些设置工作仅在创建 Embed 对象时发生一次。我们本可以使用一个简单的函数来分配工作,但是必须为每个批次完成此设置工作,当您要进行设置工作时,这不是正确的设计。

接下来,我们需要初始化 Ray 集群。


ray.init(
   #address="ray://ray-cluster-kuberay-head-svc:10001",
   runtime_env={
       "env_vars": {
           "MINIO_URL": MINIO_URL,
           "MINIO_ACCESS_KEY": MINIO_ACCESS_KEY,
           "MINIO_SECRET_KEY": MINIO_SECRET_KEY,
           "MINIO_SECURE": str(MINIO_SECURE),
           "PGVECTOR_HOST": os.environ['PGVECTOR_HOST'],
           "PGVECTOR_DATABASE": os.environ['PGVECTOR_DATABASE'],
           "PGVECTOR_USER": os.environ['PGVECTOR_USER'],
           "PGVECTOR_PASSWORD": os.environ['PGVECTOR_PASSWORD'],
           "PGVECTOR_PORT": os.environ['PGVECTOR_PORT'],
       },
       "pip": [              
           "datasets==2.19.0",
           "huggingface_hub==0.22.2",
           "minio==7.2.7",
           "psycopg2-binary==2.9.9",
           "pyarrow==16.0.0",
           "sentence-transformers==3.0.1",
           "torch==2.3.0",
           "transformers==4.40.1",
       ]
   }
)

在我们的演示中,我们将创建一个本地 Ray 实例。我没有使用 Kubernetes 集群。这是在移动到真实集群之前让代码正常工作的最佳方法。我们还没有创建任何 Ray actor - 但我们正在发送 Ray 配置信息,告诉 Ray 每个 actor 需要的环境变量和库。接下来,我们创建一个 Ray 数据集来保存我们想要发送到将在每个 Ray actor 中运行的 Embed 类实例的所有数据。在我们的例子中,每个 Ray actor 都将收到一个存储在 MinIO 中的对象引用列表(文档的路径)。我们将使用上述函数库中的“get_object_list”函数。从 “ray.data.from_items() 返回的 Ray 数据集包含的逻辑,当我们启动分布式嵌入过程时,该逻辑会将此列表转换为每个 actor 的较小批次。


# The embedding class expects bucket_name and document_name pairs - so add bucket name to each entry in the list.
document_list = eu.get_object_list(BUCKET_NAME)
list_for_ray = [[BUCKET_NAME, doc] for doc in document_list]

ray_ds = ray.data.from_items(list_for_ray)
print(type(ray_ds))
print(ray_ds.schema)

我们几乎已经准备好进行一些分布式计算,但我们还有一个编码任务要完成。我们需要将 Ray 数据集映射到我们的 Embed 类,并告诉 Ray 如何设置我们之前为此工作负载初始化的集群。这是使用 Ray 数据集的“map_batches”方法完成的。您可以将函数或可调用类发送到“map_batches”。如果发送函数,Ray Data 将使用无状态 Ray 任务。对于类,Ray Data 使用有状态的 Ray actor。

ds_embed = ray_ds.map_batches(
   Embed,
   concurrency=ACTOR_POOL_SIZE,
   batch_size=BATCH_SIZE,  # Large batch size to maximize GPU utilization.
   #num_gpus=1,            # 1 GPU for each actor.
   num_cpus=1,             # 1 CPU for each actor.
)

请注意,我们正在传入需要为每个 actor 实例化的 Embed 类。我们还指定了 actor 的数量、每次调用 actor 的批量大小,最后指定了每个 actor 可访问的 GPU 和 CPU 数量。map_batches 方法返回另一个 Ray 数据集 (ds_embed),其中包含所有 actor 的所有返回值。这是 Embedded 中“__call__”方法的返回值的集合。

最后,我们准备开始我们的分布式嵌入工作。您可能已经注意到,上一个命令运行得非常快。那是因为还没有进行任何计算。Ray 中的转换(map_batch被认为是转换)是“惰性”。在通过循环访问数据集、保存数据集或检查数据集的属性来触发数据使用之前,不会执行它们。因此,我们需要向ds_embed请求 actor 的返回值。这是在下面完成的。下面的代码片段需要一些时间才能运行。

def ray_data_task(ds_embed):
   results = []
   for row in ds_embed.iter_rows():
       documents = row['documents']
       timings = row['timings']
       results.append((documents, timings))
   return results

results = ray_data_task(ds_embed)

results


就是这样。大功告成。完成上述代码后,您将看到类似于下面显示的输出。

[('A Treatise of Human Nature.txt', 75.08733916282654),
 ('The Art of War.txt', 21.960258722305298),
 ('The Strange Case of Dr Jekyll and Mr Hyde.txt', 10.052802085876465),
 ('Twenty Thousand Leagues under the Sea.txt', 39.24100613594055)]

总结

在这篇文章中,我们构建了一个分布式嵌入子系统,可以在工程工作站和完全分布式的云原生生产环境中运行。所介绍的代码具有以下优点,可直接解决我们简介中确定的复杂性和实际问题。

  • 实验可以高效运行,从而可以对不同的配置选项进行彻底测试。

  • 除了配置选项之外,还应尝试使用解析选项。这将允许您处理多种文件类型并处理文档中的非文本数据。

  • 使用此处显示的代码时,您的生产环境将运行工程师用于测试和试验的相同代码。

  • 分布式嵌入子系统可以在集群中运行。集群可以快速扩展,以处理需要批量处理的大量文档,也可以针对实时工作负载进行扩展。

  • 本文中介绍的代码封装了向量数据库调用,使工程师可以交换不同的产品。

  • MinIO 是生成式 AI 的最佳存储解决方案。正如我们在这篇文章中看到的,嵌入模型和文档必须存储在高速、可扩展的存储解决方案中。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ES6中的Promise、async、await,超详细讲解!

Promise是es6引入的异步编程新解决方案,Promise实例和原型上有reject、resolve、all、then、catch、finally等多个方法,语法上promise就是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果,本篇文章主要介绍了ES6中的P…

(免费领源码)java#SSM#MYSQL私家车位共享APP 51842-计算机毕业设计项目选题推荐

目 录 摘要 1 绪论 1.1 课题的研究背景 1.2研究内容与研究目标 1.3ssm框架 1.4论文结构与章节安排 2 2 私家车位共享APP系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据增加流程 2.2.2 数据修改流程 2.2.3数据删除流程 2.3 系统功能分析 2.3.1功能性分析 2…

原型图绘制技巧

针对于 Axure RP绘图软件。 1、拉辅助线 目的,确定画布大小尺寸从上面和左面的刻度尺上,点击鼠标,拖动,就可以拉出一条线。 2、画布底模设为组件 右键转换为母版,方便后续其他页面使用 3、按钮 按钮字体不要太大&am…

【嵌入式】STM3212864点阵屏使用SimpleGUI单色屏接口库——(2)精简字库

一 开源库简介与移植 最近一个项目需要用12864屏幕呈现一组较为复杂的菜单界面,本着不重复造轮子的原则找到了SimpleGUI开源库。 开源地址:SimpleGUI: 一个面向单色显示屏的开源GUI接口库。 SimpleGUI是一款针对单色显示屏设计的接口库。相比于传统的GUI…

SpringBoot集成阿里百炼大模型(初始demo) 原子的学习日记Day01

文章目录 概要下一章SpringBoot集成阿里百炼大模型(多轮对话) 原子的学习日记Day02 整体架构流程技术名词解释集成步骤1,选择大模型以及获取自己的api-key(前面还有一步开通服务就没有展示啦!)2&#xff0c…

CSS学习 02 利用鼠标悬停制造按钮边框的渐变方向变化

效果 页面背景为深灰色,使用Karla字体。容器内的按钮居中显示,按钮有一个彩色渐变的边框。按钮的背景为黑色,文字为浅灰色。当鼠标悬停在按钮边框上时,边框的渐变方向变化,按钮文字变为白色,并且按钮内边距…

简单猜谜小程序开发

了解小程序的结构 项目根目录 包含小程序的配置文件和其他资源。 页面目录 每个页面都有独立的目录,通常包含 .json、.wxml、.wxss、.js 文件: .json:页面配置文件,用于配置页面的窗口表现等。 .wxml:页面的布局文件…

JS等待所有方法执行完成在执行下一个方法,promise All

在工作中会遇到这样一个场景,前端需要拿到不同接口返回的结果在执行某个逻辑,当使用链式那样的方式去请求,等一个接口响应完在请求下一个接口,这种方法就会导致请求时间特别长。这个时候就可以使用promise all,同时请求…

Python酷库之旅-第三方库Pandas(067)

目录 一、用法精讲 266、pandas.Series.dt.second属性 266-1、语法 266-2、参数 266-3、功能 266-4、返回值 266-5、说明 266-6、用法 266-6-1、数据准备 266-6-2、代码示例 266-6-3、结果输出 267、pandas.Series.dt.microsecond属性 267-1、语法 267-2、参数 …

IDEA如何去掉编辑框右侧的竖线

打开 IntelliJ Idea 软件 依次找到 File—>Settings—>Editor—>General—>Appearance 去掉勾选 Show hard wrap and visual guides (configured in Code Style options)

PHP海报在线制作系统小程序源码

创意无限,设计零门槛! 🎨 一键解锁设计大师潜能 你还在为找不到合适的设计师制作海报而烦恼吗?告别繁琐沟通,拥抱“海报在线制作系统”!这个神奇的平台,让你无需任何设计基础,也能…

后台弱口令部署war包 漏洞复现

1.搭建好环境打开页面---点击右方的manager app 默认账号密码为tomcat 2.登录完成后滑到下面点击浏览 3.将你生成的jsp木马压缩为zip格式并将后缀名改为war 4.提交此war压缩包然后在目录中点击 5.点击完后在后面加lkj.jsp,访问成功证明注入成功 5.打开哥斯拉连接…

js获取近30天近60天时间区间

1.从今天往前推30天 handleSetTime(value) {//value传入自定义时间 30/60/90//因为这里要加上今天时间,所以开始时间-1const value1 value - 1const date new Date()const startTimestamp date.getTime() - value1 * (24 * 60 * 60 * 1000 * 1)const sDate new Date(startTi…

Unity实现屏幕黑边--游戏画面和UI异形屏处理(安全区渲染)

在手机上,如果想以黑边形式显示游戏画面,比如显示区域避开异形屏、水滴屏那一行。有很多种思路,最后觉得下面这种方法比较简单合适,下面分享一下。 方法:需要分别处理,游戏画面黑边、UI黑边: …

帮助网站提升用户参与度的5个WordPress插件

仅靠编写精彩的内容、设计精美的图像和创建简化的客户旅程不足以提高网站参与度。您需要让用户在首次访问后继续与您的网站互动并成为回访者,才能真正吸引您所追求的兴趣。 幸运的是,对于 WordPress 用户来说,有数百种工具可用于提高用户参与…

html实现好看的塔罗牌、十二星座运势网站源码

文章目录 1.设计来源1.1 十二星座1.2 所有界面效果图 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板,程序开发,在线开发,在线沟通 作者:xcLeigh 文章地址:https://blog.csdn.net/weixin_43151418/article/deta…

java网络爬虫

前言 单一个项目的功能需要测试的时候,就需要使用大量的假数据做测试,但是这些数据又不能太假,必须合乎常理。于是我们需要爬虫爬取一些网站的一些允许爬取的数据,来做项目功能测试。 一.确认爬取目标 现在爬取下面三个网站的数据…

常用的GPIO的配置(基于STM32F103C8xx)

绿色:注释 紫色:实际使用代码 //开启GPIOB的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); ①可选输入值:RCC_APB2Periph_GPIOA 或者 RCC_APB2Periph_GPIOB 或者 RCC_APB2Periph_ALL ②可选输入值&a…

【云原生】恰当运用kubernetes中三种探针,确保应用程序在Kubernetes集群中保持健康、可用和可靠

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…

电脑录屏软件免费版,分享4款,轻松录制精彩瞬间

电脑录屏已成为创作、教学和分享的热门方式。然而,对于一些用户来说,购买专业的录屏软件可能会增加负担。幸运的是,市场上存在许多电脑录屏软件免费版的选择。它们提供了丰富的功能,让您可以尽情展现您的创意,无须为此…