أسرع طريقة لقراءة Excel في بايثون

ليس لدي أي بيانات لدعم هذا الادعاء، ولكنني متأكد إلى حد ما من أن Excel هو الطريقة الأكثر شيوعًا لتخزين البيانات ومعالجتها، بل وحتى تمريرها. ولهذا السبب ليس من غير المألوف أن تجد نفسك تقرأ Excel في بايثون. لقد احتجت مؤخرًا إلى ذلك، لذا اختبرت وقارنت بين عدة طرق لقراءة ملفات Excel في بايثون.

ما الذي نقوم باختباره؟

لمقارنة طرق قراءة ملفات Excel باستخدام بايثون، نحتاج أولاً إلى تحديد ما يجب قياسه، وكيف.

نبدأ بإنشاء ملف Excel بحجم 25 ميغابايت يحتوي على 500 ألف صف مع أنواع مختلفة من الأعمدة:

ملف اكسل

يدعم برنامج Excel تنسيقي الملفات xls وxlsx. سنستخدم التنسيق الأحدث xlsx.

بالنسبة للمعايير المرجعية، سننفذ دوال لاستيراد البيانات من Excel وإرجاع Iterator للقواميس:

def iter_excel(file: IO[bytes]) -> Iterator[dict[str, object]]:
    # TODO...

نقوم بإرجاع Iterator للسماح للمستهلكين بمعالجة الملف صفًا تلو الآخر. يمكن أن يؤدي هذا إلى تقليل مساحة الذاكرة من خلال عدم تخزين الملف بالكامل في الذاكرة أثناء معالجته. وكما سنرى في معايير الأداء، فإن هذا ليس ممكنًا دائمًا.

لإنتاج توقيت “نظيف”، نقوم بتكرار المولد دون القيام بأي معالجة فعلية:

for row in iter_excel(file):
    pass

سيؤدي هذا إلى جعل المولد يقوم بالتقييم الكامل مع الحد الأدنى من الأداء أو تكلفة الذاكرة.

السرعة

الشيء الأكثر وضوحًا للقياس هو الوقت، والطريقة الأكثر دقة لقياس الوقت في بايثون لأغراض الأداء هي استخدام time.perf_counter:

import time

start = time.perf_counter()
for row in iter_excel(file): pass
elapsed = time.perf_counter() - start

نبدأ تشغيل المؤقت، ونكرر المولد بأكمله ونحسب الوقت المنقضي.

الأنواع

تشتهر بعض التنسيقات مثل parquet وavro بكونها موصوفة ذاتيًا، حيث تحافظ على المخطط داخل الملف، بينما تشتهر التنسيقات الأخرى مثل CSV بعدم الاحتفاظ بأي معلومات حول البيانات التي تخزنها.

يمكن اعتبار Excel تنسيقًا يخزن معلومات النوع حول محتواه – فهناك خلايا التاريخ وخلايا الأرقام وخلايا الأعداد العشرية وغيرها، لذا عند تحميل البيانات من Excel، قد يكون من المفيد تلقي البيانات بنوعها المقصود. وهذا مفيد بشكل خاص للأنواع مثل التاريخ، حيث قد يكون التنسيق غير واضح أو غير معروف، أو السلاسل التي تحتوي على أرقام مثل أرقام الهواتف أو الرموز البريدية. في هذه المواقف، قد تؤدي محاولة شم النوع إلى نتائج غير صحيحة (بسبب تقليم الأصفار الأولية، وافتراض تنسيق غير صحيح وما إلى ذلك).

ولكي نكون منصفين، قد يجادل البعض بأنه عند تحميل البيانات إلى نظامك، يجب أن يكون لديك معرفة بمخططه، لذا فإن الحفاظ على الأنواع قد لا يكون متطلبًا صارمًا بالنسبة للبعض.

الصواب

لاختبار صحة عملية الاستيراد، قمنا بتضمين صف تحكم في بداية ملف Excel. سنستخدم صف التحكم كمرجع للتأكد من استيراد البيانات بشكل صحيح:

