Làm cách nào để lưu luồng vào tệp trong C #?


713

Tôi có một StreamReaderđối tượng mà tôi khởi tạo với một dòng suối, bây giờ tôi muốn lưu suối này vào đĩa (dòng có thể là một .gifhoặc .jpghay .pdf).

Mã hiện có:

StreamReader sr = new StreamReader(myOtherObject.InputStream);
  1. Tôi cần lưu cái này vào đĩa (tôi có tên tệp).
  2. Trong tương lai tôi có thể muốn lưu trữ cái này cho SQL Server.

Tôi cũng có loại mã hóa, cái mà tôi sẽ cần nếu tôi lưu trữ nó vào SQL Server, đúng không?


1
MyOtherObject là gì?
anhtv13

2
Vẫn không có câu trả lời được chấp nhận cho câu hỏi này?
Brett Rigby

@BrettRigby Có câu trả lời của Jon Skeet, nó được chấp nhận tự động khá nhiều: D
Ricardo Dias Morais

Câu trả lời:


913

Như Tilendor đã nhấn mạnh trong câu trả lời của Jon Skeet, các luồng có một CopyTophương thức kể từ .NET 4.

var fileStream = File.Create("C:\\Path\\To\\File");
myOtherObject.InputStream.Seek(0, SeekOrigin.Begin);
myOtherObject.InputStream.CopyTo(fileStream);
fileStream.Close();

Hoặc với usingcú pháp:

using (var fileStream = File.Create("C:\\Path\\To\\File"))
{
    myOtherObject.InputStream.Seek(0, SeekOrigin.Begin);
    myOtherObject.InputStream.CopyTo(fileStream);
}

67
Lưu ý rằng bạn phải gọi myOtherObject.InputStream.Seek(0, SeekOrigin.Begin)nếu bạn chưa bắt đầu hoặc bạn sẽ không sao chép toàn bộ luồng.
Steve Rukuts

3
Nếu luồng đầu vào này được lấy từ kết nối http thì nó sẽ đệm và tải xuống và sau đó ghi tất cả các byte từ nguồn ?????
dbw

2
Tôi đã tạo trình xem PDF nơi tôi đang sử dụng luồng, một khi tôi liên kết luồng và khi tôi lưu tệp pdf bằng cùng một luồng thì không sử dụng "Seek (0, SeekOrigin.Begin)" Tôi sẽ không thể lưu tài liệu chính xác. vì vậy +1 để đề cập đến "Tìm kiếm (0, SeekOrigin.Begin)" này
user2463514

myOtherObject.InputStream.CopyTo (fileStream); dòng này đưa ra một lỗi: truy cập bị từ chối.
sulhadin

2
myOtherObject ??
Harry

531

Bạn không được sử dụng StreamReadercho các tệp nhị phân (như gifs hoặc jpg). StreamReaderdành cho dữ liệu văn bản . Bạn gần như chắc chắn sẽ mất dữ liệu nếu bạn sử dụng nó cho dữ liệu nhị phân tùy ý. (Nếu bạn sử dụng Encoding.GetEncoding (28591), bạn có thể sẽ ổn, nhưng vấn đề là gì?)

Tại sao bạn cần phải sử dụng một StreamReader? Tại sao không chỉ giữ dữ liệu nhị phân dưới dạng dữ liệu nhị phân và ghi lại vào đĩa (hoặc SQL) dưới dạng dữ liệu nhị phân?

EDIT: Như này có vẻ là một cái gì đó mọi người muốn xem ... nếu bạn làm chỉ muốn sao chép một dòng khác (ví dụ như vào một tập tin) một cái gì đó sử dụng như thế này:

/// <summary>
/// Copies the contents of input to output. Doesn't close either stream.
/// </summary>
public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8 * 1024];
    int len;
    while ( (len = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, len);
    }    
}

Để sử dụng nó để kết xuất một luồng vào một tệp, ví dụ:

using (Stream file = File.Create(filename))
{
    CopyStream(input, file);
}

Lưu ý rằng Stream.CopyTođã được giới thiệu trong .NET 4, về cơ bản phục vụ cùng một mục đích.


6
Đây có vẻ như là một trường hợp phổ biến như vậy tôi ngạc nhiên khi nó không có trong .NET. Tôi thấy mọi người tạo các mảng byte kích thước của toàn bộ tệp, điều này có thể gây ra sự cố cho các tệp lớn.
Tilendor

81
@Tilendor: Nó hiện diện như một phương thức mở rộng trong .NET 4. (CopyTo)
Jon Skeet

33
Tôi không nghĩ rằng đó là một phương thức mở rộng, nhưng nó mới trong lớp Stream.
Kugel

9
@Kugel: Bạn nói đúng, xin lỗi. Tôi đã có nó như một phương thức mở rộng trong thư viện tiện ích, nhưng bây giờ, trong chính Stream, phương thức mở rộng của tôi không được gọi.
Jon Skeet

