difflib --- کمک‌کننده‌هایی برای محاسبه‌ی دلتاها

کد منبع: Lib/difflib.py


This module provides classes and functions for comparing sequences. Most of them compare sequences of text lines (for example lists of strings, or file objects) and produce diffs -- reports on the differences. Diffs can be produced in various formats, including HTML and context and unified diffs -- formats produced by tools like diff and git diff.

مقایسه‌ها با استفاده از الگوریتم تطبیقی انجام می‌شوند که در SequenceMatcher پیاده‌سازی شده است — کلاسی انعطاف‌پذیر برای مقایسه‌ی جفت‌هایی از دنباله‌های هر نوعی، نه فقط متن، مشروط بر اینکه عناصر دنباله hashable باشند.

هیوریستیک زباله (Junk heuristic)

difflib از یک اکتشافی junk استفاده می‌کند: برخی آیتم‌ها به‌عنوان junk تلقی می‌شوند و هنگام جست‌وجوی شباهت‌ها نادیده گرفته می‌شوند. در حالت ایده‌آل، این‌ها آیتم‌های بی‌اهمیت یا رایج هستند، مانند سطرهای خالی یا فضای سفید.

این روش اکتشافی می‌تواند سرعت الگوریتم را افزایش دهد (زیرا تعداد ترکیب‌های ممکن را کاهش می‌دهد) و می‌تواند نتایجی تولید کند که برای انسان قابل‌فهم‌تر هستند (معمولاً شکستن در فضای خالی). اما همچنین می‌تواند باعث ایجاد موارد آسیب‌شناختی شود:

  • آیتم‌های هرزی که به‌صورت نامناسب انتخاب شده‌اند، می‌توانند منجر به نتیجه‌ای به‌طور غیرمنتظره‌ای بزرگ (اما همچنان صحیح) شوند.

  • روش اکتشافی پیش‌فرض نامتقارن است: هنگام تعیین این‌که چه چیزی آشغال محسوب می‌شود، فقط دنباله‌ی دوم بررسی می‌شود؛ بنابراین مقایسه‌ی A با B می‌تواند نتایج متفاوتی نسبت به مقایسه‌ی B با A و معکوس کردن نتیجه بدهد.

به‌طور پیش‌فرض، اگر طول دومین دنباله ورودی حداقل ۲۰۰ آیتم باشد، آیتم‌هایی که بیش از ۱٪ آن را تشکیل می‌دهند، آشغال در نظر گرفته می‌شوند.

