اذهب إلى المحتوى

كيفية التعامل مع النصوص في البرمجة


أسامة دمراني

لا شك أن التعامل مع النصوص هو أكثر ما يفعله المبرمج في عمله لأنه يبرمج بلغات تتكون من كلمات ورموز، وهي نصوص في النهاية، لذلك توجد أدوات كثيرة جدًا في أغلب لغات البرمجة لتسهيل التعامل مع هذه النصوص، وسننظر في كيفية استخدام تلك الأدوات في تنفيذ المهام البرمجية المعتادة التي تشمل ما يلي:

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

سننظر في كيفية تنفيذ كل مهمة من هذه المهام باستخدام بايثون، ثم نمر عليها سريعًا في جافاسكربت وVBScript، وتُستخدم توابع السلاسل النصية في بايثون للتعامل مع السلاسل النصية، فلعلك تذكر من مقال البيانات وأنواعها أن التوابع تشبه الدوال المرتبطة ببيانات، ونستطيع الوصول إلى التوابع باستخدام نفس الترميز النقطي dot notation الذي نستخدمه للوصول إلى الدوال في الوحدات modules، لكننا سنستخدم البيانات نفسها بدلًا من استخدام اسم الوحدة. لننظر الآن في ذلك.

تقسيم السلاسل النصية

أولًا سنقسم سلسلةً نصيةً إلى عدة أجزاء، والذي نحتاج إليه عند معالجة الملفات لأننا نقرأ الملف سطرًا سطرًا، لكن قد يحتوي جزء من السطر فقط على البيانات المطلوبة. من أمثلة ذلك برنامج دليل جهات الاتصال الذي نعود إليه كل حين، فقد نرغب في الوصول إلى حقول بعينها من مدخل ما، دون الحاجة لطباعة المدخل كله، وسنستخدم لهذا الغرض التابع split()‎ في بايثون كما يلي:

>>> aString = "Here is a (short) String"
>>> print( aString.split() )
['Here', 'is', 'a', '(short)', 'String']

لاحظ كيف حصلنا على قائمة تحتوي الكلمات التي في aString مع حذف جميع المسافات، لأن الفاصل الافتراضي للدالة ‎''.split()‎ هو المسافة البيضاء، سواءً كانت سطرًا جديدًا، أم مسافةً عادية، أم مسافة جدول tab. لنجرب الآن استخدامه مرةً أخرى بجعل الفاصل قوسًا افتتاحيًا:

>>> print( aString.split('(') )
['Here is a ', 'short) String']

يكمن الفرق هنا في أننا حصلنا على عنصرين فقط في القائمة، وقد حُذف القوس الافتتاحي من بداية ‎'short)'‎، وهذه ملاحظة مهمة حول ‎''.split()‎، وهي حذفه للمحارف الفاصلة، الذي نريده غالبًا مع استثناءات قليلة.

كذلك لدينا التابع ‎''.join()‎ الذي يأخذ قائمةً -أو أي نوع آخر- من التسلسلات النصية ويدمجها معًا، لكن له خاصية قد تسبب حيرةً عند استخدامه، وهو استخدامه السلسلة التي نستدعي التابع عليها محرفًا للدمج، كما يلي:

>>> lst = ['here','is','a','list','of','words']
>>> print( '-+-'.join(lst) )
here-+-is-+-a-+-list-+-of-+-words
>>> print( ' '.join(lst) )
here is a list of words

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

عد الكلمات

سنعيد النظر مرةً أخرى في برنامج عد الكلمات الذي أوردناه في مقال البرمجة باستخدام الوحدات، الذي كانت الشيفرة الوهمية فيه كما يلي:

def numwords(aString):
    list = split(aString) # قائمة بكل عنصر وكلمة
    return len(list) # تعيد عددًا من العناصر في القائمة

for line in file:
    total = total + numwords(line) # accumulate totals for each line

print( "File had %d words" % total )

لننظر إلى متن دالة numwords()‎ بما أننا شرحنا كيفية جلب الأسطر من الملف، حيث نريد أن ننشئ قائمةً من الكلمات في سطر، وذلك باستخدام التابع ‎''.split()‎ الافتراضي. وإذا نظرنا في توثيق بايثون فسنجد أن دالة len()‎ المضمَّنة تعيد عدد العناصر في قائمة ما، ويجب أن يكون ذلك العدد في حالتنا عدد الكلمات في السلسلة النصية، وهو ما نريده بالضبط، وعلى ذلك تبدو الشيفرة النهائية كما يلي:

def numwords(aString):
    lst = aString.split() # split() aString هو تابع كائن السلسلة 
    return len(lst)       # أعد عدد العناصر في القائمة

with open("menu.txt","r") as inp:
   total = 0  # initialize to zero; also creates variable
   for line in inp:
      total += numwords(line)  # راكم إجمالي كل سطر

print( "File had %d words" % total )

لكن هذه الشيفرة تحسب محارفًا مثل & على أنها كلمات، وهذا ليس صحيحًا، كما أنه لا يمكن استخدامها إلا على ملف واحد فقط هو menu.txt، رغم إمكانية تحويلها لتقرأ اسم الملف من سطر الأوامر argv[1]‎، أو عبر input()‎ كما رأينا في مقال كيفية قراءة البرامج لمدخلات المستخدم، وسنترك هذا تدريبًا للقارئ.

البحث في النصوص

ستكون العملية التالية التي ننظر فيها هي البحث عن سلسلة فرعية داخل سلسلة أكبر منها، وتدعم بايثون هذا من خلال تابع السلسلة ‎''.find()‎ الخاص بها، وأبسط استخدامات هذا التابع تزويده بسلسلة للبحث، وتعيد بايثون فهرس أول محرف من السلسلة الفرعية إذا وجدتها داخل السلسلة الرئيسية، أما إذا لم تجدها فستعيد ‎-1:

>>> aString = "here is a long string with a substring inside it"
>>> print( aString.find('long') )
10
>>> print( aString.find('oxen') )
-1
>>> print( aString.find('string') )
15

لقد كان المثالان الأولان واضحين ومباشرين، فالأول يعيد فهرس بداية ‎'long'‎، أما الثاني فيعيد -1، وذلك لأن ‎'oxen'‎ غير موجودة داخل aString؛ بينما المثال الثالث ففيه أمر مثير للاهتمام، إذ لا تحدد find إلا الورود الأول لسلسلة البحث فقط، لكن ماذا لو كانت سلسلة البحث مكررةً أكثر من مرة في السلسلة الأصلية؟ من الممكن هنا أن نستخدم فهرس المرة الأولى لورود سلسلة البحث لنقسم السلسلة الأصلية إلى جزأين ونبحث مرةً أخرى، ونكرر ذلك إلى أن نحصل على النتيجة ‎-1، كما يلي:

aString = "Bow wow says the dog, how many ow's are in this string?"
temp = aString[:] # استخدم التقسيم لصنع نسخة
count = 0
index = temp.find('ow')
while index != -1:
    count += 1
    temp = temp[index + 1:]  # استخدم التقسيم هنا.
    index = temp.find('ow')

print( "We found %d occurrences of 'ow' in %s" % (count, aString) )

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

يستطيع التابع find()‎ أن يسرّع من هذه العملية قليلًا باستخدام أحد المعامِلات الاختيارية الخاصة به، وهو موضع البداية في السلسلة الأصلية:

aString = "Bow wow says the dog, how many ow's are in this string?"
count = 0
index = aString.find('ow')  # استخدم البدء الافتراضي
while index != -1:
    count += 1
    index = aString.find('ow', index+1)  # اضبط بدءًا جديدًا

print( "We found %d occurrences of 'ow' in %s" % (count, aString) )

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

>>>  # قصر البحث على أول 20 محرف
>>> aString = "Bow wow says the dog, how many ow's are in the string?"
>>> print( aString.find('the',0,20) ) 

توفر بايثون عدة توابع أخرى لمواقف البحث الشائعة، مثل ‎''.startswith()‎ و‎''.endswith()‎، ونستطيع أن نخمن وظائف هذه التوابع بمجرد قراءة أسمائها، وهي تعيد إما True أو False بناءً على بدء السلسلة بسلسلة نصية معطاة أو انتهائها بها، كما يلي:

>>> print( "Python rocks!".startswith("Perl") )
False
>>> print( "Python rocks!".startswith('Python') )
True
>>> print( "Python rocks!".endswith('sucks!') )
False
>>> print( "Python rocks!".endswith('cks!') )
True

لاحظ أنها تعطينا نتائج بوليانيةً، حيث سنعلم أين نبحث إذا كانت الإجابة True. كذلك لاحظ أن سلسلة البحث لا يجب أن تكون كلمةً كاملة، بل تكفي سلسلة نصية فرعية. يمكن تزويد موضعي البدء start والانتهاء stop داخل السلسلة النصية، تمامًا مثل ‎''.find()‎، لنتحقق من وجود سلسلة نصية فرعية في أي موضع معطىً داخل السلسلة النصية، ولا تستخدَم هذه الخاصية الأخيرة في البرمجة العملية كثيرًا. كما يمكن استخدام عامل in الخاص ببايثون لإجراء اختبار بسيط للتحقق من وجود سلسلة نصية فرعية في أي مكان داخل سلسلة أخرى:

>>> if 'foo' in 'foobar': print( 'True' )
True
>>> if 'baz' in 'foobar': print( 'True' )
>>> if 'bar' in 'foobar': print( 'True' )
True

استبدال النصوص

بما أننا عثرنا على النص الذي نريده، فلنغيره الآن إلى شيء آخر. توفر توابع السلاسل النصية في بايثون لهذا حلًا متمثلًا في التابع ‎''.replace()‎ الذي يأخذ وسيطين، هما سلسلتا البحث والاستبدال، وتكون القيمة المعادة هي السلسلة الجديدة الناتجة عن الاستبدال.

>>> aString = "Mary had a little lamb, its fleece was dirty!"
>>> print( aString.replace('dirty','white') )
"Mary had a little lamb, its fleece was white!"

إن الفرق الأساسي بين ‎''.find()‎ و‎''.replace‎ هو أن الأخير يستبدل جميع مرات الحدوث في سلسلة البحث، وليس المرة الأولى فقط، ويُستخدم الوسيط الاختياري count لتقييد عدد مرات الاستبدال، كما يلي:

>>> aString = "Bow wow wow said the little dog"
>>> print( aString.replace('ow','ark') )
Bark wark wark said the little dog
>>> print( aString.replace('ow','ark',1) ) # واحد فقط
Bark wow wow said the little dog

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

تغيير حالة الأحرف

مما يجب مراعاته في النصوص هو كيفية التحويل من الحالة الصغرى للحروف إلى الحالة الكبرى، وإن كان هذا غير شائع، إلا أن بايثون توفر بعض التوابع المساعدة لذلك على أي حال:

>>> print( "MIXed Case".lower() )
mixed case
>>> print( "MIXed Case".upper() )
MIXED CASE
>>> print( "MIXed Case".swapcase() )
mixED cASE
>>> print( "MIXed Case".capitalize() )
Mixed case
>>> print( 'MIXed Case'.title() )
Mixed Case
>>> print( "TEST".isupper() )
True
>>> print( "TEST".islower() )
False

لاحظ أن التابع ‎''.capitalize()‎ يغير حالة الأحرف إلى الحالة الكبرى للسلسلة النصية كلها، وليس لكل كلمة فيها؛ أما تغيير حالة كل كلمة فينفذها التابع title()‎.

انتبه أيضًا إلى سلوك دالتي الاختبار ‎''.isupper()‎ و‎''.islower()‎، إذ توفر بايثون دوالًا توكيديةً مثل هذه لاختبار السلاسل النصية، منها: ‎''.isdigit()‎ و‎''.isalpha()‎ و‎''.isspace()‎، وتتحقق الدالة الأخيرة من جميع أنواع المسافات البيضاء، لا محارف المسافة العادية فقط.

وسنستخدم عدة توابع من هذا النمط، خاصةً في عد الكلمات.

التعامل مع النصوص في VBScript

تنحدر VBScript من لغة BASIC القديمة، لذا تحتوي على الكثير من دوال معالجة النصوص المدمجة فيها، وقد تجد 20 دالةً أو تابعًا في توثيقها، بالإضافة إلى الدوال الموجودة لمعالجة محارف اليونيكود، مما يعني أننا نستطيع فعل كل ما فعلناه في بايثون باستخدام VBScript، وسنمر عليها سريعًا فيما يلي:

تقسيم النصوص

نبدأ بدالة split:

<script type="text/vbscript">
Dim s
Dim lst
s = "Here is a string of words"
lst = Split(s) ' returns an array
MsgBox lst(1)
</script>

كما في بايثون، نستطيع إضافة قيمة فاصلة إذا أردنا، غير فاصل المسافة البيضاء العادي، كذلك لدينا دالة Join لعكس هذه العملية.

البحث عن النصوص واستبدالها

نبحث باستخدام InStr، وهو اختصار لـ In String:

<script type="text/vbscript">
Dim s,n
s = "Here is a long string of text"
n = InStr(s, "long")
MsgBox "long is found at position: " & CStr(n)
</script>

القيمة المعادة هنا هي الموضع الذي تبدأ فيه السلسلة الفرعية داخل السلسلة الأصلية، فإذا لم توجد السلسلة الفرعية فستعيد صفرًا، وهذه ليست مشكلةً بما أن VBScript تبدأ فهارسها بواحد وليس صفر، لذا لا يُعَد الصفر هنا فهرسًا صالحًا؛ أما إذا كانت إحدى السلسلتين Null، فستعيد القيمة Null، مما يجعل عملية اختبار شروط الخطأ أكثر صعوبةً وتحتاج إلى عدة اختبارات معًا.

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

<script type="text/vbscript">
Dim s,n
s = "Here is a long string of text"
n = InStr(6, s, "long") ' start at position 6
If n = 0 or n = Null Then ' check for errors
   MsgBox "long was not found"
Else
   MsgBox "long is found at position: " & CStr(n)
End If
</script>

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

تستبدَل النصوص باستخدام الدالة Replace كما يلي:

<script type="text/vbscript">
Dim s
s = "The quick yellow fox jumped over the log"
MsgBox Replace(s, "yellow", "brown")
</script>

ونستطيع توفير وسيط اختياري ليحدد عدد مرات الحدوث في سلسلة البحث التي يجب استبدالها، والوضع الافتراضي هو استبدالها جميعًا، لكن يمكن تحديد موضع بدء كما في InStr أعلاه.

تغيير الحالة

تغيّر الحالة في VBScript بواسطة UCase وLCase، لكن لا يوجد فيها ما يكافئ التابعين capitalize وtitle الموجودين في بايثون:

<script type="text/vbscript">
Dim s
s = "MIXed Case"
MsgBox LCase(s)
MsgBox UCase(s)
</script>

وهذا ما سنغطيه من معالجة النصوص في VBScript، فإذا أردت المزيد فارجع إلى ملف مساعدة VBScript لترى القائمة الكاملة للدوال.

التعامل مع النصوص في جافاسكربت

تُعَد جافاسكربت أقل اللغات الثلاث تجهيزًا للتعامل مع النصوص، لكن العمليات الأساسية موجودة إلى حد ما، رغم افتقارها إلى العدد الكبير من التوابع والدوال الموجودة في بايثون وVBScript للتعامل مع النصوص، وهي تعوض ذلك بدعم قوي للتعابير النمطية، حيث تعوض بدائية الدوال الموجودة كثيرًا، لكن على حساب تعقيد التعابير النمطية، وهي تأخذ المنظور كائني التوجه، مثل بايثون في التعامل مع السلاسل النصية، حيث تُنفَّذ المهام باستخدام توابع الصنف String.

تقسيم النصوص

تقسَّم النصوص في جافاسكربت باستخدام التابع split:

<script type="text/javascript">
var aList, aString = "Here is a short string";
aList = aString.split(" ");
document.write(aList[1]);
</script>

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

  aList.join(" ")  // ادمج عناصر مفصولة بمسافات

 البحث في النصوص

نبحث في النصوص في جافاسكربت باستخدام التابع search()‎:

<script type="text/javascript">
var aString = "Round and Round the ragged rock ran a rascal";
document.write( "ragged is at position: " + aString.search("ragged"));
</script>

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

اقتباس

توفر جافاسكربت عملية بحث أخرى لها سلوك مختلف تسمى match()‎، لكننا لن نشرح استخدامها هنا.

استبدال النصوص

يُستخدم التابع replace()‎ لاستبدال النصوص كما يلي:

<script type="text/javascript">
var aString = "Humpty Dumpty sat on a cat";
document.write(aString.replace("cat","wall"));
</script>

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

تغيير الحالة

تغيَّر حالة الأحرف في جافاسكربت باستخدام دالتين هما toLowerCase()‎ وtoUpperCase()‎:

<script type="text/javascript">
var aString = "This string has Mixed Case";
document.write(aString.toLowerCase()+ "<BR>");
document.write(aString.toUpperCase()+ "<BR>");
</script>

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

خاتمة

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

  • معالجة النصوص عملية شائعة لها دعم قوي مضمَّن في أغلب اللغات.
  • أكثر المهام شيوعًا هي تقسيم النصوص والبحث فيها، واستبدالها، وتغيير حالة الأحرف فيها.
  • توفر كل لغة مستويات مختلفة من الدعم، لكن العمليات الثلاث الأساسية متاحة دومًا.

ترجمة -بتصرف- للفصل الثالث عشر: Manipulating Text من كتابة Learning To Program لصاحبه Alan Gauld.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...