البحث والاستبدال داخل عدة ملفات TXT دفعة واحدة باستخدام بايثون

البحث والاستبدال داخل عدة ملفات TXT دفعة واحدة باستخدام بايثون

إذا كان لديك مجلد يحتوي عدة ملفات TXT وتريد استبدال كلمة أو عبارة داخلها دفعة واحدة، يمكنك المرور على الملفات باستخدام pathlib، قراءة المحتوى، فحص وجود النص، ثم استخدام replace() وكتابة النتيجة.

أبسط نسخة توضح الفكرة:

from pathlib import Path

folder = Path("documents")
old_text = "Python"
new_text = "بايثون"

for file_path in folder.glob("*.txt"):
    content = file_path.read_text(
        encoding="utf-8"
    )

    if old_text in content:
        updated = content.replace(
            old_text,
            new_text
        )

        file_path.write_text(
            updated,
            encoding="utf-8"
        )

هذه النسخة ليست السكربت النهائي. لا تشغلها مباشرة على ملفات مهمة. عند التعديل الجماعي اتبع التسلسل: بحث → معاينة → نسخة احتياطية → استبدال.

{alertWarning}ابدأ على نسخة تجريبية من المجلد. التعديل الجماعي قد يغيّر عشرات الملفات خلال لحظات، لذلك المعاينة والنسخة الاحتياطية جزء من الحل نفسه.

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

مثال Before / After

قبل التعديل:

file1.txt
---------
Learn Python today.

file2.txt
---------
Python is easy to start.

file3.txt
---------
No matching word here.

بعد استبدال Python بـبايثون:

file1.txt
---------
Learn بايثون today.

file2.txt
---------
بايثون is easy to start.

file3.txt
---------
No matching word here.

الملف الثالث لا يحتوي النص المستهدف، لذلك لا نحتاج إلى إعادة كتابته.

ابدأ بالبحث داخل جميع ملفات TXT بدون تعديلها

from pathlib import Path

folder = Path("documents")
search_text = "Python"

for file_path in folder.glob("*.txt"):
    content = file_path.read_text(
        encoding="utf-8"
    )

    if search_text in content:
        print(file_path.name)

ناتج محتمل:

file1.txt
file2.txt

بهذا تعرف الملفات المطابقة قبل أن تغير أي ملف.

اعرض عدد مرات التطابق داخل كل ملف

count = content.count(search_text)

if count:
    print(
        f"{file_path.name}: "
        f"{count}"
    )

مثال:

file1.txt: 2
file2.txt: 1

count() تحسب مرات ظهور النص المطابق حرفيًا وتساعدك على تقدير حجم التغيير قبل التنفيذ.

مراحل البحث والمعاينة والاستبدال داخل عدة ملفات TXT باستخدام بايثون

Preview قبل التنفيذ

بدل طباعة محتوى الملفات كاملًا، اعرض اسم الملف وعدد التطابقات:

[Preview] file1.txt: 2 matches
[Preview] file2.txt: 1 matches

وإذا أردت رؤية الأسطر المطابقة نفسها قبل التعديل، يمكنك استخدام:

keyword = "Python"

for line_number, line in enumerate(
    content.splitlines(),
    start=1
):
    if keyword in line:
        print(
            file_path.name,
            line_number,
            line
        )

اجعل DRY_RUN هو الوضع الافتراضي

DRY_RUN = True

مع True لا يتغير أي ملف ولا تُنشأ Backup؛ ترى فقط ما سيحدث. بعد المراجعة غيّرها إلى:

DRY_RUN = False

عندها يبدأ النسخ الاحتياطي ثم التنفيذ الحقيقي.

استخدام DRY_RUN لمعاينة استبدال النص قبل تعديل الملفات فعليًا

أنشئ Backup قبل الاستبدال

في النسخة النهائية نستخدم shutil.copy2(). وهي تنسخ الملف وتحاول الاحتفاظ ببيانات Metadata أكثر من copy() عندما يسمح النظام بذلك.

ينشئ السكربت مجلدًا جديدًا بجوار مجلد المصدر، مثل:

