Repository navigation
Optional wolfxl reader for .xlsx files in ExcelExtractor #44848
Description
Self Checks
- I have read the Contributing Guide and Language Policy.
- I have searched for existing issues search for existing issues, including closed ones.
- I confirm that I am using English to submit this report, otherwise it will be closed.
- Please do not modify this template :) and fill in all the required fields.
1. Is this request related to a challenge you're experiencing? Tell me about your story.
ExcelExtractor eagerly reads .xlsx files with openpyxl. For large workbooks, time spent in that read and row-extraction loop can be substantial. Would an optional reader dependency group be acceptable for installations that prioritize this workload?
I have prepared a small change against Dify main adb27d0d016f372fb643fb53c38de91c014be034: a non-default xlsx-wolfxl = ["wolfxl>=2.0.17,<2.1"] dependency group and a guarded reader import. Without wolfxl installed, openpyxl remains the reader. Installing the group selects wolfxl. Missing dependencies inside an installed wolfxl package and workbook parsing errors propagate; they are not retried with openpyxl. The .xls path is unchanged. Official Docker images would remain on openpyxl because they install the default groups.
I maintain wolfxl and would author the change. I am opening this issue to discuss the packaging choice and compatibility tradeoffs. The change is up as a draft PR for reference and will stay in draft until the packaging question is settled here.
2. Additional context or comments
Historical measurements used published wolfxl 2.0.17 and openpyxl 3.1.5, with Pillow 13.0.0. The current prepared lock resolves wolfxl 2.0.18; it has focused test/type-check coverage, but has not been benchmarked or corpus-tested here.
| Copied extractor loop, synthetic input | openpyxl median seconds | wolfxl median seconds | openpyxl / wolfxl median peak RSS, MiB |
|---|---|---|---|
| Linux x86_64, CPython 3.13.15, 300,000 rows x 7 | 15.331 | 6.392 | 946.1 / 737.3 |
| macOS arm64, CPython 3.13.9, 300,000 rows x 7 | 12.085 | 6.207 | 1029.1 / 1271.3 |
| Linux, 300 rows x 7 | 0.012 | 0.012 | 29.1 / 30.2 |
| macOS, 300 rows x 7 | 0.013 | 0.010 | 33.9 / 34.6 |
These are loop-only observations from a standalone copy of the extractor, based on Dify 12ab116fffd4706c20eb618d706cc71e094b519e. They exclude process startup, imports, Document construction, database and storage. The inputs contain no images. Each arm ran five times in alternating openpyxl-then-wolfxl order, with fresh processes, warm page cache, no discarded warmup, 180-second child timeouts and an 1800-second outer timeout. Hosts were shared and unpinned. Cold-cache behavior was not measured. There is no demonstrated small-file gain; rounded millisecond timings overlap. macOS peak RSS is worse with wolfxl for both inputs, as is Linux small-input RSS. This is not an end-to-end Dify speed or memory-reduction claim.
On 353 Apache POI .xlsx fixtures at 4f3c25563919e7efa4a75feccf6cef1ac3d05a04, the 2.0.17 comparison on Linux and macOS found 312 identical document hash/count and image-count results, 5 differing document results, 30 shared exceptions, 1 file read only by openpyxl, 4 read only by wolfxl and 1 hostile file killed with both readers. The 1 wolfxl-only rejection is duplicate-filename-case-insensitive.xlsx, deliberately rejected for duplicate ZIP names. The differing and lenient reads have not been independently certified. This is not full parser parity or a bounded-memory claim. The Linux corpus used CPython 3.12.14; macOS used 3.13.9. Wheel bytes were verified against PyPI; source-commit-to-wheel provenance is unverified.
The evidence archive dify-xlsx-evidence-2.0.17.zip (SHA256 4ef9ad279313ab2bc6c72cfd42b212bdcf1323475be0dc7c4f1e4548af671901) contains exact synthetic inputs, unmodified drivers, raw benchmark and corpus observations, results and SHA256SUMS. To reproduce the large-input measurement from its extracted directory on Linux (POSIX shell):
sha256sum -c SHA256SUMS.txt
UV_NO_BUILD_PACKAGE=wolfxl uv run --no-project --python 3.13.15 --with wolfxl==2.0.17 --with openpyxl==3.1.5 --with Pillow==13.0.0 python run_rss_safe.py bench big300k.xlsx out-linux-bigFor the small input, use small300.xlsx and a new output directory. On native macOS arm64, use CPython 3.13.9 and run.py instead; verify hashes with shasum -a 256 -c SHA256SUMS.txt. Keep the input names and use fresh output directories, since raw logs append. The drivers run five iterations per reader with 180-second child timeouts and need PyPI network access. Expected output is 300,000 documents, zero images, text hash prefix 33bcefba97527c1a for the large input, and 300 documents with ff18ac2b8b8ea16c for the small input. Absolute timing and RSS depend on the host.
On the prepared change, uv lock --check, changed-file ruff check/format, make type-check-core, and all 10 tests in test_excel_extractor.py passed. Type checks and tests ran with the optional dependency both absent and installed; actual reader identity was asserted in both environments. These scoped checks do not replace upstream CI.
3. Can you help us with this feature?
- I am interested in contributing to this feature.
Activity
xz567001-afk commentedon Oct 30, 2026
One concrete compatibility gate before deciding the packaging policy: exercise an image-only row through the real installed reader, including locked 2.0.18.
The draft's real-workbook test covers scalar cells; its image tests replace
load_workbookwith_FakeWorkbook. They don't verify the optional reader's drawing objects. The extractor treats a missing_imagesattribute as no images, so an otherwise blank image row could produce no document without raising.A small
.xlsxfixture with an embedded PNG, real reader selection, and test storage/database could assert the markdown image, one upload, and retry reuse in both dependency environments. This would complement the disclosed 2.0.17 corpus results; I haven't run either backend or observed image loss.dify/api/tests/unit_tests/core/rag/extractor/test_excel_extractor.py
Lines 93 to 271 in 2542aaf
dify/api/core/rag/extractor/excel_extractor.py
Lines 129 to 176 in 91a94a9
wolfiesch commentedon Oct 31, 2026
Thanks, the test you described found a real gap. The cause is row iteration, not image loading.
Both readers return the embedded image with the same anchor, format and bytes.
ExcelExtractoronly iterated rows up tosheet.max_row, and a row that holds only an image has no cells, so it was skipped:iter_rows(max_row=10)) creates empty cells and padsmax_rowto 10, so image-only rows after row 10 are dropped on current main.max_row, so an image-only row directly below the data was dropped as well.#44849 now runs the row loop to the last image anchor (sheets without images iterate as before) and adds a real-workbook test: embedded PNGs anchored at C3 and C12, the installed reader, test storage and database. It asserts the markdown links, one upload per image, and upload reuse on a second
extract(). It fails without the fix in both environments (openpyxl misses row 12; wolfxl misses rows 3 and 12) and passes with it.