# Test correctness of imported data using a control row
for key, expected_value in (
    ('number', 1),
    ('decimal', 1.1),
    ('date', datetime.date(2000, 1, 1)),
    ('boolean', True),
    ('text', 'CONTROL ROW'),
):
    try:
        value = control_row[key]
    except KeyError:
        print(f'🔴 "{key}" missing')
        continue
    if type(expected_value) != type(value):
        print(f'🔴 "{key}" expected type "{type(expected_value)}" received type "{type(value)}"')
    elif expected_value != value:
        print(f'🔴 "{key}" expected value "{expected_value}" received "{value}"')
    else:
        print(f'🟢 "{key}"')

سنقوم بتشغيل هذا الاختبار بعد كل معيار للتأكد من وجود جميع المفاتيح المتوقعة في صف التحكم، وأن الأنواع والقيم كما نتوقع.

قراءة Excel في بايثون

لدينا الآن ملف عينة، وطريقة لاختبار المحتويات وقمنا بتحديد ما يجب قياسه – نحن جاهزون لاستيراد بعض البيانات!

قراءة Excel باستخدام Pandas

Pandas، مكتبة تحليل البيانات لـبايثون، هي الخيار الأمثل لأي شيء يتعلق بالبيانات في بايثون، لذا فهي مكان جيد للبدء.

import pandas

def iter_excel_pandas(file: IO[bytes]) -> Iterator[dict[str, object]]:
    yield from pandas.read_excel(file).to_dict('records')

ما عليك سوى ربط أمرين معًا للحصول على قائمة بالقواميس من ملف Excel. وهذا هو شكل صف واحد من النتيجة:

>>> with open('file.xlsx', 'rb') as f:
...     rows = iter_excel_pandas(f)
...     row = next(rows)
...     print(row)
...
{'boolean': True,
 'date': Timestamp('2000-01-01 00:00:00'),
 'decimal': 1.1,
 'number': 1,
 'text': 'CONTROL ROW'}

في لمحة سريعة، يمكننا أن نرى أن التاريخ ليس datetime.date بل هو Timestamp في pandas. يبدو الباقي جيدًا. إذا كانت Timestamp مشكلة وأصررت على datetime.date، فيمكنك توفير دالة تحويل إلى read_excel:

import pandas

def iter_excel_pandas(file: IO[bytes]) -> Iterator[dict[str, object]]:
    yield from pandas.read_excel(file, converters={
        'date': lambda ts: ts.date(),
    }).to_dict('records')

يقبل المحول Timestamp من نوع pandas ويحوله إلى datetime.date. هذا هو صف التحكم مع المحول المخصص:

{
    'number': 1,
    'decimal': 1.1,
    'date': datetime.date(2000, 1, 1),
    'boolean': True,
    'text': 'CONTROL ROW',
}

إذا كنت تستخدم pandas لقراءة البيانات من Excel، فليس من غير المعقول أن تفترض أنك تريد أيضًا متابعة تحليلك باستخدام pandas، لذا سنقبل Timestamp كنوع صالح لمعيارنا.

بعد ذلك، قم بتشغيل معيار الأداء على ملف Excel الكبير:

iter_excel_pandas
elapsed 32.98058952600695
🟢 "number"
🟢 "decimal"
🔴 "date" expected type "<class 'datetime.date'>" received type "<class 'pandas._libs.tslibs.timestamps.Timestamp'>"
🟢 "boolean"
🟢 "text"

استغرق الاستيراد حوالي 32 ثانية حتى اكتمل. نوع حقل التاريخ هو Timestamp وليس datetime.date، ولكن لا بأس بذلك.

قراءة Excel باستخدام Tablib

Tablib هي إحدى المكتبات الأكثر شهرة في بايثون لاستيراد وتصدير البيانات بتنسيقات مختلفة. تم تطويرها في الأصل بواسطة منشئ مكتبة Requests الشهيرة، وبالتالي تتميز بتركيز مماثل على تجربة المطور وبيئة العمل.

