CSV文件的格式规范及其在常见软件中的导入导出方法

计算机技术
作者

zenggyu

发布日期

2019-12-04

摘要
介绍CSV文件格式的规范,以及如何在常用软件中导入和导出该类文件。

引言

逗号分隔值(Comma-Separated-Values, CSV)格式是指以半角逗号作为分隔符来分隔文本中不同字段的文件格式。由于存储形式和规则简单,该格式能够被几乎所有数据库、数据分析软件识别,因此常常被用作数据传输的中间格式。

尽管CSV格式被广泛使用,但它们的存储规则仍缺乏一个唯一的标准,不同的软件可能会采用基于不同规范的实现。如果不事先了解各种程序所依据规范并据此做出应对,那么在使用该格式进行数据传输时可能就会对数据完整性造成破坏。

本文将研究几种常用软件对CSV文件的读入和写出行为,以期得到实现数据完整传输的方案。为达到该目的,本文将引入最接近标准的CSV规范——RFC 4180 (Shafranovich 2005) 作为参考依据,研究如何使用各种软件导入导出符合该规范的CSV文件;另外,对于无法兼容该规范的软件,本文还将介绍相应的注意事项。

关于RFC 4180及W3C组织的有关建议

RFC 4180是互联网工程任务组(Internet Engineering Task Force, IETF)在2005年发布的CSV文件规范 (Shafranovich 2005) 。尽管前面提到CSV文件格式仍然缺乏世界公认的标准,但从W3C组织对RFC 4180的介绍 (CSV on the Web Working Group 2015) 可以看出,该规范就是事实上的标准;因此,在处理CSV文件时应尽可能地遵循该规范。

RFC 4180对CSV文件格式的核心定义如下:

  1. 文件中的各条记录必须位于不同行,其间以换行符CRLF分隔。例如:
aaa,bbb,ccc CRLF
zzz,yyy,xxx CRLF
  1. 最后一条记录的末尾可以不包括换行符。例如:
aaa,bbb,ccc CRLF
zzz,yyy,xxx
  1. 文件中的首条记录可以是字段名(但这不是必要的),且其所含的名称数量及存储规则须与其他记录保持一致。
field_name,field_name,field_name CRLF
aaa,bbb,ccc CRLF
zzz,yyy,xxx CRLF
  1. 每条记录中可以包含一个或多个字段,每个字段以半角逗号分隔。文件中的所有记录必须拥有相同数量的字段。字段中的空格属于字段取值,不可忽略。每条记录的最后一个字段之后不应再添加半角逗号。例如
aaa,bbb,ccc
  1. 每个字段可以用半角双引号括起来(但这不一定是必要的)。如果字段没有被双引号括起来,那么字段中不应该出现双引号。例如:
"aaa","bbb","ccc" CRLF
zzz,yyy,xxx
  1. 含有换行符、半角双引号或半角逗号的字段应该用半角双引号括起来。例如:
"aaa","b CRLF
bb","ccc" CRLF
zzz,yyy,xxx
  1. 如果字段被半角双引号括起来了,那么在表示字段取值中本身含有的半角双引号时,需要在其前方增加一个半角双引号。例如:
"aaa","b""bb","ccc"

W3C组织在RFC 4180的基础上提出了更多CSV文件的使用建议 (CSV on the Web Working Group 2015) ,其中三项特别值得注意的是:

  1. 在(类)Unix系统上,可使用LF(而不是CRLF)作为CSV文件的换行符;
  2. 无论是在(类)Unix系统还是在Windows系统上,均建议使用UTF-8作为文本的字符编码;
  3. 日期、时间戳类数据必须使用规范的文本格式表示(例如ISO8601)。

测试数据

