Files
deepset-ai--haystack/haystack/components/preprocessors/csv_document_cleaner.py
T
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

179 lines
7.1 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from copy import deepcopy
from io import StringIO
from typing import Optional
from haystack import Document, component, logging
from haystack.lazy_imports import LazyImport
with LazyImport("Run 'pip install pandas'") as pandas_import:
import pandas as pd
logger = logging.getLogger(__name__)
@component
class CSVDocumentCleaner:
"""
A component for cleaning CSV documents by removing empty rows and columns.
This component processes CSV content stored in Documents, allowing
for the optional ignoring of a specified number of rows and columns before performing
the cleaning operation. Additionally, it provides options to keep document IDs and
control whether empty rows and columns should be removed.
"""
def __init__(
self,
*,
ignore_rows: int = 0,
ignore_columns: int = 0,
remove_empty_rows: bool = True,
remove_empty_columns: bool = True,
keep_id: bool = False,
) -> None:
"""
Initializes the CSVDocumentCleaner component.
:param ignore_rows: Number of rows to ignore from the top of the CSV table before processing.
:param ignore_columns: Number of columns to ignore from the left of the CSV table before processing.
:param remove_empty_rows: Whether to remove rows that are entirely empty.
:param remove_empty_columns: Whether to remove columns that are entirely empty.
:param keep_id: Whether to retain the original document ID in the output document.
Rows and columns ignored using these parameters are preserved in the final output, meaning
they are not considered when removing empty rows and columns.
"""
self.ignore_rows = ignore_rows
self.ignore_columns = ignore_columns
self.remove_empty_rows = remove_empty_rows
self.remove_empty_columns = remove_empty_columns
self.keep_id = keep_id
pandas_import.check()
@component.output_types(documents=list[Document])
def run(self, documents: list[Document]) -> dict[str, list[Document]]:
"""
Cleans CSV documents by removing empty rows and columns while preserving specified ignored rows and columns.
:param documents: List of Documents containing CSV-formatted content.
:return: A dictionary with a list of cleaned Documents under the key "documents".
Processing steps:
1. Reads each document's content as a CSV table.
2. Retains the specified number of `ignore_rows` from the top and `ignore_columns` from the left.
3. Drops any rows and columns that are entirely empty (if enabled by `remove_empty_rows` and
`remove_empty_columns`).
4. Reattaches the ignored rows and columns to maintain their original positions.
5. Returns the cleaned CSV content as a new `Document` object, with an option to retain the original
document ID.
"""
if len(documents) == 0:
return {"documents": []}
ignore_rows = self.ignore_rows
ignore_columns = self.ignore_columns
cleaned_documents = []
for document in documents:
try:
df = pd.read_csv(StringIO(document.content), header=None, dtype=object)
except Exception as e:
logger.exception(
"Error processing document {id}. Keeping it, but skipping cleaning. Error: {error}",
id=document.id,
error=e,
)
cleaned_documents.append(document)
continue
if ignore_rows > df.shape[0] or ignore_columns > df.shape[1]:
logger.warning(
"Document {id} has fewer rows {df_rows} or columns {df_cols} "
"than the number of rows {rows} or columns {cols} to ignore. "
"Keeping the entire document.",
id=document.id,
df_rows=df.shape[0],
df_cols=df.shape[1],
rows=ignore_rows,
cols=ignore_columns,
)
cleaned_documents.append(document)
continue
final_df = self._clean_df(df=df, ignore_rows=ignore_rows, ignore_columns=ignore_columns)
clean_doc = Document(
id=document.id if self.keep_id else "",
content=final_df.to_csv(index=False, header=False, lineterminator="\n"),
blob=document.blob,
meta=deepcopy(document.meta),
score=document.score,
embedding=document.embedding,
sparse_embedding=document.sparse_embedding,
)
cleaned_documents.append(clean_doc)
return {"documents": cleaned_documents}
def _clean_df(self, df: "pd.DataFrame", ignore_rows: int, ignore_columns: int) -> "pd.DataFrame":
"""
Cleans a DataFrame by removing empty rows and columns while preserving ignored sections.
:param df: The input DataFrame representing the CSV data.
:param ignore_rows: Number of top rows to ignore.
:param ignore_columns: Number of left columns to ignore.
"""
# Get ignored rows and columns
ignored_rows = self._get_ignored_rows(df=df, ignore_rows=ignore_rows)
ignored_columns = self._get_ignored_columns(df=df, ignore_columns=ignore_columns)
final_df = df.iloc[ignore_rows:, ignore_columns:]
# Drop rows that are entirely empty
if self.remove_empty_rows:
final_df = final_df.dropna(axis=0, how="all")
# Drop columns that are entirely empty
if self.remove_empty_columns:
final_df = final_df.dropna(axis=1, how="all")
# Reattach ignored rows
if ignore_rows > 0 and ignored_rows is not None:
# Keep only relevant columns
ignored_rows = ignored_rows.loc[:, final_df.columns]
final_df = pd.concat([ignored_rows, final_df], axis=0)
# Reattach ignored columns
if ignore_columns > 0 and ignored_columns is not None:
# Keep only relevant rows
ignored_columns = ignored_columns.loc[final_df.index, :]
final_df = pd.concat([ignored_columns, final_df], axis=1)
return final_df
@staticmethod
def _get_ignored_rows(df: "pd.DataFrame", ignore_rows: int) -> Optional["pd.DataFrame"]:
"""
Extracts the rows to be ignored from the DataFrame.
:param df: The input DataFrame.
:param ignore_rows: Number of rows to extract from the top.
"""
if ignore_rows > 0:
return df.iloc[:ignore_rows, :]
return None
@staticmethod
def _get_ignored_columns(df: "pd.DataFrame", ignore_columns: int) -> Optional["pd.DataFrame"]:
"""
Extracts the columns to be ignored from the DataFrame.
:param df: The input DataFrame.
:param ignore_columns: Number of columns to extract from the left.
"""
if ignore_columns > 0:
return df.iloc[:, :ignore_columns]
return None