用文件模型绑定接口:IFormFile (小文件上传)


当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发ASP.NET Core MVC的Controller中的Action方法。这种情况下,如果上传一些小文件是没问题的,但是如果上传大文件,势必会造成服务器内存大量被占用甚至溢出,所以IFormFile接口只适合小文件上传。

一个文件上传页面的Html代码一般如下所示:

<form method="post" enctype="multipart/form-data" action="/Upload">
<div>
<p>Upload one or more files using this form:</p>
<input type="file" name="files" />
</div>
<div>
<input type="submit" value="Upload" />
</div>
</form>

为了支持文件上传,form标签上一定要记得声明属性enctype="multipart/form-data",否则你会发现ASP.NET Core MVC的Controller中死活都读不到任何文件。Input type="file"标签在html 5中支持上传多个文件,加上属性multiple即可。

使用IFormFile接口上传文件非常简单,将其声明为Controller中Action的集合参数即可:

[HttpPost]
public async Task<IActionResult> Post(List<IFormFile> files)
{
long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + );//注意formFile.FileName包含上传文件的文件路径,所以要进行Substring只取出最后的文件名 if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

注意上面Action方法Post的参数名files,必须要和上传页面中的Input type="file"标签的name属性值一样。

不要直接用Request.Form.Files

上面例子是我们知道Input type="file"标签的name属性值时的情况,如果你不知道Input type="file"标签的name属性值(例如前端用javascript动态生成的Input type="file"标签),有什么办法可以获取所有的上传文件吗?

也许有同学会想到可以用Request.Form.Files来获取当前Http请求中,所有的上传文件,如下所示:

[HttpPost]
public async Task<IActionResult> Post()
{
IFormFileCollection files = Request.Form.Files;
long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + ); if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

然后执行上面的代码你会发现,代码执行到Request.Form.Files的时候,就一直卡住了,如下所示:

然后现在我们给Post方法随便加一个参数string parameter,如下所示:

[HttpPost]
public async Task<IActionResult> Post(string parameter)
{
IFormFileCollection files = Request.Form.Files;
long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + ); if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

运行代码,你会发现虽然Post方法的参数string parameter没得到任何值为null,但是这次Post方法却没有卡在Request.Form.Files,文件上传成功,Post方法成功执行完毕:

这是因为当ASP.NET Core MVC中Controller的Action方法没有定义参数的时候,Request.Form不会做数据绑定,也就是说当我们在上面Post方法没有定义参数的时候,Request.Form根本就没有被ASP.NET Core初始化,所以只要一访问Request.Form代码就会被卡住,所以当我们随便给Post方法定义一个string parameter参数后,Request.Form就被初始化了,这时就可以访问Request.Form中的数据了。

既然必须要给Post方法定义参数,那我们就定义有意义的参数,而不是胡乱定义一个没有用的。我们将Post方法的代码改为如下:

[HttpPost]
public async Task<IActionResult> Post([FromForm]IFormCollection formData)
{
IFormFileCollection files = formData.Files;//等价于Request.Form.Files long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var inputName = formFile.Name;//可以通过IFormFile.Name属性获得每个上传文件,在页面上所属Input type="file"标签的name属性值
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + ); if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

我们给Post方法定义了一个IFormCollection类型的参数formData,并且标记了[FromForm]特性标签,表示IFormCollection formData参数使用Http请求中的表单(Form)数据进行初始化,所以这下formData其实就等价于Request.Form了。

我们可以从formData中访问表单(Form)提交的任何数据,获得所有的上传文件。其实Post方法的参数名字叫什么并不重要(本例中我们取名为formData),但是其参数必须是IFormCollection类型才会绑定Http请求中的表单(Form)数据,这才是关键。

执行上面的代码,文件成功上传,代码成功执行完毕:

用文件流 (大文件上传)


在介绍这个方法之前我们先来看看一个包含上传文件的Http请求是什么样子的:

Content-Type=multipart/form-data; boundary=---------------------------99614912995
-----------------------------99614912995
Content-Disposition: form-data; name="SOMENAME" Formulaire de Quota
-----------------------------99614912995
Content-Disposition: form-data; name="OTHERNAME" SOMEDATA
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 001.jpg" SDFESDSDSDJXCK+DSDSDSSDSFDFDF423232DASDSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 002.jpg" ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 003.jpg" TGUHGSDSDJXCK+DSDSDSSDSFDFDSAOJDIOASSAADDASDASDASSADASDSDSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995--

