مقارنة ملفين Excel واستخراج الصفوف الجديدة والمعدلة والمحذوفة باستخدام بايثون وPandas

مقارنة ملفين Excel واستخراج الاختلافات باستخدام بايثون وPandas

لديك ملف Excel قديم وملف جديد، وتريد معرفة ما الذي تغير بينهما: ما الصفوف التي أضيفت؟ ما السجلات التي حُذفت؟ وما القيم التي تغيرت داخل الصفوف الموجودة في الملفين؟ هذه من أكثر المهام العملية في تقارير المخزون والأسعار والموظفين والفواتير وقوائم العملاء.

المقارنة اليدوية تصبح صعبة عندما يحتوي كل ملف على مئات أو آلاف الصفوف. بدل فتح الملفين والبحث صفًا صفًا، تستطيع استخدام بايثون وPandas لبناء مقارنة تعتمد على مفتاح فريد مثل product_id أو invoice_id أو employee_id، ثم إخراج تقرير جديد يوضح الصفوف الجديدة والمحذوفة والمعدلة.

في هذا الدرس سنقارن ملفين Excel من البداية إلى النهاية، ونشرح لماذا لا يجب الاعتماد على ترتيب الصفوف، وكيف نكتشف القيم القديمة والجديدة، وكيف نتعامل مع اختلاف ترتيب البيانات وNaN والمسافات والـID المكرر، ثم نحفظ النتيجة في ملف Excel يحتوي Sheets منفصلة باسم Added وRemoved وChanged.

{alertInfo} الحل المختصر: استخدم مفتاحًا فريدًا مثل product_id، ثم نفذ merge(..., how="outer", indicator=True) لاكتشاف الصفوف الجديدة والمحذوفة، وبعد ذلك قارن الأعمدة في السجلات الموجودة في الملفين لاستخراج القيم المعدلة.

{getToc} $title={محتوى المقال}

متى تحتاج إلى مقارنة ملفين Excel؟

  • مقارنة قائمة أسعار قديمة بقائمة أسعار جديدة.
  • معرفة المنتجات التي أضيفت أو حذفت من المخزون.
  • مقارنة بيانات الموظفين بين شهرين.
  • اكتشاف العملاء الجدد أو الحسابات التي تغيرت بياناتها.
  • مقارنة تقرير الأمس بتقرير اليوم.
  • معرفة الفواتير الجديدة أو الملغاة.
  • مقارنة قوائم SKU بين نسختين من نفس التقرير.

الفرق بين دمج ملفات Excel ومقارنتها

المهمةالأداة
جمع ملفات متشابهة فوق بعضهاpd.concat()
ربط سجل بنظيره باستخدام مفتاحpd.merge()
مقارنة قيم جدولين متوافقين في البنيةDataFrame.compare()

في ملفات الأعمال الواقعية تكون merge غالبًا نقطة البداية الأفضل، لأن ترتيب الصفوف قد يتغير وقد توجد سجلات أضيفت أو حذفت بالكامل.

مثال عملي للمقارنة

سنستخدم ملفين:

old.xlsx
new.xlsx

الملف القديم يحتوي مثلًا:

product_id  product    price  status
P001        Keyboard   100    Active
P002        Mouse       50    Active
P003        Monitor    300    Active

والملف الجديد:

product_id  product    price  status
P001        Keyboard   120    Active
P003        Monitor    300    Inactive
P004        Webcam      80    Active

النتيجة التي نريد الوصول إليها:

  • P004 سجل جديد.
  • P002 سجل محذوف.
  • سعر P001 تغير من 100 إلى 120.
  • حالة P003 تغيرت من Active إلى Inactive.

تثبيت Pandas وopenpyxl

python -m pip install pandas openpyxl

قراءة ملفي Excel

import pandas as pd

old_df = pd.read_excel("old.xlsx")
new_df = pd.read_excel("new.xlsx")

print(old_df.head())
print(new_df.head())

أهم قاعدة: استخدم مفتاحًا فريدًا

لا تقارن الصف الأول بالصف الأول والثاني بالثاني. ترتيب الصفوف قد يتغير في أي وقت. اختر عمودًا ثابتًا يميز كل سجل.

key = "product_id"

قد يكون المفتاح في مشروع آخر:

  • invoice_id للفواتير.
  • employee_id للموظفين.
  • customer_id للعملاء.
  • sku للمنتجات.

التحقق من وجود المفتاح في الملفين

if key not in old_df.columns:
    raise ValueError("المفتاح غير موجود في الملف القديم")

