Як розділити текст без пробілів на список слів?


106

Введення: "tableapplechairtablecupboard..." багато слів

Що було б ефективним алгоритмом, щоб поділити такий текст до списку слів і отримати:

Вихід: ["table", "apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]

Перше, що вам спадає на думку, - це пройти всі можливі слова (починаючи з першої літери) і знайти найдовше можливе слово, продовжуйте з position=word_position+len(word)

PS
У нас є список усіх можливих слів.
Слово "шафа" може бути "чашка" і "дошка", вибирайте найдовше.
Мова: python, але головне - сам алгоритм.


14
Ви впевнені, що рядок не починається зі слів "вкладка" та "стрибок"?
Роб Грушка

Так, здається, це неможливо зробити однозначно.
demalexx

@RobHruska, в такому випадку я писав, вибравши якнайдовше.
Сергій

2
@Sergey - Ваш критерій "найдовшого можливого" мав на увазі, що це стосується складних слів. І в такому випадку, що трапилося б, якби струна була "килимовою". Це був би "килим" чи "буревісник"?
Роб Грушка

2
У вашому рядку є багато розділових слів:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

Відповіді:


200

Наївний алгоритм не дасть хороших результатів при застосуванні до реальних даних. Ось 20-рядковий алгоритм, який використовує відносну частоту слів, щоб дати точні результати для тексту в реальному слові.

(Якщо ви хочете відповісти на своє початкове запитання, в якому не використовується частота слів, вам потрібно уточнити, що саме означає "найдовше слово": чи краще мати 20-літерне слово та десять 3-літерних слів, чи це краще мати п’ять букв з десяти літер? Після того, як ви зупинитесь на точному визначенні, вам просто потрібно змінити рядок, що визначає, wordcostщоб відобразити передбачуваний зміст.)

Ідея

Найкращий спосіб продовжити - моделювати розподіл результатів. Хорошим першим наближенням є припущення, що всі слова розподілені незалежно. Тоді вам потрібно лише знати відносну частоту всіх слів. Доцільно припустити, що вони відповідають закону Зіпфа, тобто слово з рангом n у списку слів має ймовірність приблизно 1 / ( n log N ), де N - кількість слів у словнику.

Після того як ви зафіксували модель, ви можете використовувати динамічне програмування, щоб визначити положення пробілів. Найбільш вірогідне речення - це те, що максимально добуває ймовірність кожного окремого слова, і його легко обчислити за допомогою динамічного програмування. Замість того, щоб безпосередньо використовувати ймовірність, ми використовуємо витрати, визначені як логарифм зворотної ймовірності, щоб уникнути переповнення.

Код

from math import log

# Build a cost dictionary, assuming Zipf's law and cost = -math.log(probability).
words = open("words-by-frequency.txt").read().split()
wordcost = dict((k, log((i+1)*log(len(words)))) for i,k in enumerate(words))
maxword = max(len(x) for x in words)

def infer_spaces(s):
    """Uses dynamic programming to infer the location of spaces in a string
    without spaces."""

    # Find the best match for the i first characters, assuming cost has
    # been built for the i-1 first characters.
    # Returns a pair (match_cost, match_length).
    def best_match(i):
        candidates = enumerate(reversed(cost[max(0, i-maxword):i]))
        return min((c + wordcost.get(s[i-k-1:i], 9e999), k+1) for k,c in candidates)

    # Build the cost array.
    cost = [0]
    for i in range(1,len(s)+1):
        c,k = best_match(i)
        cost.append(c)

    # Backtrack to recover the minimal-cost string.
    out = []
    i = len(s)
    while i>0:
        c,k = best_match(i)
        assert c == cost[i]
        out.append(s[i-k:i])
        i -= k

    return " ".join(reversed(out))

якими ви можете користуватися

s = 'thumbgreenappleactiveassignmentweeklymetaphor'
print(infer_spaces(s))

Результати

Я використовую цей швидкий і брудний словник в 125 тисяч слів, який я зібрав з невеликої підмножини Вікіпедії.

Перед: велике пальцева активність присвоєнняметометафора.
Після: великий палець зеленого яблука активне призначення щотижневої метафори.

Перш: тудиспроможнешлякштекнформація людей, що коментують, щовирішенезмінено, апростуватимемохарактеристикипредприміткомпредставленнямпунктивуєпризначенняммехафото, швидше, зрештою, змайструватимете папкузреагувати шрифт на голову.

Після: є маса текстової інформації коментарів людей, яка розбирається з html, але в них немає обмежених символів, наприклад, великий палець зеленого яблука активне призначення щотижневої метафори, мабуть, є великий палець зелене яблуко тощо в рядку, у мене також є великий словник для запитайте, чи це слово розумне, і який найшвидший спосіб видобування thx багато.

Попередньо : це булопарковою вечірньою вечіршньою фальшивою ділянкою зафіксованих випадкових інтервалів, які перевірялися під час віолементарного переходу вітрів, що передбачає найголовніші рейтинги, проголошення, що увімкняться, умиротворені довгі будинки, ідифтер, що рухаються, уважаючи, що це означає, що у них буде вигляд

Після: була темна і бурхлива ніч, дощ випав у потоках, за винятком періодичних проміжок часу, коли його перевіряли бурхливі пориви вітру, які прокотилися вулицями, адже саме в Лондоні наша сцена лежить грохотом по хатах і люто хвилюється мізерне полум’я ламп, що боролися проти темряви.

Як бачите, це по суті бездоганно. Найважливіша частина - переконатися, що ваш список слів був привчений до корпусу, схожого на те, що ви насправді зіткнетеся, інакше результати будуть дуже поганими.


Оптимізація

Реалізація вимагає лінійної кількості часу та пам'яті, тому вона є досить ефективною. Якщо вам потрібні подальші прискорення, ви можете створити суфіксне дерево зі списку слів, щоб зменшити розмір набору кандидатів.

Якщо вам потрібно обробити дуже великий послідовний рядок, було б розумно розділити рядок, щоб уникнути зайвого використання пам'яті. Наприклад, ви можете обробити текст блоками по 10000 символів плюс запас 1000 символів з обох сторін, щоб уникнути граничних ефектів. Це дозволить мінімізувати використання пам’яті і майже не матиме жодного впливу на якість.


1
як щодо тексту двох рядків?
Ліфія

11
Цей код змусив мене оніміти. Я трохи не зрозумів. Я не розумію речей журналу. Але я перевірив цей код на своєму комп’ютері. Ви геній.
Aditya Singh

1
Який час виконання цього алгоритму? Чому б вам не скористатися ахокоразиком?
RetroCode

8
Це чудово. Я перетворив його на пакет з піп: pypi.python.org/pypi/wordninja pip install wordninja
keredson

2
@wittrup words.txtмістить ваш "comp": `` `$ grep" ^ comp $ "words.txt comp` ``, і це сортується за алфавітом. цей код передбачає, що він сортується за зменшенням частоти появи (що є загальним для таких списків n-грамів). якщо ви використовуєте правильно відсортований список, ваш рядок виходить нормальним: `` `>>> wordninja.split ('namethecompanywherebonniewaseasewwwstartarteddating') ['name', 'the', 'company', 'where', 'bonnie', ' було ',' зайнято ',' коли ',' ми ',' почалося ',' побачення '] ``
Кередсон

50

На основі чудової роботи у верхній відповіді я створив pipпакет для простого використання.

>>> import wordninja
>>> wordninja.split('derekanderson')
['derek', 'anderson']

Щоб встановити, запустіть pip install wordninja.

Єдині відмінності незначні. Це повертається, listа не a str, він працює python3, він включає список слів і належним чином розбивається, навіть якщо є неафазні символи (наприклад, підкреслення, тире тощо).

Ще раз дякую Generic Human!

https://github.com/keredson/wordninja


2
Дякуємо, що створили це.
Мохіт Бхатія

1
Дякую! Мені подобається, що ти зробив це пакетом. Основний метод не дуже добре працював для мене. Наприклад, "лежаки" були розбиті на "лаунж" і "rs"
Гаррі М

@keredson - Перш за все, дякую за рішення. Це поводиться добре. Однак він знімає спеціальні символи, такі як "-" і т. Д. Це іноді не дає належного розколу, як, наприклад, довгий рядок сказати - "WeatheringPropertiesbyMaterial Trade Graph Graph 2-1. Зміна кольору, E, після Арізони, Флорида, Cycolac® / Системи смол Geloy® порівняно з ПВХ. [15] 25 20 15 ∆E 10 5 0 ПВХ, білий ПВХ, коричневий C / G, коричневий C / G. Капсток - це матеріал, який використовується як поверхневий шар, нанесений на зовнішню поверхню профілю екструзія. Накладка смоли Geloy® над підкладкою Cycolac® забезпечує надзвичайну вологозахищеність. [25] "
ламп

ви можете відкрити випуск у GH?
Кередсон

1
Приємна робота, дякую за старання. Це дійсно врятувало мені багато часу.
Ян Цайсвейс

17

Ось рішення за допомогою рекурсивного пошуку:

def find_words(instring, prefix = '', words = None):
    if not instring:
        return []
    if words is None:
        words = set()
        with open('/usr/share/dict/words') as f:
            for line in f:
                words.add(line.strip())
    if (not prefix) and (instring in words):
        return [instring]
    prefix, suffix = prefix + instring[0], instring[1:]
    solutions = []
    # Case 1: prefix in solution
    if prefix in words:
        try:
            solutions.append([prefix] + find_words(suffix, '', words))
        except ValueError:
            pass
    # Case 2: prefix not in solution
    try:
        solutions.append(find_words(suffix, prefix, words))
    except ValueError:
        pass
    if solutions:
        return sorted(solutions,
                      key = lambda solution: [len(word) for word in solution],
                      reverse = True)[0]
    else:
        raise ValueError('no solution')

print(find_words('tableapplechairtablecupboard'))
print(find_words('tableprechaun', words = set(['tab', 'table', 'leprechaun'])))

врожайність

['table', 'apple', 'chair', 'table', 'cupboard']
['tab', 'leprechaun']

працює "поза коробкою", дякую! Я думаю також використовувати структуру трійки, як сказав Міку, а не просто набір усіх слів. Все одно, дякую!
Сергій

11

Використовуючи структуру даних трійки , яка містить список можливих слів, було б не надто складно зробити наступне:

  1. Попередній покажчик (у з'єднаному рядку)
  2. Знайдіть і зберігайте відповідний вузол у трійці
  3. Якщо у вузлі трійки є діти (наприклад, є довші слова), перейдіть до 1.
  4. Якщо у досягнутому вузлі немає дітей, відбулося найдовше збіг слів; додайте слово (збережене у вузлі або просто з'єднане під час проходження трійки) до списку результатів, скиньте вказівник у трійці (або скиньте посилання) та починайте спочатку

3
Якщо ціль полягає у споживанні всього рядка, вам потрібно буде відступити, "tableprechaun"після чого його слід розділити "tab".
Даніель Фішер

Плюс до згадки про трійку, але я також погоджуюся з Даніелем, що потрібно робити зворотний трек.
Сергій

@Daniel, пошук із найдовшим збігом не потребує зворотного відстеження, ні. Що змушує вас це думати? А що з алгоритмом вище?
Девін Жанп'єр

1
@Devin Справа в тому, що "tableprechaun"найдовший збіг з самого початку - це "table"відхід "prechaun", який неможливо розділити на словникові слова. Таким чином, ви повинні взяти коротший матч, "tab"залишивши вас з a "leprechaun".
Даніель Фішер

@Daniel, Вибач, так. Я неправильно зрозумів проблему. Відкоригований алгоритм повинен відслідковувати всі можливі позиції дерева одночасно - AKA лінійного часу NFA-пошуку. Або, звичайно, зворотний трек, але це найгірший експоненційний час.
Девін Жанп'єр

9

Рішення Унутбу було досить близьким, але мені здається, що код важко читати, і він не дав очікуваного результату. Рішення Generic Human має недолік, що йому потрібні частоти слова. Не підходить для всіх випадків використання.

Ось просте рішення за допомогою алгоритму «Розділити та перемогти» .

  1. Це намагається зробити звести до мінімуму кількість слів Eg find_words('cupboard')буде повертати , ['cupboard']а не ['cup', 'board'](за умови , що cupboard, cupі boardзнаходяться в dictionnary)
  2. Оптимальне рішення - це НЕ є унікальним , реалізація нижче доходностей рішення. може повернутися або, можливо, повернеться (як показано нижче). Ви могли досить легко змінити алгоритм, щоб повернути всі оптимальні рішення.find_words('charactersin')['characters', 'in']['character', 'sin']
  3. У цій реалізації рішення запам'ятовуються таким чином, що вони працюють у розумний час.

Код:

words = set()
with open('/usr/share/dict/words') as f:
    for line in f:
        words.add(line.strip())

solutions = {}
def find_words(instring):
    # First check if instring is in the dictionnary
    if instring in words:
        return [instring]
    # No... But maybe it's a result we already computed
    if instring in solutions:
        return solutions[instring]
    # Nope. Try to split the string at all position to recursively search for results
    best_solution = None
    for i in range(1, len(instring) - 1):
        part1 = find_words(instring[:i])
        part2 = find_words(instring[i:])
        # Both parts MUST have a solution
        if part1 is None or part2 is None:
            continue
        solution = part1 + part2
        # Is the solution found "better" than the previous one?
        if best_solution is None or len(solution) < len(best_solution):
            best_solution = solution
    # Remember (memoize) this solution to avoid having to recompute it
    solutions[instring] = best_solution
    return best_solution

На моїй машині 3 ГГц це займе близько 5 секунд:

result = find_words("thereismassesoftextinformationofpeoplescommentswhichisparsedfromhtmlbuttherearenodelimitedcharactersinthemforexamplethumbgreenappleactiveassignmentweeklymetaphorapparentlytherearethumbgreenappleetcinthestringialsohavealargedictionarytoquerywhetherthewordisreasonablesowhatsthefastestwayofextractionthxalot")
assert(result is not None)
print ' '.join(result)

рейси маси текстової інформації коментарів людей, які розбираються з html, але немає обмежених символів грішити їх, наприклад, великий палець зелене яблуко активне призначення щотижнева метафора, мабуть, є великий палець зелене яблуко тощо в рядку, у мене також є великий словник для запитання, чи це слово розумне, так що найшвидший спосіб видобутку thxa партії


Немає жодних підстав вважати, що текст не може закінчуватися однолітерним словом. Вам слід розглянути ще один розкол.
панда-34,

7

Відповідь https://stackoverflow.com/users/1515832/generic-human чудова. Але найкраща реалізація цього, що я коли-небудь бачив, був написаний самим Пітер Норвігом у своїй книзі «Красиві дані».

Перш ніж я вставити його код, дозвольте мені розкрити, чому метод Норвіга є більш точним (хоча трохи повільніше і довше з точки зору коду).

1) Дані трохи кращі - і за розміром, і за точністю (він використовує кількість слів, а не простий рейтинг) 2) Більш важливо, саме логіка, що стоїть за n-грамами, дійсно робить підхід таким точним .

Приклад, який він наводить у своїй книзі, - це проблема розділення рядка 'присідання'. Тепер небіграмовий метод розбиття рядків вважатиме p ('sit') * p ('down'), і якщо це менше p ('sitdown') - що траплятиметься досить часто - він НЕ розщеплюється це, але ми хотіли б, щоб це (більшість часу).

Однак, якщо у вас є модель біграму, ви можете оцінити p («сісти») як біграму проти p («присідання») і колишній виграш. Якщо ви не використовуєте біграми, то ймовірність слів, які ви розділяєте, вважає незалежними, що не так, деякі слова, швидше за все, з’являються одне за одним. На жаль, це також слова, які часто скріплюються у багатьох випадках і плутають спліттер.

Ось посилання на дані (це дані для 3 окремих проблем, а сегментація - лише одна. Будь ласка, прочитайте розділ для детальної інформації): http://norvig.com/ngrams/

і ось посилання на код: http://norvig.com/ngrams/ngrams.py

Ці посилання працювали деякий час, але я все-таки скопіюю вставити сегментаційну частину коду

import re, string, random, glob, operator, heapq
from collections import defaultdict
from math import log10

def memo(f):
    "Memoize function f."
    table = {}
    def fmemo(*args):
        if args not in table:
            table[args] = f(*args)
        return table[args]
    fmemo.memo = table
    return fmemo

def test(verbose=None):
    """Run some tests, taken from the chapter.
    Since the hillclimbing algorithm is randomized, some tests may fail."""
    import doctest
    print 'Running tests...'
    doctest.testfile('ngrams-test.txt', verbose=verbose)

################ Word Segmentation (p. 223)

@memo
def segment(text):
    "Return a list of words that is the best segmentation of text."
    if not text: return []
    candidates = ([first]+segment(rem) for first,rem in splits(text))
    return max(candidates, key=Pwords)

def splits(text, L=20):
    "Return a list of all possible (first, rem) pairs, len(first)<=L."
    return [(text[:i+1], text[i+1:]) 
            for i in range(min(len(text), L))]

def Pwords(words): 
    "The Naive Bayes probability of a sequence of words."
    return product(Pw(w) for w in words)

#### Support functions (p. 224)

def product(nums):
    "Return the product of a sequence of numbers."
    return reduce(operator.mul, nums, 1)

class Pdist(dict):
    "A probability distribution estimated from counts in datafile."
    def __init__(self, data=[], N=None, missingfn=None):
        for key,count in data:
            self[key] = self.get(key, 0) + int(count)
        self.N = float(N or sum(self.itervalues()))
        self.missingfn = missingfn or (lambda k, N: 1./N)
    def __call__(self, key): 
        if key in self: return self[key]/self.N  
        else: return self.missingfn(key, self.N)

def datafile(name, sep='\t'):
    "Read key,value pairs from file."
    for line in file(name):
        yield line.split(sep)

def avoid_long_words(key, N):
    "Estimate the probability of an unknown word."
    return 10./(N * 10**len(key))

N = 1024908267229 ## Number of tokens

Pw  = Pdist(datafile('count_1w.txt'), N, avoid_long_words)

#### segment2: second version, with bigram counts, (p. 226-227)

def cPw(word, prev):
    "Conditional probability of word, given previous word."
    try:
        return P2w[prev + ' ' + word]/float(Pw[prev])
    except KeyError:
        return Pw(word)

P2w = Pdist(datafile('count_2w.txt'), N)

@memo 
def segment2(text, prev='<S>'): 
    "Return (log P(words), words), where words is the best segmentation." 
    if not text: return 0.0, [] 
    candidates = [combine(log10(cPw(first, prev)), first, segment2(rem, first)) 
                  for first,rem in splits(text)] 
    return max(candidates) 

def combine(Pfirst, first, (Prem, rem)): 
    "Combine first and rem results into one (probability, words) pair." 
    return Pfirst+Prem, [first]+rem 

Це добре працює, але коли я намагаюся застосувати це до всього мого набору даних, він постійно говоритьRuntimeError: maximum recursion depth exceeded in cmp
Гаррі М

nграм, безумовно, дасть вам можливість підвищення точності з експоненціально більшим частотним малюнком, пам'яттю та використанням обчислень. btw функція нагадування просочується пам'яттю, як сито. слід очистити це між дзвінками.
Кередсон

3

Ось прийнята відповідь, переведена на JavaScript (потрібен node.js, і файл "wordninja_words.txt" з https://github.com/keredson/wordninja ):

var fs = require("fs");

var splitRegex = new RegExp("[^a-zA-Z0-9']+", "g");
var maxWordLen = 0;
var wordCost = {};

fs.readFile("./wordninja_words.txt", 'utf8', function(err, data) {
    if (err) {
        throw err;
    }
    var words = data.split('\n');
    words.forEach(function(word, index) {
        wordCost[word] = Math.log((index + 1) * Math.log(words.length));
    })
    words.forEach(function(word) {
        if (word.length > maxWordLen)
            maxWordLen = word.length;
    });
    console.log(maxWordLen)
    splitRegex = new RegExp("[^a-zA-Z0-9']+", "g");
    console.log(split(process.argv[2]));
});


function split(s) {
    var list = [];
    s.split(splitRegex).forEach(function(sub) {
        _split(sub).forEach(function(word) {
            list.push(word);
        })
    })
    return list;
}
module.exports = split;


function _split(s) {
    var cost = [0];

    function best_match(i) {
        var candidates = cost.slice(Math.max(0, i - maxWordLen), i).reverse();
        var minPair = [Number.MAX_SAFE_INTEGER, 0];
        candidates.forEach(function(c, k) {
            if (wordCost[s.substring(i - k - 1, i).toLowerCase()]) {
                var ccost = c + wordCost[s.substring(i - k - 1, i).toLowerCase()];
            } else {
                var ccost = Number.MAX_SAFE_INTEGER;
            }
            if (ccost < minPair[0]) {
                minPair = [ccost, k + 1];
            }
        })
        return minPair;
    }

    for (var i = 1; i < s.length + 1; i++) {
        cost.push(best_match(i)[0]);
    }

    var out = [];
    i = s.length;
    while (i > 0) {
        var c = best_match(i)[0];
        var k = best_match(i)[1];
        if (c == cost[i])
            console.log("Alert: " + c);

        var newToken = true;
        if (s.slice(i - k, i) != "'") {
            if (out.length > 0) {
                if (out[-1] == "'s" || (Number.isInteger(s[i - 1]) && Number.isInteger(out[-1][0]))) {
                    out[-1] = s.slice(i - k, i) + out[-1];
                    newToken = false;
                }
            }
        }

        if (newToken) {
            out.push(s.slice(i - k, i))
        }

        i -= k

    }
    return out.reverse();
}

2

Якщо ви попередньо компілюєте список слів у DFA (що буде дуже повільним), то час, необхідний для відповідності вводу, буде пропорційним довжині рядка (насправді лише трохи повільніше, ніж просто ітерація над рядком).

Це фактично більш загальна версія алгоритму трійки, про яку згадувалося раніше. Я згадую його лише для бездоганності - поки що немає жодної реалізації DFA, яку ви можете просто використовувати. RE2 спрацював би, але я не знаю, чи вкладення Python дозволяють вам настроїти, наскільки великим ви дозволяєте DFA, перш ніж він просто викине зібрані дані DFA і не буде шукати NFA.


особливо плюс для re2, не користувався ним раніше
Сергій

0

Здається, що досить приземлене зворотне опромінення зробить це. Почніть з початку рядка. Скануйте право, доки не з’явиться слово. Потім викличте функцію на іншій частині рядка. Функція повертається "помилково", якщо вона сканує повністю вправо, не розпізнаючи слово. В іншому випадку повертає знайдене слово та список слів, повернутих за допомогою рекурсивного дзвінка.

Приклад: "столовий". Знаходить "вкладку", потім "стрибок", але жодного слова в "пле". Жодного іншого слова в «стрибку». Знаходить «таблицю», потім «додаток». "ле" не слово, тому намагається яблуко, розпізнає, повертає.

Щоб отримати якомога довше, продовжуйте, лише випромінюючи (а не повертаючи) правильні рішення; потім виберіть оптимальний за будь-яким обраним вами критерієм (maxmax, minmax, середній тощо)


Хороший алгоритм, думав над цим. unutbu навіть написав код.
Сергій

@Sergey, зворотний пошук - алгоритм експоненціального часу. Що в цьому "добре"?
Девін Жанп'єр

1
Це просто, не сказати, що це швидко
Сергій

0

На основі рішення unutbu я реалізував версію Java:

private static List<String> splitWordWithoutSpaces(String instring, String suffix) {
    if(isAWord(instring)) {
        if(suffix.length() > 0) {
            List<String> rest = splitWordWithoutSpaces(suffix, "");
            if(rest.size() > 0) {
                List<String> solutions = new LinkedList<>();
                solutions.add(instring);
                solutions.addAll(rest);
                return solutions;
            }
        } else {
            List<String> solutions = new LinkedList<>();
            solutions.add(instring);
            return solutions;
        }

    }
    if(instring.length() > 1) {
        String newString = instring.substring(0, instring.length()-1);
        suffix = instring.charAt(instring.length()-1) + suffix;
        List<String> rest = splitWordWithoutSpaces(newString, suffix);
        return rest;
    }
    return Collections.EMPTY_LIST;
}

Вхід: "tableapplechairtablecupboard"

Вихід: [table, apple, chair, table, cupboard]

Вхід: "tableprechaun"

Вихід: [tab, leprechaun]



0

Розширення на пропозицію @ miku використовувати a Trie, лише додаток Trieє відносно прямим для реалізації у python:

class Node:
    def __init__(self, is_word=False):
        self.children = {}
        self.is_word = is_word

class TrieDictionary:
    def __init__(self, words=tuple()):
        self.root = Node()
        for word in words:
            self.add(word)

    def add(self, word):
        node = self.root
        for c in word:
            node = node.children.setdefault(c, Node())
        node.is_word = True

    def lookup(self, word, from_node=None):
        node = self.root if from_node is None else from_node
        for c in word:
            try:
                node = node.children[c]
            except KeyError:
                return None

        return node

Тоді ми можемо побудувати Trieсловник на основі набору слів:

dictionary = {"a", "pea", "nut", "peanut", "but", "butt", "butte", "butter"}
trie_dictionary = TrieDictionary(words=dictionary)

Яке дерево створить таке схоже ( *вказує на початок чи кінець слова):

* -> a*
 \\\ 
  \\\-> p -> e -> a*
   \\              \-> n -> u -> t*
    \\
     \\-> b -> u -> t*
      \\             \-> t*
       \\                 \-> e*
        \\                     \-> r*
         \
          \-> n -> u -> t*

Ми можемо включити це до рішення, поєднавши його з евристикою про те, як вибрати слова. Наприклад, ми можемо віддавати перевагу більш довгим словам перед короткими словами:

def using_trie_longest_word_heuristic(s):
    node = None
    possible_indexes = []

    # O(1) short-circuit if whole string is a word, doesn't go against longest-word wins
    if s in dictionary:
        return [ s ]

    for i in range(len(s)):
        # traverse the trie, char-wise to determine intermediate words
        node = trie_dictionary.lookup(s[i], from_node=node)

        # no more words start this way
        if node is None:
            # iterate words we have encountered from biggest to smallest
            for possible in possible_indexes[::-1]:
                # recurse to attempt to solve the remaining sub-string
                end_of_phrase = using_trie_longest_word_heuristic(s[possible+1:])

                # if we have a solution, return this word + our solution
                if end_of_phrase:
                    return [ s[:possible+1] ] + end_of_phrase

            # unsolvable
            break

        # if this is a leaf, append the index to the possible words list
        elif node.is_word:
            possible_indexes.append(i)

    # empty string OR unsolvable case 
    return []

Ми можемо використовувати цю функцію так:

>>> using_trie_longest_word_heuristic("peanutbutter")
[ "peanut", "butter" ]

Тому що ми зберігаємо нашу позицію в , Trieяк ми шукати довше і більш довгі слова, обхід trieне більше одного разу за можливим рішення (а не 2раз для peanut: pea, peanut). Остаточне коротке замикання врятує нас від ходіння по-різному через струну в гіршому випадку.

Кінцевий результат - лише кілька інспекцій:

'peanutbutter' - not a word, go charwise
'p' - in trie, use this node
'e' - in trie, use this node
'a' - in trie and edge, store potential word and use this node
'n' - in trie, use this node
'u' - in trie, use this node
't' - in trie and edge, store potential word and use this node
'b' - not in trie from `peanut` vector
'butter' - remainder of longest is a word

Перевага цього рішення полягає в тому, що ви дуже швидко знаєте, чи існують довші слова із заданим префіксом, що позбавляє від необхідності вичерпного тестування комбінацій послідовностей проти словника. Це також робить доступ доunsolvable відповідь порівняно дешевою для інших реалізацій.

Мінуси цього рішення - це великий слід пам’яті trieта вартість будівництва trieпередової.


0

Якщо у вас є вичерпний список слів, що містяться в рядку:

word_list = ["table", "apple", "chair", "cupboard"]

Використання розуміння списку для перегляду списку, щоб знайти слово та скільки разів воно з’являється.

string = "tableapplechairtablecupboard"

def split_string(string, word_list):

    return ("".join([(item + " ")*string.count(item.lower()) for item in word_list if item.lower() in string])).strip()

Функція повертає stringвисновок слів у порядку спискуtable table apple chair cupboard


0

Дякую за допомогу в https://github.com/keredson/wordninja/

Невеликий внесок того ж в Java з мого боку.

Загальнодоступний метод splitContiguousWordsможе бути вбудований з іншими двома методами у класі, що має ninja_words.txt у тому самому каталозі (або модифікований відповідно до вибору кодера). І метод splitContiguousWordsміг би бути використаний з метою.

public List<String> splitContiguousWords(String sentence) {

    String splitRegex = "[^a-zA-Z0-9']+";
    Map<String, Number> wordCost = new HashMap<>();
    List<String> dictionaryWords = IOUtils.linesFromFile("ninja_words.txt", StandardCharsets.UTF_8.name());
    double naturalLogDictionaryWordsCount = Math.log(dictionaryWords.size());
    long wordIdx = 0;
    for (String word : dictionaryWords) {
        wordCost.put(word, Math.log(++wordIdx * naturalLogDictionaryWordsCount));
    }
    int maxWordLength = Collections.max(dictionaryWords, Comparator.comparing(String::length)).length();
    List<String> splitWords = new ArrayList<>();
    for (String partSentence : sentence.split(splitRegex)) {
        splitWords.add(split(partSentence, wordCost, maxWordLength));
    }
    log.info("Split word for the sentence: {}", splitWords);
    return splitWords;
}

private String split(String partSentence, Map<String, Number> wordCost, int maxWordLength) {
    List<Pair<Number, Number>> cost = new ArrayList<>();
    cost.add(new Pair<>(Integer.valueOf(0), Integer.valueOf(0)));
    for (int index = 1; index < partSentence.length() + 1; index++) {
        cost.add(bestMatch(partSentence, cost, index, wordCost, maxWordLength));
    }
    int idx = partSentence.length();
    List<String> output = new ArrayList<>();
    while (idx > 0) {
        Pair<Number, Number> candidate = bestMatch(partSentence, cost, idx, wordCost, maxWordLength);
        Number candidateCost = candidate.getKey();
        Number candidateIndexValue = candidate.getValue();
        if (candidateCost.doubleValue() != cost.get(idx).getKey().doubleValue()) {
            throw new RuntimeException("Candidate cost unmatched; This should not be the case!");
        }
        boolean newToken = true;
        String token = partSentence.substring(idx - candidateIndexValue.intValue(), idx);
        if (token != "\'" && output.size() > 0) {
            String lastWord = output.get(output.size() - 1);
            if (lastWord.equalsIgnoreCase("\'s") ||
                    (Character.isDigit(partSentence.charAt(idx - 1)) && Character.isDigit(lastWord.charAt(0)))) {
                output.set(output.size() - 1, token + lastWord);
                newToken = false;
            }
        }
        if (newToken) {
            output.add(token);
        }
        idx -= candidateIndexValue.intValue();
    }
    return String.join(" ", Lists.reverse(output));
}


private Pair<Number, Number> bestMatch(String partSentence, List<Pair<Number, Number>> cost, int index,
                      Map<String, Number> wordCost, int maxWordLength) {
    List<Pair<Number, Number>> candidates = Lists.reverse(cost.subList(Math.max(0, index - maxWordLength), index));
    int enumerateIdx = 0;
    Pair<Number, Number> minPair = new Pair<>(Integer.MAX_VALUE, Integer.valueOf(enumerateIdx));
    for (Pair<Number, Number> pair : candidates) {
        ++enumerateIdx;
        String subsequence = partSentence.substring(index - enumerateIdx, index).toLowerCase();
        Number minCost = Integer.MAX_VALUE;
        if (wordCost.containsKey(subsequence)) {
            minCost = pair.getKey().doubleValue() + wordCost.get(subsequence).doubleValue();
        }
        if (minCost.doubleValue() < minPair.getKey().doubleValue()) {
            minPair = new Pair<>(minCost.doubleValue(), enumerateIdx);
        }
    }
    return minPair;
}

що робити, якщо у нас немає списку слів?
shirazy

Якщо я правильно зрозумів запит: Отже, у наведеному вище підході publicметод приймає речення типу, Stringяке розділяється на основі першого рівня з регулярним виразом. А його список ninja_wordsдоступний для завантаження з git repo.
Арнаб Дас


-1

Вам потрібно визначити свій словниковий запас - можливо, будь-який список вільних слів зробить.

Закінчивши, використовуйте цей словник для створення дерева суфіксів і порівняйте потік вводу з цим: http://en.wikipedia.org/wiki/Suffix_tree


Як би це працювало на практиці? Як побудували суфіксне дерево, як би ви знали, що відповідати?
Джон Курлак

@JohnKurlak Як і будь-який інший детермінований кінцевий автомат - кінець повного слова є приймальним станом.
Marcin

Чи не такий підхід вимагає зворотного відстеження? Ви не згадали про зворотний трек у своїй відповіді ...
Джон Курлак

Чому ні? Що станеться, якщо у вас є «столовий прилад», про який було сказано нижче? Він буде відповідати найдовшому слову, яке він може "таблиця", і тоді він не знайде іншого слова. Доведеться відступити назад на "вкладку", а потім відповідати "лепрекон".
Джон Курлак

@JohnKurlak Кілька "гілок" можна жити одночасно. Насправді ви натискаєте маркер у дерево для кожної літери, яка є можливим початком слова, і ця сама буква може просувати інші живі лексеми.
Marcin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.