בעיה י"ט
הוראות המופיעות בגוף של מבנה while ומתבצעות כל עוד תנאי הלולאה מתקיים צריכות להיות מוזזות ימינה ביחס לשורה הראשונה במבנה. הקוד התקין:
age = input(‘Insert age; -1 to stop: ‘)
while age != ‘-1’:
print(age)
age = input(‘Insert age; -1 to stop: ‘)
כתבו את הפונקציה compareFreqs –
def compareFreqs(filename1, filename2, puncts, exclude):
לפונקציה ארבעה פרמטרים אלה –
– filename1 ו-filename2 – שמות של קבצי טקסט הנמצאים בתיקיית העבודה
– puncts – מחרוזת של סימני פיסוק
– exclude – רשימת מילים
הפונקציה תתחיל בקריאת כל תוכן הקובץ filename1 לממחרוזת אחת שתוצב במשתנה text1. בשלב הבא הפונקציה תהפוך את כל האותיות ב-text1 ל”אותיות קטנות” (ב-lower case), ו”תנקה” מ-text1 את כל סימני הפיסוק שיש בה ומופיעים במחרוזת puncts; הניקוי ייעשה באמצעות החלפת כל סימן פיסוק ברווח יחיד. הפונקציה תבצע פעולות אלה גם על תוכן הקובץ filename2 ותוצאת פעולותיה אלה תהיה מחרוזת שתישמר במשתנה בשם text2.
לאחר מכן הפונקציה תיצור רשומה של רשומות פנימיות בשם wordsFreqs1. בכל רשומה פנימית תופיע מילה אחת המופיעה במחרוזת text1, ושכיחותה במחרוזת זו, בסדר כרצונכם. מילה מוגדרת בתור רצף של תווים שאינם תווי רווח שהתקבל מפיצול המחרוזת text1 לפי כל תו רווח. הפונקציה תיצור רשומה כזו גם עבור המילים המופיעות במחרוזת text2, ושם הרשומה הזאת יהיה wordsFreqs2. הן ברשומה wordsFreqs1 הן ברשומה wordsFreqs2 לא תופיע מילה יותר מפעם אחת.
בשלב האחרון הפונקציה תיצור רשומה נוספת, ובה 10 רשומות פנימיות. הרשומות הפנימיות יכילו מידע על אודות 10 המילים השכיחות ביותר בקובץ filename1, לא כולל המילים שברשימה exclude. אפשר להניח שיש בקובץ filename1 לפחות 10 מילים שאינן המילים ברשימה exclude. כל רשומה פנימית תכיל את אחת ממילים אלה, שכיחותה בקובץ זה, ושכיחותה בקובץ filename2, בסדר זה; אם המילה אינה מופיעה בקובץ filename2, הערך השלישי בכל רשומה פנימית יהיה המחרוזת “NA”. רשומת הרשומות תהיה ממויינת לפי השכיחויות בקובץ filename1. הפונקציה תחזיר רשומה זו.
לדוגמה: נניח שהקובץ waves.txt מכיל את הספר The Waves מאת וירג’יניה וולף, והקובץ room.txt מכיל את הספר A Room of One’s Own, גם כן מאת וירג’יניה וולף. אם נזמן את הפונקציה כך –
compareFreqs(“waves.txt”, “room.txt”, “.,?!-”, [‘a’, ‘an’, ‘the’])
הפונקציה תכניס את כל תוכן הספר The Waves בתור מחרוזת אחת למשתנה text1, תהפוך את כל המחרוזת השמורה במשתנה זה ל-lower case, ותחליף כל סימן נקודה, פסיק, סימן שאלה וסימן קריאה ברווח יחיד. היא תטפל באופן זהה בתוכן הספר A Room of One’s Own, ותכניס את המחרוזת שהתקבלה למשתנה text2. לאחר מכן הפונקציה תיצור את הרשומות wordsFreqs1 ו-wordsFreqs2, ועל פיהן תיצור רשומה של רשומות המכילה את 10 המילים השכיחות ביותר בטקסט שהתקבל במשתנה text1 לאחר כל השינויים האמורים, לא כולל המילים ‘a’, ‘an’ ו-‘the’, את שכיחויותיהן של מילים אלו בספר The Waves, ואת שכיחויותיהן בספר A Room of One’s Own. הרשומה המוחזרת תמוין לפי שכיחויות המילים בספר The Waves, והיא יכולה להיות זו –
((‘and’, 2605, 1382), (‘i’, 2297, 538), (‘of’, 1991, 1332), (‘to’, 1430, 1022), (‘in’, 1319, 672), (‘is’, 1037, 437), (‘my’, 935, 155), (‘with’, 876, 233), (‘that’, 747, 648), (‘on’, 629, 208))
לפתרון בקוד ראו כאן.
פתרון מפורט
נתחיל ונעיר כי זו הבעיה הקשה והמורכבת ביותר באוסף בעיות זה. היא משלבת הרבה מהחומרים, הגישות והשיטות שננקטו בפתרונות לבעיות הקודמות. לכן רצוי מאוד להגיע אליה לאחר שפתרתם את רוב הבעיות הבאות לפניה ועיינתם בפתרונותיהן.
הבעיה מגדירה באופן ברור למדי ארבעה רכיבים של הפתרון, ואלה הם:
(א) קריאת תוכן מלא של קובץ למחרוזת
(ב) עריכת כמה שינויים במחרוזת שנקראה מהקובץ
(ג) הכנת רשומה מהמילים במחרוזת שהוכנה בשלב ב’ ומשכיחויותיהן
(ד) הכנת הרשומה שהפונקציה מחזירה
כן ברור לגמרי מתיאור הבעיה שאת סדרת הפעולות ברכיב א’ יש לבצע פעמיים – בכל פעם על קובץ אחד משני הקבצים ששמותיהם מועברים לפונקציה, וכך גם בנוגע לסדרות הפעולות ברכיבים ב’ וג’– את כל אחת מהן יש לבצע פעם על הקובץ ששמו filename1 ופעם נוספת על הקובץ ששמו filename2. כדי לא לכתוב את הקוד המבצע את שלוש סדרות הפעולות האלה פעמיים, כלומר כדי להימנע משכפול קוד, נכתוב פונקציה נפרדת עבור כל אחת משלוש סדרות פעולות אלו. שיקול נוסף המוביל אותנו להחלטה זאת הוא שנראה ברור שהפונקציות שנכתוב יוכלו לשרת אותנו גם בתכניות אחרות. למשל פונקציה הקוראת תוכן מלא של קובץ מסוים למחרוזת אחת יכול להועיל בתכניות אחרות העוסקות בניתוח של טקסטים, וכך גם פונקציה ההופכת מחרוזת נתונה ל”אותיות קטנות” (lower case) ו”מנקה” סימני פיסוק ממנה. אם כן בכתיבת הפתרון לבעיה נתקדם כך –
• נכתוב פונקציה נפרדת עבור סדרת הפעולות של רכיב א’, נקרא לה getFileString
• נכתוב פונקציה נפרדת עבור סדרת הפעולות של רכיב ב’, נקרא לה getCleanFile
• נכתוב פונקציה נוספת עבור סדרת הפעולות ברכיב ג’, נקרא לה findWordsAndFreqs
• לבסוף נממש את הפונקציה compareFreqs והיא תשתמש בפונקציות הנ”ל.
אם טרם פתרתם את הבעיה בכוחות עצמכם רצוי מאוד שתעצרו את הקריאה כאן ותנסו לכתוב אחת או יותר משלוש הפונקציות הראשונות קודם שתמשיכו לעיין בפתרון.
הנה הצעה למימוש הפונקציה getFileString –
def getFileString(filename):
f = open(filename, ‘r’)
s = f.read()
f.close()
return s
לפונקציה getFileString פרמטר אחד: filename, שם של קובץ טקסט הנמצא בתיקיית העבודה. גוף הפונקציה מתחיל בפתיחת הקובץ filename לקריאה וטעינת כל הטקסט שבו למשתנה מחרוזת s. לפני שמוחזרת המחרוזת s נסגר הקובץ, למען הסדר הטוב.
הנה דוגמה לזימון הפונקציה getFileString –
waves = getFileString(“waves.txt”)
הוראה זו תציב במשתנה waves את תוכנו של כל הספר The Waves, בתור מחרוזת אחת.
לפונקציה getCleanFile נציע שני מימושים שונים זה מזה. הנה הראשון –
def getCleanFile(s, puncts):
s = s.lower()
newS = “”
for c in s:
if c not in puncts:
newS = newS + c
else:
newS = newS + ” “
return newS
לפונקציה getCleanFile שני פרמטרים: s, מחרוזת כלשהי, ו-puncts – מחרוזת המכילה סימני פיסוק. ההוראה הראשונה יוצרת מחרוזת חדשה, newS, בהתבסס על s. במחרוזת החדשה כל אות “גדולה” (upper case) מומרת לאות “קטנה” (lower case). שימו לב שמחרוזת היא סוג רצף שאי אפשר לשנותו במקום. שינוי שמבצעת הפונקציה lower אינו במקום אלא יוצר מחרוזת חדשה ואותה יש לקלוט בהוראת השמה. לכן אם היינו כותבים כך –
s.lower()
ביצוע הוראה זו היה משאיר את המחרוזת במשתנה s כפי שהיא.
קטע הקוד המסיים את הפונקציה הוא זה –
newS = “”
for c in s:
if c not in puncts:
newS = newS + c
else:
newS = newS + ” “
תכלית הקטע היא ליצור, לפי המחרוזת הנמצאת במשתנה s (זו שהוחזרה מהפונקציה lower), מחרוזת חדשה שבה במקום כל סימן פיסוק המופיע בפרמטר puncts יש רווח. הבעיה הכללית שהקוד הזה פותר דומה לבעיה שנפתרה בבעיה א’. בבעיה ההיא היה נתון אוסף והיה עלינו למחוק ממנו ערכים המקיימים תנאי מסוים. אחד הפתרונות שהצענו שם היה ליצור אוסף חדש המכיל את כל הערכים באוסף הנתון שאינם מקיימים את התנאי. זה מה שנעשה בקוד כאן, בהבדל אחד: כאן אנו מכניסים לאוסף ההולך ונבנה ערך מסוים אחד במקום כל הערכים באוסף הנתון המקיימים את התנאי. ספציפית: הקוד יוצר מחרוזת חדשה, newS, ולאחר מכן סורק תו-תו במחרוזת s. הוא מוסיף לסוף המחרוזת news תווים שאינם סימן פיסוק המופיע במחרוזת puncts. כמו כן במקום כל תו שהוא סימן פיסוק כזה, נוסף תו רווח יחיד לסוף המחרוזת news. בסוף הפונקציה מוחזרת המחרוזת news.
הבה נבחן את אופן פעולתה של הפונקציה getCleanFile . עיינו בקטע קוד זה –
partOfWaves = ‘ As they neared the shore each bar rose, heaped itself, broke and swept a thin veil of white water across the sand. The’
cleanedWaves = getCleanFile(partOfWaves, “.,?!-“)
המחרוזת שתוצב במשתנה cleanedWaves תכיל את המקטע הזה –
‘as they neared the shore each bar rose heaped itself broke and swept a thin veil of white water across the sand the’
אנו רואים כאן את ההמרה מאות “גדולה” לאות “קטנה” (במילה As) וכן את ההחלפה של הסימן פסיק והסימן נקודה – כל אחד בתו רווח יחיד. נעיר כי ההחלפה בתו רווח יחיד אינה הכרחית, ואפשר שנרצה להחליף במחרוזת ריקה. אולם אם נחליף כל סימן פיסוק במחרוזת ריקה נצטרך לתת את הדעת שצירופי מילים באמצעות מקף, למשל –
brother-in-law
sixteenth-century
יומרו למחרוזת אחת ללא מקפים –
brotherinlaw
sixteenthcentury
אמנם בהחלפה בתו רווח יחיד, כשנבוא לפצל את הקובץ למילים, תאבד הזיקה בין המילים המרכיבות צירופים כאלה. כך למשל לאחר פיצול לפי תו רווח יחיד לא נדע שהמילה law היא למעשה חלק מהצירוף brother-in-law. קיצורו של דבר, ההחלטה במה להחליף תלויה בהקשר ובהחלטות התכנוניות של הפרויקט שבמסגרתו נכתב הקוד.
אפשר לממש את הפונקציה getCleanFile גם כך –
def getCleanFile(s, puncts):
s = s.lower()
for punct in puncts:
s = s.replace(punct, ” “)
return s
מימוש זה שונה מקודמו בכל הנודע ל”הסרת” סימני הפיסוק. במקום לסרוק את התווים במחרוזת s, הוא סורק את סימני הפיסוק, ומחליף כל אחד ברווח יחיד. המימוש הזה “יקר” מקודמו מבחינת הטיפול בזכרון, בעיקר בשל פעולות ההחלפה: כל אחת מהן מייצרת אובייקט מחרוזת חדש.
נפנה עתה למימוש הפונקציה findWordsAndFreqs. הנה הצעה למימוש –
def findWordsAndFreqs(text):
words = text.split()
wordsAndFreqs = ()
for word in set(words):
wordsAndFreqs += ((word, words.count(word)),)
return wordsAndFreqs
לפונקציה findWordsAndFreqs פרמטר אחד:text – מחרוזת. הפונקציה מתחילה בפיצול text לרצפים של תווים, לפי כל תו רווח, כנדרש. בפיצול לפי כל תו רווח אין מועבר ארגומנט לפונקציה split. כך יפוצלו “מילים” בשורה אחת המופרדות זו מזו במספר כלשהו של תווי רווח, וגם יופרדו זוגות מילים המורכבים ממילה בסוף שורה וממילה בתחילת שורה ומופרדות זו מזו בתו ‘\n’. הפונקציה split מחזירה את המחרוזות שהתקבלו מהפיצול בתוך רשימה, וזו מוצבת במשתנה words. נציין שהשימוש בשם “מילה” ו-word, כאן ובמקומות אחרים בבעיה זו ובפתרונה הוא מעט מטעה, כיוון שלא כל מחרוזת המתקבלת מפיצול של ספר היא בהכרח מילה שאפשר למצאה במילון אנגלי. למשל הואיל ובניקוי הקובץ לא החלפנו את הסימן * בתו רווח יחיד, אפשר שברשימה המתקבלת מהפיצול תהתקבל המחרוזת “**”.
שאר הקוד בגוף הפונקציה findWordsAndFreqs יוצר רשומה של רשומות פנימיות. רשומת הרשומות נוצרת בלולאה. בכל סיבוב שלה נוספת לרשומה ההולכת ונבנית רשומה פנימית המכילה שני ערכים אלה: מילה מהרשימה waves, ושכיחותה במחרוזת text1, בסדר זה. נעיין מקרוב בהוראה הזאת –
wordsAndFreqs += ((word, words.count(word)),)
מטרת ההוראה היא להוסיף לרשומה ההולכת ונבנית רשומה פנימית שיש בה שני ערכים. כאן תחמנו את הרשומה המיתוספת בסוגריים עגולים, וגם הנחנו פסיק ממש לפני הסוגר העגול הסוגר. עשינו זאת כדי להבהיר שהרשומה הפנימית אמורה להיתוסף לרשומה ההולכת ונבנית בתור רשומה עצמאית ונפרדת בתוך הרשומה wordsAndFreqs, וכדי שלא תתבצע כאן הוספה של הערכים ברשומה המיתוספת, כל אחד בנפרד, לסוף ההוראה ההולכת ונבנית. כדי להבהיר את הדברים עיינו בקטע הקוד הזה –
wordsAndFreqs = ((‘night’, 5), (‘day’, 3))
wordsAndFreqs += (‘hello’, 2)
print(wordsAndFreqs)
>>>
((‘night’, 5), (‘day’, 3), ‘hello’, 2)
ההוראה השניה בקטע קוד זה מוסיפה את המחרוזת ‘hello’ ואת המספר 2, כל אחד בנפרד, לסוף הרשומה wordsAndFreqs. עכשיו עיינו בקטע הקוד הזה –
wordsAndFreqs = ((‘night’, 5), (‘day’, 3))
wordsAndFreqs += ((‘hello’, 2),)
print(wordsAndFreqs)
>>>
((‘night’, 5), (‘day’, 3), (‘hello’, 2))
כאן הוספה הרשומה (‘hello’, 2), כמקשה אחת, לסוף הרשומה wordsAndFreqs. זה מה שנדרש במקרה שאנו עוסקים בו. כאמור הפסיק שהונח מיד לפני הסוגר העגול הסוגר – הונח שם כיוון שהערך שאנו מוסיפים לרשומה wordsAndFreqs הוא רשומה שיש בה ערך יחיד (שהוא עצמו רשומה).
נעיר כי מבחינת שימוש במשאבי הזכרון, הקוד היה יעיל יותר לו היה בונה רשימה של רשומות. כפי שהוסבר בפתרון לבעיה י”א, בשימוש באופרטור =+ להוספה לרשומה, נוצרת רשומה חדשה. לעומת זאת בשימוש באופרטור זה ליצירת רשומה השינוי הוא במקום. לו היינו בונים רשימה של רשומות היינו ממירים אותה, בתור צעד אחרון בגוף הפונקציה, לרשומה, ומחזירים רשומה זו.
לפי הנדרש, ברשומה wordsAndFreqs אין מילה המופיעה ביותר מרשומה פנימית אחת. לכן לולאת ה-for אינה סורקת את כל המילים שהתקבלו מפיצול המחרוזת text1 אלא רק את אוסף המילים השונות זו מזו; אוסף זה התקבל באמצעות הפעלת הפונקציה set על הרשימה words. אמנם ספירת כל מילה ומילה נעשית לפי הרשימה words ולא לפי קבוצת המילים השונות זו מזה, כיוון שעלינו למצוא את השכיחות של המילה במחרוזת text.
הנה דוגמה לקטע המזמן את הפונקציה findWordsAndFreqs ותחילת הפלט המתקבל מהקטע –
waves = getFileString(“waves.txt”)
cleanedWaves = getCleanFile(waves, “.,?!”)
print(findWordsAndFreqs(cleanedWaves))
>>>
((‘continued’, 1), (‘works’, 3), (‘seagreen’, 1), (‘cleverly’, 1), (‘trips’, 1) . . . )
כאן קראנו את תוכן הספר The Waves לתוך מחרוזת אחת waves. על פי מחרוזת זו יצרנו מחרוזת חדשה, cleanedWaves, שבה הומרה כל אות לועזית “גדולה” לאות “קטנה”, וכל נקודה, פסיק, סימן שאלה וסימן קריאה הוחלפו בתו רווח יחיד. אחר כך יצרנו רשומה של רשומות המכילה את כל המילים השונות זו מזו שיש במחרוזת cleanedFile ואת שכיחויותיהן במחרוזת זו.
בשלב זה אנו מוכנים להתחיל לכתוב את הפונקציה compareFreqs. הנה כך נתחיל –
def compareFreqs(filename1, filename2, puncts, exclude):
cleanText1 = getCleanFile(getFileString(filename1), puncts)
wordsAndFreq1 = findWordsAndFreqs(cleanText1)
התחלנו בקוד היוצר רשומה של רשומות המכילה את כל המילים השונות זו מזו בקובץ filename1 ואת שכיחויותיהן – זו הרשומה wordsAndFreq1. עתה עלינו למצוא את 10 המילים השכיחות ביותר ברשומת רשומות זו. גישה אחת כאן היא ליצור רשימה של השכיחויות ברשומה, למיין אותה, למצוא את 10 השכיחויות הגדולות ביותר, ואז להכין רשימה של מילים שאלו השכיחויות שלהן. גישה אחרת, שנקטנו דומה לה בפתרון לבעיה ט’, היא להשתמש בפונקציית מיון – כאן זו תהיה הפונקציה sorted, בנוסף נפעיל את הפונקציה sorted על הרשומה wordsAndFreq1 בארגון אחר של הרשומות הפנימיות בה: בארגון זה השכיחות תופיע ראשונה והמילה שניה, כך –
((1, ‘continued’), (3, ‘works’), (1, ‘seagreen’), (1, ‘cleverly’), (1, ‘trips’) . . . )
הצורך במיון לפי ארגון זה ולא לפי הארגון המקורי של נובע מכך ש-sorted, בשימוש הפשוט בה, ממיינת מיון ראשי לפי הערכים באינדקס 0 ברשומות (שוות האורך) הפנימיות, אחר כך מיון משני לפי הערכים באינדקס 1, וכן הלאה. כאמור כאן אנו רוצים למיין לפי השכיחויות, ומכאן הצורך ב”המרה” הזאת. נעיר כי אפשר להימנע משינוי ארגון זה באמצעות שימוש בפרמטר key של הפונקציה sorted, אך הסבר פרמטר זה הוא מעבר לגבולותיו של הדיון כאן. מכל מקום, שינוי הארגון של הרשומה wordsAndFreq1 מעמיד לפנינו אתגר זה: הארגון הנדרש לצורך המיון – בכל רשומה פנימית קודם שכיחות ואחר כך מילה – אינו הארגון ברשומה שמחזירה הפונקציה findWordsAndFreqs. פתרון אפשרי לבעיה זו הוא לשוב לפונקציה findWordsAndFreqs, ולשכתב אותה כדי שתארגן את הרשומות הפנימיות ברשומה המוחזרת לפי הנדרש: מילה ואז שכיחות, או שכיחות ואז מילה. נעשה זאת באמצעות הוספת פרמטר שלישי לפונקציה הזאת, כך –
csvDict[‘FILENAME’] = [‘text1’]
והמילון המתקבל הוא זה –
def findWordsAndFreqs(text, wordFirst = True):
words = text.split()
wordsAndFreqs = ()
for word in set(words):
if wordFirst:
wordsAndFreqs += ((word, words.count(word)),)
else:
wordsAndFreqs += ((words.count(word), word),)
return wordsAndFreqs
לפרמטר החדש wordFirst יש ערך בררת מחדל, True. בבואנו להוסיף רשומה פנימית לרשומה wordsAndFreqs ההולכת ונבנית, אנו בודקים את הערך שיש בפרמטר זה: אם יש בו True, המילה ברשומה הפנימית מופיעה לפני השכיחות; אם יש בו False, השכיחות באה לפני המילה. בהסתייעות במימוש החדש הזה של הפונקציה findWordsAndFreqs נוכל להמשיך במימוש הפונקציה compareFreqs –
def compareFreqs(filename1, filename2, puncts, exclude):
cleanText1 = getCleanFile(getFileString(filename1), puncts)
wordsAndFreq1 = findWordsAndFreqs(cleanText1, False)
sortedPairs = sorted(wordsAndFreq1, reverse = True)
בשורה השניה של גוף הפונקציה, בזימון הפונקציה findWordsAndFreqs, העברנו אליה את הארגומנט False לפרמטר wordFirst. כך היא החזירה רשומה שבה ברשומות הפנימיות באה השכיחות לפני המילה. רשומה זו מועברת לפונקציה sorted והיא יוצרת רשימה חדשה שבה הרשומות הפנימיות ברשומה wordsAndFreqs1 ממוינות בסדר יורד (reverse = True) . כך לדוגמה יכולה להתחיל הרשומה sortedPairs –
[(4826, ‘the’), (2605, ‘and’), (2297, ‘i’), (1991, ‘of’), (1631, ‘a’) . . . ]
מהרשומה sortedPairs עלינו לקבל את 10 המילים השכיחות ביותר, לא כולל המילים הנמצאות ברשימה exclude (לפי תיאור הבעיה יש 10 מילים כאלה בקובץ filename1). אם כן נתחיל בקבלת אוסף של כל הזוגות שכיחות-מילה שאין בו זוגות שבהם המילה מופיעה ברשימה exclude. שוב אנו צריכים לפתור את הבעיה הכללית הזאת: נתון אוסף ויש ליצור אוסף חדש המכיל רק את הערכים באוסף הנתון שאינם מקיימים תנאי מסוים. הנה הקוד המטפל בבעיה במקרה הנוכחי –
def compareFreqs(filename1, filename2, puncts, exclude):
cleanText1 = getCleanFile(getFileString(filename1), puncts)
wordsAndFreq1 = findWordsAndFreqs(cleanText1, False)
sortedPairs = sorted(wordsAndFreq1, reverse = True)
nonExcluded = []
for pair in sortedPairs:
if pair[1] not in exclude:
nonExcluded.append(pair)
ארבע השורות שהוספו כאן לפונקציה ערוכות בתבנית המוכרת, זו המאתחלת אוסף ריק ואז סורקת אוסף נתון ולפי הסריקה מוסיפה – כאן לפי תנאי מסוים – ערך לסוף האוסף ההולך ונבנה (ראו בעיה א’). נוכל לקצר ולכתוב את כל ארבע השורות האלה בשורה אחת באמצעות ביטוי List Comprehension כך –
def compareFreqs(filename1, filename2, puncts, exclude):
cleanText1 = getCleanFile(getFileString(filename1), puncts)
wordsAndFreq1 = findWordsAndFreqs(cleanText1, False)
sortedPairs = sorted(wordsAndFreq1, reverse = True)
nonExcluded = [pair for pair in sortedPairs if pair[1] not in exclude]
נשים לב כי הרשימה nonExclude ממוינת בסדר יורד לפי השכיחויות ברשומות הפנימיות שלה, כיוון שהיא נבנתה על פי סריקת הרשימה sortedPairs, מתחילתה ועד סופה, והרשימה sortedPairs עצמה ממוינת בסדר יורד. מכאן יוצא שברשימה nonExcluded 10 הרשומות הפנימיות הראשונות מכילות את 10 המילים השכיחות ביותר. פירוש הדבר הוא כי בבואנו ליצור את הרשומה שהפונקציה compareFreqs תחזיר, נוכל לסרוק רק את 10 הרשומות הפנימיות ברשומה nonExclude. הנה כך תראה תחילת הסריקה –
def compareFreqs(filename1, filename2, puncts, exclude):
cleanText1 = getCleanFile(getFileString(filename1), puncts)
wordsAndFreq1 = findWordsAndFreqs(cleanText1, False)
sortedPairs = sorted(wordsAndFreq1, reverse = True)
nonExcluded = [pair for pair in sortedPairs if pair[1] not in exclude]
result = ()
for freq, word in nonExcluded[:10]:
result += ((word, freq, ?),)
שם הרשומה שהפונקציה תחזיר הוא result. אתחלנו אותה לרשומה ריקה. אחר כך כתבנו שורה ראשונה בלולאת for הסורקת רק את 10 הזוגות הראשונים, אלו המכילים את 10 המילים השכיחות ביותר בקובץ filename1, לא כולל המילים ברשימה exclude. בגוף הלולאה עלינו להוסיף רשומות שיש בכל אחת מהן שלושה ערכים: המילה ברשומה הנסרקת (word), השכיחות ברשומה הנסרקת (freq), ושכיחות המילה לפי הקובץ filename2. מציאת שכיחויות כל המילים בקובץ filename2 היא עניין פשוט למדי: הרי כבר כתבנו פונקציה בדיוק לצורך כזה: findWordsAndFreqs. אך כשתהיה לנו הרשומה שתחזיר הפונקציה הזאת עבור הקובץ filename2 תעלה השאלה: איך נקבל בקלות שכיחות של מילה מסוימת בקובץ זה? אפשר כמובן לסרוק את הרשומה שהפונקציה מחזירה, לאתר את הרשומה הפנימית המכילה את המילה, ולקבל את השכיחות מרשומה פנימית זו. אך כבר ראינו כי פשוט הרבה יותר להשתמש במילון שכיחויות (ראו בעיה ח’). במקרה שלפנינו ניצור אותו כך –
cleanText2 = getCleanFile(getFileString(filename2), puncts)
wordsAndFreq2 = findWordsAndFreqs(cleanText2)
freqsDict = dict(wordsAndFreq2)
לאחר ביצוע שתי השורות הראשונות, יש בידינו, בתוך המשתנה wordsAndFreq2, רשומה של רשומות, וכל רשומה פנימית מכילה מילה ושכיחות (בסדר זה – כאן השתמשנו בארגומנט בררת המחדל של הפרמטר wordFirst) לפי הקובץ filename2. השורה השלישית בקטע ממירה אוסף זוגות זה למילון באמצעות הפונקציה dict: כל מילה תהיה מפתח במילון זה וכל שכיחות הערך שהמפתח ממופה אליו. נזכיר כי ברשומה שמחזירה הפונקציה wordsAndFreqs2 אין כפילויות מצד המילים, ולכן כל זוג במילון הנוצר ייכנס אליו פעם אחת בלבד. כשיש בידינו את מילון השכיחויות עבור המילים (השונות זו מזו) בקובץ filename2, נוכל לקבל בקלות שכיחות של מילה המופיעה בקובץ filename1, או “NA” אם מילה זו אינה בקובץ filename2. הנה הפונקציה בגרסתה הסופית, קוד המזמן אותה, והפלט שלו –
def compareFreqs(filename1, filename2, puncts, exclude):
cleanText1 = getCleanFile(getFileString(filename1), puncts)
wordsAndFreq1 = findWordsAndFreqs(cleanText1, False)
sortedPairs = sorted(wordsAndFreq1, reverse = True)
nonExcluded = [pair for pair in sortedPairs if pair[1] not in exclude]
cleanText2 = getCleanFile(getFileString(filename2), puncts)
wordsAndFreq2 = findWordsAndFreqs(cleanText2)
freqsDict = dict(wordsAndFreq2)
result = ()
for freq, word in nonExcluded[:10]:
result += ((word, freq, freqsDict.get(word, “NA”)),)
return result
compareFreqs(“waves.txt”, “room.txt”, “.,?!”, [‘a’, ‘an’, ‘the’])
>>>
((‘and’, 2605, 1382), (‘i’, 2297, 538), (‘of’, 1991, 1332), (‘to’, 1430, 1022), (‘in’, 1319, 672), (‘is’, 1037, 437), (‘my’, 935, 155), (‘with’, 876, 233), (‘that’, 747, 648), (‘on’, 629, 208))
הערך שצוין כאן בתור ערך שלישי ברשומות הפנימיות המוכנסות לרשומה results הוא ערך החזרה של הפונקציה get המופעלת על מילון השכיחויות של המילים בקובץ filename2. אם המילה בזוג הנסרק הנוכחי (מילה מתוך הקובץ filename1) נמצאת במילון השכיחויות, תוחזר השכיחות שלה, ואם היא אינה נמצאת בו תוחזר המחרוזת “NA”.