From a97e4252e3bdbd10bc1476ecaf7e076674d7e70c Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Sat, 3 Jun 2023 15:44:12 -0400 Subject: [PATCH] feat: add `UnstructuredExcelLoader` for `.xlsx` and `.xls` files (#5617) # Unstructured Excel Loader Adds an `UnstructuredExcelLoader` class for `.xlsx` and `.xls` files. Works with `unstructured>=0.6.7`. A plain text representation of the Excel file will be available under the `page_content` attribute in the doc. If you use the loader in `"elements"` mode, an HTML representation of the Excel file will be available under the `text_as_html` metadata key. Each sheet in the Excel document is its own document. ### Testing ```python from langchain.document_loaders import UnstructuredExcelLoader loader = UnstructuredExcelLoader( "example_data/stanley-cups.xlsx", mode="elements" ) docs = loader.load() ``` ## Who can review? @hwchase17 @eyurtsev --- .../examples/example_data/stanley-cups.xlsx | Bin 0 -> 6339 bytes .../document_loaders/examples/excel.ipynb | 79 ++++++++++++++++++ langchain/document_loaders/__init__.py | 2 + langchain/document_loaders/excel.py | 22 +++++ .../document_loaders/test_excel.py | 15 ++++ .../examples/stanley-cups.xlsx | Bin 0 -> 6339 bytes 6 files changed, 118 insertions(+) create mode 100644 docs/modules/indexes/document_loaders/examples/example_data/stanley-cups.xlsx create mode 100644 docs/modules/indexes/document_loaders/examples/excel.ipynb create mode 100644 langchain/document_loaders/excel.py create mode 100644 tests/integration_tests/document_loaders/test_excel.py create mode 100644 tests/integration_tests/examples/stanley-cups.xlsx diff --git a/docs/modules/indexes/document_loaders/examples/example_data/stanley-cups.xlsx b/docs/modules/indexes/document_loaders/examples/example_data/stanley-cups.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ebc66599b2ace3435646bc31f5ae7ca4d7313e69 GIT binary patch literal 6339 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 literal 0 HcmV?d00001 diff --git a/docs/modules/indexes/document_loaders/examples/excel.ipynb b/docs/modules/indexes/document_loaders/examples/excel.ipynb new file mode 100644 index 0000000000..ecb3628eec --- /dev/null +++ b/docs/modules/indexes/document_loaders/examples/excel.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "22a849cc", + "metadata": {}, + "source": [ + "# Microsoft Excel\n", + "\n", + "The `UnstructuredExcelLoader` is used to load `Microsoft Excel` files. The loader works with both `.xlsx` and `.xls` files. The page content will be the raw text of the Excel file. If you use the loader in `\"elements\"` mode, an HTML representation of the Excel file will be available in the document metadata under the `text_as_html` key." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e6616e3a", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.document_loaders import UnstructuredExcelLoader" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a654e4d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Document(page_content='\\n \\n \\n Team\\n Location\\n Stanley Cups\\n \\n \\n Blues\\n STL\\n 1\\n \\n \\n Flyers\\n PHI\\n 2\\n \\n \\n Maple Leafs\\n TOR\\n 13\\n \\n \\n', metadata={'source': 'example_data/stanley-cups.xlsx', 'filename': 'stanley-cups.xlsx', 'file_directory': 'example_data', 'filetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'page_number': 1, 'page_name': 'Stanley Cups', 'text_as_html': '\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
TeamLocationStanley Cups
BluesSTL1
FlyersPHI2
Maple LeafsTOR13
', 'category': 'Table'})" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "loader = UnstructuredExcelLoader(\n", + " \"example_data/stanley-cups.xlsx\",\n", + " mode=\"elements\"\n", + ")\n", + "docs = loader.load()\n", + "docs[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ab94bde", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langchain/document_loaders/__init__.py b/langchain/document_loaders/__init__.py index 6153bcccb1..3ec4db3534 100644 --- a/langchain/document_loaders/__init__.py +++ b/langchain/document_loaders/__init__.py @@ -32,6 +32,7 @@ from langchain.document_loaders.email import ( ) from langchain.document_loaders.epub import UnstructuredEPubLoader from langchain.document_loaders.evernote import EverNoteLoader +from langchain.document_loaders.excel import UnstructuredExcelLoader from langchain.document_loaders.facebook_chat import FacebookChatLoader from langchain.document_loaders.figma import FigmaFileLoader from langchain.document_loaders.gcs_directory import GCSDirectoryLoader @@ -223,6 +224,7 @@ __all__ = [ "UnstructuredAPIFileLoader", "UnstructuredEPubLoader", "UnstructuredEmailLoader", + "UnstructuredExcelLoader", "UnstructuredFileIOLoader", "UnstructuredFileLoader", "UnstructuredHTMLLoader", diff --git a/langchain/document_loaders/excel.py b/langchain/document_loaders/excel.py new file mode 100644 index 0000000000..94e6fb1bd9 --- /dev/null +++ b/langchain/document_loaders/excel.py @@ -0,0 +1,22 @@ +"""Loader that loads Microsoft Excel files.""" +from typing import Any, List + +from langchain.document_loaders.unstructured import ( + UnstructuredFileLoader, + validate_unstructured_version, +) + + +class UnstructuredExcelLoader(UnstructuredFileLoader): + """Loader that uses unstructured to load Microsoft Excel files.""" + + def __init__( + self, file_path: str, mode: str = "single", **unstructured_kwargs: Any + ): + validate_unstructured_version(min_unstructured_version="0.6.7") + super().__init__(file_path=file_path, mode=mode, **unstructured_kwargs) + + def _get_elements(self) -> List: + from unstructured.partition.xlsx import partition_xlsx + + return partition_xlsx(filename=self.file_path, **self.unstructured_kwargs) diff --git a/tests/integration_tests/document_loaders/test_excel.py b/tests/integration_tests/document_loaders/test_excel.py new file mode 100644 index 0000000000..c8fbe07f69 --- /dev/null +++ b/tests/integration_tests/document_loaders/test_excel.py @@ -0,0 +1,15 @@ +import os +from pathlib import Path + +from langchain.document_loaders import UnstructuredExcelLoader + +EXAMPLE_DIRECTORY = file_path = Path(__file__).parent.parent / "examples" + + +def test_unstructured_excel_loader() -> None: + """Test unstructured loader.""" + file_path = os.path.join(EXAMPLE_DIRECTORY, "stanley-cups.xlsx") + loader = UnstructuredExcelLoader(str(file_path)) + docs = loader.load() + + assert len(docs) == 1 diff --git a/tests/integration_tests/examples/stanley-cups.xlsx b/tests/integration_tests/examples/stanley-cups.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ebc66599b2ace3435646bc31f5ae7ca4d7313e69 GIT binary patch literal 6339 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 literal 0 HcmV?d00001