Javascript-форум (https://javascript.ru/forum/)
-   Оффтопик (https://javascript.ru/forum/offtopic/)
-   -   Похожесть двух строк (https://javascript.ru/forum/offtopic/76115-pokhozhest-dvukh-strok.html)

Nexus 06.12.2018 14:27

Похожесть двух строк
 
Здравствуйте.

Есть паттерны:
1. Есть ли в наличии ([\s\S]+)?
2. Есть ли ([\s\S]+) в наличии?

Есть строка:
Если ли в наличии лопата?

В строке допущена опечатка, вместо "есть" написано "если".
Нужно как-то определить, что полученная строка удовлетворяет шаблону 1 с небольшими корректировками.
Хотел воспользоваться расстоянием Левенштейна, однако не выходит, т.к. строка сравнивается с паттерном.


Можно как-нибудь решить эту проблему без машинного обучения?

ksa 07.12.2018 08:42

Цитата:

Сообщение от Nexus
Нужно как-то определить, что полученная строка удовлетворяет шаблону 1 с небольшими корректировками.

Как вариант...

var re=/Ес\S\S ли в наличии ([\s\S]+)\?/;
var str='Есть ли в наличии булки?';
alert(str+' - '+re.test(str));
str='Если ли в наличии цветы?';
alert(str+' - '+re.test(str));

Nexus 07.12.2018 09:33

ksa, чтобы применить это на практике, нужно точно знать какие символы пользователь "перепутал". В противном случае от статичного текста придется полностью отказаться в угоду выражений, что позволит ввести любое предложение, главное, чтобы введенные символы стояли на своих местах.
Нужно будет проверить кол-во ложных срабатываний этого подхода.

ksa 07.12.2018 09:55

Цитата:

Сообщение от Nexus
чтобы применить это на практике, нужно точно знать какие символы пользователь "перепутал"

Ты сам ответил на свой вопрос, Карло... (с)

Я лишь ответил на твой... :)

Nexus 04.02.2019 13:26

condator, случай реальный.

ДЖАВАСКРИПТИЗЕР 13.03.2019 22:13

Трэд ниасилил, только название, но посоветую

https://ru.wikipedia.org/wiki/Расстояние_Левенштейна

https://www.npmjs.com/package/fast-levenshtein

Nexus 13.03.2019 22:24

ДЖАВАСКРИПТИЗЕР, пост ради поста?
Цитата:

Сообщение от Nexus
Хотел воспользоваться расстоянием Левенштейна, однако не выходит, т.к. строка сравнивается с паттерном.


ДЖАВАСКРИПТИЗЕР 13.03.2019 23:36

нет, просто я немного ленив

Почему не выходит леванштейн. Вот у тебя есть запрос: "есть ли в наличии глюкало?"

1) Матчем выдираешь слово глюкало
2) Бежишь по списку товаров (ты ведь знаешь названия товаров которые у тебя есть) и считаешь леванштейны
3) дальше все и так понятно

В машинное обучение можно смотреть если уже есть опыт иначе это грозит потерей уймы времени с нулевым результатом в итоге

Nexus 14.03.2019 09:27

Цитата:

Сообщение от ДЖАВАСКРИПТИЗЕР
Бежишь по списку товаров (ты ведь знаешь названия товаров которые у тебя есть) и считаешь леванштейны

Цитата:

Сообщение от ДЖАВАСКРИПТИЗЕР
нет, просто я немного ленив

Перечитайте текст в шапке, пожалуйста.

Цитата:

Сообщение от ДЖАВАСКРИПТИЗЕР
В машинное обучение можно смотреть если уже есть опыт иначе это грозит потерей уймы времени

Ну так я и не хотел туда лезть, ибо не обладаю достаточной компетенцией.

ДЖАВАСКРИПТИЗЕР 14.03.2019 22:42

Можно попробовать логировать запросы. Через некоторе время соберется статистика по наиболее частым и типичным опечаткам. Дальше можно составить набор правил по которым попытаться исправить ошибки. И матчить уже исправленную строку.