if key not in new_df.columns:
    raise ValueError("المفتاح غير موجود في الملف الجديد")

التحقق من IDs المكررة

old_duplicates = old_df[
    old_df[key].duplicated(keep=False)
]

new_duplicates = new_df[
    new_df[key].duplicated(keep=False)
]

print(old_duplicates)
print(new_duplicates)

إذا كان المفتاح يفترض أن يكون فريدًا ووجدت تكرارًا، لا تكمل المقارنة قبل فهم السبب. الـID المكرر قد يجعل merge تنتج صفوفًا إضافية لا تمثل فروقات حقيقية.

توحيد نوع المفتاح قبل المقارنة

قد يكون رقم المنتج نصًا في ملف ورقمًا في الآخر. وحّد النوع:

old_df[key] = (
    old_df[key]
    .astype("string")
    .str.strip()
)

new_df[key] = (
    new_df[key]
    .astype("string")
    .str.strip()
)

اكتشاف الصفوف الجديدة والمحذوفة باستخدام merge

merged = old_df.merge(
    new_df,
    on=key,
    how="outer",
    suffixes=("_old", "_new"),
    indicator=True
)

print(merged)

ما معنى _merge؟

القيمةالمعنى
left_onlyموجود في الملف القديم فقط، أي أنه محذوف من الجديد.
right_onlyموجود في الملف الجديد فقط، أي أنه سجل جديد.
bothالمفتاح موجود في الملفين ويمكن مقارنة قيمه.
مقارنة ملفين Excel باستخدام مفتاح ID واستخراج الصفوف الجديدة والمحذوفة في Pandas

استخراج الصفوف الجديدة

added_ids = merged.loc[
    merged["_merge"] == "right_only",
    key
]

added = new_df[
    new_df[key].isin(added_ids)
].copy()

استخراج الصفوف المحذوفة

removed_ids = merged.loc[
    merged["_merge"] == "left_only",
    key
]

removed = old_df[
    old_df[key].isin(removed_ids)
].copy()

استخراج السجلات الموجودة في الملفين

both = merged[
    merged["_merge"] == "both"
].copy()

وجود السجل في الملفين لا يعني أنه لم يتغير. نحتاج الآن إلى مقارنة الأعمدة المهمة.

تحديد الأعمدة التي نريد مقارنتها

columns_to_compare = [
    "product",
    "price",
    "status"
]

لا تقارن عمودًا مثل exported_at إذا كان يتغير في كل تصدير ولا يمثل تغييرًا تجاريًا حقيقيًا.

التعامل الصحيح مع NaN

إذا كانت الخلية فارغة في الملفين فنريد اعتبارها متساوية. نكتب دالة مساعدة:

def series_changed(old_series, new_series):
    same = (
        old_series.eq(new_series)
        |
        (
            old_series.isna()
            &
            new_series.isna()
        )
    )

    return ~same

استخراج الصفوف المعدلة

changed_mask = pd.Series(
    False,
    index=both.index
)

for column in columns_to_compare:
    changed_mask |= series_changed(
        both[f"{column}_old"],
        both[f"{column}_new"]
    )

changed = both[
    changed_mask
].copy()

unchanged = both[
    ~changed_mask
].copy()
اكتشاف القيم التي تغيرت بين ملفين Excel وعرض القيمة القديمة والجديدة باستخدام Pandas

عرض القيمة القديمة والجديدة

بفضل suffixes تحصل على أعمدة واضحة مثل:

price_old
price_new
status_old
status_new

وهذا يجعل تقرير التغييرات مفهومًا للمستخدم غير التقني أيضًا.

إضافة أسماء الحقول التي تغيرت

def get_changed_fields(row):
    result = []

    for column in columns_to_compare:
        old_value = row[f"{column}_old"]
        new_value = row[f"{column}_new"]

        if pd.isna(old_value) and pd.isna(new_value):
            continue

        if old_value != new_value:
            result.append(column)

    return ", ".join(result)


changed["changed_fields"] = changed.apply(
    get_changed_fields,
    axis=1
)

قد تحصل على:

product_id  changed_fields
P001        price
P003        status

حفظ الفروقات في ملف Excel واحد

with pd.ExcelWriter(
    "excel_changes.xlsx",
    engine="openpyxl"
) as writer:

    added.to_excel(
        writer,
        sheet_name="Added",
        index=False
    )

    removed.to_excel(
        writer,
        sheet_name="Removed",
        index=False
    )

    changed.to_excel(
        writer,
        sheet_name="Changed",
        index=False
    )
