はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Python difflib 覚書

概要

Pythonのdifflibのうち、私のような軟派な用途にあたり便利だな・面白いなと思ったものをまとめてみました。

docs.python.org

オレ様チョイス 3大difflibライブラリ

f:id:azotar:20200304224705p:plain

(1) difflib.SequenceMatcher・・・ ratio()

2つの文字列の類似度です。通常の文字列のequal判定とは異なり、似ている文字列どおしであれば、1に近い値が戻ります。

中身の単語の順序を入れ替えたような文字列でも0よりはそれなりに大きい数字が戻るようです。

In [162]: from difflib import SequenceMatcher

In [167]: SequenceMatcher(None, "abcd", "abce").ratio()                                                                                      
Out[167]: 0.75

In [166]: SequenceMatcher(None, "ばなな みかん りんご", "りんご みかん ばなな").ratio()                                                  
Out[166]: 0.45454545454545453

In [165]: SequenceMatcher(None, ["ばなな","みかん","りんご"], ["りんご","みかん","ばなな"]).ratio()                                          
Out[165]: 0.3333333333333333

(2) difflib.get_close_matches()

ある文字列の配列のうち、インプットの文字列に「近い」と見なされるものを戻します。

In [171]: import difflib

In [171]: difflib.get_close_matches('appel',['ape', 'apple', 'peach', 'puppy'])                                                              
Out[171]: ['apple', 'ape']

(3) difflib.HtmlDiff ・・・ make_file、make_table

差異がある部分(Added/Changed/Deleted)を、色で可視化して対比して確認できるような、htmlファイルを吐き出します。

ブラウザ起動まで含めて、ワンライナーにしておいて、2つのファイルの差異を大まかに把握するための、目grepに便利かも。

In [153]: print(difflib.HtmlDiff().make_file(["あいうえお","たちつてと","わおん"],["あいうえか","たちすてと","わを"]))                       

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>
    <meta http-equiv="Content-Type"
          content="text/html; charset=utf-8" />
    <title></title>
    <style type="text/css">
        table.diff {font-family:Courier; border:medium;}
        .diff_header {background-color:#e0e0e0}
        td.diff_header {text-align:right}
        .diff_next {background-color:#c0c0c0}
        .diff_add {background-color:#aaffaa}
        .diff_chg {background-color:#ffff77}
        .diff_sub {background-color:#ffaaaa}
    </style>
</head>

<body>
    
    <table class="diff" id="difflib_chg_to4__top"
           cellspacing="0" cellpadding="0" rules="groups" >
        <colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>
        <colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>
        
        <tbody>
            <tr><td class="diff_next" id="difflib_chg_to4__0"><a href="#difflib_chg_to4__top">t</a></td><td class="diff_header" id="from4_1">1</td><td nowrap="nowrap">あいうえ<span class="diff_chg">お</span></td><td class="diff_next"><a href="#difflib_chg_to4__top">t</a></td><td class="diff_header" id="to4_1">1</td><td nowrap="nowrap">あいうえ<span class="diff_chg">か</span></td></tr>
            <tr><td class="diff_next"></td><td class="diff_header" id="from4_2">2</td><td nowrap="nowrap">たち<span class="diff_chg">つ</span>てと</td><td class="diff_next"></td><td class="diff_header" id="to4_2">2</td><td nowrap="nowrap">たち<span class="diff_chg">す</span>てと</td></tr>
            <tr><td class="diff_next"></td><td class="diff_header" id="from4_3">3</td><td nowrap="nowrap"><span class="diff_sub">わおん</span></td><td class="diff_next"></td><td class="diff_header" id="to4_3">3</td><td nowrap="nowrap"><span class="diff_add">わを</span></td></tr>
        </tbody>
    </table>
    <table class="diff" summary="Legends">
        <tr> <th colspan="2"> Legends </th> </tr>
        <tr> <td> <table border="" summary="Colors">
                      <tr><th> Colors </th> </tr>
                      <tr><td class="diff_add">&nbsp;Added&nbsp;</td></tr>
                      <tr><td class="diff_chg">Changed</td> </tr>
                      <tr><td class="diff_sub">Deleted</td> </tr>
                  </table></td>
             <td> <table border="" summary="Links">
                      <tr><th colspan="2"> Links </th> </tr>
                      <tr><td>(f)irst change</td> </tr>
                      <tr><td>(n)ext change</td> </tr>
                      <tr><td>(t)op</td> </tr>
                  </table></td> </tr>
    </table>
</body>

</html>

difflib.SequenceMatcher再び

SequenceMatcherには、いわゆる GNU diffっぽい出力が得られるメソッドがいくつかあります。

一方で、「共通する部分」を抜き出す、正規表現を使わない変則grepぽいメソッドとして、find_longest_matchとget_matched_blocksがありますので、これらを紹介します。

なお、共通する部分を抜き出せますので、(ある程度ですが)共通しない部分も抜き出せます。

find_longest_match

最長一致部分を抜き出せるインデックスのタプルが得られます。

f:id:azotar:20200304224732p:plain

In [175]: i,j,k = SequenceMatcher(None,'あいうえおかきく','あいうEおかきく').find_longest_match(0,8,0,8)                         

In [176]: a ='あいうえおかきく'                                                                                                              

In [177]: b = 'あいうEおかきく'                                                                                                              
In [178]: i,j,k = SequenceMatcher(None,a,b).find_longest_match(0,len(a),0,len(b))                                           

In [180]: a[i:i+k]                                                                                                                           
Out[180]: 'おかきく'

In [181]: b[j:j+k]                                                                                                                           
Out[181]: 'おかきく'

In [182]: a[0:i]                                                                                                                             
Out[182]: 'あいうえ'

In [183]: b[0:j]                                                                                                                             
Out[183]: 'あいうE'

get_matching_blocks

途中の不一致部分を境に前部分と後ろ部分を抜き出せるインデックスを表すタプルの配列が得られます。 (※2020/12/25 以下、初出時より誤り・不正確なまとめがありましたので、訂正しています。)

ただし、一致部分が3つ以上の場合はエラーになるようです。

f:id:azotar:20201225180030p:plain

f:id:azotar:20201225180039p:plain

In [73]: str1,str2 = 'あいうえおかきくけこさし','あいxえxかxくけxさし'                                                                                                     

In [74]: [x,y,*_] = SequenceMatcher(None,str1,str2).get_matching_blocks()                                                                                                  

In [75]: x                                                                                                                                                                 
Out[75]: Match(a=0, b=0, size=2)

In [76]: y                                                                                                                                                                 
Out[76]: Match(a=3, b=3, size=1)

In [77]: str1[0:2]                                                                                                                                                         
Out[77]: 'あい'

In [79]: str1[3:3+1]                                                                                                                                                       
Out[79]: 'え'

In [80]: str2[0:2]                                                                                                                                                         
Out[80]: 'あい'

In [81]: str1,str2 = 'あいうえおかきくけこさし','あいxxxえxかxくけxさし'            
↑
「xxx」に注目                                                                                       

In [82]: [x,y,*_] = SequenceMatcher(None,str1,str2).get_matching_blocks()                                                                                                  

In [83]: y                                                                                                                                                                 
Out[83]: Match(a=3, b=5, size=1)

↓ 「diff」らしく同じシーケンスが現れるところまで考慮してくれる。

In [84]: str1[3:3+1]                                                                                                                                                       
Out[84]: 'え'

In [85]: str2[5:5+1]                                                                                                                                                       
Out[85]: 'え'