Давно пора было написать этот пост, благо цепи Маркова — один из распространенных алгоритмов для построения дорвеев. Они позволяют генерировать текст, если в кратце, который является уникальным в глазах поисковых алгоритмов. Разумеется он не читабелен и чем-то напоминает речь нового мэра Киева…, поэтому полезность использования данного подхода — сомнительна. Но! можно попробовать комбинировать читабельное и не читабельное, например. Давайте ознакомимся с темой по ближе.
Теория
Цепи Маркова — это вероятности получения события на основе предыдущего события.
Да, знаю, туманное определение. Но, положим у нас есть игральная кость, одна грань которой тяжелее, чем остальные. Ясно, что эта грань будет падать вниз — чаще, от чего ее шанс выпадения будет мал, по сравнению с другими гранями. Цепь Маркова, в применении к кости, будет выглядеть как таблица с … например последними 10 бросками кости и их результатами. Глядя на эту таблицу мы можем примерно предсказать, какой результат будет у следующей серии бросков. Именно такое предсказание и есть результатом работы цепи, она с определенным шансом сообщает нам результат события, которое еще не случилось.
В применении к тексту это выглядит следующим образом. Возьмем поговорку:
Свобода не в том, чтоб не сдерживать себя, а в том, чтоб владеть собой.
Разобьем это предложение на пары слов, вместе со знаками препинания, но без учета регистра.
свобода | не |
не | в |
сдерживать | |
в | том |
том | чтоб |
чтоб | не |
владеть | |
сдерживать | себя |
себя | а |
а | в |
владеть | собой |
Как мы видим, после слова «не» могут быть или «в» или «сдерживать», а после слова «чтоб» — «не» или «владеть». При большом объеме текста таких слов будет больше, причем для каждого сочетания. Алгоритм Маркова просто берет одно из таких слов и выводит его основываясь на вероятности его выпадения.
Более подробно можно почитать тут, если вы дружите с английским.
Пишем генератор Маркова
Все что требуется — это получить массив-таблицу, приведенную выше. Ну, а далее собрать из нее некий текст.
Чтобы это сделать надо:
- Очистить текст от мусора
- Разбить его по пробелам в одномерный массив.
- В цикле сгенерировать таблицу
- В цикле собрать из таблицы текст
Собственно, чистка и составление таблицы выглядят так:
$data = file_get_contents("tz.txt"); // тут исходный текст
mb_internal_encoding("UTF-8");
// знаки препинания воспринимаем как отдельные слова, то есть добавляем перед знаком пробел и после него тоже
$data = preg_replace("~([,\:\-])~u"," \$1 ",$data);
$data = preg_replace("~(\S+)[\s\r\n]*-[\s\r\n]*(\S+)~u"," \$1\$2 ",$data); // переносы объединяем
$data = preg_replace('~[^a-zёа-я0-9 -!\?\.\,]~ui',' ',$data); // убираем лишнее, включая табы, скобки и прочее
$data = mb_strtolower($data); // все в нижний регистр
$words = explode(" ",$data); // разбиваем по пробелу
$table = array(); // массив пар сочетаний
foreach($words as $key=>$word){
if( isset($words[$key+1]) ){
$word = trim($word);
$table[$word][] = trim($words[$key+1]); // пара слово -> следующее слово
$table[$word] = array_filter($table[$word],"strlen"); // убираем пустые
$table[$word] = array_unique($table[$word]); // убираем дубли
} else { /* если пар не найдено - пропускаем */ }
}
Если посмотреть на результат работы этого кода, то мы увидим следующее:
Осталось все это немного подфильтровать и объединить в цикле.
Делается это, например, так:
$text = ""; // тут будет результат
$prcount = 5; // кол-во предложений, которые надо сгенерировать
$wcount = count($table); // число элементов в таблице
$wkeys = array_keys($table); // ключи, то есть первые входные слова. Используется для генерации начал предложений.
for($i=0; $i<$prcount; $i++){
$word = $wkeys[array_rand($wkeys)];
// первое слово с заглавной буквы
$word = mb_convert_case($your_string, MB_CASE_TITLE, 'UTF-8');
$predl = array();
$predl[] = $word; // массив слов будущего предложения
$prlen = rand(5,15); // средняя длинна предложения от 6 до 16 слов(+1 слово, заглавное)
while(mb_strpos($word,".") === false){ // пока не выпадет точка
$subw = $table[$word];
$word = $subw[array_rand($subw)];
// если слово содержит точку и при этом кол-во слов в результате меньше, чем надо
if(mb_strpos($word,".") !== false && count($predl) < $prlen){
// убираем точку
$predl[] = trim($word,".");
}else
$predl[] = $word;
}
$text .= implode(" ",$predl)." ";
}
Результат будет таким:
Это, разумеется, первоначальная версия. Она не учитывает имена собственные, а так-же криво работает со знаками препинания, точнее вообще нифига с ними не работает. Так-же начала предложений тут не отмечаются заглавными буквами. Я привожу ее только чтобы описать вам принцип. Ниже будет полноценный класс, написанный мной в ходе изучения этой темы.
Код генератора Маркова
<?php
/**
* Алгоритм Маркова для Кириллических текстов, учитывающий пунктуацию и имена собственные
* минимально-допустимая версия php - 5.4, обязательно расширение mbstring
* @author Александр Штокман
* @year 2017
*/
namespace Generator;
class Markov{
# Var
private $table = array(); // массив лексем
private $text = ""; // базовый текст
private $pr_count = 15; // базовый текст
public $result = ""; // результат
/**
* Конструктор
* @var $text - исходный текст
* @var $pr_count - кол-во генерируемых предложений
*/
function __construct( $text, $pr_count = 15 ){
mb_internal_encoding("UTF-8");
$this->text = $text;
$this->pr_count = intval($pr_count);
$this->prepare();
$this->generate();
}
# Public
// получение результата
public function get_result(){
return $this->result;
}
# Private:
// генерация
private function generate(){
if(empty($this->table)) throw new Exception("Вызовите метод ->prepare перед генерацией!");
$word = "";
for( $i=0; $i < $this->pr_count; $i++ ){
$word = $this->get_random_word($word,array("!",".","?"));
// массив слов будущего предложения
$predl = array();
$predl[] = $this->mb_ucfirst($word); // с заглавной буквы - первое слово
$prlen = rand(5,15); // средняя длина предложения от 6 до 16 слов(+1 слово, заглавное)
while(!$this->in_str($word,array("!",".","?"))){ // пока не выпадет точка
$word = $this->get_random_word($word);
// если слово содержит точку и при этом кол-во слов в результате меньше, чем надо
if($this->in_str($word,array("!",".","?")) && count($predl) < $prlen){
// убираем точку
$word = str_replace(array("!",".","?"),"",$word);
}
$predl[] = $word;
}
if(mb_strlen(end($predl)) < 4){ // если кол-во букв в последнем слове предложения меньше 4
array_pop($predl); // удаляем это слово
$predl[] = "."; // и добавляем в конец точку
}
$this->result .= implode(" ",$predl)." ";
}
$this->result = preg_replace('~s([!?.,])s~u','1 ',$this->result); // убираем пробелы перед знаками препинания
}
// подготовка
private function prepare(){
if($this->text == "") throw new Exception("Ваш текст пуст!");
$data = $this->text;
//$data = preg_replace("~([,:-])~u"," $1 ",$data); // знаки препинания воспринимаем как отдельные слова, то есть добавляем перед знаком пробел и после него тоже
$data = preg_replace("~(S+)s*[rn]+-+s*[rn]+(S+)~u"," $1$2 ",$data); // переносы объединяем
$data = preg_replace('~[^a-zёа-я0-9 -!?.,]~ui',' ',$data); // убираем лишнее
$data = preg_replace('~.+~ui','.',$data); // дубли точек и многоточия объединяем
$words = explode(" ",$data); // разбиваем полученные данные по пробелу
$table = array(); // строим массив пар сочетаний
foreach($words as $key=>$word){
if( isset($words[$key+1]) ){
$word = trim($word);
$word = $this->trimUpper($word, $words[$key-1]);
$sword = $words[$key+1];
$sword = $this->trimUpper($sword, $word);
$table[$word][] = trim($sword); // пара слово -> следующее слово
$table[$word] = array_filter($table[$word],"strlen"); // убираем пустые
$table[$word] = array_unique($table[$word]); // убираем дубли
/**
* Если слово содержит за собой один из спецсимволов - убираем символ, после чего помещаем копию слова без символа в массив
*/
if($this->in_str($word,array("!",".","?"))){
$word = str_replace(array("!",".","?"),"",$word);
$table[$word][] = trim($sword);
}
} else { /* если пар не найдено - пропускаем */ }
}
$this->table = $table;
}
// проверяет есть ли символы из массива $items в строке $str
private function in_str($str,$items = array(".")){
foreach($items as $item){
if(mb_strpos($str,$item) !== false) return true;
}
return false;
}
// мультибайтовый аналог ucfirst
private function mb_ucfirst($value)
{
return mb_strtoupper(mb_substr($value, 0, 1)) . mb_substr($value, 1);
}
// убирает заглавные только в том случае, если в $previous есть знаки препинания
private function trimUpper($word, $previous = null){
if(preg_match("~[A-ZА-Я]~",$word)){
/**
* И если предыдущее слово отсутствует или содержит .!? знак, то мы опускаем его в нижний регистр т.к. это начало предложения.
* Во всех остальных случаях очевидно, что заглавные буквы являются именами собственными, то есть именами людей, стран и прочего.
*/
if(!isset($previous) || $this->in_str($previous,array("!",".","?"))){
$word = mb_strtolower($word);
}
}
return $word;
}
// генерирует уникальное случайное слово
private function get_random_word($word = "", $ex = array()){ // получает случайное слово
$nw = "";
if($word == ""){
$wkeys = array_keys($this->table); // ключи, то есть первые входные слова. Используется для генерации начал предложений.
$nw = $wkeys[array_rand($wkeys)];
}else {
$subw = $this->table[$word];
if(empty($subw)){
return $this->get_random_word("", $ex);
}
$nw = $subw[array_rand($subw)];
}
/**
* Рекурсивно исключаем дубли, слова с запрещенными символами($ex), а так-же просто пустые строчки
*/
if(!$nw || !empty($ex) && $this->in_str($nw,$ex) || $nw == $word){
return $this->get_random_word($nw, $ex);
}
return $nw;
}
}
Использовать так:
<?php
/**
* Генератор текста на цепях Маркова
*/
header('Content-Type: text/html; charset=utf-8');
require_once("class_markov.php");
$data = file_get_contents("tz.txt");
$mk = new Markov($data,15);
$text = $mk->get_result();
echo "<p style='word-wrap: break-word;'>".$text."</p>";
Демонстрация
Результат работы данного кода можно посмотреть тут.
В качестве исходника взят труд Ф. Энгельса «Крестьянская война в Германии», отсюда.
Итоговый текст выглядит так:
Тут, как вы видите, учитываются имена Собственные, есть знаки препинания(хоть и не все), да и в целом текст выглядит вполне … прилично. Я не постесняюсь заявить, что мой генератор текста по цепям Маркова — пока лучший из опубликованных в сети.
К стати, это не полноценная цепь Маркова т.к. тут не учитывается вероятность появления того или иного слова. Чтобы она учитывалась, надо убрать из кода эту строчку:
$table[$word] = array_unique($table[$word]); // убираем дубли
Если это сделать массив лексем будет выглядеть так:
Шанс выпадения дублированного слова выше, чем всех остальных. Это и есть цепь Маркова. Полноценная. Почему я не использую этот подход? Ответ прост: для большей уникальности исходного текста. Но вам, разумеется, никто не запрещает изменить код и использовать именно такую версию генератора ибо читабельность результата немного повышается.
Умеют ли поисковики детектить такой сгенерированный текст? Сложный вопрос. А вот пользователи — умеют, поэтому поведенческие у сайта с таким наполнением будут очень плохие (от чего трафика на нем не будет вообще). Я думаю, можно попробовать комбинировать читабельный копипаст и цепи Маркова, но лично экспериментировать с этим не хочу. Но вы — можете попробовать.
Доргенам — дорогу, хехе!
Исходники
Ну и, раз уж на то пошло, выложу исходники сюда и на гитхаб.