Depending on your data, you should consider turning this heuristic off (setting SequenceMatcher's autojunk argument to False) or tuning it (using the isjunk argument, perhaps to one of the predefined functions).

الگوریتم difflib

الگوریتم استفاده‌شده در SequenceMatcher از الگوریتمی که در اواخر دهه‌ی ۱۹۸۰ توسط رتکلیف و اوبرزهلپ با نام اغراق‌آمیز «تطبیق الگوی گشتالت (gestalt pattern matching)» منتشر شد، قدیمی‌تر است و کمی پیشرفته‌تر از آن است. ایده این است که طولانی‌ترین زیردنباله‌ی پیوسته‌ی مشترک بین هر دو ورودی پیدا شود، سپس بخش‌های دنباله‌ها در سمت چپ و راست زیردنباله‌ی منطبق به‌صورت بازگشتی پردازش شوند.

همچنین ملاحظه نمائید

تطبیق الگو: رویکرد گشتالت

بحثی درباره‌ی یک الگوریتم مشابه توسط John W. Ratcliff و D. E. Metzener. این مطلب در ژوئیه‌ی ۱۹۸۸ در مجله‌ی Dr. Dobb's Journal منتشر شد.

به‌عنوان گسترشی بر الگوریتم Ratcliff و Obershelp، difflib به دنبال طولانی‌ترین زیردنباله‌ی پیوسته‌ی عاری از هرز می‌گردد. برای جزئیات، بخش هیوریستیک زباله (Junk heuristic) را ببینید.

زمان‌سنجی

الگوریتم پایه‌ی Ratcliff-Obershelp در بدترین حالت دارای زمان مکعبی و در حالت مورد انتظار دارای زمان مربعی است. الگوریتم difflib در بدترین حالت دارای زمان مربعی است و رفتار آن در حالت مورد انتظار به‌شکل پیچیده‌ای به تعداد عناصر مشترک دنباله‌ها وابسته است؛ زمان بهترین حالت خطی است.

تولید تفاوت (diff)

class difflib.Differ

این یک کلاس برای مقایسه‌ی دنباله‌هایی از سطرهای متن و تولید تفاوت‌ها یا دلتاهای قابل‌خواندن برای انسان است. Differ از SequenceMatcher هم برای مقایسه‌ی دنباله‌هایی از سطرهای و هم برای مقایسه‌ی دنباله‌هایی از نویسه‌ها درون سطرهای مشابه (تقریباً منطبق) استفاده می‌کند.

هر خط از دلتای Differ با کدی دو نویسه‌ای آغاز می‌شود:

کد

معنی

'- '

خط مختص دنباله‌ی ۱

'+ '

خط یکتا برای دنباله ۲

'  '

خط مشترک بین هر دو دنباله

'? '

خط در هیچ‌یک از دنباله‌های ورودی وجود ندارد

سطرهایی که با '?' شروع می‌شوند، تلاش می‌کنند توجه را به تفاوت‌های درون‌خطی جلب کنند، و در هیچ‌یک از دنباله‌های ورودی وجود نداشتند. این سطرهای ممکن است در صورتی گیج‌کننده باشند که دنباله‌ها حاوی نویسه‌های فضای خالی مانند فاصله‌ها، تب‌ها یا سطرهای جدید باشند.

توجه داشته باشید که دلتاهای تولیدشده توسط Differ ادعایی ندارند که تفاوت‌های کمینه باشند. برعکس، تفاوت‌های کمینه اغلب برای انسان‌ها برخلاف شهود هستند، زیرا در هر جایی که ممکن باشد همگام می‌شوند، گاهی در تطابق‌های تصادفی که ۱۰۰ صفحه با یکدیگر فاصله دارند. محدود کردن نقاط همگام‌سازی به تطابق‌های متوالی، تا حدی مفهوم محلی بودن را حفظ می‌کند، هرچند گاهی به بهای تولید یک تفاوت طولانی‌تر تمام می‌شود.

کلاس Differ این سازنده را دارد:

__init__(linejunk=None, charjunk=None)

پارامترهای کلیدواژه‌ای اختیاری linejunk و charjunk برای توابع فیلتر (یا None) هستند:

linejunk: تابعی که تنها یک آرگومان رشته‌ای می‌پذیرد و اگر رشته هرز باشد، مقدار true را برمی‌گرداند. مقدار پیش‌فرض None است، به این معنا که هیچ خطی هرز در نظر گرفته نمی‌شود.

charjunk: تابعی که یک آرگومان تک‌نویسه‌ای (رشته‌ای به طول ۱) می‌پذیرد و اگر نویسه هرز باشد، مقدار درست برمی‌گرداند. مقدار پیش‌فرض None است، یعنی هیچ نویسه‌ای هرز در نظر گرفته نمی‌شود.

این توابع پالایش زباله (junk-filtering) سرعت تطبیق برای یافتن تفاوت‌ها را افزایش می‌دهند و باعث نمی‌شوند هیچ خط یا نویسه‌ی متفاوتی نادیده گرفته شود. برای توضیح، شرح پارامتر isjunk متد find_longest_match() را بخوانید.

از اشیای Differ از طریق یک متد واحد استفاده می‌شود (دلتاها تولید می‌شوند):

compare(a, b)

دو دنباله از سطرها را مقایسه می‌کند و دلتا (دنباله‌ای از سطرهای) را تولید می‌کند.

هر دنباله باید شامل رشته‌های تک‌سطری جداگانه‌ای باشد که به خط جدید ختم می‌شوند. چنین دنباله‌هایی را می‌توان از متد readlines() اشیاء شبه‌پرونده به‌دست آورد. دلتای تولیدشده نیز از رشته‌های ختم‌شده به خط جدید تشکیل شده است، که آماده‌اند به‌همین‌صورت از طریق متد writelines() یک شیء شبه‌پرونده چاپ شوند.

class difflib.HtmlDiff

از این کلاس می‌توان برای ایجاد یک جدول HTML (یا یک پرونده HTML کامل شامل جدول) استفاده کرد که مقایسه‌ی کنار هم و خط‌به‌خط متن را با برجسته‌سازی تغییرات بین‌خطی و درون‌خطی نشان می‌دهد. این جدول را می‌توان در حالت تفاوت کامل یا زمینه‌ای تولید کرد.

هشدار

سطرهای جدید پایانی پیش از diff حذف می‌شوند، بنابراین نتیجه ممکن است ناقص باشد. برای جزئیات gh-71896 را ببینید.

سازنده‌ی این کلاس عبارت است از:

__init__(tabsize=8, wrapcolumn=None, linejunk=None, charjunk=IS_CHARACTER_JUNK)

نمونه‌ای از HtmlDiff را مقداردهی اولیه می‌کند.

tabsize یک آرگومان کلیدواژه‌ای اختیاری برای تعیین فاصله‌گذاری تب است و مقدار پیش‌فرض آن 8 است.

wrapcolumn یک کلیدواژه اختیاری برای مشخص کردن شماره ستونی است که سطرهای در آن شکسته و به خط بعد منتقل می‌شوند، و به‌طور پیش‌فرض None است که در این حالت سطرها به خط بعد منتقل نمی‌شوند.

linejunk و charjunk آرگومان‌های کلیدواژه‌ای اختیاری هستند که به ndiff() ارسال می‌شوند (توسط HtmlDiff برای تولید تفاوت‌های HTML به‌صورت کنار هم استفاده می‌شود). برای مقادیر پیش‌فرض و توضیحات آرگومان‌ها، مستندات ndiff() را ببینید.

متدهای زیر عمومی هستند:

make_file(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5, *, charset='utf-8')

fromlines و tolines (فهرست‌هایی از رشته‌ها) را مقایسه می‌کند و رشته‌ای را برمی‌گرداند که یک پرونده HTML کامل حاوی جدولی است که تفاوت‌ها را به‌صورت خط‌به‌خط نشان می‌دهد و تغییرات بین‌خطی و درون‌خطی در آن برجسته شده‌اند.

fromdesc و todesc آرگومان‌های کلیدواژه‌ای اختیاری هستند که برای مشخص کردن رشته‌های سرآیند ستون پرونده مبدأ/مقصد استفاده می‌شوند (مقدار پیش‌فرض هر دو یک رشته خالی است).

context و numlines هر دو آرگومان کلیدواژه‌ای اختیاری هستند. هنگامی که باید تفاوت‌های همراه با زمینه نمایش داده شوند، context را روی True تنظیم کنید، در غیر این صورت مقدار پیش‌فرض False است تا پرونده‌های کامل نمایش داده شوند. مقدار پیش‌فرض numlines برابر 5 است. وقتی context برابر True است، numlines تعداد سطرهای زمینه‌ای را که تفاوت‌های برجسته‌شده را در بر می‌گیرند، کنترل می‌کند. وقتی context برابر False است، numlines تعداد سطرهایی را که هنگام استفاده از ابرپیوندهای «next» پیش از یک تفاوت برجسته‌شده نمایش داده می‌شوند، کنترل می‌کند (تنظیم آن روی صفر باعث می‌شود ابرپیوندهای «next» تفاوت برجسته‌شده بعدی را بدون هیچ زمینه پیشین در بالای مرورگر قرار دهند).

توجه

fromdesc و todesc به‌عنوان HTML خنثی‌نشده تفسیر می‌شوند و باید هنگام دریافت ورودی از منابع نامطمئن، به‌درستی خنثی شوند.

تغییر یافته در نسخه‌ی 3.5: آرگومان فقط کلیدواژه‌ای charset اضافه شد. مجموعه‌نویسه پیش‌فرض سند HTML از 'ISO-8859-1' به 'utf-8' تغییر کرد.

make_table(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5)

fromlines و tolines (فهرست‌هایی از رشته‌ها) را مقایسه می‌کند و رشته‌ای را برمی‌گرداند که یک جدول HTML کامل است و تفاوت‌های خط‌به‌خط را با تغییرات بین‌خطی و درون‌خطی برجسته‌شده نشان می‌دهد.

آرگومان‌های این متد همان آرگومان‌های متد make_file() هستند.

difflib.context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

a و b (فهرست‌هایی از رشته‌ها) را مقایسه می‌کند؛ دلتایی (یک تولیدگر که سطرهای دلتا را تولید می‌کند) را در قالب context diff برمی‌گرداند.

دیف‌های زمینه‌ای (context diffs) روشی فشرده برای نمایش تنها سطرهایی هستند که تغییر کرده‌اند، به‌علاوه چند خط زمینه. تغییرها به سبک پیش/پس نمایش داده می‌شوند. تعداد سطرهای زمینه توسط n تنظیم می‌شود که مقدار پیش‌فرض آن ۳ است.

به‌طور پیش‌فرض، سطرهای کنترلی تفاوت (diff) (آن‌هایی که *** یا --- دارند) با یک نویسه‌ی خط جدید در انتها ایجاد می‌شوند. این موضوع مفید است تا ورودی‌های ایجادشده از io.IOBase.readlines() به تفاوت‌هایی منجر شوند که برای استفاده با io.IOBase.writelines() مناسب باشند، زیرا هم ورودی‌ها و هم خروجی‌ها دارای نویسه‌ی خط جدید در انتها هستند.

برای ورودی‌هایی که خط جدید پایانی ندارند، آرگومان lineterm را روی "" تنظیم کنید تا خروجی به‌طور یکنواخت فاقد خط جدید باشد.

قالب diff زمینه‌ای (context diff) معمولاً دارای سرآیندی برای نام پرونده‌ها و زمان‌های تغییر است. هر یک یا همه‌ی این موارد را می‌توان با استفاده از رشته‌هایی برای fromfile، tofile، fromfiledate و tofiledate مشخص کرد. زمان‌های تغییر معمولاً در قالب ISO 8601 بیان می‌شوند. اگر مشخص نشوند، رشته‌ها به‌طور پیش‌فرض خالی خواهند بود.

>>> import sys
>>> from difflib import *
>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> sys.stdout.writelines(context_diff(s1, s2, fromfile='before.py',
...                        tofile='after.py'))
*** before.py
--- after.py
***************
*** 1,4 ****
! bacon
! eggs
! ham
  guido
--- 1,4 ----
! python
! eggy
! hamster
  guido

برای مثالی با جزئیات بیشتر، یک رابط خط فرمان برای difflib را ببینید.

difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)

