morfosegment.py 9.9 KB
# -*- coding: utf-8 -*-
import morfeusz2
import itertools
import srx_segmenter


def unique_everseen(iterable, key=None):
    "List unique elements, preserving order. Remember all elements ever seen."
    # unique_everseen('AAAABBBCCDAABBB') --> A B C D
    # unique_everseen('ABBCcAD', str.lower) --> A B C D
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in itertools.ifilterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element
                

class LogicError(Exception):
    pass

class Analyse():
    #dict_dir - katalog z słownikiem do Morfeusza
    #dict_name - nazwa słownika
    def __init__(self, dict_dir, dict_name, srx_rules, stats_dir=''):
        self.dict_dir = dict_dir
        self.dict_name = dict_name
        self.morfeusz = morfeusz2.Morfeusz(dict_name = self.dict_name, dict_path=self.dict_dir, generate=False, expand_underscore=True, expand_dot=True, expand_tags = True)
        self.srx_rules = srx_rules

    # Prosty podział tekstu na słowa dla fragmentów obcych naśladujący
    # format Morfeusza.
    # W efekcie każde słowo dostaje lemat = formie oraz interpretację "xxx".
    def fake_analyse(self, source_text):
        word_list = []
        segmenter = srx_segmenter.SrxSegmenter(self.srx_rules["ByWord"], source_text)
        words, whitespaces = segmenter.extract()
        for i, w in enumerate(words):
            word_list.append((i, i+1, (w, w, 'xxx', [], [])))
        return word_list
        
    #analiza tekstu Morfeuszem (nie ma znaczników xml wewnątrz)
    # lub prosty podział na słowa jeśli to był typ "foreign"
    def text_analyse(self, source_text, text_type):
        if text_type == 'foreign':
            morf_result = self.fake_analyse(source_text)
        else:
            morf_result = self.morfeusz.analyse(source_text)
        
        segs = {}
        by_start = {}

        for r in morf_result:
            #print(str(r))
            start_node = r[0]
            end_node = r[1]
            interp = r[2]
            word = interp[0]
            if (start_node, end_node) not in segs:
                seg = {"orth" : word, "interps" : [self.create_interp(interp)], "start" : start_node, "end" : end_node}
                segs[(start_node, end_node)] = seg
                if start_node not in by_start:
                    by_start[start_node] = []
                by_start[start_node].append(seg)
            else:
                segs[(start_node, end_node)]["interps"].append(self.create_interp(interp))

        eoffsets = {0:0}
        boffsets = {}
        maxpos = -1 
        for pos, sgs in sorted(by_start.items()):
            for s in sgs:
                b = source_text[eoffsets[s["start"]]:].find(s["orth"])+eoffsets[s["start"]]
                e = b + len(s["orth"])
                if s["start"] in boffsets and b != boffsets[s["start"]]:
                    print(repr(s))
                    print(s["orth"].encode('utf-8'))
                    print(source_text.encode('utf-8'))
                    raise LogicError("problem z pozycja segmentow w tekscie")
                if s["end"] in eoffsets and e != eoffsets[s["end"]]:
                    #print repr(s)
                    #print e
                    #print b
                    #print repr(boffsets)
                    #print repr(eoffsets)
                    raise LogicError("problem z pozycja segmentow w tekscie")
                boffsets[s["start"]] = b
                eoffsets[s["end"]] = e
                s["offset"] = boffsets[s["start"]]
                if eoffsets[s["start"]] == boffsets[s["start"]] and s["start"] > 0:
                    s["nps"] = True
            maxpos = pos

        ret = []
        pos = 0
        ###
        #for x, v in by_start.items():
            #print(x)
            #for y in v:
                #ay = dict(y)
                #del ay['interps']
                #print("  ", repr(ay))
        #print()
        ###
        while pos <= maxpos:
            c, pos = self.make_choice(by_start, pos, source_text)
            #print(pos, repr(c))
            #print()
            ret.append(c)
        
        #usuń dublety w interpretacjach - czasem pojawiają się w analizie Morfeusza
        self.uniq_interps(ret)
        
        #for seg in ret:
        #  if isinstance(seg, list): #trafiło na 'choice'
        #    for choice_elem in seg:
        #      for elem in choice_elem:
        #        for interp in elem['interps']:
        #          print('{}:{}:{}:{}'.format(elem['orth'].encode('utf-8'), interp['base'].encode('utf-8'), interp['ctag'].encode('utf-8'), interp['msd'].encode('utf-8')))
        #  else: # zwykłe interpretacje (nie w liście 'choice')
        #    for interp in seg['interps']:
        #      print('{}:{}:{}:{}'.format(seg['orth'].encode('utf-8'), interp['base'].encode('utf-8'), interp['ctag'].encode('utf-8'), interp['msd'].encode('utf-8')))

        return ret

    def uniq_interps(self, ret):
      for seg in ret:
        if isinstance(seg, list): #trafiło na 'choice'
          for choice_elem in seg:
            for elem in choice_elem:
              elem['interps'] = list(unique_everseen(elem['interps'], key=lambda e: '{}:{}:{}'.format(e['base'].encode('utf-8'), e['ctag'].encode('utf-8'), e['msd'].encode('utf-8'))))
        else: # zwykłe interpretacje (nie w liście 'choice')
          seg['interps'] = list(unique_everseen(seg['interps'], key=lambda e: '{}:{}:{}'.format(e['base'].encode('utf-8'), e['ctag'].encode('utf-8'), e['msd'].encode('utf-8'))))

    def make_choice(self, by_start, pos, source_text):
        try:
            by_start[pos]
        except:
            print('text: {}, pos: {}'.format(source_text.encode('utf-8'), pos))
            raise

        if len(by_start[pos]) == 1:
            ret = by_start[pos][0]
            retpos = ret['end']
        else:
            if len(by_start[pos]) == 0:
                raise LogicError("brakujacy segment?")
            choices = [{'s' : x["start"], 'e' : x["end"], "content" : [x]} for x in by_start[pos]]
            #print([(x['s'], x['e'], [z['orth'] for z in x['content']]) for x in choices])
            p = pos
            while max([x["e"] for x in choices]) != min([x["e"] for x in choices]):
                #musi być min(), bo inaczej nie uwzględnia gałęzi "wstecz" i zaczyna czytać poza ostatnim indeksem
                #p += 1
                p = min([x["e"] for x in choices])
                for c in choices:
                    if c["e"] > p:
                        continue
                    nc, pp = self.make_choice(by_start, p, source_text)
                    c["content"].append(nc)
                    c["e"] = pp

                #print([(x['s'], x['e'], [z['orth'] for z in x['content']]) for x in choices])

            ret = [c["content"] for c in choices]
            retpos = choices[0]["e"]
        
        return ret, retpos


    def create_interp(self, interp):
        ctag, rest_tag = self.split_tags(interp[2])
        return {'base':interp[1], 'ctag':ctag, 'msd':rest_tag}

      #rozbijanie tagu po ':'
    def split_tags(self, morph_tag):
        res = morph_tag.split(':', 1)
        if len(res) == 1:
            res.append('')
        return res