documents_backup_2026-09-16_120500_123456/

إضافة Timestamp مع جزء دقيق من الثانية تمنع الكتابة فوق Backup سابق عمليًا. ووضع Backup خارج مجلد documents يمنع دخوله ضمن البحث إذا استخدمت rglob().

إنشاء نسخة احتياطية قبل استبدال النص داخل عدة ملفات

ماذا تفعل replace()؟

content = "Python and Python"

updated_content = content.replace(
    "Python",
    "بايثون"
)

print(updated_content)

الناتج:

بايثون and بايثون

replace() ترجع String جديدة ولا تعدل النص الأصلي في مكانه، لذلك نخزن النتيجة ثم نكتبها إلى الملف. وبدون معامل count تستبدل جميع التطابقات.

استبدال أول ظهور فقط

updated_content = content.replace(
    OLD_TEXT,
    NEW_TEXT,
    1
)

الرقم 1 يعني استبدال أول ظهور فقط داخل الملف. لا نستخدمه افتراضيًا لأن هدف المقال هو الاستبدال الجماعي لكل التطابقات.

البحث العادي حساس لحالة الأحرف

Python ليست نفسها python أو PYTHON. للمطابقة غير الحساسة لحالة الأحرف يمكن استخدام Regex بصورة متقدمة:

import re

updated_content = re.sub(
    r"python",
    lambda match: "بايثون",
    content,
    flags=re.IGNORECASE
)
{alertWarning}Regex أقوى، لكنه قد يطابق نصوصًا أكثر مما تتوقع إذا كان Pattern غير دقيق. استخدم Preview أولًا.

إذا أردت كلمة كاملة فقط

في مثال إنجليزي مثل cat يمكن استخدام:

pattern = r"\bcat\b"

لكن حدود الكلمات مع العربية وUnicode لها تفاصيل إضافية، لذلك لا تقدم \b كحل عام لكل النصوص العربية دون اختبار.

glob أم rglob؟

للمجلد الحالي فقط:

files = SOURCE_DIR.glob("*.txt")

وللمجلدات الفرعية أيضًا:

files = SOURCE_DIR.rglob("*.txt")

لا تجعل rglob() افتراضيًا إذا لم تكن متأكدًا من الشجرة؛ قد تستهدف ملفات أكثر مما تتوقع.

اختيار الامتداد

PATTERN = "*.txt"

يمكن تغييره إلى *.md أو *.csv إذا كانت الملفات نصية فعلًا ومناسبًا تعديلها بهذه الطريقة.

{alertWarning}لا تستخدم هذا الأسلوب على .xlsx أو .pdf أو .docx أو الصور كأنها TXT. هذه الصيغ لها بنية مختلفة وتحتاج أدوات مناسبة.

استبعاد ملفات محددة

EXCLUDE = {
    "do-not-edit.txt",
    "config.txt",
}

السكربت النهائي يتجاوز هذه الأسماء حتى لو كانت مطابقة للامتداد.

UTF-8 والنص العربي

content = file_path.read_text(
    encoding="utf-8"
)

هذا صحيح للملفات المحفوظة بـUTF-8، لكنه لا يعني أن كل ملف نصي يستخدم هذا الترميز. إذا ظهر UnicodeDecodeError فابحث عن Encoding الحقيقي للملف بدل استخدام errors="ignore" كحل افتراضي؛ التجاهل قد يخفي بيانات أو أحرفًا.

وبعض ملفات UTF-8 التي تحتوي BOM قد تحتاج utf-8-sig في حالة فعلية محددة.

مثال عربي

OLD_TEXT = "شركة النور القديمة"
NEW_TEXT = "شركة النور"

المبدأ نفسه: عاين الملفات المطابقة وعدد التغييرات أولًا، ثم Backup، ثم التنفيذ.

السكربت النهائي: Preview + Backup + Replace

هذه النسخة تجمع المعاينة، النسخ الاحتياطي، الاستبعاد، البحث داخل المجلدات الفرعية عند الطلب، والتقرير النهائي:

from pathlib import Path
from datetime import datetime
import shutil


SOURCE_DIR = Path("documents")
PATTERN = "*.txt"