فهرستی از بهترین تطابق‌های «به‌اندازه‌ی کافی خوب» را برمی‌گرداند. word دنباله‌ای است که تطابق‌های نزدیک برای آن مطلوب است (معمولاً یک رشته)، و possibilities فهرستی از دنباله‌هاست که word با آن‌ها تطبیق داده می‌شود (معمولاً فهرستی از رشته‌ها).

آرگومان اختیاری n (پیش‌فرض 3) حداکثر تعداد تطابق‌های نزدیکی است که بازگردانده می‌شود؛ n باید بزرگ‌تر از 0 باشد.

آرگومان اختیاری cutoff (پیش‌فرض 0.6) یک عدد اعشاری در بازه [۰، ۱] است. گزینه‌هایی که امتیاز شباهت آن‌ها به word حداقل این میزان نباشد، نادیده گرفته می‌شوند.

بهترین تطابق‌ها (حداکثر n مورد) از میان گزینه‌های ممکن در یک فهرست بازگردانده می‌شوند و بر اساس امتیاز شباهت مرتب شده‌اند، به طوری که مشابه‌ترین مورد در ابتدا قرار دارد.

>>> get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'])
['apple', 'ape']
>>> import keyword
>>> get_close_matches('wheel', keyword.kwlist)
['while']
>>> get_close_matches('pineapple', keyword.kwlist)
[]
>>> get_close_matches('accept', keyword.kwlist)
['except']
difflib.ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK)