لتثبيت Tablib، قم بتنفيذ الأمر التالي:

$ pip install tablib

قراءة ملف Excel باستخدام tablib:

import tablib

def iter_excel_tablib(file: IO[bytes]) -> Iterator[dict[str, object]]:
    yield from tablib.Dataset().load(file).dict

مجرد سطر واحد من التعليمات البرمجية والمكتبة تقوم بكل العمل الشاق.

قبل أن ننتقل إلى تنفيذ معيار الأداء، هذا هو الشكل الذي يبدو عليه الصف الأول من النتائج:

>>> with open('file.xlsx', 'rb') as f:
...     rows = iter_excel_tablib(f)
...     row = next(rows)
...     print(row)
...
OrderedDict([('number', 1),
             ('decimal', 1.1),
             ('date', datetime.datetime(2000, 1, 1, 0, 0)),
             ('boolean', True),
             ('text', 'CONTROL ROW')])

OrderedDict هي فئة فرعية من قاموس بايثون مع بعض التوابع الإضافية لإعادة ترتيب ترتيب القاموس. يتم تعريفها في وحدة collections المضمنة وهي ما يعيده tablib عندما تطلب قاموسًا. نظرًا لأن OrderedDict هي فئة فرعية من القاموس ويتم تعريفها في وحدة مضمنة، فإننا لا نمانع ونعتبرها مناسبة لأغراضنا.

الآن إلى المعيار على ملف Excel الكبير:

iter_excel_tablib
elapsed 28.526969947852194
🟢 "number"
🟢 "decimal"
🔴 "date" expected type "<class 'datetime.date'>" received type "<class 'datetime.datetime'>"
🟢 "boolean"
🟢 "text"

استغرق الاستيراد 28 ثانية، وهو أسرع من pandas (32 ثانية). تم إرجاع خلية التاريخ كـ datetime.datetime بدلاً من datetime.date، وهو أمر غير معقول.

دعونا نرى ما إذا كان بإمكاننا تقليص هذا التوقيت إلى أبعد من ذلك.

قراءة Excel باستخدام Openpyxl

Openpyxl هي مكتبة لقراءة وكتابة ملفات Excel في بايثون. وعلى عكس Tablib، فإن Openpyxl مخصصة فقط لـ Excel ولا تدعم أي أنواع ملفات أخرى. في الواقع، يستخدم كل من tablib وpandas مكتبة Openpyxl تحت الغطاء عند قراءة ملفات xlsx. ربما يؤدي هذا التخصص إلى أداء أفضل.

قراءة ملف Excel باستخدام openpyxl:

import openpyxl

def iter_excel_openpyxl(file: IO[bytes]) -> Iterator[dict[str, object]]:
    workbook = openpyxl.load_workbook(file)
    rows = workbook.active.rows
    headers = [str(cell.value) for cell in next(rows)]
    for row in rows:
        yield dict(zip(headers, (cell.value for cell in row)))

هذه المرة يتعين علينا كتابة المزيد من التعليمات البرمجية، لذا دعنا نقوم بتقسيمها:

  • تحميل مصنف عمل من الملف المفتوح: تدعم الدالة load_workbook كلًا من مسار الملف وتدفق البيانات القابل للقراءة. في حالتنا، نعمل على ملف مفتوح.
  • الحصول على الورقة النشطة: يمكن أن يحتوي ملف Excel على أوراق متعددة ويمكننا اختيار الورقة التي نريد قراءتها. في حالتنا، لدينا ورقة واحدة فقط.
  • إنشاء قائمة بالرؤوس: يتضمن الصف الأول في ملف Excel الرؤوس. لاستخدام هذه الرؤوس كمفاتيح لقاموسنا، نقرأ الصف الأول وننتج قائمة بالرؤوس.
  • إرجاع النتائج: كرر الصفوف وقم بإنشاء قاموس لكل صف باستخدام الرؤوس وقيم الخلايا. يستخدم openpyxl نوع Cell الذي يتضمن كلًا من القيمة وبعض البيانات الوصفية. يمكن أن يكون هذا مفيدًا لأغراض أخرى، لكننا نحتاج فقط إلى القيم. للوصول إلى قيمة الخلية، نستخدم cell.value.