OLD_TEXT = "Python"
NEW_TEXT = "بايثون"

DRY_RUN = True
RECURSIVE = False

EXCLUDE = {
    "do-not-edit.txt",
    "config.txt",
}


if not SOURCE_DIR.exists():
    raise FileNotFoundError(
        f"Folder not found: {SOURCE_DIR}"
    )

if not SOURCE_DIR.is_dir():
    raise NotADirectoryError(
        f"Not a folder: {SOURCE_DIR}"
    )


if RECURSIVE:
    candidates = SOURCE_DIR.rglob(PATTERN)
else:
    candidates = SOURCE_DIR.glob(PATTERN)


files = sorted(
    (
        path
        for path in candidates
        if (
            path.is_file()
            and path.name not in EXCLUDE
        )
    ),
    key=lambda path: str(path).lower(),
)


scanned_files = 0
matched_files = 0
total_replacements = 0
modified_files = 0
skipped_files = 0
planned_changes = []


for file_path in files:
    scanned_files += 1

    try:
        content = file_path.read_text(
            encoding="utf-8"
        )
    except UnicodeDecodeError:
        print(
            "[Skipped: encoding] "
            f"{file_path}"
        )
        skipped_files += 1
        continue
    except PermissionError:
        print(
            "[Skipped: permission] "
            f"{file_path}"
        )
        skipped_files += 1
        continue

    count = content.count(OLD_TEXT)

    if count == 0:
        continue

    matched_files += 1
    total_replacements += count

    updated_content = content.replace(
        OLD_TEXT,
        NEW_TEXT,
    )

    relative_path = file_path.relative_to(
        SOURCE_DIR
    )

    planned_changes.append(
        (
            file_path,
            relative_path,
            updated_content,
            count,
        )
    )

    print(
        f"[Preview] {relative_path}: "
        f"{count} matches"
    )


backup_dir = None


if DRY_RUN:
    print(
        "\nDry run only. "
        "No files were changed."
    )

elif planned_changes:
    timestamp = datetime.now().strftime(
        "%Y-%m-%d_%H%M%S_%f"
    )

    backup_dir = (
        SOURCE_DIR.parent
        / (
            f"{SOURCE_DIR.name}"
            f"_backup_{timestamp}"
        )
    )

    backup_dir.mkdir(
        parents=True,
        exist_ok=False,
    )

    # Backup all matching files before changing any original file.
    for (
        file_path,
        relative_path,
        _,
        _,
    ) in planned_changes:
        backup_file = backup_dir / relative_path

        backup_file.parent.mkdir(
            parents=True,
            exist_ok=True,
        )

        shutil.copy2(
            file_path,
            backup_file,
        )

    # Apply changes only after the backup stage succeeds.
    for (
        file_path,
        relative_path,
        updated_content,
        count,
    ) in planned_changes:
        try:
            file_path.write_text(
                updated_content,
                encoding="utf-8",
            )
        except PermissionError:
            print(
                "[Write failed: permission] "
                f"{relative_path}"
            )
            continue

        modified_files += 1

        print(
            f"[Modified] {relative_path}: "
            f"{count} replacements"
        )


print("\n--- Report ---")
print(f"Scanned files: {scanned_files}")
print(f"Matched files: {matched_files}")
print(f"Replacements: {total_replacements}")
print(f"Modified files: {modified_files}")
print(f"Skipped files: {skipped_files}")

if backup_dir is not None:
    print(f"Backup: {backup_dir}")

طريقة الاستخدام

  1. أنشئ مجلد اختبار باسم documents وضع فيه ملفات TXT تجريبية.
  2. حدد OLD_TEXT وNEW_TEXT.
  3. اترك DRY_RUN = True في أول تشغيل.
  4. راجع أسماء الملفات وعدد التطابقات.
  5. إذا كانت النتيجة صحيحة، غيّر DRY_RUN = False.
  6. شغّل مرة أخرى؛ ينشئ Backup أولًا ثم يعدّل الملفات المطابقة.

لماذا يجمع السكربت التغييرات قبل الكتابة؟

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