a و b (فهرست‌هایی از رشته‌ها) را مقایسه می‌کند؛ یک دلتا به سبک Differ را برمی‌گرداند (یک تولیدگر که سطرهای دلتا را تولید می‌کند).

پارامترهای کلیدواژه‌ای اختیاری linejunk و charjunk توابع پالایشگر (یا None) هستند:

linejunk: تابعی که تنها یک آرگومان رشته‌ای را می‌پذیرد، و اگر رشته هرز باشد مقدار درست، و در غیر این صورت مقدار نادرست را برمی‌گرداند. مقدار پیش‌فرض None است. همچنین یک تابع در سطح ماژول IS_LINE_JUNK() وجود دارد که سطرهایی را که نویسه‌های قابل مشاهده ندارند، به‌جز حداکثر یک نویسه هش ('#')، فیلتر می‌کند -- با این حال، کلاس زیربنایی SequenceMatcher تحلیلی پویا انجام می‌دهد تا مشخص کند کدام سطرهای آن‌قدر پرتکرار هستند که نویز محسوب می‌شوند، و این معمولاً بهتر از استفاده از این تابع عمل می‌کند.

charjunk: تابعی که یک نویسه (رشته‌ای به طول ۱) می‌پذیرد، و اگر نویسه هرز (junk) باشد، مقداری برمی‌گرداند، و در غیر این صورت false برمی‌گرداند. مقدار پیش‌فرض، تابع IS_CHARACTER_JUNK() در سطح ماژول است که نویسه‌های فضای سفید (فاصله یا تب؛ کار بدی است که خط جدید را در آن بگنجانید!) را فیلتر می‌کند.

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> print(''.join(diff), end="")
- one
?  ^
+ ore
?  ^
- two
- three
?  -
+ tree
+ emu
difflib.restore(sequence, which)

یکی از دو دنباله‌ای را که دلتایی (delta) تولید کرده‌اند، برمی‌گرداند.

با داشتن یک دنباله تولیدشده توسط Differ.compare() یا ndiff()، سطرهایی را که از پرونده ۱ یا ۲ آمده‌اند (پارامتر which) استخراج کنید و پیشوندهای سطرها را حذف کنید.

مثال:

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> diff = list(diff) # materialize the generated delta into a list
>>> print(''.join(restore(diff, 1)), end="")
one
two
three
>>> print(''.join(restore(diff, 2)), end="")
ore
tree
emu
difflib.unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

a و b (فهرست‌هایی از رشته‌ها) را مقایسه می‌کند؛ یک دلتا (یک تولیدگر که سطرهای دلتا را تولید می‌کند) را در قالب unified diff بازمی‌گرداند.

تفاوت‌های یکپارچه (unified diffs) روشی فشرده برای نمایش تنها سطرهایی که تغییر کرده‌اند به‌همراه چند خط زمینه هستند. تغییرات به‌صورت درون‌خطی نمایش داده می‌شوند (به‌جای بلوک‌های جداگانه‌ی قبل/بعد). تعداد سطرهای زمینه با n تنظیم می‌شود که مقدار پیش‌فرض آن ۳ است.

به‌طور پیش‌فرض، سطرهای کنترلی diff (آن‌هایی که دارای ---، +++ یا @@ هستند) با یک خط جدید در پایان ایجاد می‌شوند. این موضوع مفید است تا ورودی‌هایی که از io.IOBase.readlines() ایجاد شده‌اند، منجر به diffهایی شوند که برای استفاده با io.IOBase.writelines() مناسب هستند، زیرا هم ورودی‌ها و هم خروجی‌ها دارای خط جدید در پایان هستند.

برای ورودی‌هایی که خط جدید پایانی ندارند، آرگومان lineterm را روی "" تنظیم کنید تا خروجی به‌طور یکنواخت فاقد خط جدید باشد.

قالب unified diff معمولاً دارای سرآیندی برای نام پرونده‌ها و زمان‌های تغییر است. هرکدام یا همه‌ی این موارد را می‌توان با استفاده از رشته‌هایی برای fromfile، tofile، fromfiledate و tofiledate مشخص کرد. زمان‌های تغییر معمولاً در قالب ISO 8601 بیان می‌شوند. اگر مشخص نشوند، رشته‌ها به‌طور پیش‌فرض خالی خواهند بود.

>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> sys.stdout.writelines(unified_diff(s1, s2, fromfile='before.py', tofile='after.py'))
--- before.py
+++ after.py
@@ -1,4 +1,4 @@
-bacon
-eggs
-ham
+python
+eggy
+hamster
 guido

برای مثالی با جزئیات بیشتر، یک رابط خط فرمان برای difflib را ببینید.

difflib.diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'', fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')

a و b (فهرست‌هایی از اشیای bytes) را با استفاده از dfunc مقایسه می‌کند؛ دنباله‌ای از سطرهای دلتا (که آن‌ها نیز bytes هستند) را در قالبی که dfunc برمی‌گرداند تولید می‌کند. dfunc باید یک شیء فراخوانی‌پذیر باشد، معمولاً یا unified_diff() یا context_diff().