وهذا هو الشكل الذي يبدو عليه الصف الأول من النتائج:

>>> with open('file.xlsx', 'rb') as f:
...     rows = iter_excel_openpyxl(f)
...     row = next(rows)
...     print(row)
{'boolean': True,
 'date': datetime.datetime(2000, 1, 1, 0, 0),
 'decimal': 1.1,
 'number': 1,
 'text': 'CONTROL ROW'}

يبدو واعدًا! قم بتشغيل الاختبار على الملف الكبير:

iter_excel_openpyxl
elapsed 35.62
🟢 "number"
🟢 "decimal"
🔴 "date" expected type "<class 'datetime.date'>" received type "<class 'datetime.datetime'>"
🟢 "boolean"
🟢 "text"

استغرق استيراد ملف Excel الكبير باستخدام openpyxl حوالي 35 ثانية، وهي مدة أطول من Tablib (28 ثانية) وpandas (32 ثانية).

كشف بحث سريع في الوثائق عن قسم واعد بعنوان “الأداء“. في هذا القسم، يصف openpyxl “الأوضاع المحسنة” لتسريع الأمور عند قراءة أو كتابة ملف فقط:

import openpyxl

def iter_excel_openpyxl(file: IO[bytes]) -> Iterator[dict[str, object]]:
    workbook = openpyxl.load_workbook(file, read_only=True)
    rows = workbook.active.rows
    headers = [str(cell.value) for cell in next(rows)]
    for row in rows:
        yield dict(zip(headers, (cell.value for cell in row)))

تم تحميل ورقة العمل الآن في وضع “القراءة فقط”. نظرًا لأننا نريد فقط قراءة المحتويات وليس الكتابة، فهذا أمر مقبول. دعنا نجري اختبار الأداء مرة أخرى ونرى ما إذا كان ذلك يؤثر على النتائج:

iter_excel_openpyxl
elapsed 24.79
🟢 "number"
🟢 "decimal"
🔴 "date" expected type "<class 'datetime.date'>" received type "<class 'datetime.datetime'>"
🟢 "boolean"
🟢 "text"

يؤدي فتح الملف في وضع “القراءة فقط” إلى تقليل التوقيت من 35 ثانية إلى 24 ثانية – أسرع من tablib (28 ثانية) وpandas (32 ثانية).

قراءة Excel باستخدام LibreOffice

لقد استنفدنا الآن الطرق التقليدية والواضحة لاستيراد Excel إلى بايثون. لقد استخدمنا المكتبات المخصصة وحصلنا على نتائج جيدة. لقد حان الوقت الآن للتفكير خارج الصندوق.

LibreOffice هو بديل مجاني ومفتوح المصدر لمجموعة Office الأخرى. يمكن لـ LibreOffice معالجة ملفات xls وxlsx، كما يتضمن وضعًا بدون واجهة مع بعض خيارات سطر الأوامر المفيدة:

$ libreoffice --help
LibreOffice 7.5.8.2 50(Build:2)

Usage: soffice [argument...]
       argument - switches, switch parameters and document URIs (filenames).
...

أحد خيارات سطر أوامر LibreOffice هو تحويل الملفات بين تنسيقات مختلفة. على سبيل المثال، يمكننا استخدام libreoffice لتحويل ملف xlsx إلى ملف csv:

$ libreoffice --headless --convert-to csv --outdir . file.xlsx
convert file.xlsx -> file.csv using filter: Text - txt - csv (StarCalc)