весьма недурно. Думаю если еще чутка подшаманить, может выйти вполне годный контент.
тоже об этом думаю, надо попробовать парочку доров запилить…
Сложно, бро((
разбираться не обязательно. Есть же исходники, хехе
ребят такие текста яндекс умел палить ещё в далёком 2009 году:
http://cache-mskdataline05.cdn.yandex.net/download.yandex.ru/company/A_Kustarev_A_Raigorodsky_poisk_neestestvennih_textov_statia.pdf
а гуголь ещё раньше))
Конечно круто, по твоим статьям реально можно разбираться в PHP, изучать. Но читабельности вообще нет при использовании этой поделки 🙂
Гораздо интересней перемешивать текст абзацами, это должно работать лучше. Уникальность сейчас практически не важна, гораздо важней полезность инфы. Юзер в копипаст может залипнуть. Взять из топа 10 статей, и собрать их в одну — вот это подход.
не пали темы!!)))
Такое технически реализовать сложно, я подумывал к aftparser’у такой функционал запилить. Чтобы настроил 2-3 источника, а потом из них 1 статью собирал. Но пока … пока нихрена не придумал, как это сделать так, чтобы статьи совпадали по тематикам. То есть спарсить посты с 2-3 источников ваще проблем нет. Есть проблемы с объединением этих постов в 1 запись по тематике. Типа чтобы … не отличались параграфы. И вот тут как раз и заключается основная сложность.
чувак как то сделал
[скрыто]
из топа брать ссылки по ключу
Интересно, что там по трафику. Хороше было бы на статку глянуть, хехе
ваще да, годная тема.
Глянул статку. Ох*ел, если честно, мягко выражаясь, попробую подобное сделать!
блдь. предупреждал же чтобы не палили темы.. щас начнётся. и пиздец.
не парься, такое не только лишь все могут сделать + я эту тему еще с нового года знаю ибо у громова покупал мануал
вопрос в реализации.
вот именно, что знал ты её ещё с нового года, но ничего делать даже и не собирался, как и все остальные покупатели этого мануала.
а занимался вот этим говном мамонтов 2007 года = генерённым на маркове.
не просто собирался, но даже сделал. и даже в сапе пару ссылок купил на этот сайт. + я не тупа копировал как он там советует, а с полу-рерайтом, качественно. трафика пока маловато из-за кучи ручной работы кол-во страниц с горем-пополам до 100 довел и все.
Как понять «чтобы не отличались параграфы»? Можно же просто заливать портянкой, просто преобразовывать h1 в h2, например.
Статьи будут совпадать по тематикам в силу того, что ты их берешь по одному ключу из выдачи 😉
ну разве что выдачу парсить, тогда да, хехе
мне попался текст, где было словосочетания виду «girl on girl», в итоге генератор зависает в цикле while // пока не выпадет точка
алгоритм я слишком сильно не тестировал так что все может быть
Нормалдос! База есть, остаются эксперименты и внедрение.
При хороших входных данных и везении может показаться, что алгоритм — человек который пользуется переводчиком на ходу, на каждое слово.
Разобрался.
Забавно то, что чем более старательно наполняешь цепь знаниями о зависимостях и закономерностях между словами в предложениях, тем менее осмысленным становится текст. Чем меньше обучающая выборка, тем ближе сгенерированный текст к исходному и тем больше в нём смысловой нагрузки. А так как цепь маркова не способна (и не должна) зависеть от контекста более поздних слов, то и выбор следующих состояний «непредсказуемый»