Changeset 74

Show
Ignore:
Timestamp:
12/27/06 19:02:06 (2 years ago)
Author:
rgrp
Message:

Improve ignoring of non words by the concordance/statistic builder and fix a bug introduced by changeset:72.

  • src/shakespeare/concordance.py:
    • add ignore_word function to replace simple test in add_text method and improve this using inter alia:
    • is_roman_numeral: new method to test whether a word is a roman numeral
    • non_words: attribute listing non-words
    • remove_text: (bugfix) was not removing associated Statistic only associated Concordance objects
  • src/shakespeare/concordance_test.py:
    • test_is_roman_numeral
    • test_ignore_word
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/src/shakespeare/concordance.py

    Revision 72 Revision 74
    1""" 1""" 
    2Concordance (and statistics) for texts in database. 2Concordance (and statistics) for texts in database. 
    3 3 
    4To build concordance use ConcordanceBuilder.  To access concordance/statistics 4To build concordance use ConcordanceBuilder.  To access concordance/statistics 
    5use Concordance/Statistics class.  Concordance and statistics are provided as 5use Concordance/Statistics class.  Concordance and statistics are provided as 
    6dictionaries keyed by words. 6dictionaries keyed by words. 
    7 7 
    8NB: all word keys have been lower-cased in order to render them 8NB: all word keys have been lower-cased in order to render them 
    9case-insensitive 9case-insensitive 
    10""" 10""" 
    11import re 11import re 
    12 12 
    13import sqlobject 13import sqlobject 
    14 14 
    15import shakespeare.index 15import shakespeare.index 
    16import shakespeare.cache 16import shakespeare.cache 
    17 17 
    18 18 
    19class ConcordanceBase(object): 19class ConcordanceBase(object): 
    20    """ 20    """ 
    21    TODO: caching?? 21    TODO: caching?? 
    22    """ 22    """ 
    23    sqlcc = shakespeare.dm.Concordance 23    sqlcc = shakespeare.dm.Concordance 
    24    sqlstat = shakespeare.dm.Statistic 24    sqlstat = shakespeare.dm.Statistic 
    25 25 
    26    def __init__(self, filter_names=None): 26    def __init__(self, filter_names=None): 
    27        """ 27        """ 
    28        @param filter_names: a list of id names with which to filter results 28        @param filter_names: a list of id names with which to filter results 
    29            (i.e. only return results relating to those texts) 29            (i.e. only return results relating to those texts) 
    30        """ 30        """ 
    31        self._filter_names = filter_names 31        self._filter_names = filter_names 
    32        self.sqlcc_filter = self._make_filter(self.sqlcc) 32        self.sqlcc_filter = self._make_filter(self.sqlcc) 
    33        self.sqlstat_filter = self._make_filter(self.sqlstat) 33        self.sqlstat_filter = self._make_filter(self.sqlstat) 
    34 34 
    35    def _make_filter(self, sqlobj): 35    def _make_filter(self, sqlobj): 
    36        sql_filter = True 36        sql_filter = True 
    37        if self._filter_names is not None: 37        if self._filter_names is not None: 
    38            arglist = [] 38            arglist = [] 
    39            for name in self._filter_names: 39            for name in self._filter_names: 
    40                newarg = sqlobj.q.textID == self._name2id(name) 40                newarg = sqlobj.q.textID == self._name2id(name) 
    41                arglist.append(newarg) 41                arglist.append(newarg) 
    42            sql_filter = sqlobject.OR(*arglist) 42            sql_filter = sqlobject.OR(*arglist) 
    43        return sql_filter 43        return sql_filter 
    44     44     
    45    def _name2id(self, name): 45    def _name2id(self, name): 
    46        return shakespeare.dm.Material.byName(name).id 46        return shakespeare.dm.Material.byName(name).id 
    47 47 
    48    def keys(self): 48    def keys(self): 
    49        """Return list of *distinct* words in concordance/statistics 49        """Return list of *distinct* words in concordance/statistics 
    50        """ 50        """ 
    51        all = self.sqlstat.select(self.sqlstat_filter, 51        all = self.sqlstat.select(self.sqlstat_filter, 
    52                           orderBy=self.sqlstat.q.word, 52                           orderBy=self.sqlstat.q.word, 
    53                           ) 53                           ) 
    54        words = [ xx.word for xx in list(all) ] 54        words = [ xx.word for xx in list(all) ] 
    55        distinct = list(set(words)) 55        distinct = list(set(words)) 
    56        distinct.sort() 56        distinct.sort() 
    57        return distinct 57        return distinct 
    58 58 
    59 59 
    60class Concordance(ConcordanceBase): 60class Concordance(ConcordanceBase): 
    61    """Concordance by word for a set of texts 61    """Concordance by word for a set of texts 
    62    """ 62    """ 
    63 63 
    64    def get(self, word): 64    def get(self, word): 
    65        """Get list of occurrences for word 65        """Get list of occurrences for word 
    66        @return: sqlobject query list  66        @return: sqlobject query list  
    67        """ 67        """ 
    68        select = self.sqlcc.select(sqlobject.AND(self.sqlcc_filter, self.sqlcc.q.word==word)) 68        select = self.sqlcc.select(sqlobject.AND(self.sqlcc_filter, self.sqlcc.q.word==word)) 
    69        return select 69        return select 
    70 70 
    71class Statistics(ConcordanceBase): 71class Statistics(ConcordanceBase): 
    72 72 
    73    def get(self, word): 73    def get(self, word): 
    74        select = self.sqlstat.select( 74        select = self.sqlstat.select( 
    75            sqlobject.AND(self.sqlstat_filter, self.sqlstat.q.word==word) 75            sqlobject.AND(self.sqlstat_filter, self.sqlstat.q.word==word) 
    76            ) 76            ) 
    77        total = 0 77        total = 0 
    78        for stat in select: 78        for stat in select: 
    79            total += stat.occurrences 79            total += stat.occurrences 
    80        return total 80        return total 
    81 81 
    82class ConcordanceBuilder(object): 82class ConcordanceBuilder(object): 
    83    """Build a concordance and associated statistics for a set of texts. 83    """Build a concordance and associated statistics for a set of texts. 
    84     84     
    85    """ 85    """ 
    86 86 
    87    # multiline, unicode and ignorecase 87    # multiline, unicode and ignorecase 
    88    word_regex = re.compile(r'\b(\w+)\b', re.U | re.M | re.I) 88    word_regex = re.compile(r'\b(\w+)\b', re.U | re.M | re.I) 
    89 89 
    90    words_to_ignore = [  90    words_to_ignore = [  
    91        # 'a', 'the', 'and', 'as', 'are', 'be', 'but', 'd', 'in' 91        # 'a', 'the', 'and', 'as', 'are', 'be', 'but', 'in' 
    92                        ] 92                        ] 
      93    non_words = [  
      94            'd', # accus'd 
      95            't', 
      96            ] 
      97 
      98    def is_roman_numeral(self, word): 
      99        digits = [ 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix' ] 
      100        others = [ 'l', 'x', 'c' ] 
      101        if word == 'i': return False # exception because this conflicts with I 
      102        while word[0] in others: 
      103            if len(word) == 1: 
      104                return True 
      105            else: 
      106                word = word[1:] 
      107        return word in digits 
      108 
      109    def ignore_word(self, word): 
      110        "Return True if this word should not be added to the concordance." 
      111        bool1 = word in self.words_to_ignore 
      112        bool2 = word in self.non_words 
      113        # do roman numerals 
      114        bool3 = self.is_roman_numeral(word) 
      115        return bool1 or bool2 or bool3 
    93 116 
    94    def _text_already_done(self, text): 117    def _text_already_done(self, text): 
    95        numrecs = shakespeare.dm.Concordance.select( 118        numrecs = shakespeare.dm.Concordance.select( 
    96                shakespeare.dm.Concordance.q.textID==text.id 119                shakespeare.dm.Concordance.q.textID==text.id 
    97                ).count() 120                ).count() 
    98        return numrecs > 0 121        return numrecs > 0 
    99 122 
    100    def add_text(self, name, text=None): 123    def add_text(self, name, text=None): 
    101        """Add a text to the concordance. 124        """Add a text to the concordance. 
    102        @param name: name of text to add 125        @param name: name of text to add 
    103        @param text: [optional] a file-like object containing text data. If not 126        @param text: [optional] a file-like object containing text data. If not 
    104            provided will default to using file in cache associated with named 127            provided will default to using file in cache associated with named 
    105            text 128            text 
    106        """ 129        """ 
    107        dmText = shakespeare.dm.Material.byName(name) 130        dmText = shakespeare.dm.Material.byName(name) 
    108        if self._text_already_done(dmText): 131        if self._text_already_done(dmText): 
    109            msg = 'Have already added to concordance text: %s' % dmText 132            msg = 'Have already added to concordance text: %s' % dmText 
    110            # raise ValueError(msg) 133            # raise ValueError(msg) 
    111            print msg 134            print msg 
    112            print 'Skipping' 135            print 'Skipping' 
    113            return 136            return 
    114        if text is None: 137        if text is None: 
    115            tpath = dmText.get_cache_path('plain') 138            tpath = dmText.get_cache_path('plain') 
    116            text = file(tpath) 139            text = file(tpath) 
    117        lineCount = 0 140        lineCount = 0 
    118        charIndex = 0 141        charIndex = 0 
    119        stats = {} 142        stats = {} 
    120        trans = shakespeare.dm.Concordance._connection.transaction() 143        trans = shakespeare.dm.Concordance._connection.transaction() 
    121        for line in text.readlines(): 144        for line in text.readlines(): 
    122            for match in self.word_regex.finditer(line): 145            for match in self.word_regex.finditer(line): 
    123                word = match.group().lower() # case insensitive 146                word = match.group().lower() # case insensitive 
    124                if word in self.words_to_ignore147                if self.ignore_word(word)
    125                    continue 148                    continue 
    126                shakespeare.dm.Concordance(connection=trans, 149                shakespeare.dm.Concordance(connection=trans, 
    127                                           text=dmText, 150                                           text=dmText, 
    128                                           word=word, 151                                           word=word, 
    129                                           line=lineCount, 152                                           line=lineCount, 
    130                                           char_index=charIndex+match.start()) 153                                           char_index=charIndex+match.start()) 
    131                stats[word] = stats.get(word, 0) + 1 154                stats[word] = stats.get(word, 0) + 1 
    132            lineCount += 1 155            lineCount += 1 
    133            charIndex += len(line) 156            charIndex += len(line) 
    134        trans.commit() 157        trans.commit() 
    135        trans = shakespeare.dm.Concordance._connection.transaction() 158        trans = shakespeare.dm.Concordance._connection.transaction() 
    136        for word, value in stats.items(): 159        for word, value in stats.items(): 
    137            tresults  = shakespeare.dm.Statistic.select( 160            tresults  = shakespeare.dm.Statistic.select( 
    138                sqlobject.AND( 161                sqlobject.AND( 
    139                    shakespeare.dm.Statistic.q.textID == dmText.id, 162                    shakespeare.dm.Statistic.q.textID == dmText.id, 
    140                    shakespeare.dm.Statistic.q.word == word 163                    shakespeare.dm.Statistic.q.word == word 
    141                    )) 164                    )) 
    142            try: 165            try: 
    143                dbstat = list(tresults)[0] 166                dbstat = list(tresults)[0] 
    144                dbstat.occurrences += value 167                dbstat.occurrences += value 
    145            except: 168            except: 
    146                shakespeare.dm.Statistic( 169                shakespeare.dm.Statistic( 
    147                        connection=trans, 170                        connection=trans, 
    148                        text=dmText, 171                        text=dmText, 
    149                        word=word, 172                        word=word, 
    150                        occurrences=value 173                        occurrences=value 
    151                        ) 174                        ) 
    152        trans.commit() 175        trans.commit() 
    153 176 
    154 177 
    155    def remove_text(self, name): 178    def remove_text(self, name): 
    156        """Remove a text from the concordance. 179        """Remove a text from the concordance. 
    157 180 
    158        @param name: as for add_text 181        @param name: as for add_text 
    159        """ 182        """ 
    160        dmText = shakespeare.dm.Material.byName(name) 183        dmText = shakespeare.dm.Material.byName(name) 
    161        recs = shakespeare.dm.Concordance.select( 184        recs = shakespeare.dm.Concordance.select( 
    162                shakespeare.dm.Concordance.q.textID==dmText.id 185                shakespeare.dm.Concordance.q.textID==dmText.id 
    163                ) 186                ) 
    164        for rec in recs: 187        for rec in recs: 
    165            shakespeare.dm.Concordance.delete(rec.id) 188            shakespeare.dm.Concordance.delete(rec.id) 
      189        stats = shakespeare.dm.Statistic.select( 
      190                shakespeare.dm.Statistic.q.textID==dmText.id 
      191                ) 
      192        for stat in stats: 
      193            shakespeare.dm.Statistic.delete(stat.id) 
    166 194 
  • trunk/src/shakespeare/concordance_test.py

    Revision 72 Revision 74
    1import unittest 1import unittest 
    2import StringIO 2import StringIO 
    3import tempfile 3import tempfile 
    4 4 
    5 5 
    6import shakespeare.index 6import shakespeare.index 
    7import shakespeare.concordance 7import shakespeare.concordance 
    8 8 
    9class TestConcordancer: 9class TestConcordancer: 
    10 10 
    11    inText = \ 11    inText = \ 
    12"""A fake fake line 12"""A fake fake line 
    13SUFFOLK. 13SUFFOLK. 
    14As by your high imperial Majesty 14As by your high imperial Majesty 
    15I had in charge at my depart for France, 15I had in charge at my depart for France, 
    16As procurator to your excellence, 16As procurator to your excellence, 
    17A fake imperial line. 17A fake imperial line. 
    18""" 18""" 
    19    name = 'test-concordance' 19    name = 'test-concordance' 
    20    title = 'Hamlet' 20    title = 'Hamlet' 
    21     21     
    22    # ['work_id', 'line-no', 'character-index'] } 22    # ['work_id', 'line-no', 'character-index'] } 
    23    # incomplete 23    # incomplete 
    24    expConcordance = { 24    expConcordance = { 
    25        'fake' : [ (name, 0, 2), (name, 0, 7), (name, 5, 136) ], 25        'fake' : [ (name, 0, 2), (name, 0, 7), (name, 5, 136) ], 
    26        'suffolk' : [ (name, 1, 17), ], 26        'suffolk' : [ (name, 1, 17), ], 
    27        'high' : [ (name, 2, 37), ], 27        'high' : [ (name, 2, 37), ], 
    28        'word_that_is_not_there' : [], 28        'word_that_is_not_there' : [], 
    29        } 29        } 
    30 30 
    31    # incomplete 31    # incomplete 
    32    expStats = { 32    expStats = { 
    33        'fake' : 3, 33        'fake' : 3, 
    34        'imperial' : 2, 34        'imperial' : 2, 
    35        'suffolk' : 1, 35        'suffolk' : 1, 
    36        'high' : 1, 36        'high' : 1, 
    37        'word_that_is_not_there' : 0, 37        'word_that_is_not_there' : 0, 
    38        } 38        } 
    39 39 
    40    def setup_class(cls): 40    def setup_class(cls): 
    41        cls.builder = shakespeare.concordance.ConcordanceBuilder() 41        cls.builder = shakespeare.concordance.ConcordanceBuilder() 
    42        # try deleting it first so as to be more robust to errors 42        # try deleting it first so as to be more robust to errors 
    43        # does not seem to work with the class methods 43        # does not seem to work with the class methods 
    44        # cls.teardown_class(cls) 44        # cls.teardown_class(cls) 
    45        cls.text = shakespeare.dm.Material(name=cls.name, title=cls.title) 45        cls.text = shakespeare.dm.Material(name=cls.name, title=cls.title) 
    46        cls.builder.add_text(cls.name, StringIO.StringIO(cls.inText)) 46        cls.builder.add_text(cls.name, StringIO.StringIO(cls.inText)) 
    47        cls.concordance = shakespeare.concordance.Concordance([cls.name]) 47        cls.concordance = shakespeare.concordance.Concordance([cls.name]) 
    48        cls.statistics = shakespeare.concordance.Statistics([cls.name]) 48        cls.statistics = shakespeare.concordance.Statistics([cls.name]) 
    49 49 
    50    def teardown_class(cls): 50    def teardown_class(cls): 
    51        # allow us to deal with left over stuff from previous errors 51        # allow us to deal with left over stuff from previous errors 
    52        try: 52        try: 
    53            cls.builder.remove_text(cls.name) 53            cls.builder.remove_text(cls.name) 
    54            tmp = shakespeare.dm.Material.byName(cls.name) 54            tmp = shakespeare.dm.Material.byName(cls.name) 
    55            shakespeare.dm.Material.delete(tmp.id) 55            shakespeare.dm.Material.delete(tmp.id) 
    56        except: 56        except: 
    57            pass 57            pass 
    58 58 
    59    def test__process_line(self): 59    def test__process_line(self): 
    60        line = 'the - quick, brown. fox-jumped over$ the_lazy do8g.' 60        line = 'the - quick, brown. fox-jumped over$ the_lazy do8g.' 
    61        exp = ['the', 'quick', 'brown', 'fox', 'jumped', 'over', 'the_lazy', 'do8g' ] 61        exp = ['the', 'quick', 'brown', 'fox', 'jumped', 'over', 'the_lazy', 'do8g' ] 
    62        out = self.builder.word_regex.findall(line) 62        out = self.builder.word_regex.findall(line) 
    63        assert exp == out 63        assert exp == out 
    64 64 
      65    def test_is_roman_numeral(self): 
      66        testvals = [ 'ii', 'v', 'vi', 'xi', 'xx', 'xxi', 'xlvi', 'c', 'cvi' ] 
      67        for val in testvals: 
      68            assert self.builder.is_roman_numeral(val) 
      69 
      70    def test_ignore_word(self): 
      71        testvals = [ 'd', 't' ] 
      72        for val in testvals: 
      73            assert self.builder.ignore_word(val) 
      74 
    65    def test_concordance(self): 75    def test_concordance(self): 
    66        for key, value in self.expConcordance.items(): 76        for key, value in self.expConcordance.items(): 
    67            listing = list(self.concordance.get(key)) 77            listing = list(self.concordance.get(key)) 
    68            assert len(listing) == len(value) 78            assert len(listing) == len(value) 
    69            for xx in listing: 79            for xx in listing: 
    70                assert (xx.text.name, xx.line, xx.char_index) in value 80                assert (xx.text.name, xx.line, xx.char_index) in value 
    71 81 
    72    def test_stats(self): 82    def test_stats(self): 
    73        for key, value in self.expStats.items(): 83        for key, value in self.expStats.items(): 
    74            out = self.statistics.get(key) 84            out = self.statistics.get(key) 
    75            print key 85            print key 
    76            assert out == value 86            assert out == value 
    77 87 
    78    def test_keys(self): 88    def test_keys(self): 
    79        words = self.concordance.keys() 89        words = self.concordance.keys() 
    80        assert 'a' == words[0] 90        assert 'a' == words[0] 
    81        assert 'your' == words[-1] 91        assert 'your' == words[-1] 
    82        assert 22 == len(words) 92        assert 22 == len(words)