def map_orth_and_offset(segs, src_offset, charmap, src_text):
    for s in segs:
        if isinstance(s, dict):
            s["source_offset"] = charmap[s["offset"]][0] + src_offset
            s["source_orth"] = src_text[min(charmap[s["offset"]]) : max(charmap[len(s["orth"])+s["offset"]-1])+1]
        elif isinstance(s, list):
            for c in s:
                map_orth_and_offset(c, src_offset, charmap, src_text)
        else:
            raise LogicError("smiec zamiast segmentu?")

def set_nps(s):
    if isinstance(s, dict):
        s["nps"] = True
    elif isinstance(s, list):
        for c in s:
            set_nps(c[0])
    else:
        raise LogicError("smiec zamiast segmentu?")

def get_end(s):
    if isinstance(s, dict):
        return s["offset"] + len(s["orth"])
    elif isinstance(s, list):
        return max([get_end(c[-1]) for c in s])
    else:
        raise LogicError("smiec zamiast segmentu?")

def get_beg(s):
    if isinstance(s, dict):
        return s["offset"]
    elif isinstance(s, list):
        return min([get_beg(c[0]) for c in s])
    else:
        raise LogicError("smiec zamiast segmentu?")

#morfeuszowanie listy akapitow (akapit = lista zdan, zdanie = lista fragmentow)
def morfosegment_paragraphs(parasents, dict_path, dict_name, srx_rules, tager):
    a = Analyse(dict_path, dict_name, srx_rules)
    for p in parasents:
        for i, s in enumerate(p):
            if i == 0:
                last_whitespace = True
            for frag in s:
                #print repr(frag)
                if "trans" in frag:
                    frag["segs"] = a.text_analyse(frag["trans"], frag.get('type', ''))
                    map_orth_and_offset(frag["segs"], frag["xml_offset"]+frag["offset"], frag["charmap"], frag["text"])
                    #print repr(frag)
                    if len(frag["segs"]) > 0:
                        #print(frag, last_whitespace)
                        if not last_whitespace:
                            beg = get_beg(frag["segs"][0])
                            if beg == 0:
                                set_nps(frag["segs"][0])
                        end = get_end(frag["segs"][-1])
                        if len(frag["trans"]) > end or frag['end_space']:
                            last_whitespace = True
                        else:
                            last_whitespace = False
                # if "orig_trans" in frag:
                    # frag["orig_segs"] = a.text_analyse(frag["orig_trans"], frag.get('type', ''))
                    # map_orth_and_offset(frag["orig_segs"], frag["xml_offset"]+frag["offset"], frag["orig_charmap"], frag["orig_text"])