حفظ فروقات ملفي Excel في Sheets منفصلة للجديد والمحذوف والمعدل باستخدام بايثون

حساب ملخص سريع للتغييرات

print("الجديد:", len(added))
print("المحذوف:", len(removed))
print("المعدل:", len(changed))
print("بدون تغيير:", len(unchanged))

مقارنة قائمتين أسعار واستخراج تغير السعر

إذا كان هدفك فقط معرفة المنتجات التي تغير سعرها:

prices = old_df.merge(
    new_df,
    on="product_id",
    how="inner",
    suffixes=("_old", "_new")
)

price_changes = prices[
    prices["price_old"]
    !=
    prices["price_new"]
].copy()

حساب مقدار ونسبة تغير السعر

price_changes["price_difference"] = (
    price_changes["price_new"]
    -
    price_changes["price_old"]
)

price_changes["change_percent"] = (
    (
        price_changes["price_new"]
        - price_changes["price_old"]
    )
    / price_changes["price_old"]
    * 100
)

هذه الحالة مفيدة لقوائم الموردين والمشتريات ومتابعة الأسعار الشهرية.

مقارنة عمود واحد فقط بين ملفين Excel

comparison = old_df[
    ["employee_id", "status"]
].merge(
    new_df[["employee_id", "status"]],
    on="employee_id",
    suffixes=("_old", "_new")
)

changed_status = comparison[
    comparison["status_old"]
    !=
    comparison["status_new"]
]

مقارنة باستخدام أكثر من مفتاح

إذا كان المنتج يتكرر في أكثر من فرع:

keys = [
    "branch_id",
    "product_id"
]

merged = old_df.merge(
    new_df,
    on=keys,
    how="outer",
    suffixes=("_old", "_new"),
    indicator=True
)

تنظيف أسماء الأعمدة قبل المقارنة

old_df.columns = old_df.columns.str.strip()
new_df.columns = new_df.columns.str.strip()

هذه الخطوة تمنع اختلافات مثل price وprice .

تنظيف المسافات داخل القيم النصية

text_columns = [
    "product",
    "status"
]

for column in text_columns:
    old_df[column] = (
        old_df[column]
        .astype("string")
        .str.strip()
    )

    new_df[column] = (
        new_df[column]
        .astype("string")
        .str.strip()
    )

مقارنة الأرقام العشرية

إذا كانت البيانات مالية وتريد المقارنة حتى منزلتين عشريتين:

old_df["price"] = old_df["price"].round(2)
new_df["price"] = new_df["price"].round(2)

مقارنة التواريخ بطريقة صحيحة

old_df["date"] = pd.to_datetime(
    old_df["date"],
    errors="coerce"
)

new_df["date"] = pd.to_datetime(
    new_df["date"],
    errors="coerce"
)

متى أستخدم DataFrame.compare()؟

DataFrame.compare() ممتازة عندما يكون الجدولان متوافقين من حيث labels والبنية. لكنها ليست بديلًا مباشرًا عن merge إذا كان هناك IDs أضيفت أو حذفت.

common_ids = (
    set(old_df[key])
    &
    set(new_df[key])
)

old_common = (
    old_df[old_df[key].isin(common_ids)]
    .set_index(key)
    .sort_index()
)

new_common = (
    new_df[new_df[key].isin(common_ids)]
    .set_index(key)
    .sort_index()
)

differences = old_common.compare(new_common)

print(differences)

تجاهل أعمدة لا تريد مقارنتها

ignored_columns = {
    "exported_at",
    "last_sync"
}

columns_to_compare = [
    column
    for column in old_df.columns
    if column != key
    and column not in ignored_columns
    and column in new_df.columns
]

معرفة الأعمدة الجديدة أو المحذوفة من بنية الملف

old_columns = set(old_df.columns)
new_columns = set(new_df.columns)

only_old_columns = old_columns - new_columns
only_new_columns = new_columns - old_columns

print("في القديم فقط:", only_old_columns)
print("في الجديد فقط:", only_new_columns)

هذا مهم لأن إضافة عمود جديد إلى الملف هي تغيير في بنية البيانات، وليست مجرد تغيير قيمة.

نسخة عملية كاملة

import pandas as pd

OLD_FILE = "old.xlsx"
NEW_FILE = "new.xlsx"
OUTPUT_FILE = "excel_changes.xlsx"
KEY = "product_id"

COLUMNS_TO_COMPARE = [
    "product",
    "price",
    "status"
]