$ head file.csv
number,decimal,date,boolean,text
1,1.1,01/01/2000,TRUE,CONTROL ROW
2,1.2,01/02/2000,FALSE,RANDOM TEXT:0.716658989024692
3,1.3,01/03/2000,TRUE,RANDOM TEXT:0.966075283958641

رائع! دعنا نربط الأمر معًا باستخدام بايثون. سنقوم أولاً بتحويل ملف xlsx إلى CSV ثم استيراد ملف CSV إلى بايثون:

import subprocess, tempfile, csv

def iter_excel_libreoffice(file: IO[bytes]) -> Iterator[dict[str, object]]:
    with tempfile.TemporaryDirectory(prefix='excelbenchmark') as tempdir:
        subprocess.run([
            'libreoffice', '--headless', '--convert-to', 'csv',
            '--outdir', tempdir, file.name,
        ])
        with open(f'{tempdir}/{file.name.rsplit(".")[0]}.csv', 'r') as f:
            rows = csv.reader(f)
            headers = list(map(str, next(rows)))
            for row in rows:
                yield dict(zip(headers, row))

دعونا نحللها:

  • إنشاء دليل مؤقت لتخزين ملف CSV الخاص بنا: استخدم وحدة tempfile المضمنة لإنشاء دليل مؤقت سيتم تنظيفه تلقائيًا عند الانتهاء. من الناحية المثالية، قد نرغب في تحويل ملف معين إلى كائن يشبه الملف في الذاكرة، لكن سطر أوامر libreoffice لا يوفر أي طريقة للتحويل إلى ملف معين، فقط إلى دليل.
  • التحويل إلى CSV باستخدام سطر أوامر libreoffice: استخدم وحدة  subprocess المضمنة لتنفيذ أمر نظام التشغيل.
  • اقرأ ملف CSV الناتج: افتح ملف CSV الذي تم إنشاؤه حديثًا، وقم بتحليله باستخدام وحدة csv المضمنة وإنتاج القواميس.

وهذا هو الشكل الذي يبدو عليه الصف الأول من النتائج:

>>> with open('file.xlsx', 'rb') as f:
...     rows = iter_excel_libreoffice(f)
...     row = next(rows)
...     print(row)
{'number': '1',
 'decimal': '1.1',
 'date': '01/01/2000',
 'boolean': 'TRUE',
 'text': 'CONTROL ROW'}

نلاحظ على الفور أننا فقدنا جميع معلومات النوع – كل القيم عبارة عن سلاسل.

دعنا نجري اختبارًا معياريًا لمعرفة ما إذا كان الأمر يستحق ذلك:

iter_excel_libreoffice
convert file.xlsx -> file.csv using filter : Text - txt - csv (StarCalc)
elapsed 15.279242266900837
🔴 "number" expected type "<class 'int'>" received type "<class 'str'>"
🔴 "decimal" expected type "<class 'float'>" received type "<class 'str'>"
🔴 "date" expected type "<class 'datetime.date'>" received type "<class 'str'>"
🔴 "boolean" expected type "<class 'bool'>" received type "<class 'str'>"
🟢 "text"

لأكون صادقًا، كان هذا أسرع مما كنت أتوقع! باستخدام LibreOffice لتحويل الملف إلى CSV ثم تحميله استغرق الأمر 15 ثانية فقط – أسرع من pandas (35 ثانية)، وtablib (28 ثانية)، وopenpyxl (24 ثانية).

لقد فقدنا معلومات النوع عندما قمنا بتحويل الملف إلى CSV وإذا كان علينا أيضًا تحويل الأنواع، فمن المرجح أن يستغرق الأمر بعض الوقت (قد يكون التسلسل بطيئًا كما تعلم). ولكن بشكل عام، ليس خيارًا سيئًا!

قراءة Excel باستخدام DuckDB

إذا كنا بالفعل نسير على طريق استخدام الأدوات الخارجية، فلماذا لا نعطي الوافد الجديد على الساحة فرصة للمنافسة.

