[Abp vNext 源码分析] - 23. 二进制大对象系统(BLOB)
一、简介
ABP vNext 在 v 2.9.x 版本当中添加了 BLOB 系统,主要用于存储大型二进制文件。ABP 抽象了一套通用的 BLOB 体系,开发人员在存储或读取二进制文件时,可以忽略具体实现,直接使用 IBlobContainer
或 IBlobContainer<T>
进行操作。官方的 BLOB Provider 实现有 Azure、AWS、FileSystem(文件系统存储)、Database(数据库存储)、阿里云 OSS,你也可以自己继承 BlobProviderBase
来实现其他的 Provider。
BLOB 常用于各类二进制文件存储和管理,基本就是对云服务的 OSS 进行了抽象,在使用当中也会有 Bucket 和 Object Key 的概念,在 BLOB 里面对应的就是 ContainerName 和 BlobName。
关于 BLOB 的官方使用指南,可以参考 https://docs.abp.io/en/abp/latest/Blob-Storing,本文的阅读前提是建立在你已经阅读过该指南,并有一定的使用经验。
二、源码分析
2.1 模块分析
看一个 ABP 的库项目,首先从他的 Module 入手,对应的 BLOB 核心库的 Module
就是 AbpBlobStoringModule
类,在其内部,只进行了两个操作,注入了 IBlobContainer
与 IBlobContainer<>
的实现。
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient(
typeof(IBlobContainer<>),
typeof(BlobContainer<>)
);
context.Services.AddTransient(
typeof(IBlobContainer),
serviceProvider => serviceProvider
.GetRequiredService<IBlobContainer<DefaultContainer>>()
);
}
从上述代码可以看出来,IBlobContainer
的默认实现还是基于 BlobContainer<T>
的。那么为啥会有个泛型的 Container,从简介中可以看到 OSS 里面对应的 Bucket 其实就是一个 IBlobContainer
。假如你会针对某云的多个 Bucket 进行操作,那么就需要类型化的 BlobContainer 了。
在这里可以看到,IBlobContainer
的实现是一个工厂方法,这一点在后面会进行解释。
2.2 BLOB 容器
2.2.1 容器的定义
每个容器就是一个 OSS 的 Bucket,开发人员在对 BLOB 进行操作时,会注入 IBlobContainer
/IBlobContainer<T>
,通过接口提供的 5 种方法进行操作,这五个方法分别是 保存对象、删除对象、判断对象是否存在、获取对象、获取对象(不存在返回 NULL)。
public interface IBlobContainer
{
// 保存对象
Task SaveAsync(
string name,
Stream stream,
bool overrideExisting = false,
CancellationToken cancellationToken = default
);
// 删除对象
Task<bool> DeleteAsync(
string name,
CancellationToken cancellationToken = default
);
// 判断对象是否存在
Task<bool> ExistsAsync(
string name,
CancellationToken cancellationToken = default
);
// 获取对象
Task<Stream> GetAsync(
string name,
CancellationToken cancellationToken = default
);
// 获取对象(不存在返回 NULL)
Task<Stream> GetOrNullAsync(
string name,
CancellationToken cancellationToken = default
);
//TODO: Create shortcut extension methods: GetAsArraryAsync, GetAsStringAsync(encoding) (and null versions)
}
泛型的 BLOB 容器也是集成自该接口,内部没有任何特殊的方法。
public interface IBlobContainer<TContainer> : IBlobContainer
where TContainer: class
{
}
2.2.2 容器的实现
容器的两种实现都存放在 BlobContainer.cs
文件当中,标注容器实现内部都会有一个 ContainerName
,用于标识不同的容器,并且和其他的组件作为 关联键 进行绑定。每个容器都会关联 BlobContainerConfiguration
、IBlobProvider
两个组件,它们分别提供了容器的配置信息和容器的具体实现 Provider,在容器构造的时候根据 ContainerName
分别进行初始化。
public class BlobContainer : IBlobContainer
{
protected string ContainerName { get; }
protected BlobContainerConfiguration Configuration { get; }
protected IBlobProvider Provider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IServiceProvider ServiceProvider { get; }
// ... 其他代码。
}
可以看到这里还注入了 ICurrentTenant
,注入该对象的主要作用是用来处理多租户的情况,如果当前容器启用了多租户,那么会手动 Change()
。下面以 SaveAsync()
方法为例。
public virtual async Task SaveAsync(
string name,
Stream stream,
bool overrideExisting = false,
CancellationToken cancellationToken = default)
{
// 变更当前租户信息,当启用了多租户时,会使用当前租户进行变更。
using (CurrentTenant.Change(GetTenantIdOrNull()))
{
// 根据 ContainerName 取得对应的标准化容器名称和对象名称。
var (normalizedContainerName, normalizedBlobName) = NormalizeNaming(ContainerName, name);
// 使用 ContainerName 匹配的 Provider 存储对象数据。
await Provider.SaveAsync(
new BlobProviderSaveArgs(
normalizedContainerName,
Configuration,
normalizedBlobName,
stream,
overrideExisting,
CancellationTokenProvider.FallbackToProvider(cancellationToken)
)
);
}
}
这里有两个地方需要单独分析,第一个是 NormalizeNaming()
的作用,第二个是 BlobProviderSaveArgs
对象。
2.2.3.1 名称标准化对象
IBlobNamingNormalizer
(BLOB 名称标准化对象),主要用于将一个字符串进行标准化处理,防止 Provider 无法处理这种名称。各大 OSS 都对容器的名称或对象的名称有命名要求,比如必须全部小写,不能有哪些特殊符号等等。
protected virtual (string, string) NormalizeNaming(string containerName, string blobName)
{
// 从当前的配置信息中获取对应的标准化器,如果不存在任何标准化工具对象,则直接返回原始名称。
if (!Configuration.NamingNormalizers.Any())
{
return (containerName, blobName);
}
using (var scope = ServiceProvider.CreateScope())
{
// 获取所有的标准化器,并依次进行名称的标准化处理。
foreach (var normalizerType in Configuration.NamingNormalizers)
{
var normalizer = scope.ServiceProvider
.GetRequiredService(normalizerType)
.As<IBlobNamingNormalizer>();
containerName = normalizer.NormalizeContainerName(containerName);
blobName = normalizer.NormalizeBlobName(blobName);
}
return (containerName, blobName);
}
}
2.2.3.2 BLOB 上下文
在 BLOB 里面,ABP 分别为每个操作都定义了一个 ***Args
对象,它就是一个上下文对象,用于在整个调用周期中传递参数。
2.2.3.3 BLOB 配置信息
每个 BLOB 容器都会有一个 BlobContainerConfiguration
用于存储配置信息,它主要有以下几个重要的属性。
public class BlobContainerConfiguration
{
// 当前 BLOB 容器对应的 Provider 类型。
public Type ProviderType { get; set; }
// 当前 BLOB 容器是否启用了多租户。
public bool IsMultiTenant { get; set; } = true;
// 当前 BLOB 容器的名称标准化对象。
public ITypeList<IBlobNamingNormalizer> NamingNormalizers { get; }
// 当前 BLOB 容器的属性。
[NotNull] private readonly Dictionary<string, object> _properties;
// 当尝试获取某些配置属性,但是不存在时,会从这个 Configuration 拿取数据。
[CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration;
public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null)
{
NamingNormalizers = new TypeList<IBlobNamingNormalizer>();
_fallbackConfiguration = fallbackConfiguration;
_properties = new Dictionary<string, object>();
}
[CanBeNull]
public T GetConfigurationOrDefault<T>(string name, T defaultValue = default)
{
return (T) GetConfigurationOrNull(name, defaultValue);
}
[CanBeNull]
public object GetConfigurationOrNull(string name, object defaultValue = null)
{
return _properties.GetOrDefault(name) ??
_fallbackConfiguration?.GetConfigurationOrNull(name, defaultValue) ??
defaultValue;
}
// ... 其他代码。
}
在后续各种 Provider 里面定义的配置项,本质上就是对 _properties
字典进行操作。
2.2.3 容器的构造与初始化
BLOB 容器并不是通过 IoC 容器直接解析构造的,而是通过 IBlobContainerFactory
工厂进行创建,与容器相关的配置对象和 BLOB Provider 也是在这个时候进行构造赋值。
public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency
{
protected IBlobProviderSelector ProviderSelector { get; }
protected IBlobContainerConfigurationProvider ConfigurationProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IServiceProvider ServiceProvider { get; }
public BlobContainerFactory(
IBlobContainerConfigurationProvider configurationProvider,
ICurrentTenant currentTenant,
ICancellationTokenProvider cancellationTokenProvider,
IBlobProviderSelector providerSelector,
IServiceProvider serviceProvider)
{
ConfigurationProvider = configurationProvider;
CurrentTenant = currentTenant;
CancellationTokenProvider = cancellationTokenProvider;
ProviderSelector = providerSelector;
ServiceProvider = serviceProvider;
}
public virtual IBlobContainer Create(string name)
{
// 根据容器的名称,获取对应的配置。
var configuration = ConfigurationProvider.Get(name);
// 构造一个新的容器对象。
return new BlobContainer(
name,
configuration,
// 一样的是根据容器名称,获得匹配的 Provider 类型。
ProviderSelector.Get(name),
CurrentTenant,
CancellationTokenProvider,
ServiceProvider
);
}
}
那么这个工厂方法是在什么时候调用的呢?跳转到工厂方法的实现,发现会被一个静态扩展方法所调用,重要的是这个方法是一个泛型方法,这样就与开头的类型化 BLOB 容器相对应了。
public static class BlobContainerFactoryExtensions
{
public static IBlobContainer Create<TContainer>(
this IBlobContainerFactory blobContainerFactory
)
{
// 通过 GetContainerName 方法获取容器的名字。
return blobContainerFactory.Create(
BlobContainerNameAttribute.GetContainerName<TContainer>()
);
}
}
GetContainerName()
方法也很简单,如果容器类型没有指定 BlobContainerNameAttribute
特性,那么就会默认使用类型的 FullName
作为名称。
public static string GetContainerName(Type type)
{
var nameAttribute = type.GetCustomAttribute<BlobContainerNameAttribute>();
if (nameAttribute == null)
{
return type.FullName;
}
return nameAttribute.GetName(type);
}
最后的最后,看一下这个类型化的 BLOB 容器。
public class BlobContainer<TContainer> : IBlobContainer<TContainer>
where TContainer : class
{
private readonly IBlobContainer _container;
public BlobContainer(IBlobContainerFactory blobContainerFactory)
{
_container = blobContainerFactory.Create<TContainer>();
}
// ... 其他代码。
}
对应的是模块初始化的工厂方法:
context.Services.AddTransient(
typeof(IBlobContainer),
serviceProvider => serviceProvider
.GetRequiredService<IBlobContainer<DefaultContainer>>()
这里的 DefaultContainer
就指定了该特性,所以本质上一个 IBlobContainer
就是一个类型化的容器,它的泛型参数是 DefaultContainer
。
[BlobContainerName(Name)]
public class DefaultContainer
{
public const string Name = "default";
}
2.2.3.1 BLOB 的配置提供者
BLOB 容器工厂使用 IBlobContainerConfigurationProvider
来匹配对应容器的配置信息,实现比较简单,直接注入了 AbpBlobStoringOptions
并尝试从它的 BlobContainerConfigurations
中获取配置对象。
public class DefaultBlobContainerConfigurationProvider : IBlobContainerConfigurationProvider, ITransientDependency
{
protected AbpBlobStoringOptions Options { get; }
public DefaultBlobContainerConfigurationProvider(IOptions<AbpBlobStoringOptions> options)
{
Options = options.Value;
}
public virtual BlobContainerConfiguration Get(string name)
{
return Options.Containers.GetConfiguration(name);
}
}
这里的 BlobContainerConfigurations
对象,核心就是一个键值对,键就是 BLOB 容器的名称,值就是容器对应的配置对象。
public class BlobContainerConfigurations
{
private BlobContainerConfiguration Default => GetConfiguration<DefaultContainer>();
private readonly Dictionary<string, BlobContainerConfiguration> _containers;
public BlobContainerConfigurations()
{
_containers = new Dictionary<string, BlobContainerConfiguration>
{
// 添加默认的 BLOB 容器。
[BlobContainerNameAttribute.GetContainerName<DefaultContainer>()] = new BlobContainerConfiguration()
};
}
// ... 其他代码
public BlobContainerConfigurations Configure(
[NotNull] string name,
[NotNull] Action<BlobContainerConfiguration> configureAction)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
Check.NotNull(configureAction, nameof(configureAction));
configureAction(
_containers.GetOrAdd(
name,
() => new BlobContainerConfiguration(Default)
)
);
return this;
}
public BlobContainerConfigurations ConfigureAll(Action<string, BlobContainerConfiguration> configureAction)
{
foreach (var container in _containers)
{
configureAction(container.Key, container.Value);
}
return this;
}
// ... 其他代码
}
在使用过程中,我们在模块里面调用的 Configure()
方法,就会在字典添加一个新的 Item,并为其赋值。而 ConfigureAll()
就是遍历这个字典,为每个 BLOB 容器调用委托,以便进行配置。
2.2.3.2 BLOB 的 Provider 选择器
在构造 BLOB 容器的时候,BLOB 容器工厂通过 IBlobProviderSelector
来选择对应的 BLOB Provider,具体选择哪一个是根据 BlobContainerConfiguration
里面的 ProviderType
决定的。
public virtual IBlobProvider Get([NotNull] string containerName)
{
Check.NotNull(containerName, nameof(containerName));
// 获得当前 BLOB 容器对应的配置信息。
var configuration = ConfigurationProvider.Get(containerName);
if (!BlobProviders.Any())
{
throw new AbpException("No BLOB Storage provider was registered! At least one provider must be registered to be able to use the Blog Storing System.");
}
foreach (var provider in BlobProviders)
{
// 通过配置信息匹配对应的 Provider。
if (ProxyHelper.GetUnProxiedType(provider).IsAssignableTo(configuration.ProviderType))
{
return provider;
}
}
throw new AbpException(
$"Could not find the BLOB Storage provider with the type ({configuration.ProviderType.AssemblyQualifiedName}) configured for the container {containerName} and no default provider was set."
);
}
上面的 BlobProviders
其实就是直接从 IoC 解析的 IEnumerable<IBlobProvider>
对象,我还找了半天是哪个地方进行赋值的。当 ABP 框架自动之后,会自动将已经实现的 BLOB Provider 注入到 IoC 容器中,如果某个容器在使用时指定了对应的配置参数,则会匹配对应的 BLOB Provider。
2.3 Provider 的实现
2.3.1 File System
文件系统作为 BLOB 的最简化实现,本质就是通过文件夹进行租户隔离动作,所有操作都会将数据持久化到硬盘上。核心代码就一个文件 FileSystemBlobProvider
,在这个文件内部定义了具体的执行逻辑,我们这里大概看一下 SaveAsyn()
的实现。
public override async Task SaveAsync(BlobProviderSaveArgs args)
{
var filePath = FilePathCalculator.Calculate(args);
if (!args.OverrideExisting && await ExistsAsync(filePath))
{
throw new BlobAlreadyExistsException($"Saving BLOB '{args.BlobName}' does already exists in the container '{args.ContainerName}'! Set {nameof(args.OverrideExisting)} if it should be overwritten.");
}
DirectoryHelper.CreateIfNotExists(Path.GetDirectoryName(filePath));
var fileMode = args.OverrideExisting
? FileMode.Create
: FileMode.CreateNew;
await Policy.Handle<IOException>()
.WaitAndRetryAsync(2, retryCount => TimeSpan.FromSeconds(retryCount))
.ExecuteAsync(async () =>
{
using (var fileStream = File.Open(filePath, fileMode, FileAccess.Write))
{
await args.BlobStream.CopyToAsync(
fileStream,
args.CancellationToken
);
await fileStream.FlushAsync();
}
});
}
很简单,通过 FilePathCalculator
计算出来文件的具体路径,然后结合配置参数来判断文件是否存在,以及是否进入后续操作。通过 Polly 提供的重试机制来创建文件。
2.3.2 DataBase
数据库 Provider 是利用数据库的 BLOB 类型,将这些大型对象存储到数据库当中,不太建议这样操作。这里不再进行详细介绍,基本大同小异。
2.3.3 各类 OSS (腾讯云为例)
OSS 作为云厂商的标配,基本概念和操作都与 ABP 的 BLOB 相匹配,集成起来也还是比较简单,就是将各个 OSS 的 SDK 塞进来就行。这里注意点的是,每个 BLOB Provider 都会编写一个基于 BlobContainerConfiguration
类型的静态方法,取名都叫做 UseXXX()
,并在里面对具体的配置进行赋值。
public static class TencentCloudBlobContainerConfigurationExtensions
{
public static TencentCloudBlobProviderConfiguration GetTencentCloudConfiguration(
this BlobContainerConfiguration containerConfiguration)
{
return new TencentCloudBlobProviderConfiguration(containerConfiguration);
}
public static BlobContainerConfiguration UseTencentCloud(
this BlobContainerConfiguration containerConfiguration,
Action<TencentCloudBlobProviderConfiguration> tencentCloudConfigureAction)
{
containerConfiguration.ProviderType = typeof(TencentCloudBlobProvider);
containerConfiguration.NamingNormalizers.TryAdd<TencentCloudBlobNamingNormalizer>();
tencentCloudConfigureAction(new TencentCloudBlobProviderConfiguration(containerConfiguration));
return containerConfiguration;
}
}
可能会对这个 TencentCloudBlobProviderConfiguration
有一些好奇,其实就是个套娃,因为直接传入了 BlobContainerConfiguration
对象,里面的各种属性本质上就是对配置项的那个 Dictionary<string,object>
进行操作。
public class TencentCloudBlobProviderConfiguration
{
public string AppId
{
get => _containerConfiguration.GetConfigurationOrDefault<string>(TencentCloudBlobProviderConfigurationNames.AppId);
set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.AppId, value);
}
public string SecretId
{
get => _containerConfiguration.GetConfigurationOrDefault<string>(TencentCloudBlobProviderConfigurationNames.SecretId);
set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.SecretId, value);
}
// ... 其他代码
public TencentCloudBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
{
_containerConfiguration = containerConfiguration;
}
}
腾讯云的 BLOB Provider 仓库:https://github.com/EasyAbp/Abp.BlobStoring.TencentCloud
2.4 回顾
- 开发人员可以在模块的
ConfigureService()
阶段为所有容器或者特定容器指定参数。 - ABP vNext 框架会注入所有的 BLOB Provider,并注入默认的
IBlobContainer<DefaultContainer>
容器和其他的类型化容器实现。 - 当需要使用 BLOB 时,开发人员注入了
IBlobContainer
或IBlobContainer<T>
。 - BLOB 容器的工厂会根据容器的名称匹配对应的 BLOB Provider 和配置对象。
- BLOB Provider 根据 **Args 参数内部附带的配置对象,读取对应的配置信息进行自定义的操作。
三、总结
小型项目直接集成 FileSystem 即可,中大型项目可以使用各种 OSS Provider,BLOB 系统可以简化开发人员对于大量二进制文件的管理操作。最近工作相当杂乱繁忙,下半年希望有时间继续学习更新吧。
其他相关文章,请参阅 文章目录 。
[Abp vNext 源码分析] - 23. 二进制大对象系统(BLOB)的更多相关文章
- [Abp vNext 源码分析] - 文章目录
一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...
- [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)
一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...
- [Abp vNext 源码分析] - 11. 用户的自定义参数与配置
一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 23 日晚 更新日期:暂无 ABP vNext 针对用户可编辑的配置,提供了单独的 Volo. ...
- [Abp vNext 源码分析] - 20. 电子邮件与短信支持
一.简介 ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包将短信和电子邮件作为基础设施进行了抽象,开发人员仅需要在使用的时候注入 ISmsSender ...
- [Abp vNext 源码分析] - 3. 依赖注入与拦截器
一.简要说明 ABP vNext 框架在使用依赖注入服务的时候,是直接使用的微软提供的 Microsoft.Extensions.DependencyInjection 包.这里与原来的 ABP 框架 ...
- [Abp vNext 源码分析] - 1. 框架启动流程分析
一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...
- [Abp vNext 源码分析] - 4. 工作单元
一.简要说明 统一工作单元是一个比较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,一旦某个环节出现异常自动进行回滚处理. 在 ABP vNext 框架当中,工作单元被独立出来作为一 ...
- [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)
一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...
随机推荐
- 【总结】Github通过Git Bash上传文件到仓库
1-ML新文件夹连接仓库gir clone(最好用SSH不需要输入账户密码 SSH需要设置秘钥) 2-ML中设立空文件夹与项目同名(要新建 打开隐藏文件选项 文件夹内不许有文件会报错) 3-在项 ...
- LeetCode59. 螺旋矩阵 II
这题和第54题类似,都是套一个搜索的模板. 用dx和dy表示方向,方向的顺序是先向右,再向下,再向左,再向上,再向右... 如果"撞墙"了就需要改变到下一个方向."撞墙& ...
- 几个常见CSS错误和解决办法
1.IE6下,当float存在时,margin双倍的问题 解决方法:加display:inline; 例: #content { float: left; width: 500px; ...
- Python之浅谈面向对象
目录 面向对象和面向过程 面向过程 面向对象 类与对象 定义类 在现实世界中:先有对象,再有类 在程序中,务必保证:先定义(类),后使用(产生对象) 产生对象 类 对象 产生对象 属性查找顺序 对象赋 ...
- CentOS 7安装Oracle 12c图文详解
环境: CentOS7@VMware12,分配资源:CPU:2颗,内存:4GB,硬盘空间:30GB Oracle 12C企业版64位 下载地址:http://www.oracle.com/techne ...
- day66 django进阶(2)
目录 一.choices参数(数据库字段设计常见) 二.MTV与MVC模型 三.多对多三种创建方法 1 全自动 2 纯手动 3 半自动 四.AJax 小 一.choices参数(数据库字段设计常见) ...
- 如何实现 token 加密
jwt举例 需要一个secret(随机数) 后端利用secret和加密算法(如:HMAC-SHA256)对payload(如账号密码)生成一个字符串(token),返回前端 前端每次request在h ...
- IIFE中的函数是函数表达式,而不是函数声明
下面的代码打印什么内容,为什么? var b = 10; (function b(){ b = 20; console.log(b); })(); 针对这题,在知乎上看到别人的回答说: 函数表达式与函 ...
- vs2019使用github
本人操作系统win10 第一步,下载vs github插件 直接看图 下载完成后,需要关闭所有vs2019窗口,之后会弹出下面的窗口,点击modify,完成之后,重新打开vs就安装好了 下面就可以在v ...
- scrapy 基础组件专题(十四):scrapy CookiesMiddleware源码
一 Scrapy框架--cookie的获取/传递/本地保存 1. 完成模拟登陆2. 登陆成功后提取出cookie,然后保存到本地cookie.txt文件中3. 再次使用时从本地的cookie.txt中 ...