def prepare_data(df):
    df = df.copy()
    df.columns = df.columns.str.strip()
    df[KEY] = (
        df[KEY]
        .astype("string")
        .str.strip()
    )
    return df


def series_changed(old_series, new_series):
    same = (
        old_series.eq(new_series)
        |
        (
            old_series.isna()
            &
            new_series.isna()
        )
    )
    return ~same


old_df = prepare_data(
    pd.read_excel(OLD_FILE)
)

new_df = prepare_data(
    pd.read_excel(NEW_FILE)
)

if old_df[KEY].duplicated().any():
    raise ValueError("يوجد مفتاح مكرر في الملف القديم")

if new_df[KEY].duplicated().any():
    raise ValueError("يوجد مفتاح مكرر في الملف الجديد")

merged = old_df.merge(
    new_df,
    on=KEY,
    how="outer",
    suffixes=("_old", "_new"),
    indicator=True
)

added_ids = merged.loc[
    merged["_merge"] == "right_only",
    KEY
]

removed_ids = merged.loc[
    merged["_merge"] == "left_only",
    KEY
]

added = new_df[
    new_df[KEY].isin(added_ids)
].copy()

removed = old_df[
    old_df[KEY].isin(removed_ids)
].copy()

both = merged[
    merged["_merge"] == "both"
].copy()

changed_mask = pd.Series(
    False,
    index=both.index
)

for column in COLUMNS_TO_COMPARE:
    changed_mask |= series_changed(
        both[f"{column}_old"],
        both[f"{column}_new"]
    )

changed = both[changed_mask].copy()
unchanged = both[~changed_mask].copy()

with pd.ExcelWriter(
    OUTPUT_FILE,
    engine="openpyxl"
) as writer:
    added.to_excel(
        writer,
        sheet_name="Added",
        index=False
    )
    removed.to_excel(
        writer,
        sheet_name="Removed",
        index=False
    )
    changed.to_excel(
        writer,
        sheet_name="Changed",
        index=False
    )

print(f"الجديد: {len(added)}")
print(f"المحذوف: {len(removed)}")
print(f"المعدل: {len(changed)}")
print(f"بدون تغيير: {len(unchanged)}")
print(f"تم إنشاء: {OUTPUT_FILE}")

مقارنة عدة Sheets داخل ملفي Excel

إذا كان الملفان يحتويان على عدة أوراق:

old_book = pd.ExcelFile("old.xlsx")
new_book = pd.ExcelFile("new.xlsx")

print(old_book.sheet_names)
print(new_book.sheet_names)

بعد ذلك تستطيع تحديد الأوراق المشتركة وقراءة كل Sheet ومقارنتها بشكل مستقل.

مقارنة Sheet محددة

old_df = pd.read_excel(
    "old.xlsx",
    sheet_name="Products"
)

new_df = pd.read_excel(
    "new.xlsx",
    sheet_name="Products"
)

أخطاء شائعة عند مقارنة ملفين Excel

1. المقارنة حسب ترتيب الصفوف

استخدم ID أو SKU أو مفتاحًا ثابتًا بدل الموقع.

2. ID مكرر

افحص duplicated() قبل merge، وإلا قد تنتج علاقة many-to-many.

3. اختلاف نوع ID

وحّد النوع إلى string أو نوع مناسب قبل المقارنة.

4. مسافات مخفية

استخدم str.strip() عندما تكون المسافات الزائدة غير مهمة.

5. NaN في الملفين

تعامل معها كحالة خاصة إذا كانت الخليتان الفارغتان تعنيان نفس الشيء.

6. مقارنة timestamp يتغير تلقائيًا

استبعد الأعمدة التي لا تمثل تغييرًا تجاريًا حقيقيًا.

أخطاء شائعة عند مقارنة ملفين Excel باستخدام بايثون وPandas

كيف تختبر تقرير الفروقات؟

  • اختبر أولًا على ملفين صغيرين تعرف الفرق بينهما.
  • تأكد أن Added لا تحتوي IDs من القديم.
  • تأكد أن Removed لا تحتوي IDs من الجديد.
  • راجع بعض الصفوف المعدلة يدويًا.
  • قارن عدد الصفوف قبل وبعد.
  • اختبر القيم الفارغة والتواريخ والأرقام.

قراءة أعمدة محددة فقط للملفات الكبيرة

usecols = [
    "product_id",
    "product",
    "price",
    "status"
]

old_df = pd.read_excel(
    "old.xlsx",
    usecols=usecols
)

