191

The new version of Pandas uses the following interface to load Excel files:

read_excel('path_to_file.xls', 'Sheet1', index_col=None, na_values=['NA'])

but what if I don't know the sheets that are available?

For example, I am working with excel files that the following sheets

Data 1, Data 2 ..., Data N, foo, bar

but I don't know N a priori.

Is there any way to get the list of sheets from an excel document in Pandas?

denfromufa
  • 4,995
  • 11
  • 66
  • 130
Amelio Vazquez-Reina
  • 74,000
  • 116
  • 321
  • 514

8 Answers8

336

You can still use the ExcelFile class (and the sheet_names attribute):

xl = pd.ExcelFile('foo.xls')

xl.sheet_names  # see all sheet names

xl.parse(sheet_name)  # read a specific sheet to DataFrame

see docs for parse for more options...

Ezekiel Kruglick
  • 4,006
  • 35
  • 43
Andy Hayden
  • 291,328
  • 80
  • 565
  • 500
  • 2
    Thanks @Andy. May I ask, does Pandas load the excel sheet in `ExcelFile`? Also, say I look up the list of sheets and decide to load N of them, should I at that point call `read_excel` (the new interface) for each sheet, or stick to `x1.parse`? – Amelio Vazquez-Reina Jul 31 '13 at 18:03
  • 2
    I *think* ExcelFile keeps the file open (and doesn't read it all), I think using parse (and opening the file only once) makes most sense here. tbh I missed the arrival of read_excel! – Andy Hayden Jul 31 '13 at 18:05
  • 6
    Mentioned before [here](http://stackoverflow.com/a/16896091/1240268), but I like to keep a dictionary of DataFrames using `{sheet_name: xl.parse(sheet_name) for sheet_name in xl.sheet_names}` – Andy Hayden Jul 31 '13 at 18:06
  • 2
    Wish I could give you more upvotes, this works across multiple versions of pandas too! (don't know why they like changing the API so often) Thanks for pointing me at the parse function, here is the current link though: http://pandas.pydata.org/pandas-docs/stable/generated/pandas.ExcelFile.parse.html – Ezekiel Kruglick Jul 18 '15 at 18:24
  • Agreed, imo this is the best way to load Excel files w/ pandas. – semore_1267 Apr 23 '17 at 17:45
  • read_excel provided the build-in support to iterate sheets, I think it's not necessary to use the old ExcelFile interface. please see my answer. – Nicholas Lu Aug 10 '17 at 02:02
  • 3
    @NicholasLu the downvote was unnecessary, this answer is from 2013! That said, whilst ExcelFile is the original way to parse excel files it is not deprecated and remains a perfectly valid way to do this. – Andy Hayden Aug 10 '17 at 03:38
  • When opening xlsx files, this will fail in pandas 1.1.5. But can be fixed by using `xl = pd.ExcelFile('foo.xls', engine='openpyxl')`. Related on my issue, see this [thread](https://stackoverflow.com/questions/65250207/pandas-cannot-open-an-excel-xlsx-file) – vjangus Mar 09 '21 at 09:18
51

You should explicitly specify the second parameter (sheetname) as None. like this:

 df = pandas.read_excel("/yourPath/FileName.xlsx", None);

"df" are all sheets as a dictionary of DataFrames, you can verify it by run this:

df.keys()

result like this:

[u'201610', u'201601', u'201701', u'201702', u'201703', u'201704', u'201705', u'201706', u'201612', u'fund', u'201603', u'201602', u'201605', u'201607', u'201606', u'201608', u'201512', u'201611', u'201604']

please refer pandas doc for more details: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_excel.html

Nicholas Lu
  • 1,247
  • 13
  • 13
  • 3
    This unnecessarily parses every sheet as a DataFrame, which is not required. "How to read an xls/xlsx file" is a [different question](https://stackoverflow.com/q/16888888/1240268). – Andy Hayden Aug 10 '17 at 03:34
  • 11
    @AndyHayden it might not be efficient, but it might be the best if you care about all the sheets, or you don't care about the additional overhead. – CodeMonkey Dec 08 '17 at 13:48
9

This is the fastest way I have found, inspired by @divingTobi's answer. All The answers based on xlrd, openpyxl or pandas are slow for me, as they all load the whole file first.

from zipfile import ZipFile
from bs4 import BeautifulSoup  # you also need to install "lxml" for the XML parser

with ZipFile(file) as zipped_file:
    summary = zipped_file.open(r'xl/workbook.xml').read()
soup = BeautifulSoup(summary, "xml")
sheets = [sheet.get("name") for sheet in soup.find_all("sheet")]

S.E.A
  • 91
  • 1
  • 2
3

Building on @dhwanil_shah 's answer, you do not need to extract the whole file. With zf.open it is possible to read from a zipped file directly.

import xml.etree.ElementTree as ET
import zipfile

def xlsxSheets(f):
    zf = zipfile.ZipFile(f)

    f = zf.open(r'xl/workbook.xml')

    l = f.readline()
    l = f.readline()
    root = ET.fromstring(l)
    sheets=[]
    for c in root.findall('{http://schemas.openxmlformats.org/spreadsheetml/2006/main}sheets/*'):
        sheets.append(c.attrib['name'])
    return sheets

The two consecutive readlines are ugly, but the content is only in the second line of the text. No need to parse the whole file.

This solution seems to be much faster than the read_excel version, and most likely also faster than the full extract version.

divingTobi
  • 1,271
  • 4
  • 15
2

I have tried xlrd, pandas, openpyxl and other such libraries and all of them seem to take exponential time as the file size increase as it reads the entire file. The other solutions mentioned above where they used 'on_demand' did not work for me. If you just want to get the sheet names initially, the following function works for xlsx files.

def get_sheet_details(file_path):
    sheets = []
    file_name = os.path.splitext(os.path.split(file_path)[-1])[0]
    # Make a temporary directory with the file name
    directory_to_extract_to = os.path.join(settings.MEDIA_ROOT, file_name)
    os.mkdir(directory_to_extract_to)

    # Extract the xlsx file as it is just a zip file
    zip_ref = zipfile.ZipFile(file_path, 'r')
    zip_ref.extractall(directory_to_extract_to)
    zip_ref.close()

    # Open the workbook.xml which is very light and only has meta data, get sheets from it
    path_to_workbook = os.path.join(directory_to_extract_to, 'xl', 'workbook.xml')
    with open(path_to_workbook, 'r') as f:
        xml = f.read()
        dictionary = xmltodict.parse(xml)
        for sheet in dictionary['workbook']['sheets']['sheet']:
            sheet_details = {
                'id': sheet['@sheetId'],
                'name': sheet['@name']
            }
            sheets.append(sheet_details)

    # Delete the extracted files directory
    shutil.rmtree(directory_to_extract_to)
    return sheets

Since all xlsx are basically zipped files, we extract the underlying xml data and read sheet names from the workbook directly which takes a fraction of a second as compared to the library functions.

Benchmarking: (On a 6mb xlsx file with 4 sheets)
Pandas, xlrd: 12 seconds
openpyxl: 24 seconds
Proposed method: 0.4 seconds

Since my requirement was just reading the sheet names, the unnecessary overhead of reading the entire time was bugging me so I took this route instead.

Dhwanil shah
  • 358
  • 2
  • 10
  • What are the modules you are using? – Daniel May 24 '20 at 21:45
  • @Daniel I have used only `zipfile` which is an in-built module and `xmltodict` which I used to convert the XML into an easily iterable dictionary. Although you can look at @divingTobi's answer below where you can read the same file without actually extracting the files within. – Dhwanil shah May 25 '20 at 12:08
  • When I tried openpyxl with the read_only flag it is significantly faster (200X faster for my 5 MB file). `load_workbook(excel_file).sheetnames` averaged 8.24s where `load_workbook(excel_file, read_only=True).sheetnames` averaged 39.6ms. – flutefreak7 Jun 04 '20 at 20:24
1
from openpyxl import load_workbook

sheets = load_workbook(excel_file, read_only=True).sheetnames

For a 5MB Excel file I'm working with, load_workbook without the read_only flag took 8.24s. With the read_only flag it only took 39.6 ms. If you still want to use an Excel library and not drop to an xml solution, that's much faster than the methods that parse the whole file.

flutefreak7
  • 1,951
  • 2
  • 24
  • 34
1
  1. With the load_workbook readonly option, what was earlier seen as a execution seen visibly waiting for many seconds happened with milliseconds. The solution could however be still improved.

     import pandas as pd
     from openpyxl import load_workbook
     class ExcelFile:
    
         def __init__(self, **kwargs):
             ........
             .....
             self._SheetNames = list(load_workbook(self._name,read_only=True,keep_links=False).sheetnames)
    
  2. The Excelfile.parse takes the same time as reading the complete xls in order of 10s of sec. This result was obtained with windows 10 operating system with below package versions

     C:\>python -V
     Python 3.9.1
    
     C:\>pip list
     Package         Version
     --------------- -------
     et-xmlfile      1.0.1
     numpy           1.20.2
     openpyxl        3.0.7
     pandas          1.2.3
     pip             21.0.1
     python-dateutil 2.8.1
     pytz            2021.1
     pyxlsb          1.0.8
     setuptools      49.2.1
     six             1.15.0
     xlrd            2.0.1
    
0

If you:

  • care about performance
  • don't need the data in the file at execution time.
  • want to go with conventional libraries vs rolling your own solution

Below was benchmarked on a ~10Mb xlsx, xlsb file.

xlsx, xls

from openpyxl import load_workbook

def get_sheetnames_xlsx(filepath):
    wb = load_workbook(filepath, read_only=True, keep_links=False)
    return wb.sheetnames

Benchmarks: ~ 14x speed improvement

# get_sheetnames_xlsx vs pd.read_excel
225 ms ± 6.21 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
3.25 s ± 140 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

xlsb

from pyxlsb import open_workbook

def get_sheetnames_xlsb(filepath):
  with open_workbook(filepath) as wb:
     return wb.sheets

Benchmarks: ~ 56x speed improvement

# get_sheetnames_xlsb vs pd.read_excel
96.4 ms ± 1.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
5.36 s ± 162 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Notes:

Glen Thompson
  • 5,326
  • 1
  • 37
  • 37