fix(dataset): keep CSV export columns aligned - #2079
Conversation
Mantisus
left a comment
There was a problem hiding this comment.
Thank you for finding this bug. However, we cannot use extrasaction=‘ignore’
There was a problem hiding this comment.
Pull request overview
This PR fixes CSV export for Dataset items that don’t share identical key order/sets, preventing values from being written under the wrong column when exporting heterogeneous items.
Changes:
- Update
export_csv_to_streamto write rows by key (viacsv.DictWriter) instead of positionalitem.values(). - Add regression tests covering heterogeneous item keys/order and skipping empty items during header selection.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/crawlee/_utils/file.py |
Switches CSV export implementation to key-based writing and changes how CSV headers are derived. |
tests/unit/_utils/test_file.py |
Adds regression tests for column alignment with heterogeneous items and for skipping empty mappings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
`export_csv_to_stream` built the header from the first item's keys but then wrote every row positionally with `csv.writer` and `item.values()`. Since `Dataset.push_data` accepts arbitrary mappings per item with no schema, items whose keys differ in membership or order from the first item had their values written under the wrong columns (or shifted into columns that do not exist), silently corrupting the CSV export of any non-uniform dataset. Write rows with `csv.DictWriter` keyed on the first item's fields and `extrasaction='ignore'`, so each value lands in its own column and unexpected keys are dropped. This matches the column-aligned behavior of Crawlee for JavaScript. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
5b95959 to
bde4ba2
Compare
|
pushed a revision addressing the latest review and ci feedback. |
|
Same as here #2074 (comment)
|
vdusek
left a comment
There was a problem hiding this comment.
A few important things and then a few nits.
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
|
@vdusek addressed all new threads, including js-default parity, streaming behavior, bounded default memory, and value preservation tests. rebased and re-requesting review. thank you. |
…eagerly Address review findings on the CSV export rewrite: - Warn once per export, naming every key dropped because it is absent from the first item, instead of losing those values silently. Master wrote them to the wrong column, which was wrong but at least visible. - Build a throwaway writer up front so invalid options (e.g. `delimiter='ab'`) raise even when the dataset is empty. Master validated eagerly; the lazy writer made a misconfigured export succeed on a run that scraped nothing and fail on the next one. - Pass `extrasaction='ignore'` on the buffered path too, so both paths construct the writer identically and narrowing `fieldnames` later cannot raise mid-export. - Declare `restval`, which already worked but was rejected by the type checker at every call site, and correct the `ExportDataCsvWriterKwargs` docstring that named `csv.DictWriter` while listing only `csv.writer` options. - Cover the empty-items guard, the `if item` filter on the buffered path, and `collect_all_keys` through `Dataset.export_to` and `BasicCrawler.export_data`, none of which had a test. - Document the column behavior and `collect_all_keys` in the dataset export example.
export_csv_to_stream(src/crawlee/_utils/file.py) built the CSV header from thefirst non-empty item's keys, but then wrote every row positionally with
csv.writeranditem.values().Dataset.push_dataaccepts an arbitraryMapping[str, JsonSerializable]per item with no schema enforcement, so a datasetwhere later items have a different key set or key order than the first (normal when
different handlers push different fields) had its values written under the wrong
columns, or shifted into columns that do not exist, silently corrupting the CSV.
Example: pushing
{'name': 'Alice', 'age': 30} {'name': 'Bob', 'city': 'NYC', 'age': 25} {'age': 40, 'name': 'Carol'}and exporting with
content_type='csv'producedBob's row has three values in two columns, andCarol's row putsage(40) undernameandname(Carol) underage.This change writes rows with
csv.DictWriter, keyed on the first item's fields andextrasaction='ignore', so each value lands in its own column and keys not in theheader are dropped. The same input now exports as
which matches the column-aligned behavior of Crawlee for JavaScript (whose CSV
export, by default, derives the header from the first item, maps each row by key,
and ignores extra keys). Items missing a header key get an empty cell, as before for
absent values.
The existing
lineterminator='\n'default (which avoids double line endings onWindows) is preserved: it still flows through
**kwargsintoDictWriter.Issues
export path. (Happy to open a tracking issue first if the maintainers prefer.)
Testing
tests/unit/_utils/test_file.py:test_export_csv_to_stream_keeps_columns_aligned_for_heterogeneous_items-asserts values stay under their own header column for items with differing key
sets/orders (fails on the old positional code with the exact misalignment above).
test_export_csv_to_stream_skips_empty_items- empty mappings are skipped and donot define or shift the header.
uv run pytest tests/unit/_utils/test_file.py tests/unit/storages/test_dataset.pyplus the
test_basic_crawler.pypush-and-export tests: 273 passed, 2 skipped(unrelated memory-storage persistence skips).
uv run poe lint(ruff format + check) anduv run poe type-check(ty) are cleanfor the changed files.
Checklist