به شما امکان می‌دهد داده‌هایی با کدگذاری ناشناخته یا ناهماهنگ را مقایسه کنید. همه‌ی ورودی‌ها به‌جز n باید اشیای bytes باشند، نه str. با تبدیل بدون اتلاف همه‌ی ورودی‌ها (به‌جز n) به str و فراخوانی dfunc(a, b, fromfile, tofile, fromfiledate, tofiledate, n, lineterm) کار می‌کند. سپس خروجی dfunc دوباره به bytes تبدیل می‌شود، بنابراین سطرهای دلتایی که دریافت می‌کنید، همان کدگذاری‌های ناشناخته/ناهماهنگ a و b را دارند.

اضافه شده در نسخه‌ی 3.5.

توابع تعریف زباله

difflib.IS_LINE_JUNK(line)

برای سطرهای قابل‌چشم‌پوشی True برمی‌گرداند. خط line قابل‌چشم‌پوشی است اگر line خالی باشد یا شامل یک '#' باشد، در غیر این صورت قابل‌چشم‌پوشی نیست. در نسخه‌های قدیمی‌تر به‌عنوان پیش‌فرض پارامتر linejunk در ndiff() استفاده می‌شد.

difflib.IS_CHARACTER_JUNK(ch)

True را برای نویسه‌های قابل چشم‌پوشی برمی‌گرداند. نویسه ch در صورتی قابل چشم‌پوشی است که ch فاصله یا تب باشد، در غیر این صورت قابل چشم‌پوشی نیست. به‌عنوان پیش‌فرض برای پارامتر charjunk در ndiff() استفاده می‌شود.

اشیای SequenceMatcher

class difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)

آرگومان اختیاری isjunk باید None (پیش‌فرض) یا تابعی با یک آرگومان باشد که عنصری از یک دنباله را می‌گیرد و اگر و تنها اگر آن عنصر «زائد» باشد و باید نادیده گرفته شود، مقدار درست برمی‌گرداند. پاس دادن None برای isjunk معادل پاس دادن lambda x: False است؛ به عبارت دیگر، هیچ عنصری نادیده گرفته نمی‌شود. برای مثال، این را پاس دهید:

lambda x: x in " \t"

اگر سطرها را به‌عنوان دنباله‌هایی از نویسه‌ها مقایسه می‌کنید و نمی‌خواهید بر اساس فاصله‌ها یا تب‌های سخت همگام‌سازی کنید.

آرگومان‌های اختیاری a و b دنباله‌هایی هستند که با هم مقایسه می‌شوند؛ مقدار پیش‌فرض هر دو، رشته‌های خالی است. عناصر هر دو دنباله باید hashable باشند.

می‌توانید از آرگومان اختیاری autojunk برای غیرفعال کردن هیوریستیک خودکار هرز استفاده کنید.

تغییر یافته در نسخه‌ی 3.2: پارامتر autojunk اضافه شد.

اشیای SequenceMatcher سه ویژگی داده دارند: bjunk مجموعه‌ای از عناصر b است که isjunk برای آن‌ها True است؛ bpopular مجموعه‌ای از عناصر غیرزائد است که توسط اکتشافی، محبوب در نظر گرفته می‌شوند (اگر غیرفعال نشده باشد)؛ b2j دیکشنری است که عناصر باقی‌مانده‌ی b را به فهرستی از موقعیت‌هایی که در آن‌ها ظاهر می‌شوند نگاشت می‌کند. هر سه هر زمان که b با set_seqs() یا set_seq2() بازنشانی شود، بازنشانی می‌شوند.

اضافه شده در نسخه‌ی 3.2: ویژگی‌های bjunk و bpopular.

اشیای SequenceMatcher متدهای زیر را دارند:

set_seqs(a, b)

دو دنباله را برای مقایسه تنظیم می‌کند.

SequenceMatcher جزئیات مربوط به دنباله دوم را محاسبه و در نهانگاه ذخیره می‌کند، بنابراین اگر می‌خواهید یک دنباله را با چندین دنباله مقایسه کنید، از set_seq2() استفاده کنید تا دنباله‌ی پرکاربرد را یک بار تنظیم کنید و set_seq1() را به‌طور مکرر، یک بار برای هر یک از دنباله‌های دیگر، فراخوانی کنید.

set_seq1(a)

نخستین دنباله‌ی مورد مقایسه را تنظیم کنید. دومین دنباله‌ی مورد مقایسه تغییر نمی‌کند.

set_seq2(b)

دومین دنباله‌ی مورد مقایسه را تنظیم کنید. اولین دنباله‌ی مورد مقایسه تغییر نمی‌کند.

find_longest_match(alo=0, ahi=None, blo=0, bhi=None)

طولانی‌ترین بلوک منطبق را در a[alo:ahi] و b[blo:bhi] بیابید.

