Path: csiph.com!x330-a1.tempe.blueboxinc.net!usenet.pasdenom.info!gegeweb.org!de-l.enfer-du-nord.net!feeder1.enfer-du-nord.net!tudelft.nl!txtfeed1.tudelft.nl!feeder2.cambriumusenet.nl!feed.tweaknews.nl!194.109.133.85.MISMATCH!newsfeed.xs4all.nl!newsfeed6.news.xs4all.nl!xs4all!post.news.xs4all.nl!not-for-mail Return-Path: X-Original-To: python-list@python.org Delivered-To: python-list@mail.python.org X-Spam-Status: OK 0.000 X-Spam-Evidence: '*H*': 1.00; '*S*': 0.00; 'elif': 0.04; 'filename:fname piece:py': 0.04; 'skip:p 40': 0.04; 'library,': 0.05; '"""': 0.07; 'check.': 0.07; 'inserts': 0.07; 'python': 0.08; '21,': 0.09; '__name__': 0.09; 'compares': 0.09; 'given,': 0.09; 'highlight': 0.09; 'instance.': 0.09; 'satisfying': 0.09; 'subject:library': 0.09; 'url:publications': 0.09; 'utf-8': 0.09; '25,': 0.12; 'wrote:': 0.15; '"__main__":': 0.16; "'',": 0.16; '(sorry': 0.16; '-*-': 0.16; 'additions': 0.16; 'coding:': 0.16; 'deletions': 0.16; 'naive': 0.16; 'simplified': 0.16; 'wind': 0.16; '\xa0once': 0.16; 'algorithm': 0.16; 'case.': 0.16; 'operations,': 0.16; 'tries': 0.16; 'def': 0.16; 'programming': 0.18; 'detect': 0.22; 'header:In-Reply-To:1': 0.22; 'additionally': 0.23; 'produces': 0.23; '\xa0if': 0.23; 'received:209.85.212.46': 0.23; 'received:mail- vw0-f46.google.com': 0.23; 'code': 0.24; '(and': 0.27; 'sort': 0.28; 'url:edu': 0.28; 'skip:" 30': 0.28; 'message- id:@mail.gmail.com': 0.28; 'import': 0.29; '27,': 0.29; 'skip:i 30': 0.29; 'problem': 0.29; 'blocks': 0.30; 'luck,': 0.30; 'rough': 0.30; 'thanks': 0.31; 'subject:?': 0.31; 'changes': 0.31; 'print': 0.32; 'chris': 0.32; "skip:' 10": 0.32; 'references': 0.32; 'implement': 0.33; 'skip:" 20': 0.33; 'usually': 0.33; 'to:addr:python-list': 0.34; 'however,': 0.34; 'received:209.85.212': 0.34; 'that,': 0.35; '17,': 0.35; 'similar': 0.37; 'comparing': 0.37; 'some': 0.37; 'using': 0.37; 'received:google.com': 0.38; 'received:209.85': 0.38; 'subject:: ': 0.38; 'two': 0.38; 'comments': 0.38; 'i.e.': 0.39; 'allows': 0.39; 'skip:s 20': 0.39; 'under': 0.39; 'to:addr:python.org': 0.39; 'received:209': 0.40; 'called': 0.40; 'give': 0.60; 'easily': 0.61; 'results': 0.62; 'content-type:application/octet- stream': 0.64; 'tag': 0.64; 'taking': 0.65; '26,': 0.67; 'subject:like': 0.67; '11,': 0.68; 'relevant': 0.69; 'article': 0.76; 'obtained': 0.80; 'blocks.': 0.84; 'delete,': 0.84; 'insert,': 0.84; 'isolated': 0.84; "man's": 0.84; 'partially': 0.84; 'removals': 0.84; 'subject:moved': 0.84; 'tag,': 0.84; 'viable': 0.84; 'complexity': 0.93; 'river': 0.93 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=mime-version:in-reply-to:references:date:message-id:subject:from:to :content-type; bh=yx7bslQfb0xATpXqj5dqdfgDHtXv5tHuUoldFpofFKA=; b=d9ajw0Y5zD6L3WuU3WB/KXwxaKPhBLZHznno8YpURiEhec5WcQqDPLalQh6sAYtM4t ejfcgFMmWSP5gTJnimzqIImfg9Em0whNudyXuPELirmDKvsMFto8i/CoF8Ggkhw9w5wj 2y1qSxxaHIJg0U7pRPCITg4o7I61EViSwDfNc= MIME-Version: 1.0 In-Reply-To: References: Date: Fri, 15 Jul 2011 23:49:53 +0200 Subject: Re: difflib-like library supporting moved blocks detection? From: Vlastimil Brom To: python-list@python.org Content-Type: multipart/mixed; boundary=20cf307f35de2fb10304a822a114 X-BeenThere: python-list@python.org X-Mailman-Version: 2.1.12 Precedence: list List-Id: General discussion list for the Python programming language List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Newsgroups: comp.lang.python Message-ID: Lines: 140 NNTP-Posting-Host: 2001:888:2000:d::a6 X-Trace: 1310766597 news.xs4all.nl 23900 [2001:888:2000:d::a6]:52485 X-Complaints-To: abuse@xs4all.nl Xref: x330-a1.tempe.blueboxinc.net comp.lang.python:9574 --20cf307f35de2fb10304a822a114 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: quoted-printable 2011/7/14 Chris Torek : > In article > Vlastimil Brom =A0 wrote: >>I'd like to ask about the availability of a text diff library, like >>difflib, which would support the detection of moved text blocks. > > If you allow arbitrary moves, the "minimal edit distance" problem > (string-to-string edit) becomes substantially harder. =A0If you only > allow insert, delete, or in-place-substitute, you have what is > called the "Levenshtein distance" case. =A0If you allow transpositions > you get "Damerau-Levenshtein". =A0These are both solveable with a > dynamic programming algorithm. =A0Once you allow move operations, > though, the problem becomes NP-complete. > > See http://pages.cs.brandeis.edu/~shapird/publications/JDAmoves.pdf > for instance. =A0(They give an algorithm that produces "usually > acceptable" results in polynomial time.) > -- > In-Real-Life: Chris Torek, Wind River Systems > > Thanks for the references and explanation! I do realise the added complexity with taking the moves into account; given that, my current needs and the usually satisfying results obtained easily with difflib, I am not going to try to implement some more complex diffing algorithm. However, it turns out that the mentioned naive approach with just recomparing the text additions and removals may be partially viable - with some luck, i.e. given, the relevant segments are identified as deletions and inserts and isolated by difflib in the first place (and not subsumed under larger changes or split). For illustration, the rough simplified code is attached (sorry for the style and possible quirks...) Just now the more similar text segments are just collected, it would be also possible to sort them on their similarity ratio; the current approach also allows to highlight potentially multiple moved segments. Comments and suggestions are, of course, welcome, regards, vbr # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #! Python # -*- coding: utf-8 -*- import difflib import itertools def compare_moves(a, b, similarity_threshold=3D0.6): """ Poor man's text comparison with simple moves check. Compares two strings using difflib and additionally tries to detect moved blocks by comparing similar deleted and inserted segments with each other - given the similarity_threshold. """ seq_matcher =3D difflib.SequenceMatcher(isjunk=3DNone, a=3Da, b=3Db, au= tojunk=3DFalse) diff_raw =3D [[tag, i1, i2, j1, j2, a[i1:i2], b[j1:j2]] for tag, i1, i2, j1, j2 in seq_matcher.get_opcodes()] deleted, inserted =3D {}, {} for tag, i1, i2, j1, j2 in seq_matcher.get_opcodes(): if tag =3D=3D 'delete': deleted[(i1, i2)] =3D [tag, i1, i2, j1, j2, a[i1:i2]] elif tag =3D=3D 'insert': inserted[(i1, i2)] =3D [tag, i1, i2, j1, j2, b[j1:j2]] possibly_moved_blocks =3D [] for deleted_item, inserted_item in itertools.product(deleted.values(), inserted.values()): similarity_ratio =3D difflib.SequenceMatcher(isjunk=3DNone, a=3Ddeleted_item[5], b=3Dinserted_item[5], autojunk=3DFalse).ratio() if similarity_ratio >=3D similarity_threshold: possibly_moved_blocks.append([deleted_item, inserted_item, similarity_ratio]) print diff_raw print possibly_moved_blocks if __name__ =3D=3D "__main__": compare_moves("abcXYZdeABfghijklmnopABBCq", "ABCDabcdeACfgXYXZhijklmnopq", similarity_threshold =3D 0.6) # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # output: # [['insert', 0, 0, 0, 4, '', 'ABCD'], ['equal', 0, 3, 4, 7, 'abc', 'abc'], ['delete', 3, 6, 7, 7, 'XYZ', ''], ['equal', 6, 9, 7, 10, 'deA', 'deA'], ['replace', 9, 10, 10, 11, 'B', 'C'], ['equal', 10, 12, 11, 13, 'fg', 'fg'], ['insert', 12, 12, 13, 17, '', 'XYXZ'], ['equal', 12, 21, 17, 26, 'hijklmnop', 'hijklmnop'], ['delete', 21, 25, 26, 26, 'ABBC', ''], ['equal', 25, 26, 26, 27, 'q', 'q']] [[['delete', 21, 25, 26, 26, 'ABBC'], ['insert', 0, 0, 0, 4, 'ABCD'], 0.75], [['delete', 3, 6, 7, 7, 'XYZ'], ['insert', 12, 12, 13, 17, 'XYXZ'], 0.8571428571428571]] --20cf307f35de2fb10304a822a114 Content-Type: application/octet-stream; name="compare_moves.py" Content-Disposition: attachment; filename="compare_moves.py" Content-Transfer-Encoding: base64 X-Attachment-Id: f_gq5nqkel0 IyEgUHl0aG9uDQojIC0qLSBjb2Rpbmc6IHV0Zi04IC0qLQ0KDQppbXBvcnQgZGlmZmxpYg0KaW1w b3J0IGl0ZXJ0b29scw0KDQpkZWYgY29tcGFyZV9tb3ZlcyhhLCBiLCBzaW1pbGFyaXR5X3RocmVz aG9sZD0wLjYpOg0KICAgICIiIg0KICAgIFBvb3IgbWFuJ3MgdGV4dCBjb21wYXJpc29uIHdpdGgg c2ltcGxlIG1vdmVzIGNoZWNrLiBDb21wYXJlcyB0d28gc3RyaW5ncyB1c2luZyBkaWZmbGliIA0K ICAgIGFuZCBhZGRpdGlvbmFsbHkgdHJpZXMgdG8gZGV0ZWN0IG1vdmVkIGJsb2NrcyANCiAgICBi eSBjb21wYXJpbmcgc2ltaWxhciBkZWxldGVkIGFuZCBpbnNlcnRlZCBzZWdtZW50cyB3aXRoIGVh Y2ggb3RoZXIgLSBnaXZlbiB0aGUgc2ltaWxhcml0eV90aHJlc2hvbGQuDQogICAgIiIiDQoNCiAg ICBzZXFfbWF0Y2hlciA9IGRpZmZsaWIuU2VxdWVuY2VNYXRjaGVyKGlzanVuaz1Ob25lLCBhPWEs IGI9YiwgYXV0b2p1bms9RmFsc2UpDQogICAgZGlmZl9yYXcgPSBbW3RhZywgaTEsIGkyLCBqMSwg ajIsIGFbaTE6aTJdLCBiW2oxOmoyXV0gZm9yIHRhZywgaTEsIGkyLCBqMSwgajIgaW4gc2VxX21h dGNoZXIuZ2V0X29wY29kZXMoKV0NCg0KICAgIGRlbGV0ZWQsIGluc2VydGVkID0ge30sIHt9DQog ICAgZm9yIHRhZywgaTEsIGkyLCBqMSwgajIgaW4gc2VxX21hdGNoZXIuZ2V0X29wY29kZXMoKToN CiAgICAgICAgaWYgdGFnID09ICdkZWxldGUnOg0KICAgICAgICAgICAgZGVsZXRlZFsoaTEsIGky KV0gPSBbdGFnLCBpMSwgaTIsIGoxLCBqMiwgYVtpMTppMl1dDQogICAgICAgIGVsaWYgdGFnID09 ICdpbnNlcnQnOg0KICAgICAgICAgICAgaW5zZXJ0ZWRbKGkxLCBpMildID0gW3RhZywgaTEsIGky LCBqMSwgajIsIGJbajE6ajJdXQ0KDQogICAgcG9zc2libHlfbW92ZWRfYmxvY2tzID0gW10NCiAg ICBmb3IgZGVsZXRlZF9pdGVtLCBpbnNlcnRlZF9pdGVtIGluIGl0ZXJ0b29scy5wcm9kdWN0KGRl bGV0ZWQudmFsdWVzKCksIGluc2VydGVkLnZhbHVlcygpKToNCiAgICAgICAgc2ltaWxhcml0eV9y YXRpbyA9IGRpZmZsaWIuU2VxdWVuY2VNYXRjaGVyKGlzanVuaz1Ob25lLCBhPWRlbGV0ZWRfaXRl bVs1XSwgYj1pbnNlcnRlZF9pdGVtWzVdLCBhdXRvanVuaz1GYWxzZSkucmF0aW8oKQ0KICAgICAg ICBpZiBzaW1pbGFyaXR5X3JhdGlvID49IHNpbWlsYXJpdHlfdGhyZXNob2xkOg0KICAgICAgICAg ICAgcG9zc2libHlfbW92ZWRfYmxvY2tzLmFwcGVuZChbZGVsZXRlZF9pdGVtLCBpbnNlcnRlZF9p dGVtLCBzaW1pbGFyaXR5X3JhdGlvXSkNCg0KICAgIHByaW50IGRpZmZfcmF3DQogICAgcHJpbnQg cG9zc2libHlfbW92ZWRfYmxvY2tzDQoNCg0KaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoNCiAg ICBjb21wYXJlX21vdmVzKCJhYmNYWVpkZUFCZmdoaWprbG1ub3BBQkJDcSIsICJBQkNEYWJjZGVB Q2ZnWFlYWmhpamtsbW5vcHEiLCBzaW1pbGFyaXR5X3RocmVzaG9sZCA9IDAuNikNCg== --20cf307f35de2fb10304a822a114--