DuckDB هو “نظام إدارة قاعدة بيانات SQL OLAP قيد التنفيذ”. لا يوضح هذا الوصف على الفور سبب كون DuckDB مفيدًا في هذه الحالة، ولكنه مفيد بالفعل. DuckDB جيد جدًا في نقل البيانات وتحويلها بين التنسيقات.

قراءة ملف Excel باستخدام duckdb في بايثون:

import duckdb

def iter_excel_duckdb(file: IO[bytes]) -> Iterator[dict[str, object]]:
    duckdb.install_extension('spatial')
    duckdb.load_extension('spatial')
    rows = duckdb.sql(f"""
        SELECT * FROM st_read(
            '{file.name}',
            open_options=['HEADERS=FORCE', 'FIELD_TYPES=AUTO'])
    """)
    while row := rows.fetchone():
        yield dict(zip(rows.columns, row))

دعونا نحللها:

تثبيت وتحميل الامتداد spatial: لاستيراد البيانات من Excel باستخدام duckdb، تحتاج إلى تثبيت الامتداد spatial. هذا غريب بعض الشيء لأن الامتداد spatial يستخدم للتلاعبات الجغرافية، ولكن هذا ما يريده.

الاستعلام عن الملف: عند تنفيذ الاستعلامات مباشرةً باستخدام المتغير العالمي duckdb، سيتم استخدام قاعدة بيانات في الذاكرة افتراضيًا، على غرار استخدام sqlite مع خيار :memory:. لاستيراد ملف Excel فعليًا، نستخدم الدالة st_read مع المسار إلى الملف كحجة أولى. في خيارات الدالة، نضبط الصف الأول كرؤوس، وننشط خيار الكشف التلقائي عن الأنواع (هذا هو الإعداد الافتراضي أيضًا).

إنشاء النتيجة: كرر الصفوف وقم بإنشاء القواميس باستخدام قائمة الرؤوس وقيم كل صف.

هذا هو الشكل الذي يبدو عليه الصف الأول عند استخدام DuckDB لاستيراد ملف Excel:

>>> with open('file.xlsx', 'rb') as f:
...     rows = iter_excel_duckdb(f)
...     row = next(rows)
...     print(row)
{'boolean': True,
 'date': datetime.date(2000, 1, 1),
 'decimal': 1.1,
 'number': 1,
 'text': 'CONTROL ROW'}

الآن بعد أن أصبح لدينا عملية قراءة ملف Excel باستخدام DuckDB إلى بايثون، دعنا نرى كيفية عملها:

iter_excel_duckdb
elapsed 11.36
🟢 "number"
🟢 "decimal"
🟢 "date"
🟢 "boolean"
🟢 "text"

أولاً وقبل كل شيء، لدينا فائز فيما يتعلق بالأنواع! فقد تمكن DuckDB من اكتشاف جميع الأنواع بشكل صحيح. بالإضافة إلى ذلك، سجل DuckDB 11 ثانية فقط، وهو ما يقربنا من توقيت رقم واحد!

كان أحد الأشياء التي أزعجتني في هذا التنفيذ هو أنه على الرغم من بذلي قصارى جهدي، لم أتمكن من استخدام معلمة لاسم الملف باستخدام دالة duckdb.sql. إن استخدام تجميع السلاسل لتوليد SQL أمر خطير وعرضة للحقن ويجب تجنبه قدر الإمكان.

في إحدى محاولاتي لحل هذه المشكلة، حاولت استخدام duckdb.execute بدلاً من duckdb.sql، والذي بدا أنه يقبل المعلمات في هذه الحالة:

import duckdb

def iter_excel_duckdb_execute(file: IO[bytes]) -> Iterator[dict[str, object]]:
    duckdb.install_extension('spatial')
    duckdb.load_extension('spatial')
    conn = duckdb.execute(
        "SELECT * FROM st_read(?, open_options=['HEADERS=FORCE', 'FIELD_TYPES=AUTO'])",
        [file.name],
    )
    headers = [header for header, *rest in conn.description]
    while row := conn.fetchone():
        yield dict(zip(headers, row))