4
@Florian: Đó là tùy ý hợp lý - một giá trị đủ nhỏ để tránh chiếm quá nhiều bộ nhớ và đủ lớn để chuyển một đoạn hợp lý tại một thời điểm. Sẽ là ổn nếu là 16K, 32K có lẽ - Tôi chỉ cần cẩn thận để không kết thúc với đống đối tượng lớn.
Jon Skeet

77
public void CopyStream(Stream stream, string destPath)
{
  using (var fileStream = new FileStream(destPath, FileMode.Create, FileAccess.Write))
  {
    stream.CopyTo(fileStream);
  }
}

28
Bạn có thể không nên đặt streamđối tượng trong dấu using(){}ngoặc. Phương pháp của bạn không tạo ra luồng, vì vậy nó không nên loại bỏ nó.
LarsTech

2
Thay vào đó, bạn cần đặt FileStreamthay vì sử dụng, nếu không nó sẽ được giữ cho đến khi rác được thu gom.
Pavel Chikulaev

Tôi thấy rằng cách tiếp cận của bạn gần hơn để giải quyết vấn đề của tôi trong WinForms với lớp cổng lớp AWS S3 của tôi! cảm ơn bạn!
Luiz Eduardo

2
Điều này chạy tốt nhưng tôi đã nhận được đầu ra 0 KB. Thay vào đó tôi phải làm điều này cho đầu ra chính xác : File.WriteAllBytes(destinationFilePath, input.ToArray());. Trong trường hợp của tôi, inputMemoryStreamđến từ bên trong a ZipArchive.
SNag

23
private void SaveFileStream(String path, Stream stream)
{
    var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write);
    stream.CopyTo(fileStream);
    fileStream.Dispose();
}

1
Điều này chạy tốt nhưng tôi đã nhận được đầu ra 0 KB. Thay vào đó tôi phải làm điều này cho đầu ra chính xác : File.WriteAllBytes(destinationFilePath, input.ToArray());. Trong trường hợp của tôi, inputMemoryStreamđến từ bên trong a ZipArchive.
SNag

2
Điều này giúp tôi tìm ra những gì tôi đã làm sai. Tuy nhiên, đừng quên di chuyển đến đầu dòng: stream.Seek(0, SeekOrigin.Begin);
Nathan Bills

9

Tôi không nhận được tất cả các câu trả lời bằng cách sử dụng CopyTo , trong đó có thể các hệ thống sử dụng ứng dụng có thể chưa được nâng cấp lên .NET 4.0+. Tôi biết một số người muốn buộc mọi người nâng cấp, nhưng khả năng tương thích cũng rất tốt.

Một điều nữa, tôi không được sử dụng một luồng để sao chép từ một luồng khác ở nơi đầu tiên. Tại sao không làm:

byte[] bytes = myOtherObject.InputStream.ToArray();

Khi bạn có các byte, bạn có thể dễ dàng ghi chúng vào một tệp:

public static void WriteFile(string fileName, byte[] bytes)
{
    string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    if (!path.EndsWith(@"\")) path += @"\";

    if (File.Exists(Path.Combine(path, fileName)))
        File.Delete(Path.Combine(path, fileName));

    using (FileStream fs = new FileStream(Path.Combine(path, fileName), FileMode.CreateNew, FileAccess.Write))
    {
        fs.Write(bytes, 0, (int)bytes.Length);
        //fs.Close();
    }
}

Mã này hoạt động như tôi đã kiểm tra nó với một .jpgtệp, mặc dù tôi thừa nhận tôi chỉ sử dụng nó với các tệp nhỏ (dưới 1 MB). Một luồng, không sao chép giữa các luồng, không cần mã hóa, chỉ cần viết các byte! Không cần quá phức tạp hóa mọi thứ với StreamReadernếu bạn đã có một luồng bạn có thể chuyển đổi bytestrực tiếp với .ToArray()!

Chỉ có nhược điểm tiềm năng mà tôi có thể thấy khi thực hiện theo cách này là nếu bạn có một tệp lớn, có nó dưới dạng luồng và sử dụng .CopyTo()hoặc tương đương cho phép FileStreamtruyền phát thay vì sử dụng một mảng byte và đọc từng byte một. Kết quả có thể chậm hơn theo cách này. Nhưng nó không nên bị nghẹt vì .Write()phương thức FileStreamxử lý ghi byte và nó chỉ thực hiện một byte mỗi lần, vì vậy nó sẽ không làm tắc bộ nhớ, ngoại trừ việc bạn sẽ phải có đủ bộ nhớ để giữ luồng như một byte[]đối tượng . Trong tình huống của tôi khi tôi sử dụng nó, nhận được một OracleBlob, tôi đã phải đi đến mộtbyte[] , nó đủ nhỏ, và bên cạnh đó, dù sao cũng không có luồng phát nào cho tôi, vì vậy tôi chỉ gửi các byte của mình cho hàm của tôi, ở trên.

Một tùy chọn khác, sử dụng một luồng, sẽ là sử dụng nó với CopyStreamchức năng của Jon Skeet trong một bài đăng khác - điều này chỉ sử dụng FileStreamđể lấy luồng đầu vào và tạo tệp trực tiếp từ nó. Nó không sử dụng File.Create, giống như anh ta đã làm (điều này ban đầu có vẻ là vấn đề đối với tôi, nhưng sau đó thấy nó có khả năng chỉ là một lỗi VS ...).

/// <summary>
/// Copies the contents of input to output. Doesn't close either stream.
/// </summary>
public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8 * 1024];
    int len;
    while ( (len = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, len);
    }    
}

