CSV 文件的形式多种多样,有些文件包含许多错误,这使得干净地读取它们变得非常困难。为了帮助用户读取这些文件,DuckDB 支持详细的错误消息、跳过错误行的功能,以及将错误行存储到临时表中的功能,从而辅助用户进行数据清洗。
结构错误
DuckDB 支持检测并跳过多种不同的结构错误。在本节中,我们将通过示例逐一介绍这些错误。在这些示例中,请参考下表:
CREATE TABLE people (name VARCHAR, birth_date DATE);
DuckDB 检测以下错误类型:
CAST:当 CSV 文件中的列无法转换为预期的模式值时,会发生转换错误。例如,行Pedro,The 90s会导致错误,因为字符串The 90s无法转换为日期。MISSING COLUMNS:如果 CSV 文件中的某一行包含的列数少于预期,就会发生此错误。在我们的示例中,我们预期有两列;因此,仅有一个值的行(例如Pedro)会触发此错误。TOO MANY COLUMNS:如果 CSV 中的某一行包含的列数多于预期,就会发生此错误。在我们的示例中,任何超过两列的行都会导致此错误,例如Pedro,01-01-1992,pdet。UNQUOTED VALUE:CSV 行中的引号值必须在结尾处正确闭合;如果一个引号值在整个过程中保持开启状态,则会导致错误。例如,假设我们的扫描器使用quote='"',那么行"pedro"holanda, 01-01-1992将会提示“未引用值”错误。LINE SIZE OVER MAXIMUM:DuckDB 有一个参数用于设置 CSV 文件允许的最大行大小,默认值为 2,097,152 字节。假设我们的扫描器设置为max_line_size = 25,则行Pedro Holanda, 01-01-1992将产生错误,因为它超过了 25 字节。INVALID ENCODING:DuckDB 支持 UTF-8、UTF-16 和 Latin-1 编码的字符串。包含其他字符的行会产生错误。例如,行pedro\xff\xff, 01-01-1992将会出现问题。
CSV 错误剖析
默认情况下,在执行 CSV 读取时,如果遇到任何结构错误,扫描器将立即停止扫描过程并将错误抛给用户。这些错误旨在提供尽可能多的信息,以便用户直接在其 CSV 文件中进行评估。
以下是完整错误消息的示例:
Conversion Error:
CSV Error on Line: 5648
Original Line: Pedro,The 90s
Error when converting column "birth_date". date field value out of range: "The 90s", expected format is (DD-MM-YYYY)
Column date is being converted as type DATE
This type was auto-detected from the CSV file.
Possible solutions:
* Override the type for this column manually by setting the type explicitly, e.g., types={'birth_date': 'VARCHAR'}
* Set the sample size to a larger value to enable the auto-detection to scan more values, e.g., sample_size=-1
* Use a COPY statement to automatically derive types from an existing table.
file= people.csv
delimiter = , (Auto-Detected)
quote = " (Auto-Detected)
escape = " (Auto-Detected)
new_line = \r\n (Auto-Detected)
header = true (Auto-Detected)
skip_rows = 0 (Auto-Detected)
date_format = (DD-MM-YYYY) (Auto-Detected)
timestamp_format = (Auto-Detected)
null_padding=0
sample_size=20480
ignore_errors=false
all_varchar=0
第一个数据块向我们提供了有关错误发生位置的信息,包括行号、原始 CSV 行以及出现问题的字段。
Conversion Error:
CSV Error on Line: 5648
Original Line: Pedro,The 90s
Error when converting column "birth_date". date field value out of range: "The 90s", expected format is (DD-MM-YYYY)
第二个数据块为我们提供了潜在的解决方案。
Column date is being converted as type DATE
This type was auto-detected from the CSV file.
Possible solutions:
* Override the type for this column manually by setting the type explicitly, e.g., types={'birth_date': 'VARCHAR'}
* Set the sample size to a larger value to enable the auto-detection to scan more values, e.g., sample_size=-1
* Use a COPY statement to automatically derive types from an existing table.
由于该字段的类型是自动检测的,它建议将该字段定义为 VARCHAR,或者充分利用整个数据集进行类型检测。
最后,最后一个数据块展示了扫描器中使用的一些可能导致错误的选项,并指明它们是自动检测到的还是由用户手动设置的。
使用 ignore_errors 选项
在某些情况下,CSV 文件可能存在多个结构错误,而用户只想跳过这些错误并读取正确的数据。通过使用 ignore_errors 选项,可以读取有误的 CSV 文件。设置此选项后,那些原本会导致 CSV 解析器生成错误的行将被忽略。在我们的示例中,我们将演示一个 CAST 错误,但请注意,“结构错误”部分中描述的任何错误都会导致错误行被跳过。
例如,考虑以下 CSV 文件 faulty.csv:
Pedro,31
Oogie Boogie, three
如果您在读取 CSV 文件时指定第一列为 VARCHAR,第二列为 INTEGER,则加载文件会失败,因为字符串 three 无法转换为 INTEGER。
例如,以下查询将抛出转换错误:
FROM read_csv('faulty.csv', columns = {'name': 'VARCHAR', 'age': 'INTEGER'});
然而,设置 ignore_errors 后,文件中的第二行将被跳过,仅输出完整的首行。例如:
FROM read_csv(
'faulty.csv',
columns = {'name': 'VARCHAR', 'age': 'INTEGER'},
ignore_errors = true
);
输出
| name | age |
|---|---|
| Pedro | 31 |
需要注意的是,CSV 解析器会受到投影下推(projection pushdown)优化的影响。因此,如果我们只选择 name 列,那么两行都会被视为有效,因为针对 age 列的转换错误将永远不会发生。例如:
SELECT name
FROM read_csv('faulty.csv', columns = {'name': 'VARCHAR', 'age': 'INTEGER'});
输出
| name |
|---|
| Pedro |
| Oogie Boogie |
检索有误的 CSV 行
能够读取有误的 CSV 文件很重要,但对于许多数据清洗操作,还需要确切地知道哪些行已损坏以及解析器在这些行上发现了哪些错误。对于此类场景,可以使用 DuckDB 的 CSV 拒绝表(Rejects Table)功能。默认情况下,此功能会创建两个临时表。
reject_scans:存储有关 CSV 扫描器参数的信息。reject_errors:存储有关每个 CSV 错误行以及它们在哪个 CSV 扫描器中发生的信息。
请注意,“结构错误”部分中描述的任何错误都会存储在拒绝表中。此外,如果一行有多个错误,则会为同一行存储多个条目,每个错误对应一个条目。
拒绝扫描(Reject Scans)
CSV 拒绝扫描表返回以下信息:
| 列名 | 描述 | 类型 |
|---|---|---|
scan_id |
DuckDB 中用于表示该扫描器的内部 ID | UBIGINT |
file_id |
扫描器可能跨越多个文件,因此 file_id 代表扫描器中的唯一文件 | UBIGINT |
file_path |
文件路径 | VARCHAR |
delimiter |
使用的分隔符,例如 ; | VARCHAR |
quote |
使用的引号,例如 " | VARCHAR |
escape |
使用的引号,例如 " | VARCHAR |
newline_delimiter |
使用的换行符,例如 \r\n | VARCHAR |
skip_rows |
文件顶部跳过的行数 | UINTEGER |
has_header |
文件是否包含标题行 | BOOLEAN |
columns |
文件的模式(即所有列名和类型) | VARCHAR |
date_format |
日期类型使用的格式 | VARCHAR |
timestamp_format |
时间戳类型使用的格式 | VARCHAR |
user_arguments |
用户手动设置的任何额外扫描器参数 | VARCHAR |
拒绝错误(Reject Errors)
CSV 拒绝错误表返回以下信息:
| 列名 | 描述 | 类型 |
|---|---|---|
scan_id |
DuckDB 中用于表示该扫描器的内部 ID,用于与拒绝扫描表连接 | UBIGINT |
file_id |
file_id 代表扫描器中的唯一文件,用于与拒绝扫描表连接 | UBIGINT |
line |
CSV 文件中发生错误的行号。 | UBIGINT |
line_byte_position |
发生错误的行起始处的字节位置。 | UBIGINT |
byte_position |
发生错误的字节位置。 | UBIGINT |
column_idx |
如果错误发生在特定列中,则为该列的索引。 | UBIGINT |
列名 (column_name) |
如果错误发生在特定列中,则为该列的名称。 | VARCHAR |
error_type |
发生的错误类型。 | ENUM |
csv_line |
原始的 CSV 行。 | VARCHAR |
error_message |
DuckDB 产生的错误消息。 | VARCHAR |
参数
下面列出的参数在 read_csv 函数中使用,用于配置 CSV 拒绝表。
| 名称 | 描述 | 类型 | 默认值 |
|---|---|---|---|
store_rejects |
如果设置为 true,则文件中的任何错误都将被跳过并存储在默认的拒绝临时表中。 | BOOLEAN |
False |
rejects_scan |
存储有误 CSV 文件的扫描信息所使用的临时表名称。 | VARCHAR |
reject_scans |
rejects_table |
存储 CSV 文件错误行信息所使用的临时表名称。 | VARCHAR |
reject_errors |
rejects_limit |
记录在拒绝表中的 CSV 文件错误记录数量的上限。使用 0 表示不应用限制。 | BIGINT |
0 |
要将有误 CSV 行的信息存储在拒绝表中,用户只需将 store_rejects 选项设置为 true。例如:
FROM read_csv(
'faulty.csv',
columns = {'name': 'VARCHAR', 'age': 'INTEGER'},
store_rejects = true
);
然后,您可以查询 reject_scans 和 reject_errors 表,以检索有关被拒绝元组的信息。例如:
FROM reject_scans;
输出
| scan_id | file_id | file_path | delimiter | quote | escape | newline_delimiter | skip_rows | has_header | columns | date_format | timestamp_format | user_arguments |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 0 | faulty.csv | , | " | " | \n | 0 | false | {'name': 'VARCHAR','age': 'INTEGER'} | store_rejects=true |
FROM reject_errors;
输出
| scan_id | file_id | line | line_byte_position | byte_position | column_idx | 列名 (column_name) | error_type | csv_line | error_message |
|---|---|---|---|---|---|---|---|---|---|
| 5 | 0 | 2 | 10 | 23 | 2 | age | CAST | Oogie Boogie, three | 转换列 "age" 时出错。无法将字符串 " three" 转换为 'INTEGER' |