这就是一个multipart/form-data格式的Http请求,我们可以看到第一行信息是Http header,这里我们只列出了Content-Type这一行Http header信息,这和我们在html页面中form标签上的enctype属性值一致,第一行中接着有一个boundary=---------------------------99614912995,boundary=后面的值是随机生成的,这个其实是在声明Http请求中表单数据的分隔符是什么,其代表的是在Http请求中每读到一行 ---------------------------99614912995,表示一个section数据,一个section有可能是一个表单的键值数据,也有可能是一个上传文件的文件数据。每个section的第一行是section header,其中Content-Disposition属性都为form-data,表示这个section来自form标签提交的表单数据,如果section header拥有filename或filenamestar属性,那么表示这个section是一个上传文件的文件数据,否则这个section是一个表单的键值数据,section header之后的行就是这个section真正的数据行。例如我们上面的例子中,前两个section就是表单键值对,后面三个section是三个上传的图片文件。

那么接下来,我们来看看怎么用文件流来上传大文件,避免一次性将所有上传的文件都加载到服务器内存中。用文件流来上传比较麻烦的地方在于你无法使用ASP.NET Core MVC的模型绑定器来将上传文件反序列化为C#对象(如同前面介绍的IFormFile接口那样)。首先我们需要定义类MultipartRequestHelper,用于识别Http请求中的各个section类型(是表单键值对section,还是上传文件section)

using System;
using System.IO;
using Microsoft.Net.Http.Headers; namespace AspNetCore.MultipartRequest
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec says 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
//var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary);// .NET Core <2.0
var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
} //注意这里的boundary.Length指的是boundary=---------------------------99614912995中等号后面---------------------------99614912995字符串的长度,也就是section分隔符的长度,上面也说了这个长度一般不会超过70个字符是比较合理的
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
} return boundary;
} public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= ;
} //如果section是表单键值对section,那么本方法返回true
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value"
} //如果section是上传文件section,那么本方法返回true
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); // For .NET Core <2.0 remove ".Value"
} // 如果一个section的Header是: Content-Disposition: form-data; name="files"; filename="F:\Misc 002.jpg"
// 那么本方法返回: files
public static string GetFileContentInputName(ContentDispositionHeaderValue contentDisposition)
{
return contentDisposition.Name.Value;
} // 如果一个section的Header是: Content-Disposition: form-data; name="myfile1"; filename="F:\Misc 002.jpg"
// 那么本方法返回: Misc 002.jpg
public static string GetFileName(ContentDispositionHeaderValue contentDisposition)
{
return Path.GetFileName(contentDisposition.FileName.Value);
}
}
}

然后我们需要定义一个扩展类叫FileStreamingHelper,其中的StreamFiles扩展方法用于读取上传文件的文件流数据,并且将数据写入到服务器的硬盘上,其接受一个参数targetDirectory,用于声明将上传文件存储到服务器的哪个文件夹下。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks; namespace AspNetCore.MultipartRequest
{
public static class FileStreamingHelper
{
private static readonly FormOptions _defaultFormOptions = new FormOptions(); public static async Task<FormValueProvider> StreamFiles(this HttpRequest request, string targetDirectory)
{
if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
{
throw new Exception($"Expected a multipart request, but got {request.ContentType}");
} // Used to accumulate all the form url encoded key value pairs in the
// request.
var formAccumulator = new KeyValueAccumulator(); var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, request.Body); var section = await reader.ReadNextSectionAsync();//用于读取Http请求中的第一个section数据
while (section != null)
{
ContentDispositionHeaderValue contentDisposition;
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition); if (hasContentDispositionHeader)
{
/*
用于处理上传文件类型的的section
-----------------------------99614912995
Content - Disposition: form - data; name = "files"; filename = "Misc 002.jpg" ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
*/
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
} var fileName = MultipartRequestHelper.GetFileName(contentDisposition); var loadBufferBytes = ;//这个是每一次从Http请求的section中读出文件数据的大小,单位是Byte即字节,这里设置为1024的意思是,每次从Http请求的section数据流中读取出1024字节的数据到服务器内存中,然后写入下面targetFileStream的文件流中,可以根据服务器的内存大小调整这个值。这样就避免了一次加载所有上传文件的数据到服务器内存中,导致服务器崩溃。 using (var targetFileStream = System.IO.File.Create(targetDirectory + "\\" + fileName))
{
//section.Body是System.IO.Stream类型,表示的是Http请求中一个section的数据流,从该数据流中可以读出每一个section的全部数据,所以我们下面也可以不用section.Body.CopyToAsync方法,而是在一个循环中用section.Body.Read方法自己读出数据(如果section.Body.Read方法返回0,表示数据流已经到末尾,数据已经全部都读取完了),再将数据写入到targetFileStream
await section.Body.CopyToAsync(targetFileStream, loadBufferBytes);
} }
/*
用于处理表单键值数据的section
-----------------------------99614912995
Content - Disposition: form - data; name = "SOMENAME" Formulaire de Quota
-----------------------------99614912995
*/
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"
//
// value // Do not limit the key name length here because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
var encoding = GetEncoding(section);
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: ,
leaveOpen: true))
{
// The value length limit is enforced by MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
{
value = String.Empty;
}
formAccumulator.Append(key.Value, value); // For .NET Core <2.0 remove ".Value" from key if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
{
throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
}
}
}
} // Drains any remaining section body that has not been consumed and
// reads the headers for the next section.
section = await reader.ReadNextSectionAsync();//用于读取Http请求中的下一个section数据
} // Bind form data to a model
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture); return formValueProvider;
} private static Encoding GetEncoding(MultipartSection section)
{
MediaTypeHeaderValue mediaType;
var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
// UTF-7 is insecure and should not be honored. UTF-8 will succeed in
// most cases.
if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
{
return Encoding.UTF8;
}
return mediaType.Encoding;
}
}
}