هناك فرقين رئيسيين هنا:

  • استخدم duckdb.execute بدلاً من duckdb.sql: باستخدام execute تمكنت من توفير اسم الملف كمعلمة بدلاً من استخدام تجميع السلسلة. هذا أكثر أمانًا.
  • إنشاء الرؤوس: وفقًا لمرجع واجهة برمجة التطبيقات، يقوم duckdb.sql بإرجاع DuckDBPyRelation بينما يقوم duckdb.execute بإرجاع DuckDBPyConnection. لإنتاج قائمة بالرؤوس من كائن الاتصال، لم أتمكن من الوصول إلى .columns كما في السابق، لذا كان عليّ إلقاء نظرة على خاصية description الخاصة بالاتصال، والتي أتخيل أنها تصف مجموعة النتائج الحالية.

أدى تشغيل المعيار باستخدام الدالة الجديدة إلى بعض النتائج المثيرة للاهتمام:

iter_excel_duckdb_execute
elapsed 5.73
🔴 "number" expected type "<class 'int'>" received type "<class 'str'>"
🔴 "decimal" expected type "<class 'float'>" received type "<class 'str'>"
🔴 "date" expected type "<class 'datetime.date'>" received type "<class 'str'>"
🔴 "boolean" expected type "<class 'bool'>" received type "<class 'str'>"
🟢 "text"

باستخدام execute، تمكنا من معالجة الملف في 5.7 ثانية فقط – أي ضعف سرعة المحاولة الأخيرة، ولكننا فقدنا الأنواع. بدون الكثير من المعرفة والخبرة في استخدام DuckDB، لا يمكنني إلا أن أفترض أن إنشاء العلاقة والتحويل إلى الأنواع الصحيحة يتطلب بعض النفقات الإضافية.

قبل أن ننتقل إلى خيارات أخرى، دعنا نتحقق مما إذا كان التحميل المسبق وتثبيت الإضافات يحدث أي فرق كبير:

 import duckdb

+duckdb.install_extension('spatial')
+duckdb.load_extension('spatial')
+
 def iter_excel_duckdb_execute(file: IO[bytes]) -> Iterator[dict[str, object]]:
-    duckdb.install_extension('spatial')
-    duckdb.load_extension('spatial')
     rows = duckdb.execute(
        "SELECT * FROM st_read(?, open_options=['HEADERS=FORCE', 'FIELD_TYPES=AUTO'])",

تنفيذ الدالة عدة مرات:

iter_excel_duckdb_execute
elapsed 5.28
elapsed 5.69
elapsed 5.28

لم يكن للتحميل المسبق للامتدادات تأثيرًا كبيرًا على التوقيت.

دعونا نرى ما إذا كان إزالة اكتشاف النوع التلقائي له أي تأثير:

duckdb.load_extension('spatial')
 def iter_excel_duckdb_execute(file: IO[bytes]) -> Iterator[dict[str, object]]:
     conn = duckdb.execute(
-        "SELECT * FROM st_read(?, open_options=['HEADERS=FORCE', 'FIELD_TYPES=AUTO'])",
+        "SELECT * FROM st_read(?, open_options=['HEADERS=FORCE', 'FIELD_TYPES=STRING'])",
         [file.name],
     )
     headers = [header for header, *rest in conn.description]

تنفيذ الدالة عدة مرات:

iter_excel_duckdb_execute
elapsed 5.80
elapsed 7.21
elapsed 6.45

يبدو أيضًا أن إزالة اكتشاف النوع التلقائي لم يكن له أي تأثير كبير على التوقيت.

قراءة Excel باستخدام Calamine

في السنوات الأخيرة، يبدو الأمر وكأن كل مشكلة تتعلق بالأداء في بايثون يتم حلها باستخدام لغة أخرى. وبصفتي مطور بايثون، أعتبر هذا نعمة حقيقية. فهذا يعني أنني أستطيع الاستمرار في استخدام اللغة التي اعتدت عليها والاستمتاع بفوائد الأداء التي توفرها جميع اللغات الأخرى!

Calamine هي مكتبة Rust خالصة لقراءة ملفات Excel وجداول البيانات OpenDocument.

قراءة ملف Excel باستخدام calamine في بايثون:

import python_calamine

def iter_excel_calamine(file: IO[bytes]) -> Iterator[dict[str, object]]:
    workbook = python_calamine.CalamineWorkbook.from_filelike(file)  # type: ignore[arg-type]
    rows = iter(workbook.get_sheet_by_index(0).to_python())
    headers = list(map(str, next(rows)))
    for row in rows:
        yield dict(zip(headers, row))

المرور بنفس الروتين مرة أخرى – تحميل المصنف، واختيار الورقة، وجلب الرؤوس من الصف الأول، وتكرار النتائج وإنشاء قاموس من كل صف.

وهذا هو شكل الصف الأول:

>>> with open('file.xlsx', 'rb') as f:
...     rows = iter_excel_calamine(f)
...     row = next(rows)
...     print(row)
{'boolean': True,
 'date': datetime.date(2000, 1, 1),
 'decimal': 1.1,
 'number': 1.0,
 'text': 'CONTROL ROW'}

تشغيل المعيار:

iter_excel_calamine
elapsed 3.58
🔴 "number" expected type "<class 'int'>" received type "<class 'float'>"
🟢 "decimal"
🟢 "date"
🟢 "boolean"
🟢 "text"

هذه قفزة كبيرة! باستخدام python-calamine، قمنا بمعالجة الملف بالكامل في 3.5 ثانية فقط – وهي الأسرع حتى الآن! النقطة الحمراء الوحيدة هنا هي العدد الصحيح لدينا تم تفسيره على أنه عدد عشري – وهو أمر غير غير معقول تمامًا.

بعد البحث قليلاً، كانت المشكلة الوحيدة التي وجدتها مع python-calamine هي أنه لا يمكنه إنتاج نتائج كمكرر. ستقوم الدالة CalamineWorkbook.from_filelike بتحميل مجموعة البيانات بالكامل في الذاكرة، وهو ما قد يشكل مشكلة اعتمادًا على حجم الملف.

ملخص النتائج

فيما يلي ملخص لطرق قراءة ملفات Excel باستخدام بايثون:

MethodTiming (seconds)TypesVersion
Pandas32.98Yes2.1.3
Tablib28.52Yes3.5.0
Openpyxl35.62Yes3.1.2
Openpyxl (readonly)24.79Yes3.1.2
LibreOffice15.27No7.5.8.2
DuckDB (sql)11.36Yes0.9.2
DuckDB (execute)5.73No0.9.2
Calamine (python-calamine)3.58Yes0.22.1 (0.1.7)

إذن، أيهما ينبغي عليك استخدامه؟ الأمر يعتمد على… هناك بعض الاعتبارات الإضافية بخلاف السرعة عند اختيار مكتبة للعمل مع ملفات Excel في بايثون:

  • القدرة على الكتابة: قمنا باختبار طرق قراءة ملفات Excel، ولكن في بعض الأحيان يكون من الضروري إنتاج ملفات Excel أيضًا. بعض المكتبات التي قمنا باختبارها لا تدعم الكتابة. على سبيل المثال، لا تستطيع Calamine كتابة ملفات Excel، بل القراءة فقط.
  • التنسيقات الإضافية: قد يتطلب النظام تحميل وإنتاج ملفات بتنسيقات أخرى غير Excel. تدعم بعض المكتبات، مثل pandas وTablib، مجموعة متنوعة من التنسيقات الإضافية، بينما تدعم calamine وopenpyxl Excel فقط.

اكتشاف المزيد من بايثون العربي

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading