difflib — 델타 계산 헬퍼

difflib — 델타 계산 헬퍼

이 모듈은 시퀀스를 비교하는 클래스와 함수를 제공해요. 대부분 텍스트 줄의 시퀀스(예: 문자열 리스트나 파일 객체)를 비교해서 델타(diff)를 만들어요. 델타는 HTML, 컨텍스트·통합 diff처럼 diffgit diff 같은 도구가 만드는 형식을 포함한 여러 형식으로 만들 수 있어요.

출처: Python 표준 라이브러리

본문

비교는 SequenceMatcher에 구현된 매칭 알고리즘으로 이뤄져요 — 시퀀스 요소가 해시 가능하기만 하면, 텍스트뿐 아니라 모든 타입의 시퀀스 쌍을 비교하는 유연한 클래스죠.

정크 휴리스틱(Junk heuristic)

difflib는 정크 휴리스틱을 사용해요: 어떤 항목을 정크로 간주하고 유사성 검색 때 무시해요. 이상적으로는 빈 줄이나 공백처럼 흥미롭지 않거나 흔한 항목이죠.

이 휴리스틱은 가능한 조합 수를 줄여 알고리즘을 빠르게 하고, 인간이 더 이해하기 쉬운 결과(보통 공백에서 끊김)를 만들어요. 하지만 병리적인 경우를 만들 수도 있어요.

  • 부적절하게 선택된 정크 항목은 예기치 않게 큰(그래도 정확한) 결과를 만들 수 있어요.
  • 기본 휴리스틱은 비대칭이에요: 무엇이 정크인지 결정할 때 두 번째 시퀀스만 검사해요. 그래서 A를 B와 비교하는 것과 B를 A와 비교한 뒤 결과를 뒤집는 것은 결과가 다를 수 있어요.
  • 기본적으로 두 번째 입력 시퀀스가 200개 이상 항목이면, 그것의 1% 이상을 차지하는 항목은 정크로 간주돼요.

데이터에 따라 이 휴리스틱을 끄거나(SequenceMatcherautojunk 인자를 False로) 조정하는 것(isjunk 인자 사용, 아마 미리 정의된 함수 중 하나로)을 고려해 보세요.

difflib 알고리즘

SequenceMatcher에서 쓰는 알고리즘은 1980년대 후반 Ratcliff와 Obershelp가 "gestalt pattern matching"이라는 과장된 이름으로 발표한 알고리즘보다 앞서며 조금 더 화려해요. 아이디어는 두 입력에 공통인 가장 긴 연속 부분 시퀀스를 찾은 다음, 매칭 부분의 왼쪽과 오른쪽 시퀀스 조각을 재귀적으로 처리하는 거예요.

Ratcliff와 Obershelp 알고리즘의 확장으로, difflib는 정크가 없는 가장 긴 연속 부분 시퀀스를 검색해요. 자세한 내용은 정크 휴리스틱 절을 참고하세요.

CPython 구현 세부 사항 — 타이밍: 기본 Ratcliff-Obershelp 알고리즘은 최악의 경우 세제곱 시간, 기대하는 경우 이차 시간이에요. difflib의 알고리즘은 최악의 경우 이차 시간이고, 기대하는 경우 동작은 두 시퀀스가 얼마나 많은 요소를 공유하는지에 복잡한 방식으로 의존하며, 최선의 경우 시간은 선형이에요.

델타 생성

class difflib.Differ

텍스트 줄의 시퀀스를 비교해서 인간이 읽을 수 있는 차이(델타)를 만드는 클래스예요. DifferSequenceMatcher를 줄 시퀀스 비교와 유사한(거의 매칭되는) 줄 안의 문자 시퀀스 비교 양쪽에 사용해요.

Differ 델타의 각 줄은 두 글자 코드로 시작해요.

코드 의미
'- ' 시퀀스 1에만 있는 줄
'+ ' 시퀀스 2에만 있는 줄
' ' 두 시퀀스에 공통인 줄
'? ' 두 입력 시퀀스 어디에도 없는 줄

'?'로 시작하는 줄은 줄 내부(intraline) 차이로 눈을 이끌려고 하는 것이고, 두 입력 시퀀스 어디에도 없었어요. 시퀀스에 공백, 탭, 줄바꿈 같은 공백 문자가 있으면 이 줄들이 혼란스러울 수 있어요.

Differ가 만든 델타가 최소 diff를 보장하지는 않는다는 점에 주의하세요. 반대로 최소 diff는 어디서든 동기화해서 때로는 100페이지 떨어진 우연한 일치에서 동기화되기 때문에 인간에게 직관에 어긋나는 경우가 많아요. 동기화 지점을 연속 일치로 제한하면 어떤 지역성을 보존하고, 대신 더 긴 diff를 만들기도 해요.

Differ 클래스는 이 생성자를 가져요.

init(linejunk=None, charjunk=None)

선택적 키워드 매개변수 linejunkcharjunk는 필터 함수용이에요(또는 None).

  • linejunk: 단일 문자열 인자를 받아 그 문자열이 정크면 true를 돌려주는 함수. 기본값은 None으로, 어떤 줄도 정크로 간주하지 않아요.
  • charjunk: 단일 문자 인자(길이 1의 문자열)를 받아 그 문자가 정크면 true를 돌려주는 함수. 기본값은 None으로, 어떤 문자도 정크로 간주하지 않아요.

이 정크 필터링 함수는 차이를 찾는 매칭을 빠르게 하지만, 다른 줄·문자가 무시되게 하지는 않아요. find_longest_match() 메서드의 isjunk 매개변수 설명을 읽어 보세요.

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으로 줄이 감싸지지 않아요. linejunkcharjunkndiff()(HtmlDiff가 나란히 HTML 차이를 생성하는 데 사용)로 전달되는 선택적 키워드 인자예요. 인자 기본값과 설명은 ndiff() 문서를 참고하세요.

공개 메서드는 다음과 같아요.

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

fromlinestolines(문자열 리스트)를 비교해서, 줄 간·줄 내부 변경이 강조된 줄별 차이 테이블을 담은 완전한 HTML 파일인 문자열을 돌려줘요.

fromdesctodesc는 from/to 파일 열 헤더 문자열을 지정하는 선택적 키워드 인자예요(둘 다 기본값은 빈 문자열). contextnumlines는 둘 다 선택적 키워드 인자예요. 컨텍스트 차이를 보여 주려면 contextTrue로 설정하고, 그렇지 않으면 기본값 False는 전체 파일을 보여 줘요. numlines 기본값은 5예요. contextTruenumlines는 차이 강조를 둘러싼 컨텍스트 줄 수를 제어해요. contextFalsenumlines는 "next" 하이퍼링크를 쓸 때 차이 강조 앞에 표시되는 줄 수를 제어해요(0으로 설정하면 "next" 하이퍼링크가 앞 컨텍스트 없이 다음 차이 강조를 브라우저 맨 위에 놓게 돼요).

참고fromdesctodesc는 이스케이프되지 않은 HTML로 해석되므로, 신뢰할 수 없는 소스에서 입력을 받을 때는 제대로 이스케이프해야 해요.

버전 3.5에서 변경: charset 키워드 전용 인자가 추가됨. HTML 문서의 기본 charset이 'ISO-8859-1'에서 'utf-8'로 변경됨.

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

fromlinestolines(문자열 리스트)를 비교해서, 줄 간·줄 내부 변경이 강조된 줄별 차이를 보여 주는 완전한 HTML 테이블인 문자열을 돌려줘요.

이 메서드의 인자는 make_file() 메서드와 같아요.

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

ab(문자열 리스트)를 비교해서 컨텍스트 diff 형식의 델타(델타 줄을 생성하는 제너레이터)를 돌려줘요.

컨텍스트 diff는 바뀐 줄과 몇 줄의 컨텍스트만 보여 주는 간결한 방법이에요. 변경은 before/after 스타일로 보여 줘요. 컨텍스트 줄 수는 기본값이 3인 n으로 설정돼요.

기본적으로 diff 제어 줄(*** 또는 --- 있는 줄)은 끝에 새 줄과 함께 만들어져요. io.IOBase.readlines()로 만든 입력이 io.IOBase.writelines()에 적합한 diff가 되게 해 주므로 도움이 돼요(입력·출력 모두 끝에 새 줄이 있으니까).