Например у нас есть куча таких запросов:
Как снять двушку?

1) Для начала нормализуем строку удалив все двойные пробелы
2) Слово "двушку" смело меняем на "девушку" ибо ни кто в здравом уме двушки в 11 вечера не снимает
3) Пытаемся подобрать паттерн

Пример шуточный. Наиболее частые ошибки можно пытаться исправлять, на остальные забить метиз с метрической резьбой.

Malleys 16.03.2019 00:06

ДЖАВАСКРИПТИЗЕР,
Цитата:

Сообщение от ДЖАВАСКРИПТИЗЕР
Как снять двушку?
...
Слово "двушку" смело меняем на "девушку" ибо никто в здравом уме двушки в 11 вечера не снимает...

Раз так, то... ответ с 0:25
<iframe width="560" height="315" src="https://www.youtube.com/embed/tWEBbYoDaU8?start=25" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="margin: auto;"></iframe>
На все ваши возражения смотрите 1:03 там же!

Nexus, Пусть имеются строки a и b, задача заключается в том, чтобы найти такую функцию F(x), чтобы для похожих в некотором смысле строк выполнялось F(a) === F(b)

Первым приближением F(x) может быть перевод в нижний регистр. Действительно, F("Есть ли") === F("есть ли")

Вторым приближением может быть добавленная транслитерация, F("Есть ли") === F("еst li") или F("Ест ли") === F("есть ли")

Третим приближением может быть добавленный алгоритм сравнения двух строк по их звучанию (например, SOUNDEX (произносится как /ˈsaʊndɛks/ или /ˈsaʊɾ̃ɛks/), но для русского языка лучше подходит «Полифон»[1]), F("Ест ли") === F("есть ли") или F("Если ли") === F("есть ли")

Конечно, простое регулярное выражение не подойдёт для определения, подходит ли строка под РегВыр, для этого его нужно трансформировать по определённым правилам, но можно обойтись обёрткой над простыми РегВырами. (я назвал её Rx)

Алгоритм ставит определённому сочетанию букв одинаковый код, если они звучат одинаково.

Рассмотрите пример. Слово «сердце» произносится как /ˈs̪ʲɛ̝rt̻͡s̪ɨ̞/, из-за этого оно может писаться с ошибкой, писаться так, как оно звучит — «серце», или «сертце». Рассматривая /rt̻͡s̪/, становится понятно, что этой группе согласных может соответствовать «рдц», «рц» или «ртц». Согласные заменяются на числа, причём похожим по звучанию буквам/группам букв соответствуют одинаковые числа.

Проверить подходить ли предложение под Rx регулярное выражение можно так
const ok = Rx`Есть ли ${/.+/} в наличии?`.test("Если ли лапата в налеции?");


Если же нужно именно слово, которое совпало в захватывающей группе, то его напрямую получить нельзя. (И действительно, если вы получите из этого предложения «лапата», то что с этим делать дальше? Вместо этого, предлагается код этого слова 873. У вас в базе данных может храниться слово «лопата», вычислив Soundex("лопата") получите 873, сравнив узнаете, что «лапата» оказывается — «лопата»)
const match = Rx`Есть ли ${/.+/} в наличии?`.match("Если ли лапата в налеции?");


В предложенном варианте Soundex это не SOUNDEX, а приспособленное к русскому языку поделие, основанное на алгоритмах SOUNDEX и «Полифон», нуждается в некоторой доработке в плане данных объекта Soundex.prototype._codes

Итоговый пример, в котором вы можете ввести предложение, и программа проверит под какое Rx регулярное выражение подходит предложение (походящее выделится зелёным цветом!)

Продолжение следует...

Malleys 16.03.2019 00:06

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<style>

#inputField {
	font-size: 200%;
	width: 100%;
	box-sizing: border-box;
}
		
#patternsField > * {
	border: 1px solid #eee;
	border-radius: .4em;
	margin: 1em 0;
	padding: 1em;
	font: bold 1em sans-serif;
}

#patternsField > .matches {
	background-color: #4CAF50;
	color: white;
}

	</style>
</head>
<body>
	<input id="inputField" value="Если ли в нолечие лапата?">
	<div id="patternsField"></div>
	<script>

function Soundex(expression) {
	if(this instanceof Soundex === false)
		return new Soundex(expression);

	this.expression = expression;
	this.calculate(this.expression);
}

Soundex.prototype = {
	__proto__: Array.prototype,
	_codes: {
		"a":{
			"0":[0,-1,-1],
			"i":[[0,1,-1]],
			"j":[[0,1,-1]],
			"y":[[0,1,-1]],
			"u":[[0,7,-1]]
		},
		"b":[[7,7,7]],
		"c":{
			"0":[5,5,5],
			"1":[4,4,4],
			"z":{"0":[4,4,4],"s":[[4,4,4]]},
			"s":{"0":[4,4,4],"z":[[4,4,4]]},
			"k":[[5,5,5],[45,45,45]],
			"h":{"0":[5,5,5],"1":[4,4,4],"s":[[5,54,54]]}
		},
		"d":{
			"0":[3,3,3],
			"t":[[3,3,3]],
			"z":{"0":[4,4,4],"h":[[4,4,4]],"s":[[4,4,4]]},
			"s":{"0":[4,4,4],"h":[[4,4,4]],"z":[[4,4,4]]},
			"r":{"s":[[4,4,4]],"z":[[4,4,4]]}
		},
		"e":{
			"0":[0,-1,-1],
			"i":[[0,1,-1]],
			"j":[[0,1,-1]],
			"y":[[0,1,-1]],
			"u":[[1,1,-1]],
			"w":[[1,1,-1]]
		},
		"f":{"0":[7,7,7],"b":[[7,7,7]]},
		"g":[[5,5,5]],
		"h":[[5,5,-1]],
		"i":{
			"0":[0,-1,-1],
			"a":[[1,-1,-1]],
			"e":[[1,-1,-1]],
			"o":[[1,-1,-1]],
			"u":[[1,-1,-1]]
		},
		"j":[[4,4,4]],
		"k":{"0":[5,5,5],"h":[[5,5,5]],"s":[[5,54,54]]},
		"l":[[8,8,8]],
		"m":{"0":[6,6,6],"n":[[66,66,66]]},
		"n":{"0":[6,6,6],"m":[[66,66,66]]},
		"o":{"0":[0,-1,-1],"i":[[0,1,-1]],"j":[[0,1,-1]],"y":[[0,1,-1]]},
		"p":{"0":[7,7,7],"f":[[7,7,7]],"h":[[7,7,7]]},
		"q":[[5,5,5]],
		"r":{
			"0":[9,9,9],
			"d": {"0":[93,93,93],"c":[[95,95,95]]},
			"t": {"0":[93,93,93],"c":[[95,95,95]]},
			"z":[[94,94,94],[94,94,94]],
			"s":[[94,94,94],[94,94,94]]
		},
		"s":{
			"0":[4,4,4],
			"l":{"0":[2,43,43]},
			"z":{
				"0":[4,4,4],
				"t":[[2,43,43]],
				"c":{"z":[[2,4,4]],"s":[[2,4,4]]},
				"d":[[2,43,43]]
			},
			"d":[[2,43,43]],
			"t":{
				"0":[2,43,43],
				"r":{"z":[[2,4,4]],"s":[[2,4,4]]},
				"n":[[26,46,46]],
				"c":{"h":[[2,4,4]]},
				"s":{"h":[[2,4,4]],"c":{"h":[[2,4,4]]}}
			},
			"c":{
				"0":[2,4,4],
				"h":{
					"0":[4,4,4],
					"t":{
						"0":[2,43,43],
						"s":{"c":{"h":[[2,4,4]]},"h":[[2,4,4]]},
						"c":{"h":[[2,4,4]]}
					},
					"d":[[2,43,43]]
				}
			},
			"h":{
				"0":[4,4,4],
				"t":{"0":[2,43,43],"c":{"h":[[2,4,4]]},"s":{"h":[[2,4,4]]}},
				"c":{"h":[[2,4,4]]},
				"d":[[2,43,43]]
			}
		},
		"t":{
			"0":[3,3,3],
			"c":{"0":[4,4,4],"h":[[4,4,4]]},
			"z":{"0":[4,4,4],"s":[[4,4,4]]},
			"s":{"0":[4,4,4],"z":[[4,4,4]],"h":[[4,4,4]],"c":{"h":[[4,4,4]]}},
			"t":{
				"s":{"0":[4,4,4],"z":[[4,4,4]],"c":{"h":[[4,4,4]]}},
				"c":{"h":[[4,4,4]]},
				"z":[[4,4,4]]
			},
			"h":[[3,3,3]],
			"r":{"z":[[4,4,4]],"s":[[4,4,4]]}
		},
		"u":{
			"0":[0,-1,-1],
			"e":[[0,-1,-1]],
			"i":[[0,1,-1]],
			"j":[[0,1,-1]],
			"y":[[0,1,-1]]
		},
		"v":[[7,7,7]],
		"w":[[7,7,7]],
		"x":[[5,54,54]],
		"y":[[1,-1,-1]],
		"z":{
			"0":[4,4,4],
			"d":{"0":[2,43,43],"z":{"0":[2,4,4],"h":[[2,4,4]]}},
			"h":{"0":[4,4,4],"d":{"0":[2,43,43],"z":{"h":[[2,4,4]]}}},
			"s":{"0":[4,4,4],"h":[[4,4,4]],"c":{"h":[[4,4,4]]}}
		}
	},
	_map: {
		а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ё: "e", ж: "zh", з: "z",
		и: "i", й: "i", к: "k", л: "l", м: "m", н: "n", о: "o", п: "p", р: "r",
		с: "s", т: "t", у: "u", ф: "f", х: "h", ц: "c", ч: "ch", ш: "sh", щ: "sch",
		ь: "'", ы: "y", ъ: "'", э: "e", ю: "yu", я: "ya"
	},
	_accented: {
		a: /[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ⒜ⒶⓐAa]/ig,
		b: /[BbᴮᵇḂ-ḇℬ⒝ⒷⓑBb]/ig,
		c: /[CcÇçĆ-čᶜ℀ℂ℃ℭⅭⅽ⒞Ⓒⓒ㏄-㏇Cc]/ig,
		d: /[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟ⒹⓓDd]/ig,
		e: /[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑℯℰⅇ⒠ⒺⓔEe]/ig,
		f: /[FfᶠḞḟ℉ℱ℻⒡Ⓕⓕff-fflFf]/ig,
		g: /[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢ⒼⓖGg]/ig,
		h: /[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣ⒽⓗHh]/ig,
		i: /[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈ⒤ⒾⓘIi]/ig,
		j: /[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]/ig,
		k: /[KkĶķǨǩᴷᵏḰ-ḵK⒦ⓀⓚKk]/ig,
		l: /[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓⅬⅼ⒧ⓁⓛLl]/ig,
		m: /[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨ⓂⓜMm]/ig,
		n: /[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩ⓃⓝNn]/ig,
		o: /[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒℴ⒪ⓄⓞOo]/ig,
		p: /[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㎀㎊㎩Pp]/ig,
		q: /[Qqℚ⒬Ⓠⓠ㏃Qq]/ig,
		r: /[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭ⓇⓡRr]/ig,
		s: /[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮ⓈⓢstSs]/ig,
		t: /[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㏏ſtstTt]/ig,
		u: /[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]/ig,
		v: /[VvᵛᵥṼ-ṿ⒱ⓋⓥⱽVv]/ig,
		w: /[WwŴŵʷᵂẀ-ẉẘ⒲ⓌⓦWw]/ig,
		x: /[XxˣẊ-ẍₓ⒳Ⓧⓧ㏓Xx]/ig,
		y: /[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴ⓎⓨYy]/ig,
		z: /[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵ⓏⓩZz]/ig
	},
	_cache: {},
	word(str, isCyrillic = true) {
		var length = str.length,
			result = "",
			i = 0,
			previousCode = -1,
			maxLength = 6,
			code;

		while(i < length) {
			var current = last = this._codes[str[i]];

			for(var j = k = 1; k <= maxLength; k++) {
				if(!str[i + k] || !current[str[i + k]]) break;

				current = current[str[i + k]];

				if(current[0]) {
					last = current;
					j = k + 1;
				}
			}

			if(i === 0)
				code = last[0][0];
			else
				code = (isCyrillic || "1" in last === false ? last[0] : last[1])[
					str[i + j] && this._codes[str[i + j]][0][0] === 0 ? 1 : 2
				];

			if(code !== -1 && code !== previousCode) result += code;

			previousCode = code;
			i += j;
		}

		while(result && result.length < 3)
			result += "0";
	 
		return result;
	},

	calculate(str) {
		str = str.trim().toLowerCase();
		var cyrillicMatch = str.match(/[ёа-я]/g);

		if(cyrillicMatch)
			str = this.transliterate(str, cyrillicMatch);

		for(var letter in this._accented)
			str = str.replace(this._accented[letter], letter);

		str = str
			.replace(/[^\sa-z]/g, "")
			.replace(/\s{2,}/g, " ");

		if(!str) return [];

		str = str.split(" ").map(word => {
			if(word in this._cache === false)
				this._cache[word] = this.word(word, cyrillicMatch !== null);

			return this._cache[word];
		});

		Object.assign(this, str);
		this.length = str.length;
	},

	transliterate(str, matches) {
		for(const match of matches)
			str = str.replace(match, this._map[match]);

		return str;
	},

	toString() {
		return this.join(" ");
	}
};

