mirror of
https://github.com/apache/arrow.git
synced 2026-10-29 09:05:47 +08:00
ARROW-6231: [C++] Allow generating CSV column names
Add an option that, if enabled, will autogenerate column names for the target table rather than read them from the CSV file.
This commit is contained in:
1 parent
cef82b41f5
commit
cc7fdc6997
5 files changed
+97
-18
No files matched your search
@@ -100,8 +100,13 @@ struct ARROW_EXPORT ReadOptions {
|
||||
|
||||
// Number of header rows to skip (not including the row of column names, if any)
|
||||
int32_t skip_rows = 0;
|
||||
// Column names (if empty, will be read from first row after `skip_rows`)
|
||||
// Column names for the target table.
|
||||
// If empty, fall back on autogenerate_column_names.
|
||||
std::vector<std::string> column_names;
|
||||
// Whether to autogenerate column names if `column_names` is empty.
|
||||
// If true, column names will be of the form "f0", "f1"...
|
||||
// If false, column names will be read from the first CSV row after `skip_rows`.
|
||||
bool autogenerate_column_names = false;
|
||||
|
||||
static ReadOptions Defaults();
|
||||
};
|
||||
|
||||
+28
-12
@@ -162,29 +162,34 @@ class BaseTableReader : public csv::TableReader {
|
||||
}
|
||||
|
||||
if (read_options_.column_names.empty()) {
|
||||
// Read one row with column names
|
||||
// Parse one row (either to read column names or to know the number of columns)
|
||||
BlockParser parser(pool_, parse_options_, num_csv_cols_, 1);
|
||||
uint32_t parsed_size = 0;
|
||||
RETURN_NOT_OK(parser.Parse(reinterpret_cast<const char*>(cur_data_),
|
||||
static_cast<uint32_t>(cur_size_), &parsed_size));
|
||||
if (parser.num_rows() != 1) {
|
||||
return Status::Invalid(
|
||||
"Could not read column names from CSV file, either "
|
||||
"Could not read first row from CSV file, either "
|
||||
"file is too short or header is larger than block size");
|
||||
}
|
||||
if (parser.num_cols() == 0) {
|
||||
return Status::Invalid("No columns in CSV file");
|
||||
}
|
||||
// Read column names from last header row
|
||||
auto visit = [&](const uint8_t* data, uint32_t size, bool quoted) -> Status {
|
||||
column_names_.emplace_back(reinterpret_cast<const char*>(data), size);
|
||||
return Status::OK();
|
||||
};
|
||||
RETURN_NOT_OK(parser.VisitLastRow(visit));
|
||||
DCHECK_EQ(static_cast<size_t>(parser.num_cols()), column_names_.size());
|
||||
// Skip parsed header row
|
||||
cur_data_ += parsed_size;
|
||||
cur_size_ -= parsed_size;
|
||||
|
||||
if (read_options_.autogenerate_column_names) {
|
||||
column_names_ = GenerateColumnNames(parser.num_cols());
|
||||
} else {
|
||||
// Read column names from header row
|
||||
auto visit = [&](const uint8_t* data, uint32_t size, bool quoted) -> Status {
|
||||
column_names_.emplace_back(reinterpret_cast<const char*>(data), size);
|
||||
return Status::OK();
|
||||
};
|
||||
RETURN_NOT_OK(parser.VisitLastRow(visit));
|
||||
DCHECK_EQ(static_cast<size_t>(parser.num_cols()), column_names_.size());
|
||||
// Skip parsed header row
|
||||
cur_data_ += parsed_size;
|
||||
cur_size_ -= parsed_size;
|
||||
}
|
||||
} else {
|
||||
column_names_ = read_options_.column_names;
|
||||
}
|
||||
@@ -271,6 +276,17 @@ class BaseTableReader : public csv::TableReader {
|
||||
return ColumnBuilder::MakeNull(pool_, type, task_group_, out);
|
||||
}
|
||||
|
||||
std::vector<std::string> GenerateColumnNames(int32_t num_cols) {
|
||||
std::vector<std::string> res;
|
||||
res.reserve(num_cols);
|
||||
for (int32_t i = 0; i < num_cols; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "f" << i;
|
||||
res.push_back(ss.str());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Trigger conversion of parsed block data
|
||||
Status ProcessData(const std::shared_ptr<BlockParser>& parser, int64_t block_index) {
|
||||
for (auto& builder : column_builders_) {
|
||||
|
||||
+26
-5
@@ -55,8 +55,13 @@ cdef class ReadOptions:
|
||||
The number of rows to skip at the start of the CSV data, not
|
||||
including the row of column names (if any).
|
||||
column_names: list, optional
|
||||
The column names in the CSV file. If empty, column names will be
|
||||
read from the first row after `skip_rows`.
|
||||
The column names of the target table. If empty, fall back on
|
||||
`autogenerate_column_names`.
|
||||
autogenerate_column_names: bool, optional (default False)
|
||||
Whether to autogenerate column names if `column_names` is empty.
|
||||
If true, column names will be of the form "f0", "f1"...
|
||||
If false, column names will be read from the first CSV row
|
||||
after `skip_rows`.
|
||||
"""
|
||||
cdef:
|
||||
CCSVReadOptions options
|
||||
@@ -65,7 +70,7 @@ cdef class ReadOptions:
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, use_threads=None, block_size=None, skip_rows=None,
|
||||
column_names=None):
|
||||
column_names=None, autogenerate_column_names=None):
|
||||
self.options = CCSVReadOptions.Defaults()
|
||||
if use_threads is not None:
|
||||
self.use_threads = use_threads
|
||||
@@ -75,6 +80,8 @@ cdef class ReadOptions:
|
||||
self.skip_rows = skip_rows
|
||||
if column_names is not None:
|
||||
self.column_names = column_names
|
||||
if autogenerate_column_names is not None:
|
||||
self.autogenerate_column_names= autogenerate_column_names
|
||||
|
||||
@property
|
||||
def use_threads(self):
|
||||
@@ -115,8 +122,8 @@ cdef class ReadOptions:
|
||||
@property
|
||||
def column_names(self):
|
||||
"""
|
||||
The column names in the CSV file. If empty, column names will be
|
||||
read from the first row after `skip_rows`.
|
||||
The column names of the target table. If empty, fall back on
|
||||
`autogenerate_column_names`.
|
||||
"""
|
||||
return [frombytes(s) for s in self.options.column_names]
|
||||
|
||||
@@ -126,6 +133,20 @@ cdef class ReadOptions:
|
||||
for item in value:
|
||||
self.options.column_names.push_back(tobytes(item))
|
||||
|
||||
@property
|
||||
def autogenerate_column_names(self):
|
||||
"""
|
||||
Whether to autogenerate column names if `column_names` is empty.
|
||||
If true, column names will be of the form "f0", "f1"...
|
||||
If false, column names will be read from the first CSV row
|
||||
after `skip_rows`.
|
||||
"""
|
||||
return self.options.autogenerate_column_names
|
||||
|
||||
@autogenerate_column_names.setter
|
||||
def autogenerate_column_names(self, value):
|
||||
self.options.autogenerate_column_names = value
|
||||
|
||||
|
||||
cdef class ParseOptions:
|
||||
"""
|
||||
|
||||
@@ -1145,6 +1145,7 @@ cdef extern from "arrow/csv/api.h" namespace "arrow::csv" nogil:
|
||||
int32_t block_size
|
||||
int32_t skip_rows
|
||||
vector[c_string] column_names
|
||||
c_bool autogenerate_column_names
|
||||
|
||||
@staticmethod
|
||||
CCSVReadOptions Defaults()
|
||||
|
||||
@@ -80,12 +80,23 @@ def test_read_options():
|
||||
opts.column_names = ["ab", "cd"]
|
||||
assert opts.column_names == ["ab", "cd"]
|
||||
|
||||
assert opts.autogenerate_column_names is False
|
||||
opts.autogenerate_column_names = True
|
||||
assert opts.autogenerate_column_names is True
|
||||
|
||||
opts = cls(block_size=1234, use_threads=False, skip_rows=42,
|
||||
column_names=["a", "b", "c"])
|
||||
assert opts.block_size == 1234
|
||||
assert opts.use_threads is False
|
||||
assert opts.skip_rows == 42
|
||||
assert opts.column_names == ["a", "b", "c"]
|
||||
assert opts.autogenerate_column_names is False
|
||||
|
||||
opts = cls(autogenerate_column_names=True)
|
||||
assert opts.use_threads is True
|
||||
assert opts.skip_rows == 0
|
||||
assert opts.column_names == []
|
||||
assert opts.autogenerate_column_names is True
|
||||
|
||||
|
||||
def test_parse_options():
|
||||
@@ -318,6 +329,31 @@ class BaseTestCSVRead:
|
||||
"y": ["kl", "op"],
|
||||
}
|
||||
|
||||
def test_header_autogenerate_column_names(self):
|
||||
rows = b"ab,cd\nef,gh\nij,kl\nmn,op\n"
|
||||
|
||||
opts = ReadOptions()
|
||||
opts.autogenerate_column_names = True
|
||||
table = self.read_bytes(rows, read_options=opts)
|
||||
self.check_names(table, ["f0", "f1"])
|
||||
assert table.to_pydict() == {
|
||||
"f0": ["ab", "ef", "ij", "mn"],
|
||||
"f1": ["cd", "gh", "kl", "op"],
|
||||
}
|
||||
|
||||
opts.skip_rows = 3
|
||||
table = self.read_bytes(rows, read_options=opts)
|
||||
self.check_names(table, ["f0", "f1"])
|
||||
assert table.to_pydict() == {
|
||||
"f0": ["mn"],
|
||||
"f1": ["op"],
|
||||
}
|
||||
|
||||
# Not enough rows, impossible to infer number of columns
|
||||
opts.skip_rows = 4
|
||||
with pytest.raises(pa.ArrowInvalid):
|
||||
table = self.read_bytes(rows, read_options=opts)
|
||||
|
||||
def test_include_columns(self):
|
||||
rows = b"ab,cd\nef,gh\nij,kl\nmn,op\n"
|
||||
|
||||
|
||||
Reference in new issue
Block a user