اگر isjunk حذف شده باشد یا None باشد، find_longest_match() (i, j, k) را برمی‌گرداند به‌طوری که a[i:i+k] برابر با b[j:j+k] است، که در آن alo <= i <= i+k <= ahi و blo <= j <= j+k <= bhi. برای همه‌ی (i', j', k') که این شرایط را دارند، شرایط اضافی k >= k'، i <= i' و اگر i == i'، j <= j' نیز برقرار هستند. به عبارت دیگر، از میان همه‌ی بلوک‌های منطبق بیشینه، یکی را برمی‌گرداند که زودترین شروع را در a دارد، و از میان همه‌ی آن بلوک‌های منطبق بیشینه که زودترین شروع را در a دارند، آن را برمی‌گرداند که زودترین شروع را در b دارد.

>>> s = SequenceMatcher(None, " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=0, b=4, size=5)

اگر isjunk ارائه شده باشد، ابتدا طولانی‌ترین بلوک منطبق همان‌گونه که در بالا توضیح داده شد تعیین می‌شود، اما با این محدودیت اضافی که هیچ عنصر هرز (junk) در بلوک وجود نداشته باشد. سپس آن بلوک تا حد ممکن با تطبیق (فقط) عناصر هرز در هر دو طرف گسترش می‌یابد. بنابراین بلوک حاصل هرگز بر عناصر هرز منطبق نمی‌شود، مگر آنکه عناصر هرز یکسان به‌طور تصادفی مجاور یک تطبیق معنادار باشند.

این همان مثال پیشین است، اما فاصله‌ها را هرز (junk) در نظر می‌گیریم. این امر مانع از آن می‌شود که ' abcd' مستقیماً با ' abcd' در انتهای دنباله دوم تطابق داشته باشد. در عوض، تنها 'abcd' می‌تواند تطابق داشته باشد و با چپ‌ترین 'abcd' در دنباله دوم مطابقت دارد:

>>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=1, b=0, size=4)

اگر هیچ بلوکی مطابقت نداشته باشد، (alo, blo, 0) برگردانده می‌شود.

این متد یک named tuple با قالب Match(a, b, size) برمی‌گرداند.

تغییر یافته در نسخه‌ی 3.9: آرگومان‌های پیش‌فرض اضافه شد.

get_matching_blocks()

فهرستی از سه‌تایی‌ها را برمی‌گرداند که زیردنباله‌های منطبق غیرهمپوشان را توصیف می‌کنند. هر سه‌تایی به شکل (i, j, n) است و به این معناست که a[i:i+n] == b[j:j+n]. سه‌تایی‌ها از نظر i و j به‌طور یکنواخت صعودی هستند.

آخرین سه‌تایی ساختگی است و مقدار (len(a), len(b), 0) را دارد. این تنها سه‌تایی با n == 0 است. اگر (i, j, n) و (i', j', n') سه‌تایی‌های مجاور در فهرست باشند و دومی آخرین سه‌تایی در فهرست نباشد، آنگاه i+n < i' یا j+n < j' برقرار است؛ به عبارت دیگر، سه‌تایی‌های مجاور همیشه بلوک‌های برابرِ غیرمجاور را توصیف می‌کنند.

>>> s = SequenceMatcher(None, "abxcd", "abcd")
>>> s.get_matching_blocks()
[Match(a=0, b=0, size=2), Match(a=3, b=2, size=2), Match(a=5, b=4, size=0)]
get_opcodes()

فهرستی از ۵-تایی‌ها برمی‌گرداند که چگونگی تبدیل a به b را توصیف می‌کنند. هر ۵-تایی به شکل (tag, i1, i2, j1, j2) است. در اولین ۵-تایی، i1 == j1 == 0 است، و در ۵-تایی‌های باقی‌مانده، i1 برابر با i2 از ۵-تایی پیشین است، و به همین ترتیب، j1 برابر با j2 پیشین است.

مقادیر tag رشته‌هایی با این معانی هستند:

مقدار

معنی

'replace'

a[i1:i2] باید با b[j1:j2] جایگزین شود.

'delete'

a[i1:i2] باید حذف شود. توجه داشته باشید که در این حالت j1 == j2.

'insert'

b[j1:j2] باید در a[i1:i1] درج شود. توجه داشته باشید که در این حالت i1 == i2 است.

'equal'

a[i1:i2] == b[j1:j2] (زیردنباله‌ها برابر هستند).

برای مثال:

>>> a = "qabxcd"
>>> b = "abycdf"
>>> s = SequenceMatcher(None, a, b)
>>> for tag, i1, i2, j1, j2 in s.get_opcodes():
...     print('{:7}   a[{}:{}] --> b[{}:{}] {!r:>8} --> {!r}'.format(
...         tag, i1, i2, j1, j2, a[i1:i2], b[j1:j2]))
delete    a[0:1] --> b[0:0]      'q' --> ''
equal     a[1:3] --> b[0:2]     'ab' --> 'ab'
replace   a[3:4] --> b[2:3]      'x' --> 'y'
equal     a[4:6] --> b[3:5]     'cd' --> 'cd'
insert    a[6:6] --> b[5:6]       '' --> 'f'
get_grouped_opcodes(n=3)

یک تولیدگر از گروه‌هایی با حداکثر n خط از زمینه برمی‌گرداند.

این متد با شروع از گروه‌های برگردانده‌شده توسط get_opcodes()، خوشه‌های تغییر کوچک‌تر را جدا می‌کند و بازه‌های میانی را که تغییری ندارند حذف می‌کند.

گروه‌ها در همان قالب get_opcodes() برگردانده می‌شوند.

ratio()

معیاری از شباهت دنباله‌ها را به‌صورت یک عدد اعشاری در بازه‌ی [۰، ۱] برمی‌گرداند.

اگر T تعداد کل عناصر هر دو دنباله و M تعداد تطابق‌ها باشد، این مقدار برابر با 2.0*M / T است. توجه داشته باشید که اگر دنباله‌ها یکسان باشند، این مقدار 1.0 است و اگر هیچ اشتراکی نداشته باشند، 0.0 است.

محاسبه‌ی این مقدار در صورتی پرهزینه است که get_matching_blocks() یا get_opcodes() از قبل فراخوانی نشده باشند؛ در این صورت ممکن است بخواهید ابتدا quick_ratio() یا real_quick_ratio() را امتحان کنید تا کران بالایی به دست آورید.

quick_ratio()

یک کران بالا برای ratio() نسبتاً سریع برمی‌گرداند.

real_quick_ratio()

یک کران بالا برای ratio() را بسیار سریع برمی‌گرداند.

سه متدی که نسبت نویسه‌های منطبق به کل نویسه‌ها را برمی‌گردانند، می‌توانند به دلیل سطوح متفاوت تقریب، نتایج متفاوتی داشته باشند، اگرچه quick_ratio() و real_quick_ratio() همواره دست‌کم به بزرگی ratio() هستند:

>>> s = SequenceMatcher(None, "abcd", "bcde")
>>> s.ratio()
0.75
>>> s.quick_ratio()
0.75
>>> s.real_quick_ratio()
1.0

مثال‌ها

مثال‌های SequenceMatcher

این مثال دو رشته را با در نظر گرفتن فاصله‌ها به‌عنوان «زائد» مقایسه می‌کند:

>>> s = SequenceMatcher(lambda x: x == " ",
...                     "private Thread currentThread;",
...                     "private volatile Thread currentThread;")

ratio() یک عدد اعشاری در بازه‌ی [۰، ۱] برمی‌گرداند که شباهت دنباله‌ها را می‌سنجد. به‌عنوان یک قاعده سرانگشتی، مقدار ratio() بیش از ۰٫۶ به این معناست که دنباله‌ها تطابق‌های نزدیکی هستند:

>>> print(round(s.ratio(), 3))
0.866

اگر فقط به محل تطابق دنباله‌ها علاقه‌مند هستید، get_matching_blocks() مفید است:

>>> for block in s.get_matching_blocks():
...     print("a[%d] and b[%d] match for %d elements" % block)
a[0] and b[0] match for 8 elements
a[8] and b[17] match for 21 elements
a[29] and b[38] match for 0 elements

توجه داشته باشید که آخرین چندگانه‌ای که get_matching_blocks() برمی‌گرداند همیشه یک چندگانه ساختگی، (len(a), len(b), 0)، است و این تنها حالتی است که آخرین عنصر چندگانه (تعداد عناصر مطابقت‌داده‌شده) برابر 0 است.

اگر می‌خواهید بدانید چگونه دنباله اول را به دنباله دوم تبدیل کنید، از get_opcodes() استفاده کنید:

>>> for opcode in s.get_opcodes():
...     print("%6s a[%d:%d] b[%d:%d]" % opcode)
 equal a[0:8] b[0:8]
insert a[8:8] b[8:17]
 equal a[8:29] b[17:38]

همچنین ملاحظه نمائید

مثال Differ

این مثال دو متن را مقایسه می‌کند. ابتدا متن‌ها را آماده می‌کنیم، دنباله‌هایی از رشته‌های تک‌سطری جداگانه که با خط جدید پایان می‌یابند (چنین دنباله‌هایی را همچنین می‌توان از متد readlines() اشیای شبه‌پرونده به دست آورد):

>>> text1 = '''  1. Beautiful is better than ugly.
...   2. Explicit is better than implicit.
...   3. Simple is better than complex.
...   4. Complex is better than complicated.
... '''.splitlines(keepends=True)
>>> len(text1)
4
>>> text1[0][-1]
'\n'
>>> text2 = '''  1. Beautiful is better than ugly.
...   3.   Simple is better than complex.
...   4. Complicated is better than complex.
...   5. Flat is better than nested.
... '''.splitlines(keepends=True)

سپس یک شیء Differ را نمونه‌سازی می‌کنیم:

>>> d = Differ()

توجه داشته باشید که هنگام نمونه‌سازی یک شیء Differ، می‌توانید توابعی را برای فیلتر کردن «اضافات» خطی و نویسه‌ای ارسال کنید. برای جزئیات، سازنده‌ی Differ() را ببینید.

در نهایت، این دو را با هم مقایسه می‌کنیم:

>>> result = list(d.compare(text1, text2))

result یک فهرست از رشته‌ها است، بنابراین بیایید آن را زیبانویسی کنیم (pretty-print):

>>> from pprint import pprint
>>> pprint(result)
['    1. Beautiful is better than ugly.\n',
 '-   2. Explicit is better than implicit.\n',
 '-   3. Simple is better than complex.\n',
 '+   3.   Simple is better than complex.\n',
 '?     ++\n',
 '-   4. Complex is better than complicated.\n',
 '?            ^                     ---- ^\n',
 '+   4. Complicated is better than complex.\n',
 '?           ++++ ^                      ^\n',
 '+   5. Flat is better than nested.\n']

به‌صورت یک رشته‌ی چندخطی واحد، به این شکل است:

>>> import sys
>>> sys.stdout.writelines(result)
    1. Beautiful is better than ugly.
-   2. Explicit is better than implicit.
-   3. Simple is better than complex.
+   3.   Simple is better than complex.
?     ++
-   4. Complex is better than complicated.
?            ^                     ---- ^
+   4. Complicated is better than complex.
?           ++++ ^                      ^
+   5. Flat is better than nested.

یک رابط خط فرمان برای difflib

این مثال نشان می‌دهد که چگونه از difflib برای ایجاد یک ابزار مشابه diff استفاده کنید.

""" Command-line interface to difflib.py providing diffs in four formats:

* ndiff:    lists every line and highlights interline changes.
* context:  highlights clusters of changes in a before/after format.
* unified:  highlights clusters of changes in an inline format.
* html:     generates side by side comparison with change highlights.

"""

import sys, os, difflib, argparse
import datetime as dt

def file_mtime(path):
    t = dt.datetime.fromtimestamp(os.stat(path).st_mtime,
                                  dt.timezone.utc)
    return t.astimezone().isoformat()

def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('-c', action='store_true', default=False,
                        help='Produce a context format diff (default)')
    parser.add_argument('-u', action='store_true', default=False,
                        help='Produce a unified format diff')
    parser.add_argument('-m', action='store_true', default=False,
                        help='Produce HTML side by side diff '
                             '(can use -c and -l in conjunction)')
    parser.add_argument('-n', action='store_true', default=False,
                        help='Produce a ndiff format diff')
    parser.add_argument('-l', '--lines', type=int, default=3,
                        help='Set number of context lines (default 3)')
    parser.add_argument('fromfile')
    parser.add_argument('tofile')
    options = parser.parse_args()

    n = options.lines
    fromfile = options.fromfile
    tofile = options.tofile

    fromdate = file_mtime(fromfile)
    todate = file_mtime(tofile)
    with open(fromfile) as ff:
        fromlines = ff.readlines()
    with open(tofile) as tf:
        tolines = tf.readlines()

    if options.u:
        diff = difflib.unified_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=n)
    elif options.n:
        diff = difflib.ndiff(fromlines, tolines)
    elif options.m:
        diff = difflib.HtmlDiff().make_file(fromlines,tolines,fromfile,tofile,context=options.c,numlines=n)
    else:
        diff = difflib.context_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=n)

    sys.stdout.writelines(diff)