لا تنسخ الملفات غير المطابقة إلى Backup

الملف الذي لا يحتوي OLD_TEXT لا يدخل planned_changes، لذلك لا يُنسخ ولا يُكتب من جديد.

التقرير النهائي

القيمةالمعنى
Scanned filesملفات TXT التي فُحصت بعد الاستبعاد.
Matched filesالملفات التي تحتوي النص.
Replacementsإجمالي التطابقات التي تم التخطيط لاستبدالها.
Modified filesالملفات التي نجحت كتابتها في التنفيذ الحقيقي.
Skipped filesملفات تجاوزها السكربت بسبب Encoding أو Permission أثناء القراءة.

في DRY_RUN = True تكون Modified files صفرًا لأن أي ملف لم يُكتب.

مجلد غير موجود أو مسار غير صحيح

السكربت يتحقق أن SOURCE_DIR موجود وأنه مجلد. إذا كانت المشكلة في المسار نفسه، راجع حل خطأ FileNotFoundError في بايثون.

PermissionError

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

الملفات الكبيرة جدًا

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

ملاحظة عن Line Endings

القراءة ثم الكتابة النصية قد تؤثر في طريقة تمثيل نهاية الأسطر حسب النظام وبيئة التشغيل. إذا كان الحفاظ الحرفي على Line Endings مهمًا، اختبر على نسخة ولا تعتمد على الافتراض.

العلاقة مع درس التعامل مع الملفات

هذا المقال لا يعيد شرح open() وread() وwrite() من البداية؛ هو تطبيق Batch على عدة ملفات. إذا احتجت الأساس أولًا فراجع أساسيات بايثون 22: التعامل مع الملفات في بايثون.

إذا كانت مهمتك تغيير أسماء الملفات نفسها

هذا Search Intent مختلف. راجع إعادة تسمية مئات الملفات دفعة واحدة باستخدام بايثون.

الخلاصة

البحث والاستبدال داخل عدة ملفات لا يجب أن يبدأ بـwrite_text(). ابدأ بالبحث وعدّ التطابقات، اعرض Preview، ثم أنشئ Backup، وبعدها عدّل الملفات المطابقة فقط.

{alertSuccess}القاعدة العملية: Search → Preview → Backup → Replace. في التعديلات الجماعية، الأمان جزء من الحل وليس خطوة إضافية.

أسئلة شائعة

كيف أبحث داخل جميع ملفات TXT باستخدام بايثون؟

استخدم Path.glob("*.txt") ثم اقرأ كل ملف وابحث عن النص باستخدام in أو count().

كيف أستبدل كلمة في عدة ملفات دفعة واحدة؟

استخدم replace() على محتوى الملفات المطابقة ثم اكتب النص الجديد بعد المعاينة والنسخ الاحتياطي.

كيف أعاين التغييرات قبل تعديل الملفات؟

استخدم DRY_RUN = True واطبع اسم الملف وعدد التطابقات بدون تنفيذ الكتابة.

كيف أنشئ نسخة احتياطية قبل الاستبدال؟

أنشئ مجلد Backup مستقلًا باسم يحتوي Timestamp واستخدم shutil.copy2() لنسخ الملفات التي ستتغير قبل الكتابة عليها.

كيف أبحث داخل المجلدات الفرعية؟

استخدم Path.rglob("*.txt") بدل glob()، ثم راجع Preview لأن نطاق البحث يصبح أوسع.

هل replace حساسة لحالة الأحرف؟

نعم. للمطابقة غير الحساسة لحالة الأحرف يمكن استخدام re.sub() مع re.IGNORECASE.

كيف أتعامل مع الملفات العربية وUTF-8؟

استخدم encoding="utf-8" عندما تكون الملفات محفوظة بهذا الترميز. إذا ظهر UnicodeDecodeError فتحقق من الترميز الحقيقي.

هل يمكن استخدام الكود مع PDF أو Excel؟

ليس بهذه الطريقة. هذه الملفات ليست TXT عادية وتحتاج مكتبات وأساليب مناسبة لبنيتها.

إرسال تعليق

أحدث أقدم