끝에 새 줄이 없는 입력은 lineterm 인자를 ""로 설정해서 출력을 균일하게 새 줄이 없게 만들어요.

컨텍스트 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는 가까운 일치를 원하는 시퀀스(보통 문자열)이고, possibilitiesword와 매칭할 시퀀스 목록(보통 문자열 리스트)이에요.

선택적 인자 n(기본값 3)은 돌려줄 최대 가까운 일치 수로, 0보다 커야 해요.

선택적 인자 cutoff(기본값 0.6)는 [0, 1] 범위의 float예요. 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)

ab(문자열 리스트)를 비교해서 Differ 스타일 델타(델타 줄을 생성하는 제너레이터)를 돌려줘요.

선택적 키워드 매개변수 linejunkcharjunk는 필터링 함수(또는 None)예요.

  • linejunk: 단일 문자열 인자를 받아 문자열이 정크면 true, 아니면 false를 돌려주는 함수. 기본값은 None. 모듈 레벨 함수 IS_LINE_JUNK()도 있는데, 보이는 문자가 없는 줄(해시 문자 '#' 하나는 제외)을 걸러내요. 다만 기본 SequenceMatcher 클래스는 어떤 줄이 노이즈를 구성할 만큼 빈번한지 동적 분석을 하고, 보통 이 함수를 쓰는 것보다 잘 작동해요.
  • charjunk: 문자(길이 1의 문자열)를 받아 문자가 정크면 true, 아니면 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)

델타를 만든 두 시퀀스 중 하나를 돌려줘요.