if __name__ == '__main__':
    main()

مثال ndiff

این مثال نشان می‌دهد که چگونه از difflib.ndiff() استفاده کنید.

"""ndiff [-q] file1 file2
    or
ndiff (-r1 | -r2) < ndiff_output > file1_or_file2

Print a human-friendly file difference report to stdout.  Both inter-
and intra-line differences are noted.  In the second form, recreate file1
(-r1) or file2 (-r2) on stdout, from an ndiff report on stdin.

In the first form, if -q ("quiet") is not specified, the first two lines
of output are

-: file1
+: file2

Each remaining line begins with a two-letter code:

    "- "    line unique to file1
    "+ "    line unique to file2
    "  "    line common to both files
    "? "    line not present in either input file

Lines beginning with "? " attempt to guide the eye to intraline
differences, and were not present in either input file.  These lines can be
confusing if the source files contain tab characters.

The first file can be recovered by retaining only lines that begin with
"  " or "- ", and deleting those 2-character prefixes; use ndiff with -r1.

The second file can be recovered similarly, but by retaining only "  " and
"+ " lines; use ndiff with -r2; or, on Unix, the second file can be
recovered by piping the output through

    sed -n '/^[+ ] /s/^..//p'
"""

__version__ = 1, 7, 0

import difflib, sys