本文将以 表格 1 所示数据作为测试数据,来研究如何在各种软件中导入导出符合前述规范的CSV文件。该表格取自维基百科 (Wikipedia 2019) ,笔者对其进行了少量修改,使修改后的数据中同时含有以下特殊字符(串):

  • 半角逗号:该字符在CSV文件中具有特殊含义,是字段分隔符;
  • 半角双引号:该字符在CSV文件中具有特殊含义,用于封闭含有特殊字符的字段取值;
  • 换行符:该字符在CSV文件中具有特殊含义,是记录分隔符;
  • 反斜杠:有的程序在读取CSV文本时会将该字符视为转义符,使得以该字符开头的字符串表达出特殊含义(例如\t等同于制表符),而RFC 4180并未定义这种用法;
  • 中文字符:中文字符必须使用ASCII以外的字符编码进行存储,这要求相关程序能够按指定的方式对CSV文件内容进行解码、编码;
  • 空字符串(empty string)和空值(null):表中model字段含有一个空字符串,而description字段含有一个空值(为区别前者, 表格 1 中的空值以NULL表示,但不同软件有不同的表示方法,详见后文),这两者将被用于测试有关程序是否能对其进行正确区分(注:RFC 4180并没有明确定义空字符串和空值的区分方法,此测试只是为了能够更深入了解有关程序的行为);
  • 时间戳:时间戳字符串具有特定的格式,还可能涉及时区信息,需要相关程序进行额外的解析。

只要含有上述字符(串)的内容能够被准确地导入、导出,那么理论上同样的方法就能够准确地处理任何内容。

表格 1: tb_test数据集(NULL表示空值)
date_time manufacturer model description price
1997-01-01 00:00:01 福特 E350 ac, abs, moon 3000
1999-01-01 00:00:01 雪佛兰 Venture “Extended Edition” \t 4900
1999-01-01 00:00:01 雪佛兰 NULL 5000
1996-01-01 00:00:01 吉普 Grand Cherokee MUST SELL!
air, moon roof, loaded
4799

以下是 表格 1 所示数据在一个符合RFC 4180规范的CSV文件tb_test.csv中的存储形式;特别值得注意的是,表中的空字符串被半角双引号所围绕,而空值则没有(见第三行):

1997-01-01 00:00:01,福特,E350,"ac, abs, moon",3000
1999-01-01 00:00:01,雪佛兰,"Venture ""Extended Edition""",\t,4900
1999-01-01 00:00:01,雪佛兰,"",,5000
1996-01-01 00:00:01,吉普,Grand Cherokee,"MUST SELL!
air, moon roof, loaded",4799

因为有的数据库软件不支持在CSV文件中存储字段名,所以本文使用不包含字段名的文件进行测试;此外,本文还采纳W3C的建议,以LF作为换行符、UTF8作为字符编码,并选取符合ISO8601标准的时间戳格式存储时间数据。

正文

以下开始介绍如何在各种软件中导入tb_test.csv文件内的数据,以及如何在各软件中生成 表格 1 所示测试数据并以CSV格式导出。为了充分展示各软件在处理CSV文件时可调节的相关参数、使操作方法能得到推广,本文会尽可能减少对默认参数的依赖,并显式定义各种参数。另外,针对数据库类软件,本文将优先介绍通过类SQL指令实现的方法,因为一般来说各种数据库均会提供这种方法。

R