现在我们还需要创建一个ASP.NET Core MVC的自定义拦截器DisableFormValueModelBindingAttribute,该拦截器实现接口IResourceFilter,用来禁用ASP.NET Core MVC的模型绑定器,这样当一个Http请求到达服务器后,ASP.NET Core MVC就不会在将请求的所有上传文件数据都加载到服务器内存后,才执行Controller的Action方法,而是当Http请求到达服务器时,就立刻执行Controller的Action方法。

ASP.NET Core 2.X使用下面的代码:

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Linq; namespace AspNetCore.MultipartRequest
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValueProviderFactory = context.ValueProviderFactories
.OfType<FormValueProviderFactory>()
.FirstOrDefault();
if (formValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(formValueProviderFactory);
} var jqueryFormValueProviderFactory = context.ValueProviderFactories
.OfType<JQueryFormValueProviderFactory>()
.FirstOrDefault();
if (jqueryFormValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
}
} public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}

ASP.NET Core 3.X使用下面的代码:

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Linq; namespace AspNetCore.MultipartRequest
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
} public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}

最后我们在Controller中定义一个叫Index的Action方法,并注册我们定义的DisableFormValueModelBindingAttribute拦截器,来禁用Action的模型绑定。Index方法会调用我们前面定义的FileStreamingHelper类中的StreamFiles方法,其参数为用来存储上传文件的文件夹路径。StreamFiles方法会返回一个FormValueProvider,用来存储Http请求中的表单键值数据,之后我们会将其绑定到MVC的视图模型viewModel上,然后将viewModel传回给客户端浏览器,来告述客户端浏览器文件上传成功。

[HttpPost]
[DisableFormValueModelBinding]
public async Task<IActionResult> Index()
{
FormValueProvider formModel;
formModel = await Request.StreamFiles(@"F:\UploadingFiles"); var viewModel = new MyViewModel(); var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
valueProvider: formModel); if (!bindingSuccessful)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
} return Ok(viewModel);
}

视图模型viewModel的定义如下:

public class MyViewModel
{
public string Username { get; set; }
}

最后我们用于上传文件的html页面和前面几乎一样:

<form method="post" enctype="multipart/form-data" action="/Home/Index">
<div>
<p>Upload one or more files using this form:</p>
<input type="file" name="files" multiple />
</div>
<div>
<p>Your Username</p>
<input type="text" name="username" />
</div>
<div>
<input type="submit" value="Upload" />
</div>
</form>

这就是所有的代码,希望对大家有所帮助!

参考文献:

File uploads in ASP.NET Core

Uploading Files In ASP.net Core

What is the boundary parameter in an HTTP multi-part (POST) Request?