def fail(msg):
    out = sys.stderr.write
    out(msg + "\n\n")
    out(__doc__)
    return 0

# open a file & return the file object; gripe and return 0 if it
# couldn't be opened
def fopen(fname):
    try:
        return open(fname)
    except IOError as detail:
        return fail("couldn't open " + fname + ": " + str(detail))

# open two files & spray the diff to stdout; return false iff a problem
def fcompare(f1name, f2name):
    f1 = fopen(f1name)
    f2 = fopen(f2name)
    if not f1 or not f2:
        return 0

    a = f1.readlines(); f1.close()
    b = f2.readlines(); f2.close()
    for line in difflib.ndiff(a, b):
        print(line, end=' ')

    return 1

# crack args (sys.argv[1:] is normal) & compare;
# return false iff a problem

def main(args):
    import getopt
    try:
        opts, args = getopt.getopt(args, "qr:")
    except getopt.error as detail:
        return fail(str(detail))
    noisy = 1
    qseen = rseen = 0
    for opt, val in opts:
        if opt == "-q":
            qseen = 1
            noisy = 0
        elif opt == "-r":
            rseen = 1
            whichfile = val
    if qseen and rseen:
        return fail("can't specify both -q and -r")
    if rseen:
        if args:
            return fail("no args allowed with -r option")
        if whichfile in ("1", "2"):
            restore(whichfile)
            return 1
        return fail("-r value must be 1 or 2")
    if len(args) != 2:
        return fail("need 2 filename args")
    f1name, f2name = args
    if noisy:
        print('-:', f1name)
        print('+:', f2name)
    return fcompare(f1name, f2name)

# read ndiff output from stdin, and print file1 (which=='1') or
# file2 (which=='2') to stdout

def restore(which):
    restored = difflib.restore(sys.stdin.readlines(), which)
    sys.stdout.writelines(restored)

if __name__ == '__main__':
    main(sys.argv[1:])