Numbers-parser - Python module for parsing Apple Numbers .numbers files

Overview

numbers-parser

build:

numbers-parser is a Python module for parsing Apple Numbers .numbers files. It supports Numbers files generated by Numbers version 10.3, and all 11.x up to 11.2 (current as of November 2021).

It supports and is tested against Python versions from 3.6 onwards. It is not compatible with earlier versions of Python.

Currently supported features of Numbers files are:

  • Multiple sheets per document
  • Multiple tables per sheet
  • Text, numeric, date, currency, duration, percentage cell types

Formulas have very limited support and rely wholly on Numbers saving values in cells as part of the saved document, which is not always guaranteed. When a formula value is not present, the value *FORMULA* is returned. Any formula that results in a Numbers error returns a value *ERROR*.

Installation

python3 -m pip install numbers-parser

Usage

Reading documents:

from numbers_parser import Document
doc = Document("my-spreasdsheet.numbers")
sheets = doc.sheets()
tables = sheets[0].tables()
rows = tables[0].rows()

Referring to sheets and tables

Both sheets and names can be accessed from lists of these objects using an integer index (list syntax) and using the name of the sheet/table (dict syntax):

# list access method
sheet_1 = doc.sheets()[0]
print("Opened sheet", sheet_1.name)

# dict access method
table_1 = sheets["Table 1"]
print("Opened table", table_1.name)

Accessing data

Table objects have a rows method which contains a nested list with an entry for each row of the table. Each row is itself a list of the column values. Empty cells in Numbers are returned as None values.

data = sheets["Table 1"].rows()
print("Cell A1 contains", data[0][0])
print("Cell C2 contains", data[2][1])

Cell references

In addition to extracting all data at once, individual cells can be referred to as methods

doc = Document("my-spreasdsheet.numbers")
sheets = doc.sheets()
tables = sheets["Sheet 1"].tables()
table = tables["Table 1"]

# row, column syntax
print("Cell A1 contains", table.cell(0, 0))
# Excel/Numbers-style cell references
print("Cell C2 contains", table.cell("C2"))

Merged cells

When extracting data using data() merged cells are ignored since only text values are returned. The cell() method of Table objects returns a Cell type object which is typed by the type of cell in the Numbers table. MergeCell objects indicates cells removed in a merge.

doc = Document("my-spreasdsheet.numbers")
sheets = doc.sheets()
tables = sheets["Sheet 1"].tables()
table = tables["Table 1"]