new_df = pd.read_excel(
    "new.xlsx",
    usecols=usecols
)

روابط داخلية مفيدة من بايثون العرب

بعد نشر مقال دمج ملفات Excel: أضف هنا رابطًا داخليًا مباشرًا إلى درس «دمج ملفات Excel متعددة في ملف واحد باستخدام بايثون وPandas» لأنه يمثل الخطوة السابقة الطبيعية لهذا الموضوع.

مصادر رسمية للتوسع

الخلاصة

مقارنة ملفين Excel باستخدام بايثون وPandas تصبح أكثر دقة عندما تتوقف عن التفكير في «الصف الأول مقابل الصف الأول» وتبدأ بالمقارنة باستخدام مفتاح فريد مثل product_id أو employee_id.

استخدم merge(..., how="outer", indicator=True) لاكتشاف السجلات الجديدة والمحذوفة، ثم قارن الأعمدة في السجلات الموجودة في الملفين لاستخراج الصفوف المعدلة والقيم القديمة والجديدة. نظف المفتاح والنصوص قبل المقارنة، وتعامل مع NaN، واستبعد الأعمدة التي تتغير تلقائيًا ولا تمثل تغييرًا حقيقيًا.

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

{alertSuccess} القاعدة المهمة: جودة مقارنة Excel تعتمد أولًا على اختيار مفتاح فريد صحيح وتنظيف البيانات قبل المقارنة. إذا كان المفتاح غير صحيح أو مكررًا، فلن تنقذك أي دالة مقارنة من نتيجة مضللة.

أسئلة شائعة

كيف أقارن ملفين Excel باستخدام بايثون؟

اقرأ الملفين باستخدام pd.read_excel()، اختر مفتاحًا فريدًا، ثم استخدم merge لاكتشاف الجديد والمحذوف، وقارن الأعمدة لاكتشاف القيم التي تغيرت.

كيف أعرف الصفوف الجديدة بين ملفين Excel؟

استخدم outer merge مع indicator=True، ثم اختر الصفوف التي تكون قيمة _merge فيها right_only.

كيف أعرف الصفوف المحذوفة؟

الصفوف التي تحمل left_only موجودة في الملف القديم فقط.

كيف أستخرج القيم التي تغيرت؟

اربط السجلات باستخدام ID ثم قارن أعمدة مثل price_old وprice_new.

كيف أقارن ملفين حتى لو تغير ترتيب الصفوف؟

لا تعتمد على الترتيب. استخدم ID أو SKU أو مفتاحًا ثابتًا.

كيف أقارن عمودًا واحدًا فقط؟

اقرأ المفتاح والعمود المطلوب من الملفين، نفذ merge باستخدام المفتاح، ثم قارن النسخة القديمة والجديدة من العمود.

كيف أقارن قائمة أسعار قديمة وجديدة؟

استخدم معرف المنتج كمفتاح، ثم قارن price_old وprice_new ويمكنك حساب فرق السعر ونسبة التغير.

ما الفرق بين merge وcompare في Pandas؟

merge تربط السجلات باستخدام مفتاح وتناسب وجود صفوف جديدة أو محذوفة، أما DataFrame.compare() فتناسب أكثر الجداول المتوافقة في البنية والفهارس.

لماذا تظهر فروقات رغم أن القيم تبدو متشابهة؟

قد توجد مسافات إضافية أو أنواع بيانات مختلفة أو NaN أو اختلاف في حالة الأحرف. نظف البيانات ووحد الأنواع قبل المقارنة.

كيف أحفظ الفروقات في ملف Excel جديد؟

استخدم pd.ExcelWriter() ثم احفظ Added وRemoved وChanged في Sheets منفصلة باستخدام to_excel().

كيف أقارن باستخدام أكثر من عمود كمفتاح؟

مرر قائمة أعمدة إلى on في merge، مثل on=["branch_id", "product_id"].

كيف أتأكد أن ID غير مكرر؟

استخدم df["id"].duplicated().any() أو duplicated(keep=False) لعرض السجلات المكررة.

هل يمكن مقارنة عدة Sheets؟

نعم. استخدم pd.ExcelFile() لمعرفة أسماء الأوراق ثم اقرأ كل Sheet وقارنها بشكل مستقل.

هل يمكن تلوين الخلايا التي تغيرت؟

نعم. بعد إنشاء تقرير الفروقات يمكنك استخدام openpyxl لتطبيق ألوان على الخلايا أو الصفوف المعدلة.

إرسال تعليق

أحدث أقدم