在CSV文件中传输二进制数据(图像等)



除了一些存储为字符串、数字等的其他元数据信息外,我还有一些二进制数据要传输。我拥有的二进制数据是作为 blob 列存储在数据库中的图像文件,我想将 blob 列包含在 csv 文件中并将 csv 文件存储在文件系统或 sftp 服务器中,我想它存储在哪里并不重要。

如何将二进制数据存储为 csv 文件中的另一列?以这种方式传输二进制数据是否是一种好的做法?

Base64

通常(也是正确的(方法是在 Base64 中对二进制数据进行编码。这将使数据放大 4:3。

虽然 CSV 文件通常被视为文本文件,但您可以将原始二进制数据写入该文件。

然后,该数据应括在双引号中,并且数据中所有现有的双引号都必须使用另一个双引号进行转义。引用字段也将处理二进制数据中的任何换行符,但读者必须支持这一点。里面也可能有空字节,如果读取器知道它正在读取二进制数据(即,如果您自己提供读取器(,则应该不是问题。

但是,如果您的数据必须采用某种 unicode 形式,则可能会出现问题。

因此,一般来说,将原始二进制数据写入 csv 文件不是好的做法,最好使用base64编码。

正如Danny_ds所讨论的正确答案所讨论的那样,是的,您确实可以在CSV中包含二进制数据,但您必须以文本和CSV友好的方式对该数据进行编码。如前所述,这种文本和CSV友好的明显选择是Base64。

示例应用

下面是示例应用的完整源代码。

这个应用程序从互联网上下载了几张图片。也就是说,来自姐妹网站的徽标 StackOverflow.com 和 StackExchange.com。下载后,这些图像被编码为 Base64 字符串。

然后写入一个 2 列 CSV 文件。列是名称和图像数据。对于此示例,我们有 2 行,上面列出的每个站点各一行。请注意,Base64 编码不涉及逗号或引号,因此无需将 Base64 与标准 CSV 格式的引号括起来。

为了证明这有效,然后读取 CSV 文件。图像数据从 Base64 解码回二进制数据。二进制数据将写入存储。您可以自己打开 PNG 图像文件。他们应该看起来像这样和这个。

CSV的写入和读取是使用Apache Commons CSV库完成的。

package work.basil.example;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class CsvImager
{
    // Write a CSV file, of two columns: name of the image, image in Base64 data.
    private void writeCsv ( final Map < String, BufferedImage > map , final Path path )
    {
        Objects.requireNonNull( map );
        CSVFormat format = CSVFormat.RFC4180.withHeader( "Name" , "Image" );
        try (
                BufferedWriter writer = Files.newBufferedWriter( path , StandardCharsets.UTF_8 ) ;
                CSVPrinter printer = new CSVPrinter( writer , format ) ;
        )
        {
            // Print rows.
            for ( String key : map.keySet() )
            {
                printer.print( key );
                BufferedImage image = Objects.requireNonNull( map.get( key ) );
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                ImageIO.write( image , "PNG" , stream );
                String imageData = Base64.getEncoder().encodeToString( stream.toByteArray() );
                printer.print( imageData );
                printer.println();
            }
        } catch ( IOException e )
        {
            e.printStackTrace();
        }
    }
    // Read images from a CSV file in storage.
    public Map < String, BufferedImage > readCsv ( final Path path )
    {
        Objects.requireNonNull( path );
        Map < String, BufferedImage > map = Map.of();
        try ( BufferedReader reader = Files.newBufferedReader( path ) )
        {
            map = new HashMap <>();
            CSVFormat format = CSVFormat.RFC4180.withHeader( "Name" , "Image" ).withFirstRecordAsHeader();
            Iterable < CSVRecord > records = format.parse( reader );
            for ( CSVRecord record : records )
            {
                String name = record.get( "Name" );
                String imageBase64 = record.get( "Image" );
//                System.out.println("imageBase64:n" + imageBase64 + "n");
                byte[] bytes = Base64.getDecoder().decode( imageBase64 );
                ByteArrayInputStream stream = new ByteArrayInputStream( bytes );
                BufferedImage image = ImageIO.read( stream );
                map.put( name , image );
            }
        } catch ( IOException e )
        {
            e.printStackTrace();
        }
        return map;
    }
    // Download images from the Internet.
    private Map < String, BufferedImage > fetchImages ()
    {
        Map < String, BufferedImage > map = Map.of(); // Initialize to empty map.
        try
        {
            URL urlStackOverflow = null, urlStackExchange = null;
            urlStackOverflow = new URL( "https://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/so-logo.png" );
            urlStackExchange = new URL( "https://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/se/se-logo.png" );
            BufferedImage imageStackOverflow = ImageIO.read( urlStackOverflow );
            BufferedImage imageStackExchange = ImageIO.read( urlStackExchange );
            System.out.println( "imageStackOverflow: " + imageStackOverflow );
            System.out.println( "imageStackExchange: " + imageStackExchange );
            map = Map.of( "logoStackOverflow.png" , imageStackOverflow , "logoStackExchange.png" , imageStackExchange );
        } catch ( MalformedURLException e ) // `new URL` fail.
        {
            e.printStackTrace();
        } catch ( IOException e ) // `ImageIO.read` fail.
        {
            e.printStackTrace();
        }
        ;
        return map;
    }
    // Produce individual image files on disk, to manually verify that the downloaded images were successfully Base64 endcoded, written to CSV, read from CSV, and decoded back to images.
    public void writeImages ( final Map < String, BufferedImage > map , final Path pathToFolder )
    {
        Objects.requireNonNull( map );
        Objects.requireNonNull( pathToFolder );
        if ( map.isEmpty() )
        {
            throw new IllegalArgumentException( "The Map should have elements but is empty. Message # 77063b5a-4398-49f0-b1a4-442255a13b77." );
        }
        if ( ! Files.isDirectory( pathToFolder ) )
        {
            throw new IllegalArgumentException( "The specified path must lead to an existing folder. Message # 6a19313d-b8a9-4a53-9b82-7672172923f9." );
        }
        for ( String key : map.keySet() )
        {
            Path pathToFile = pathToFolder.resolve( key );
            try (
                    OutputStream stream = Files.newOutputStream( pathToFile ) ;
            )
            {
                BufferedImage image = Objects.requireNonNull( map.get( key ) );
                ImageIO.write( image , "PNG" , stream );
            } catch ( IOException e )
            {
                e.printStackTrace();
            }
        }
    }
    // --------| Demo  |-----------------------
    public void demo ()
    {
        Map < String, BufferedImage > map = this.fetchImages(); // Pairs of name & image.
        Path path = Paths.get( "/Users/basilbourque/images.csv" );
        this.writeCsv( map , path );
        Map < String, BufferedImage > mapOut = this.readCsv( path );
        Path pathOut = path.getParent();
        this.writeImages( mapOut , pathOut );
    }
    public static void main ( String[] args )
    {
        CsvImager app = new CsvImager();
        app.demo();
        System.out.println( "Done." );
    }
}

提示:CSV中列名的魔术字符串NameImage的分散使用应替换为Apache Commons CSV支持的枚举的使用。我把它留给读者练习。

最新更新