R语言的readr1提供了read_delim()write_delim()等函数分别用于导入、导出CSV文件。以下是在R语言中导入tb_test.csv的方法:

  • 1 本次测试所用的readr包版本为1.3.1。

  • library(readr)
    
    tb_test_r <- read_delim(
      "tb_test.csv",
      col_names = c("date_time", "manufacturer", "model", "description", "price"),
      col_types = cols(date_time = col_datetime(),
                       manufacturer = col_character(),
                       model = col_character(),
                       description = col_character(),
                       price = col_integer()),
      locale = locale(encoding = "UTF8"),
      delim = ",",
      quote = "\"",
      na = ""
    )

    导入后的结果如 表格 3 所示:

    表格 2: 在R语言中导入tb_test后得到的结果
    date_time manufacturer model description price
    1997-01-01 00:00:01 福特 E350 ac, abs, moon 3000
    1999-01-01 00:00:01 雪佛兰 Venture “Extended Edition” \t 4900
    1999-01-01 00:00:01 雪佛兰 NA NA 5000
    1996-01-01 00:00:01 吉普 Grand Cherokee MUST SELL!
    air, moon roof, loaded
    4799

    对比 表格 1 ,可以发现 表格 3 中model字段下的空字符串被判断成了空值NA,而其他单元格的数据则与前者一致。

    以下代码在R语言中重构了 表格 1 所示的数据,并使用write_delim()函数将结果导出至tb_test_r.csv文件中:

    tb_test <- tibble::tibble(
      date_time = lubridate::ymd_hms(c("1997-01-01 00:00:01", "1999-01-01 00:00:01", "1999-01-01 00:00:01", "1996-01-01 00:00:01")),
      manufacturer = c("福特", "雪佛兰", "雪佛兰", "吉普"),
      model = c("E350", 'Venture "Extended Edition"', "", "Grand Cherokee"),
      description = c("ac, abs, moon", "\\t", NA, "MUST SELL!\nair, moon roof, loaded"),
      price = c(3000, 4900, 5000, 4799)
    )
    
    # `write_delim()`函数强制使用UTF-8编码写出结果
    write_delim(tb_test,
                "tb_test_r.csv",
                delim = ",",
                na = "",
                col_names = F)

    以下是tb_test_r.csv中的内容:

    1997-01-01T00:00:01Z,福特,E350,"ac, abs, moon",3000
    1999-01-01T00:00:01Z,雪佛兰,"Venture ""Extended Edition""",\t,4900
    1999-01-01T00:00:01Z,雪佛兰,"",,5000
    1996-01-01T00:00:01Z,吉普,Grand Cherokee,"MUST SELL!
    air, moon roof, loaded",4799
    

    对比tb_test.csv,可以发现tb_test_r.csv中的时间戳字段增加了时区信息(write_demin()强制使用UTC标准时),而其他内容与前者一致。如果想去除时间戳的时区信息,可以在导出数据前先使用as.character()函数将date_time字段转换为字符串类型。

    Python

    Python的pandas2提供了read_csv()函数和to_csv()方法分别用于导入、导出CSV文件。以下操作演示了如何在Python中导入tb_test.csv

  • 2 本次测试所用的pandas包版本为0.25.3。

  • import pandas as pd
    
    tb_test_py = pd.read_csv(
      "tb_test.csv",
      names = ["date_time", "manufacturer", "model", "description", "price"],
      dtype = {"date_time": str, "manufacturer": str, "model": str, "description": str, "price": int},
      parse_dates = ["date_time"],
      encoding = "UTF8",
      sep = ",",
      quotechar = "\"",
      na_values = ""
    )

    导入后的结果如 表格 3 所示:

    表格 3: 在Python语言中导入tb_test后得到的结果
    date_time manufacturer model description price
    1997-01-01 00:00:01 福特 E350 ac, abs, moon 3000
    1999-01-01 00:00:01 雪佛兰 Venture “Extended Edition” \t 4900
    1999-01-01 00:00:01 雪佛兰 NaN NaN 5000
    1996-01-01 00:00:01 吉普 Grand Cherokee MUST SELL!
    air, moon roof, loaded
    4799

    对比 表格 1 ,可以发现 表格 3 中model字段下的空字符串被判断成了空值NaN,而其他单元格的数据则与前者一致。

    以下代码在Python中重构了 表格 1 所示的数据,并使用to_csv()方法将结果导出至tb_test_py.csv文件中:

    tb_test = pd.DataFrame(
      {"date_time": pd.to_datetime(["1997-01-01 00:00:01", "1999-01-01 00:00:01", "1999-01-01 00:00:01", "1996-01-01 00:00:01"], format = "%Y-%m-%d %H:%M:%S"),
      "manufacturer": ["福特", "雪佛兰", "雪佛兰", "吉普"],
      "model": ["E350", 'Venture "Extended Edition"', "", "Grand Cherokee"],
      "description": ["ac, abs, moon", "\\t", None, "MUST SELL!\nair, moon roof, loaded"],
      "price": [3000, 4900, 5000, 4799]}
    )
    
    tb_test.to_csv("tb_test_py.csv",
                   sep = ",",
                   na_rep = "",
                   header = False,
                   encoding = "UTF8",
                   index = False,
                   quotechar = "\"")

    以下是tb_test_py.csv中的内容:

    1997-01-01 00:00:01,福特,E350,"ac, abs, moon",3000
    1999-01-01 00:00:01,雪佛兰,"Venture ""Extended Edition""",\t,4900
    1999-01-01 00:00:01,雪佛兰,,,5000
    1996-01-01 00:00:01,吉普,Grand Cherokee,"MUST SELL!
    air, moon roof, loaded",4799
    

    对比tb_test.csv,可以发现tb_test_py.csv的内容与前者基本一致,但没有对空值和空字符串进行区分。

    PostgreSQL

    PostgreSQL3内置的COPY指令可以用来导入、导出CSV文件。以下操作演示了如何在PostgreSQL中导入tb_test.csv

  • 3 本次测试所用的PostgreSQL数据库版本为12.1。

  • CREATE TABLE tb_test_pg (
      date_time    TIMESTAMP,
      manufacturer VARCHAR(200),
      model        VARCHAR(200),
      description  VARCHAR(200),
      price        INTEGER
    );
    
    COPY tb_test_pg
      FROM 'tb_test.csv'
      WITH (FORMAT CSV, ENCODING 'UTF8', DELIMITER ',', QUOTE '"', NULL '', HEADER FALSE
      );

    导入后的结果如 表格 4 所示:

    表格 4: 在PostgreSQL中导入tb_test后得到的结果
    date_time manufacturer model description price
    1997-01-01 00:00:01 福特 E350 ac, abs, moon 3000
    1999-01-01 00:00:01 雪佛兰 Venture “Extended Edition” \t 4900
    1999-01-01 00:00:01 雪佛兰 NULL 5000
    1996-01-01 00:00:01 吉普 Grand Cherokee MUST SELL!
    air, moon roof, loaded
    4799

    对比 表格 1 ,可以发现 表格 4 所示内容完全与前者一致,符合预期。

    以下代码显示了如何将tb_test_pg中的数据导出至tb_test_pg.csv文件中:

    COPY tb_test_pg
      TO 'tb_test_pg.csv'
      WITH (FORMAT CSV, ENCODING 'UTF8', DELIMITER ',', QUOTE '"', NULL '', HEADER FALSE
      );

    以下是tb_test_pg.csv中的内容:

    1997-01-01 00:00:01,福特,E350,"ac, abs, moon",3000
    1999-01-01 00:00:01,雪佛兰,"Venture ""Extended Edition""",\t,4900
    1999-01-01 00:00:01,雪佛兰,"",,5000
    1996-01-01 00:00:01,吉普,Grand Cherokee,"MUST SELL!
    air, moon roof, loaded",4799
    

    对比tb_test.csv,可以发现tb_test_py.csv的内容与前者完全一致。

    MySQL

    MySQL4内置的LOAD DATA指令可以用来导入CSV文件。以下操作演示了如何在MySQL中导入tb_test.csv

  • 4 本次测试所用的MySQL数据库版本为5.8。

  • CREATE TABLE tb_test_mysql (
      date_time    DATETIME,
      manufacturer VARCHAR(100),
      model        VARCHAR(100),
      description  VARCHAR(100),
      price        INT
    );
    
    LOAD DATA
      INFILE 'tb_test.csv'
        INTO TABLE tb_test_mysql
        CHARACTER SET 'UTF8'
        FIELDS
        TERMINATED BY ','
        OPTIONALLY ENCLOSED BY '"'
        ESCAPED BY '\\'
        IGNORE 0 LINES;

    导入后的结果如 表格 5 所示:

    表格 5: 在MySQL中导入tb_test后得到的结果
    date_time manufacturer model description price
    1997-01-01 00:00:01 福特 E350 ac, abs, moon 3000
    1999-01-01 00:00:01 雪佛兰 Venture “Extended Edition” 4900
    1999-01-01 00:00:01 雪佛兰 5000
    1996-01-01 00:00:01 吉普 Grand Cherokee MUST SELL!
    air, moon roof, loaded
    4799

    对比 表格 1 ,可以发现 表格 5 中description字段下的空值被识别成了空字符串,且该字段下的字符串\t被错误地识别成了制表符

    以下代码在MySQL中重构了 表格 1 所示的数据,并使用SELECT INTO指令将结果导出至tb_test_mysql.csv文件中:

    TRUNCATE tb_test_mysql;
    INSERT INTO
      tb_test_mysql
    VALUES
      ('1997-01-01 00:00:01', '福特', 'E350', 'ac, abs, moon', 3000),
      ('1999-01-01 00:00:01', '雪佛兰', 'Venture "Extended Edition"', '\\t', 4900),
      ('1999-01-01 00:00:01', '雪佛兰', '', NULL, 5000),
      ('1996-01-01 00:00:01', '吉普', 'Grand Cherokee', 'MUST SELL!\nair, moon roof, loaded', 4799);
    
    SELECT * FROM tb_test_mysql
    INTO OUTFILE 'tb_test_mysql.csv'
      CHARACTER SET 'UTF8'
      FIELDS
      TERMINATED BY ','
      OPTIONALLY ENCLOSED BY '"'
      ESCAPED BY '\\';

    以下是tb_test_mysql.csv中的内容:

    "1997-01-01 00:00:01","福特","E350","ac, abs, moon",3000
    "1999-01-01 00:00:01","雪佛兰","Venture \"Extended Edition\"","\\t",4900
    "1999-01-01 00:00:01","雪佛兰","",\N,5000
    "1996-01-01 00:00:01","吉普","Grand Cherokee","MUST SELL!\
    air, moon roof, loaded",4799
    

    对比tb_test.csv,可以发现tb_test_mysql.csv中除了数值以外的字段均被用半角双引号括了起来;数据中所含的半角双引号、反斜杠、换行符等特殊符号均前置了反斜杠进行转义;另外,空值必须使用字符串\N表示。

    Hive

    Hive内置的LOAD DATA指令可以用来导入CSV文件。以下操作演示了如何在Hive中导入tb_test.csv

    CREATE TABLE tb_test_hive (
      date_time    TIMESTAMP,
      manufacturer STRING,
      model        STRING,
      description  STRING,
      price        INT
    )
    ROW FORMAT delimited
    FIELDS TERMINATED BY ',' ESCAPED BY '\\' NULL defined AS ''
    STORED AS textfile;
    
    LOAD DATA LOCAL INPATH 'tb_test.csv' INTO TABLE tb_test_hive;

    导入后的结果如 表格 6 所示:

    表格 6: 在Hive中导入tb_test后得到的结果
    date_time manufacturer model description price
    1997-01-01 00:00:01 福特 E350 “ac NULL
    1999-01-01 00:00:01 雪佛兰 “Venture”“Extended Edition”“” t 4900
    1999-01-01 00:00:01 雪佛兰 “” NULL 5000
    1996-01-01 00:00:01 吉普 Grand Cherokee “MUST SELL! NULL
    NULL moon roof loaded” 4799 NULL
    NULL NULL NULL NULL NULL

    对比 表格 1 ,可以发现 表格 6 的数据完整性遭受了严重的破坏;并且,还可以观察到以下情况:1. 半角双引号不能被识别为封闭字符,而是作为字段取值被读入;2. 反斜杠既未被当做普通字符读入,也未将字母t转义成制表符;3. 换行符被强行视作记录分隔符,且空行会被视为一条字段全为空的记录。

    以下代码试图在Hive中重构 表格 1 所示的数据,并使用INSERT指令将结果导出至tb_test_hive/目录中:

    TRUNCATE TABLE tb_test_hive;
    INSERT INTO TABLE
      tb_test_hive
    VALUES
      ('1997-01-01 00:00:01', '福特', 'E350', 'ac, abs, moon', 3000),
      ('1999-01-01 00:00:01', '雪佛兰', 'Venture "Extended Edition"', '\\t', 4900),
      ('1999-01-01 00:00:01', '雪佛兰', '', NULL, 5000),
      ('1996-01-01 00:00:01', '吉普', 'Grand Cherokee', 'MUST SELL!\nair, moon roof, loaded', 4799);
    
    INSERT OVERWRITE LOCAL DIRECTORY 'tb_test_hive/'
      ROW FORMAT DELIMITED
      FIELDS TERMINATED BY ',' ESCAPED BY '\\'
      NULL defined AS ''
      SELECT * FROM tb_test_hive;

    以下是tb_test_hive/目录中文件的内容:

    1997-01-01 00:00:01,福特,E350,ac\, abs\, moon,3000
    1999-01-01 00:00:01,雪佛兰,Venture "Extended Edition",\\t,4900
    1999-01-01 00:00:01,雪佛兰,,,5000
    1996-01-01 00:00:01,吉普,Grand Cherokee,MUST SELL!,
    ,4799,,,

    从结果可以看出,字段中内嵌的换行符被识别成了记录分隔符,导致数据完整性遭到了破坏。实际上,若检视Hive中tb_test_hive表中的内容,会发现同样的问题;这表明在使用INSERT INTO TABLE ... VALUES语句生成表记录时,换行符已经被识别成记录分隔符。上述情况提示Hive中的字段取值不可包含换行符。

    小结

    R语言readr包对CSV文件导入导出操作均能够符合RFC 4180规范和W3C组织的有关建议,但有两点需要注意。首先,在导入CSV文件时,该软件包不能准确地区分空字符串和空值;为解决此问题,可以预先定义一个非空字符串代表空值,以之区别空字符串。其次,该软件包导出的CSV文件中的时间戳数据带有时区信息,有时这并不是使用者想要的;为解决此问题,可以在导出数据前先使用as.character()函数将时间戳字段转换为字符型字段。

    Python的pandas包对CSV文件导入导出操作也均能够符合RFC 4180规范和W3C组织的有关建议。但需要注意的是,无论是导入还是导出CSV文件,该软件包均不区分空字符串和空值;为解决此问题,可以预先定义一个非空字符串代表空值,以之区别空字符串。

    PostgreSQL的COPY指令对CSV文件的导入导出操作完全符合RFC 4180规范和W3C组织的有关建议,并且能够完整地按照预期处理复杂的测试数据,因此大可放心地使用。

    MySQL的LOAD DATA INFILESELECT INTO OUTFILE指令的行为与RFC 4180和W3C组织的有关建议存在较大的差异,使用者利用MySQL处理CSV文件时需要多加注意。以下是根据笔者经验总结的几条使用建议:

    • 在使用LOAD DATA INFILE导入CSV文件或使用SELECT INTO OUTFILE导出CSV文件时,均使用\作为转义字符、"作为封闭字符;
    • 在使用LOAD DATA INFILE导入CSV文件时:
      • 若数据本身含有特殊字符(包括转义字符、封闭字符、记录分隔符、字段分隔符等),则先将数据中的转义字符替换为两个连续的转义字符,然后再在其他特殊字符前面前置一个转义字符;
      • 数据中的空值以\N表示。

    例如,针对tb_test中的数据,在R语言中可以以如下方式(假设转义字符为\、封闭字符为"、记录分隔符为\n、字段分隔符为,)导出CSV文件,使之能够被顺利地读入MySQL中:

    tb_test %>%
      mutate_if(is.character, ~gsub("\\", "\\\\", ., fixed = T)) %>%
      mutate_if(is.character, ~gsub("\n", "\\\n", ., fixed = T)) %>%
      mutate_if(is.character, ~gsub("\"", "\\\"", ., fixed = T)) %>%
      mutate_if(is.character, ~gsub(",", "\\,", ., fixed = T)) %>%
      mutate_if(lubridate::is.POSIXt, ~as.character(.)) %>% # 去除时区信息
      write_csv("tb_test.csv", na = "\\N", quote_escape = "none", col_names = F)

    与上述各种软件相比,Hive的LOAD DATAINSERT指令对CSV文件的处理方式与RFC 4180及W3C组织的有关建议的吻合度最低,因此在使用时需多加注意。其中,尤其需要注意的是换行符在Hive中被强制视为记录分隔符,因此为保证数据完整性,字段中绝不可含有该字符。此外,反斜杠只能对字段分隔符进行转义,使后者能够嵌入字段取值中,但对其他字符则无转义作用(制表符等特殊字符只能以该字符本身表示)。

    注意事项:如果要用R(或Python)等软件读入从Hive导出的CSV文件时,最好将封闭字符设置为空。这是因为后者输出的CSV文件不含封闭字符,而前者在读取文件时会默认文件含有封闭字符;这可能导致某些换行符在读入时被误认为是数据内容,导致最终所得的行数变少。同样地,从R(或Python)等软件生成用于导入Hive的CSV文件时,也应该将封闭字符设为空,否则封闭字符将出现在字段取值之中。

    附录

    本附录提供了一些shell函数,可以用于在常见数据库中导入或导出csv文件。

    hive_to_csv () {
      # 用法:hive_to_csv [选项]... 源表 目标文件
    
      local delim='\t'
      local escape='\\'
      local null='\\N' # 与MySQL的LOAD DATA指令相兼容
      local table=${@: -2:1}
      local file=${@: -1:1}
    
      local OPTIND
      while getopts 'd:e:n:' OPTION; do
        case "${OPTION}" in
          d) local delim="${OPTARG}";;
          e) local escape="${OPTARG}";;
          n) local null="${OPTARG}";;
        esac
      done
    
      if [[ -z "${table}" || -z "${file}" ]]; then
        echo "请指定源表和目标文件"
        exit 1
      fi
    
      if [[ -f "${file}" ]]; then
        rm "${file}"
      elif [[ -d "${file}" ]]; then
        echo "${file} 是一个目录"
        exit 1
      fi
    
      sql=$(cat <<eof
        INSERT OVERWRITE LOCAL DIRECTORY '${file}'
          ROW FORMAT DELIMITED
          FIELDS TERMINATED BY '${delim}' ESCAPED BY '${escape}'
          NULL DEFINED AS '${null}'
          SELECT * FROM ${table};
    eof
      )
    
      hive -e "${sql}"
    
      if [[ ${?} -ne 0 ]]; then exit 1; fi
    
      find ${file} -regextype egrep -regex '.*/[0-9_]*' -exec cat {} \; > ${file}.tmp && rm -r ${file} && mv ${file}.tmp ${file} # 注意不要将*.crc文件和数据文件混到一起
    
      return
    }
    csv_to_mysql () {
      # 用法:csv_to_mysql [选项]... 源文件 目标表
      # 运行此函数前必须先通过alias指定数据库登录信息
      # alias mysql="mysql -h $host -u ${user} -D ${database} --password ${password}")
    
      local delim='\t'
      local escape='\\'
      local quote='"'
      local skip=0
      local file=${@: -2:1}
      local table=${@: -1:1}
      
      local OPTIND
      while getopts 'd:e:q:s:' OPTION; do
        case "${OPTION}" in
          d) local delim="${OPTARG}";;
          e) local escape="${OPTARG}";;
          q) local quote="${OPTARG}";;
          s) local skip="${OPTARG}";;
        esac
      done
    
      if [[ -z "${table}" || -z "${file}" ]]; then
        echo "请指定源文件和目标表"
        exit 1
      fi
    
      sql=$(cat <<eof
        LOAD DATA
          INFILE '${file}'
            INTO TABLE ${table}
            CHARACTER SET 'UTF8'
            FIELDS
            TERMINATED BY '${delim}'
            OPTIONALLY ENCLOSED BY '${quote}'
            ESCAPED BY '${escape}'
            IGNORE ${skip} LINES;
    eof
      )
    
      mysql -e "${sql}"
    
      if [[ ${?} -ne 0 ]]; then exit 1; fi
    
      return
    }

    参考文献

    CSV on the Web Working Group. 2015. 《Model for Tabular Data and Metadata on the Web. W3C Recommendation. 2015年. https://www.w3.org/TR/tabular-data-model/.
    Shafranovich, Yakov. 2005. 《Common Format and MIME Type for Comma-Separated Values (CSV) Files》. Internet Engineering Task Force. https://www.ietf.org/rfc/rfc4180.txt.
    Wikipedia. 2019. 《Comma-Separated Values》. 2019年. https://en.wikipedia.org/wiki/Comma-separated_values.