إذا كان لديك مجلد يحتوي عدة ملفات 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() تحسب مرات ظهور النص المطابق حرفيًا وتساعدك على تقدير حجم التغيير قبل التنفيذ.
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
عندها يبدأ النسخ الاحتياطي ثم التنفيذ الحقيقي.
أنشئ 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أو.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}")
طريقة الاستخدام
- أنشئ مجلد اختبار باسم
documentsوضع فيه ملفات TXT تجريبية. - حدد
OLD_TEXTوNEW_TEXT. - اترك
DRY_RUN = Trueفي أول تشغيل. - راجع أسماء الملفات وعدد التطابقات.
- إذا كانت النتيجة صحيحة، غيّر
DRY_RUN = False. - شغّل مرة أخرى؛ ينشئ 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 عادية وتحتاج مكتبات وأساليب مناسبة لبنيتها.