cell = table.cell("A1")
print(cell.merge_range)
print(f"Cell A1 merge size is {cell.size[0]},{cell.size[1]})

Row and column iterators

Tables have iterators for row-wise and column-wise iteration with each iterator returning a list of the cells in that row or column

for row in table.iter_rows(min_row=2, max_row=7, values_only=True):
   sum += row
for col in table.iter_cole(min_row=2, max_row=7):
   sum += col.value

Pandas

Since the return value of data() is a list of lists, you should be able to pass it straight to pandas like this

import pandas as pd

doc = Document("simple.numbers")
sheets = doc.sheets()
tables = sheets[0].tables()
data = tables[0].rows(values_only=True)
df = pd.DataFrame(data, columns=["A", "B", "C"])

Numbers File Formats

Numbers uses a proprietary, compressed binary format to store its tables. This format is comprised of a zip file containing images, as well as Snappy-compressed Protobuf .iwa files containing metadata, text, and all other definitions used in the spreadsheet.

Protobuf updates

As numbers-parser includes private Protobuf definitions extracted from a copy of Numbers, new versions of Numbers will inevitably create .numbers files that cannot be read by numbers-parser. As new versions of Numbers are released, the following steps must be undertaken:

  • Run proto-dump on the new copy of Numbers to dump new Proto files.
    • proto-dump assumes version 2.5.0 of Google Protobuf which may need changes to build on more modern OSes. The version linked here is maintained by the author and tested on recent macOS for both arm64 and x86_64 architectures.
    • Any . characters in the Protobuf definitions must be changed to _ characters manually, or via the rename_proto_files.py script in the protos directory of this repo.
  • Connect to a running copy of Numbers with lldb (or any other debugger) and manually copy and reformat the results of po [TSPRegistry sharedRegistry] into mapping.py.
    • Versions of macOS >= 10.11 may protect Numbers from being attached to by a debugger - to attach, temporarily disable System IntegrityProtection to get this data.
    • The generate_mapping.py script in protos should help turn the output from this step into a recreation of mapping.py

Running make bootstrap will perform all of these steps and generate the Python protos files as well as mapping.py. The makefile assumes that proto-dump is in a repo parallel to this one, but the make variable PROTO_DUMP can be overridden to pass the path to a working version of proto-dump.

Credits

numbers-parser was built by Jon Connell but derived enormously from prior work by Peter Sobot. Both modules are derived from previous work by Sean Patrick O'Brien.

Decoding the data structures inside Numbers files was helped greatly by previous work by Steven Lott.

Formula tests were adapted from JavaScript tests used in fast-formula-parser.

License

All code in this repository is licensed under the MIT License

Comments
  • Duration formatting

    Duration formatting

    https://github.com/SheetJS/test_files/blob/master/duration_112/duration_112.numbers test file.

    There are a few places where the duration formatting details are stored. For modern sheets, they are in the format table (.TST.DataStore field 22)

    There are three types of duration format and 21 manually specified field sets (stored as start and end granularity) as well as an "automatic" option.

    opened by SheetJSDev 10
  • merge ranges

    merge ranges

    File under "undocumented black magic": merge-test.zip

    It's ok if you don't think this is valid -- Numbers 11.2 won't write this field, but it parses correctly all the way back to Numbers 3.5.

    opened by SheetJSDev 9
  • Numbers 11.2 Support

    Numbers 11.2 Support

    Unable to read a Numbers file without throwing an error on Numbers 11.2:

    >>> from numbers_parser import Document
    >>> doc = Document('numbers-test.numbers')
    Traceback (most recent call last):
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/iwafile.py", line 148, in from_buffer
        klass = ID_NAME_MAP[message_info.type]
    KeyError: 6372
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/iwafile.py", line 25, in from_buffer
        chunk, data = IWACompressedChunk.from_buffer(data, filename)
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/iwafile.py", line 84, in from_buffer
        archive, data = IWAArchiveSegment.from_buffer(data, filename)
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/iwafile.py", line 150, in from_buffer
        raise NotImplementedError(
    NotImplementedError: Don't know how to parse Protobuf message type 6372
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/unpack.py", line 62, in _extract_iwa_archives
        iwaf = IWAFile.from_buffer(contents, filename)
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/iwafile.py", line 31, in from_buffer
        raise ValueError("Failed to deserialize " + filename) from e
    ValueError: Failed to deserialize Index/CalculationEngine-860970.iwa
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/document.py", line 35, in __init__
        self._object_store = ObjectStore(filename)
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/containers.py", line 54, in __init__
        read_numbers_file(path,
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/unpack.py", line 35, in read_numbers_file
        _get_objects_from_zip_stream(zipf, handler, store_objects)
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/unpack.py", line 51, in _get_objects_from_zip_stream
        _extract_iwa_archives(contents, filename, handler, store_objects)
      File "/usr/local/lib/python3.9/site-packages/numbers_parser/unpack.py", line 64, in _extract_iwa_archives
        raise FileFormatError(f"{filename}: invalid IWA file {filename}") from e
    NameError: name 'FileFormatError' is not defined
    >>>
    
    opened by tsouchlarakis 7
  • Unable to read image cell

    Unable to read image cell

    When I read a cell with an image, I got EmptyCell.

    >>> table.cell("B2")
    <numbers_parser.cell.EmptyCell object at 0x10e1b2b80>
    
    opened by yifeikong 6
  • performance against large files

    performance against large files

    test file https://github.com/SheetJS/nodeno/blob/master/large_strings.numbers

    From SheetJS, you can get a sense for expected performance by cloning the repo https://github.com/SheetJS/nodeno and either:

    A) setting up https://bun.sh/ and running time bun read.js large_strings.numbers

    B) setting up https://nodejs.org/en/ and running time node read.mjs large_strings.numbers

    It takes ~3 seconds on a 2018 i9 MBP either way.

    Using the PyPI version (2.3.12) with time cat-numbers -b large_strings.numbers on Python 3.9.13, it runs close to 57 seconds.

    Is there some library option or missing setup to improve performance?

    opened by SheetJSDev 6
  • IndexError: list index out of range

    IndexError: list index out of range

    Rename attachment to .numbers

    numbers_parser_issue.zip

      File "/usr/local/lib/python3.9/site-packages/numbers_parser/model.py", line 131, in owner_id_map
        ce_id = self.find_refs("CalculationEngineArchive")[0]
    IndexError: list index out of range
    
    opened by reviewher 6
  • Error installing package

    Error installing package

    I don't know what it means but got this error trying to install this package. Thanks for looking at it.

    ERROR: Command errored out with exit status 1: /Applications/Xcode.app/Contents/Developer/usr/bin/python3 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/pc/3r24mm_j7vj7pbsccb25x9fh0000gn/T/pip-install-6xw1j_pi/python-snappy/setup.py'"'"'; file='"'"'/private/var/folders/pc/3r24mm_j7vj7pbsccb25x9fh0000gn/T/pip-install-6xw1j_pi/python-snappy/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(file);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, file, '"'"'exec'"'"'))' install --record /private/var/folders/pc/3r24mm_j7vj7pbsccb25x9fh0000gn/T/pip-record-t9m3oii6/install-record.txt --single-version-externally-managed --user --prefix= --compile --install-headers /Users/paul/Library/Python/3.8/include/python3.8/python-snappy Check the logs for full command output.

    opened by walmer-bolfet 6
  • Request for numbers-parser information

    Request for numbers-parser information

    I wouldn’t say this is an issue, but a request for information.

    1. I have one.numbers file with multiple sheets and one table per sheet. Is it possible to use numbers-parser to replace a table (or sheet) in this one.numbers file with a table (or sheet) from another.numbers file? It is not intuitive to me how this might be done.

    2. Since numbers can open a .csv file, is it possible to use numbers-parser to open a .csv file and save it as a .numbers file?

    opened by walmer-bolfet 5
  • Bug with Numerical Zero as Cell Value None

    Bug with Numerical Zero as Cell Value None

    I have a basic numbers spreadsheet with a cell value of 0 (cell data format set to Automatic).

    2021-12-12 at 10 44 AM

    When run through numbers-parser I see...

    2021-12-12 10:42:35,792 DEBUG Table 1@[3,3]: value=0.0

    But the cell value from iter_rows is None.

    If I change the data format to Text, the result is a string "0" (which I expect).

    The None result seems like a bug in conditional logic.

    (p.s. Thank you so much for this library. I'm finally adding Numbers support to my app 🥳)

    opened by semireg 5
  • Cannot parse sheets if edited in iCloud.com

    Cannot parse sheets if edited in iCloud.com

    Any numbers file which is edited online on icloud.com cannot be parsed with doc.sheets(). I get an error:

    File "numbers_parser/model.py", line 49, in sheet_ids
        return [o.identifier for o in self.objects[1].sheets]
      File "numbers_parser/containers.py", line 40, in __getitem__
        return self._objects[key]
    
    KeyError: 1
    

    Probably the parser isn't build for numbers created or edited on icloud.com Numbers but could be an added feature.

    opened by nikostito-electobox 4
  • '\xa0'

    '\xa0'

    Hello again mate, When reading .numbers file I have to use some additional code to remove / replace '\xa0' from strings in pandas columns. This occurs all the time when there is a space (' ') OR ('-') and string look like this: 'python\xa0is\xa0cool'

    There is no need to add these when I use standard read from excel in pandas.

    Any solution to make it work from the start without these additional strings replacing?

    opened by meeffe 4
  • 3.9.1 dependency issues

    3.9.1 dependency issues

    When trying to install globally (sudo -H python3 -m pip install numbers_parser) there were a few missing deps:

    1. the package depends on poetry. Installation error:
    ...
      File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
      File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
      File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
      File "<frozen importlib._bootstrap>", line 1004, in _find_and_load_unlocked
    ModuleNotFoundError: No module named 'poetry'
    

    Looking at the current tarball (https://files.pythonhosted.org/packages/8e/d4/d32d8935fc3a3fbe777e988925e3bf3b60e0daa45f7b9d8a7fe689f4fb84/numbers_parser-3.9.1.tar.gz) the setup.py does not reference poetry dependency:

    1. the package depends on libmagic. Runtime error:
      File "/usr/local/lib/python3.10/site-packages/numbers_parser/model.py", line 23, in <module>
        from numbers_parser.cell import (
      File "/usr/local/lib/python3.10/site-packages/numbers_parser/cell.py", line 1, in <module>
        import magic
      File "/usr/local/lib/python3.10/site-packages/magic/__init__.py", line 209, in <module>
        libmagic = loader.load_lib()
      File "/usr/local/lib/python3.10/site-packages/magic/loader.py", line 49, in load_lib
        raise ImportError('failed to find libmagic.  Check your installation')
    ImportError: failed to find libmagic.  Check your installation
    

    This was resolved with brew install libmagic.

    opened by SheetJSDev 2
Releases(v3.0.0)
Owner
Jon Connell
Jon Connell
jsoooooooon derulo - Make sure your 'jason derulo' is featured as the first part of your json data

jsonderulo Make sure your 'jason derulo' is featured as the first part of your json data Install: # python pip install jsonderulo poetry add jsonderul

jesse 3 Sep 13, 2021
API for obtaining results from the Beery-Bukenica test of the visomotor integration development (VMI) 4th edition.

VMI API API for obtaining results from the Beery-Bukenica test of the visomotor integration development (VMI) 4th edition. Install docker-compose up -

Victor Vargas Sandoval 1 Oct 26, 2021
A Python package implementing various colour checker detection algorithms and related utilities.

A Python package implementing various colour checker detection algorithms and related utilities.

colour-science 147 Dec 29, 2022
Tool for generating Memory.scan() compatible instruction search patterns

scanpat Tool for generating Frida Memory.scan() compatible instruction search patterns. Powered by r2. Examples $ ./scanpat.py arm.ks:64 'sub sp, sp,

Ole André Vadla Ravnås 13 Sep 19, 2022
Make your functions return something meaningful, typed, and safe!

Make your functions return something meaningful, typed, and safe! Features Brings functional programming to Python land Provides a bunch of primitives

dry-python 2.6k Jan 09, 2023
An OData v4 query parser and transpiler for Python

odata-query is a library that parses OData v4 filter strings, and can convert them to other forms such as Django Queries, SQLAlchemy Queries, or just plain SQL.

Gorilla 39 Jan 05, 2023
A simple API that will return a key-value pair of randomly generated UUID

A simple API that will return a key-value pair of randomly generated UUID. Key will be a timestamp and value will be UUID. While the server is running, whenever the API is called, it should return al

Pius Lucky 2 Jan 18, 2022
Local backup made easy, with Python and shutil

KTBackup BETA Local backup made easy, with Python and shutil Features One-command backup and restore Minimalistic (only using stdlib) Convenient direc

kelptaken 1 Dec 27, 2021
Greenery - tools for parsing and manipulating regular expressions

Greenery - tools for parsing and manipulating regular expressions

qntm 242 Dec 15, 2022
Regression Metrics Calculation Made easy

Regression Metrics Mean Absolute Error Mean Square Error Root Mean Square Error Root Mean Square Logarithmic Error Root Mean Square Logarithmic Error

Ashish Patel 12 Jan 02, 2023
RapidFuzz is a fast string matching library for Python and C++

RapidFuzz is a fast string matching library for Python and C++, which is using the string similarity calculations from FuzzyWuzzy

Max Bachmann 1.7k Jan 04, 2023
Python code to divide big numbers

divide-big-num Python code to divide big numbers

VuMinhNgoc 1 Oct 15, 2021
腾讯云轻量服务流量超出限制自动关机

LightHouse_Automatic_Shutdown 腾讯云轻量服务流量超出限制自动关机

132 Dec 14, 2022
Minimal Windows system information tool written in Python

wfetch wfetch is a Minimal Windows system information tool written in Python (Only works on Windows) Installation First of all have python installed.

zJairO 3 Jan 24, 2022
Compute the fair market value (FMV) of staking rewards at time of receipt.

tendermint-tax A tool to help calculate the tax liability of staking rewards on Tendermint chains. Specifically, this tool calculates the fair market

5 Jan 07, 2022
An awesome tool to save articles from RSS feed to Pocket automatically.

RSS2Pocket An awesome tool to save articles from RSS feed to Pocket automatically. About the Project I used to use IFTTT to save articles from RSS fee

Hank Liao 10 Nov 12, 2022
A dictionary that can be flattened and re-inflated

deflatable-dict A dictionary that can be flattened and re-inflated. Particularly useful if you're interacting with yaml, for example. Installation wit

Lucas Sargent 2 Oct 18, 2021
Set of scripts for some automation during Magic Lantern development

~kitor Magic Lantern scripts A few automation scripts I wrote to automate some things in my ML development efforts. Used only on Debian running over W

Kajetan Krykwiński 1 Jan 03, 2022
A python module to manipulate XCode projects

This module can read, modify, and write a .pbxproj file from an Xcode 4+ projects. The file is usually called project.pbxproj and can be found inside the .xcodeproj bundle. Because some task cannot b

Ignacio Calderon 1.1k Jan 02, 2023
Fraud Multiplication Table Detection in python

Fraud-Multiplication-Table-Detection-in-python In this program, I have detected fraud multiplication table using python without class. Here, I have co

Sachin Vinayak Dabhade 4 Sep 24, 2021