Differ.compare() 또는 ndiff()가 만든 시퀀스가 주어지면, 파일 1 또는 2(매개변수 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')

ab(문자열 리스트)를 비교해서 통합 diff 형식의 델타(델타 줄을 생성하는 제너레이터)를 돌려줘요.

통합 diff는 바뀐 줄과 몇 줄의 컨텍스트만 보여 주는 간결한 방법이에요. 변경은 인라인 스타일(별도의 before/after 블록 대신)로 보여 줘요. 컨텍스트 줄 수는 기본값이 3인 n으로 설정돼요.

기본적으로 diff 제어 줄(---, +++, @@ 있는 줄)은 끝에 새 줄과 함께 만들어져요. io.IOBase.readlines()로 만든 입력이 io.IOBase.writelines()에 적합한 diff가 되게 해 주므로 도움이 돼요(입력·출력 모두 끝에 새 줄이 있으니까).

끝에 새 줄이 없는 입력은 lineterm 인자를 ""로 설정해서 출력을 균일하게 새 줄이 없게 만들어요.

통합 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')

ab(bytes 객체 리스트)를 dfunc으로 비교해서 dfunc이 돌려주는 형식의 델타 줄 시퀀스(역시 bytes)를 생성해요. dfunc은 호출 가능해야 하고, 보통 unified_diff() 또는 context_diff()예요.

알 수 없거나 일관되지 않은 인코딩의 데이터를 비교할 수 있게 해 줘요. n을 제외한 모든 입력은 str이 아니라 bytes 객체여야 해요. 모든 입력(n 제외)을 손실 없이 str로 변환하고 dfunc(a, b, fromfile, tofile, fromfiledate, tofiledate, n, lineterm)을 호출해서 동작해요. dfunc의 출력은 다시 bytes로 변환되므로 받는 델타 줄은 ab와 같은 알 수 없거나 일관되지 않은 인코딩을 가져요.

버전 3.5에 추가됨.

정크 정의 함수

difflib.IS_LINE_JUNK(line)

무시할 수 있는 줄이면 True를 돌려줘요. 줄이 비어 있거나 '#' 하나만 포함하면 무시할 수 있고, 그렇지 않으면 무시할 수 없어요. 이전 버전의 ndiff()에서 linejunk 매개변수의 기본값으로 쓰였어요.

difflib.IS_CHARACTER_JUNK(ch)

무시할 수 있는 문자면 True를 돌려줘요. 문자가 공백이나 탭이면 무시할 수 있고, 그렇지 않으면 무시할 수 없어요. ndiff()에서 charjunk 매개변수의 기본값으로 쓰여요.

SequenceMatcher 객체

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

선택적 인자 isjunkNone(기본값)이거나 시퀀스 요소를 받아 그 요소가 "정크"여서 무시해야 하면 그때만 true를 돌려주는 단일 인자 함수여야 해요. isjunkNone을 전달하는 것은 lambda x: False를 전달하는 것과 같아요. 즉 어떤 요소도 무시되지 않아요. 예를 들어 줄을 문자 시퀀스로 비교하면서 빈 칸이나 하드 탭에서 동기화하고 싶지 않다면 다음을 전달하세요.

lambda x: x in " \t"

선택적 인자 ab는 비교할 시퀀스로, 둘 다 기본값은 빈 문자열이에요. 두 시퀀스의 요소는 해시 가능해야 해요.

선택적 인자 autojunk는 자동 정크 휴리스틱을 비활성화하는 데 쓸 수 있어요.

버전 3.2에서 변경: autojunk 매개변수 추가.

SequenceMatcher 객체는 세 개의 데이터 속성을 가져요: bjunkisjunkTrueb의 요소 집합, bpopular는 휴리스틱이 (비활성화되지 않았다면) 인기 있다고 여기는 정크가 아닌 요소 집합, b2jb의 나머지 요소를 그것들이 나타나는 위치 목록으로 매핑하는 dict예요. 세 개 모두 set_seqs()set_seq2()b가 리셋될 때마다 리셋돼요.

버전 3.2에 추가됨: bjunkbpopular 속성.

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()a[i:i+k]b[j:j+k]와 같은 (i, j, k)를 돌려주는데, alo <= i <= i+k <= ahiblo <= j <= j+k <= bhi를 만족해요. 그 조건들을 만족하는 모든 (i', j', k')에 대해 k >= k', i <= i', 그리고 i == i'이면 j <= j'라는 추가 조건도 만족해요. 다시 말해 모든 최대 매칭 블록 중 a에서 가장 먼저 시작하는 것을, 그리고 그중 b에서 가장 먼저 시작하는 것을 돌려줘요.

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

isjunk가 제공되면, 먼저 위와 같이 가장 긴 매칭 블록을 결정하되 블록에 정크 요소가 나타나지 않는다는 추가 제한이 있어요. 그런 다음 (오직) 양쪽의 정크 요소를 매칭해 그 블록을 가능한 한 확장해요. 그래서 결과 블록은 흥미로운 매칭에 인접해 일어난 동일한 정크를 제외하면 정크에서 매칭하지 않아요.

앞의 예시와 같지만 빈 칸을 정크로 간주한 경우예요. 그러면 ' 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)을 돌려줘요.

이 메서드는 명명된 튜플 Match(a, b, size)를 돌려줘요.

버전 3.9에서 변경: 기본 인자 추가.

get_matching_blocks()

겹치지 않는 매칭 부분 시퀀스를 설명하는 3-튜플 리스트를 돌려줘요. 각 튜플은 (i, j, n) 형식이고, a[i:i+n] == b[j:j+n]을 의미해요. 튜플은 ij에서 단조 증가해요.

마지막 튜플은 더미로 (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()

ab로 바꾸는 방법을 설명하는 5-튜플 리스트를 돌려줘요. 각 튜플은 (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()

두 시퀀스의 유사도를 [0, 1] 범위의 float로 돌려줘요.

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()는 시퀀스의 유사도를 측정하는 [0, 1] 범위의 float를 돌려줘요. 경험상 ratio() 값이 0.6을 넘으면 시퀀스가 가까운 일치라는 뜻이에요.

>>> 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는 문자열 리스트이니 예쁘게 출력해 볼게요.

>>> 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:])

더 알아보기

  • 이 모듈의 get_close_matches() 함수는 SequenceMatcher에 기반한 간단한 코드로 유용한 작업을 할 수 있는 방법을 보여 줘요.
  • SequenceMatcher로 만든 소규모 애플리케이션용 간단한 버전 관리 레시피.