function Rx(template, ...substitutions) {
	if(this instanceof Rx === false) return new Rx(template, ...substitutions);

	this.raw = template[0] + substitutions.map((v, i) => `(${v.source})${template[i + 1]}`).join("");
	
	this.pattern = (template.slice(1).map((part, index) => `(${substitutions[index].source}) ${Soundex(part)}`));
	this.pattern.unshift(Soundex(template[0]));
	this.pattern = new RegExp(this.pattern.join(" ").trim());
}

Rx.prototype = {
	test(expression) {
		return this.pattern.test(Soundex(expression));
	},

	match(expression) {
		return (this.pattern.exec(Soundex(expression)) || []).slice(1);
	}
};

// пример
const patterns = [
	Rx`Есть ли в наличии ${/[\s\S]+/}?`,
	Rx`Есть ли ${/[\s\S]+/} в наличии?`,
];

inputField.addEventListener("input", () => {
	patternsField.innerHTML = patterns.map(pattern => {
		const ok = pattern.test(inputField.value);
		return `<div class="${ok ? "matches" : ""}">${pattern.raw}</div>`;
	}).join("");
});

inputField.dispatchEvent(new Event("input"));
	
	</script>
</body>
</html>


[1]: Paramonov V., Shigarov A., Ruzhnikov G., Belykh P. Polyphon: An Algorithm for Phonetic String Matching in Russian Language // Communications in Computer and Information Science. Springer. 2016. Vol. 639, pp. 568-579.

ДЖАВАСКРИПТИЗЕР 16.03.2019 01:24

давно такого дерьма не смотрел, как уснуть то теперь

Nexus 16.03.2019 11:05

Malleys, Это гениально.
Большое спасибо за столь развернутый ответ, отдельное спасибо за пример.

К сожалению из-за ограничений сайта не могу вам карму увеличить.


Часовой пояс GMT +3, время: 03:43.