ASP.NET Core MVC如何上传文件及处理大文件上传的更多相关文章

  1. ASP.NET Core MVC压缩样式、脚本及总是复制文件到输出目录

    前言 在.NET Core之前对于压缩样式文件和脚本我们可能需要借助第三方工具来进行压缩,但在ASP.NET MVC Core中则无需借助第三方工具来完成,本节我们来看看ASP.NET Core MV ...

  2. 002.Create a web API with ASP.NET Core MVC and Visual Studio for Windows -- 【在windows上用vs与asp.net core mvc 创建一个 web api 程序】

    Create a web API with ASP.NET Core MVC and Visual Studio for Windows 在windows上用vs与asp.net core mvc 创 ...

  3. 004.Create a web app with ASP.NET Core MVC using Visual Studio on Windows --【在 windows上用VS创建mvc web app】

    Create a web app with ASP.NET Core MVC using Visual Studio on Windows 在 windows上用VS创建mvc web app 201 ...

  4. .NET Core 如何上传文件及处理大文件上传

    当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发ASP.NET Core MVC的Controller中的A ...

  5. ASP.NET Core MVC 模型绑定 (转载)

    ASP.NET Core MVC的Model Binding会将HTTP Request数据,以映射的方式对应到参数中.基本上跟ASP.NET MVC差不多,但能Binding的来源更多了一些.本篇将 ...

  6. asp.net core mvc 读取配置文件appsettings.json

    上一篇我们将了读取自定义配置文件.这篇我们讲一下asp.net core mvc里读取自带的配置文件 appsettings.json 首先创建个asp.net core mvc项目,项目里有Prog ...

  7. ASP.NET Core MVC的Razor视图中,使用Html.Raw方法输出原生的html

    我们在ASP.NET Core MVC项目中,有一个Razor视图文件Index.cshtml,如下: @{ Layout = null; } <!DOCTYPE html> <ht ...

  8. 解决ASP.NET Core Mvc文件上传限制问题

    一.简介 在ASP.NET Core MVC中,文件上传的最大上传文件默认为20MB,如果我们想上传一些比较大的文件,就不知道怎么去设置了,没有了Web.Config我们应该如何下手呢? 二.设置上传 ...

  9. ASP.NET Core MVC上传、导入、导出知多少

    前言 本君已成夜猫子,本节我们来讲讲ASP.NET Core MVC中的上传,这两天才研究批量导入功能,本节顺便简单搞搞导入.导出,等博主弄妥当了再来和大家一并分享. .NET Core MVC上传 ...

随机推荐

  1. php从身份证获取性别和出生年月

    //通过身份证号查询出性别与生日 $birth = strlen($idcard)==15 ? ('19' . substr($idcard, 6, 6)) : substr($idcard, 6, ...

  2. Js的核心:找到DOM

    掌握 JavaScript 的核心之一:DOM,能够熟悉DOM相关操作,了解JavaScript事件机制 一.使用getElementById().getElementsByTagName().chi ...

  3. 第1章:程序设计和C语言(C语言入门)

    一.程序和程序语言 1,程序的概念:完成某项事物所预设的活动方式. 2,程序设计:人们描述计算机要做的工作. 二 .程序设计语言及其发展 1.机器语言,2汇编语言,3高级语言{a)编译,b)解释}: ...

  4. js小数乘法精确率问题

    研究拓扑图百分比乘法计算,带小数位计算会出现值溢出的问题 JS里做小数的乘法运算时会出现浮点错误:  结果是251.89999999999998 而不是251.9  这个问题想必有很多人为之头痛. 那 ...

  5. js的函数作用域跟块级作用域

    js的函数作用域跟块级作用域(原文地址:http://blog.csdn.net/huangjq36sysu/article/details/51085674)

  6. Codeforces Round #413 A. Carrot Cakes

    A. Carrot Cakes time limit per test   1 second memory limit per test   256 megabytes   In some game ...

  7. Js 对Dom的操作

    一.DOM的概述 DOM(Document Object Model,文档对象模型)描绘了一个层次化的节点树,允许开发人员添加.移除和修改页面的某一部分.这使得JavaScript操作HTML,不是在 ...

  8. Maven 安装与使用(一)

    1. 安装 参考:http://maven.apache.org/install.html A. win7环境下,官网下载maven安装文件 B. 解压缩maven文件 C. 确认已配置好JAVA环境 ...

  9. Java中short、int、long、float、double的取值范围

    一.基本数据类型的特点,位数,最大值和最小值.1.基本类型:short 二进制位数:16 包装类:java.lang.Short 最小值:Short.MIN_VALUE=-32768 (-2的15此方 ...

  10. Day01——Python简介

    一.Python简介 python的创始人为吉多·范罗苏姆(Guido van Rossum).1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解释程序,作为ABC ...