public static void WriteFile(string fileName, Stream inputStream)
{
    string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    if (!path.EndsWith(@"\")) path += @"\";

    if (File.Exists(Path.Combine(path, fileName)))
        File.Delete(Path.Combine(path, fileName));

    using (FileStream fs = new FileStream(Path.Combine(path, fileName), FileMode.CreateNew, FileAccess.Write)
    {
        CopyStream(inputStream, fs);
    }

    inputStream.Close();
    inputStream.Flush();
}

1
Không cần gọi Closeusing()
Alex78191

@ Alex78191 Nếu bạn đang nói về inputStream.Close(), hãy nhìn lại - inputStreamđược gửi dưới dạng một biến. Các usinglà trên path+filenamedòng đầu ra. Nếu bạn đang nói về fs.Close()ở giữa using, xin lỗi, bạn đã đúng về điều đó và tôi loại bỏ đó.
vapcguy

8
//If you don't have .Net 4.0  :)

public void SaveStreamToFile(Stream stream, string filename)
{  
   using(Stream destination = File.Create(filename))
      Write(stream, destination);
}

//Typically I implement this Write method as a Stream extension method. 
//The framework handles buffering.

public void Write(Stream from, Stream to)
{
   for(int a = from.ReadByte(); a != -1; a = from.ReadByte())
      to.WriteByte( (byte) a );
}

/*
Note, StreamReader is an IEnumerable<Char> while Stream is an IEnumbable<byte>.
The distinction is significant such as in multiple byte character encodings 
like Unicode used in .Net where Char is one or more bytes (byte[n]). Also, the
resulting translation from IEnumerable<byte> to IEnumerable<Char> can loose bytes
or insert them (for example, "\n" vs. "\r\n") depending on the StreamReader instance
CurrentEncoding.
*/

16
Sao chép một luồng byte theo byte (sử dụng ReadByte / WriteByte) sẽ chậm hơn nhiều so với sao chép bộ đệm theo bộ đệm (sử dụng Read (byte [], int, int) / Write (byte [], int, int)).
Kevin

6

Tại sao không sử dụng một đối tượng FileStream?

public void SaveStreamToFile(string fileFullPath, Stream stream)
{
    if (stream.Length == 0) return;

    // Create a FileStream object to write a stream to a file
    using (FileStream fileStream = System.IO.File.Create(fileFullPath, (int)stream.Length))
    {
        // Fill the bytes[] array with the stream data
        byte[] bytesInStream = new byte[stream.Length];
        stream.Read(bytesInStream, 0, (int)bytesInStream.Length);

        // Use FileStream object to write to the specified file
        fileStream.Write(bytesInStream, 0, bytesInStream.Length);
     }
}

46
Điều gì xảy ra nếu luồng đầu vào dài 1GB - mã này sẽ cố gắng phân bổ bộ đệm 1GB :)
Buthrakaur

1
Điều này không hoạt động với FeedbackStream, vì nó có độ dài uknown.
Tomas Kubes

Mặc dù đúng là bạn phải có sẵn bộ nhớ cho byte[], tôi nghĩ rằng sẽ hiếm khi bạn phát trực tuyến 1 GB + blob đến một tệp ... trừ khi bạn có một trang web lưu giữ DVD ... , hầu hết các máy tính đều có ít nhất 2 GB RAM trong những ngày này, dù sao đi nữa .... Caveat là hợp lệ, nhưng tôi nghĩ đây là trường hợp có thể "đủ tốt" cho hầu hết các công việc.
vapcguy

Máy chủ web sẽ không chấp nhận trường hợp như thế này, trừ khi trang web chỉ có một người dùng hoạt động cùng một lúc.
NateTheGreatt

6

Một tùy chọn khác là lấy luồng đến byte[]và sử dụng File.WriteAllBytes. Điều này nên làm:

using (var stream = new MemoryStream())
{
    input.CopyTo(stream);
    File.WriteAllBytes(file, stream.ToArray());
}

Gói nó trong một phương thức mở rộng giúp đặt tên tốt hơn:

public void WriteTo(this Stream input, string file)
{
    //your fav write method:

    using (var stream = File.Create(file))
    {
        input.CopyTo(stream);
    }

    //or

    using (var stream = new MemoryStream())
    {
        input.CopyTo(stream);
        File.WriteAllBytes(file, stream.ToArray());
    }

    //whatever that fits.
}

3
Nếu đầu vào quá lớn, bạn sẽ có ngoại lệ bộ nhớ. Tùy chọn sao chép nội dung từ luồng đầu vào vào luồng phim tốt hơn nhiều
Ykok

4
public void testdownload(stream input)
{
    byte[] buffer = new byte[16345];
    using (FileStream fs = new FileStream(this.FullLocalFilePath,
                        FileMode.Create, FileAccess.Write, FileShare.None))
    {
        int read;
        while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
        {
             fs.Write(buffer, 0, read);
        }
    }
}

Cung cấp luồng đầu vào được đệm trực tiếp vào FileStream- tốt đẹp!
vapcguy

3

Dưới đây là một ví dụ sử dụng cách sử dụng phù hợp và triển khai idis Dùng một lần:

static void WriteToFile(string sourceFile, string destinationfile, bool append = true, int bufferSize = 4096)
{
    using (var sourceFileStream = new FileStream(sourceFile, FileMode.OpenOrCreate))
    {
        using (var destinationFileStream = new FileStream(destinationfile, FileMode.OpenOrCreate))
        {
            while (sourceFileStream.Position < sourceFileStream.Length)
            {
                destinationFileStream.WriteByte((byte)sourceFileStream.ReadByte());
            }
        }
    }
}

... và cũng có cái này

    public static void WriteToFile(FileStream stream, string destinationFile, int bufferSize = 4096, FileMode mode = FileMode.OpenOrCreate, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.ReadWrite)
    {
        using (var destinationFileStream = new FileStream(destinationFile, mode, access, share))
        {
            while (stream.Position < stream.Length) 
            {
                destinationFileStream.WriteByte((byte)stream.ReadByte());
            }
        }
    }

Điều quan trọng là hiểu được cách sử dụng đúng cách (nên được thực hiện khi khởi tạo đối tượng thực hiện idis Dùng như trên) và có một ý tưởng tốt về cách các thuộc tính hoạt động cho các luồng. Position đúng nghĩa là chỉ mục trong luồng (bắt đầu từ 0) được theo sau khi mỗi byte được đọc bằng phương thức readbyte. Trong trường hợp này, về cơ bản, tôi đang sử dụng nó thay cho biến vòng lặp for và chỉ đơn giản là để nó đi theo suốt chiều dài cho đến hết chiều dài của toàn bộ luồng (tính bằng byte). Bỏ qua các byte bởi vì nó thực tế là giống nhau và bạn sẽ có một cái gì đó đơn giản và thanh lịch như thế này để giải quyết mọi thứ sạch sẽ.

Cũng nên nhớ rằng phương thức ReadByte chỉ đơn giản chuyển byte thành int trong tiến trình và có thể được chuyển đổi trở lại.

Tôi sẽ thêm một triển khai khác mà gần đây tôi đã viết để tạo một bộ đệm động sắp xếp để đảm bảo ghi dữ liệu tuần tự để tránh quá tải lớn

private void StreamBuffer(Stream stream, int buffer)
{
    using (var memoryStream = new MemoryStream())
    {
        stream.CopyTo(memoryStream);
        var memoryBuffer = memoryStream.GetBuffer();

        for (int i = 0; i < memoryBuffer.Length;)
        {
            var networkBuffer = new byte[buffer];
            for (int j = 0; j < networkBuffer.Length && i < memoryBuffer.Length; j++)
            {
                networkBuffer[j] = memoryBuffer[i];
                i++;
            }
            //Assuming destination file
            destinationFileStream.Write(networkBuffer, 0, networkBuffer.Length);
        }
    }
}

Giải thích khá đơn giản: chúng tôi biết rằng chúng tôi cần ghi nhớ toàn bộ bộ dữ liệu chúng tôi muốn viết và chúng tôi chỉ muốn viết một số lượng nhất định, vì vậy chúng tôi muốn vòng lặp đầu tiên có tham số cuối cùng trống (giống như trong khi ). Tiếp theo, chúng tôi khởi tạo bộ đệm mảng byte được đặt theo kích thước của những gì đã qua và với vòng lặp thứ hai, chúng tôi so sánh j với kích thước của bộ đệm và kích thước của bộ đệm ban đầu và nếu nó lớn hơn kích thước của bộ gốc mảng byte, kết thúc chạy.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.