В процессе своей профессиональной деятельности мне приходится достаточно много работать с текстовыми документами, подготавливаемыми другими лицами. Одной из задач проверки качества документов является определение степени уникальности текста. Конечно, можно проверять каждый документ в сервисе проверки заимствований (к, примеру в «Антиплагиат-ВУЗ», к которому есть официальный безлимитный доступ), а для автоматизации этого процесса можно использовать API. Однако, на этапе предварительной проверки, это немного избыточно.

Цель статьи: показать один из способов реализации автоматического выявления наличия «подозрительных» символов, форматирования и иного вмешательства в документ формата docx.

В рамках разработки СДО, о которой можно прочитать здесь, был создан функционал, позволяющий предварительно проверять загружаемые пользователями документы перед их загрузкой в «Антиплагиат-ВУЗ» (и не только), а также проверки содержания.

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

  • вставка пробелов, символов в середине слова, которому устанавливается маленький шрифт (до 5 кегля выявляется «АП-ВУЗ», но не во всех случаях – об это далее), светло-серый цвет (оттенки белого выявляются «АП-ВУЗ», а вот серый не всегда), интервал 0.1 и масштаб текста (на слишком маленький < 33%). В документе отключается проверка орфографии, чтобы не подчеркивались слова и внешне текст выглядит идентично с оригиналом.

  • В режиме форматирования это правки легко выявляются.

  • по аналогии с вышеуказанным методом вставляются (дублируется) каждое 3-5 слово, которому также придаются маскирующие особенности.

  • встречались «улучшенные» разновидности работы с форматированием, суть которых заключалась в создании собственного стиля оформления, который задает параметры форматирования текста, не поддающиеся установлению ни через макросы VBA, ни через интерфейс программы WORD. Более глубокий анализ показал, что подвергается редактированию файл стилей (styles.xml) в документе docx, по сути, представляющий собой архив с папками и файлами. Более подробно о формате docx можно прочитать здесь.  

  • попадались интересные примеры попыток обхода, заключающиеся в использовании колонтитулов, подмене кодов написания символов и букв в самом шрифте, надписями (текстовыми областями).

К примеру, в самом документе буква «Н» соответствовала букве «М» и при извлечении текста, либо копировании буквы менялись местами и в отчете слово «например» оказывалось «мапринер». Выявление таких способов достигается путем проверки орфографии.

При изучении этой темы я также столкнулся с особенностью стандарта XML, в котором есть теги, позволяющие устанавливать атрибуты отдельным частям документа. В Word есть вкладка «Рецензирование», отвечающая за работу как раз с такими атрибутами. Особенностью проверки в системе «АП-ВУЗ» оказалось, что если в документе не приняты исправления, то эти части в финальном отчете видны не будут. В самом XML это выглядит так.

<w:p>  // открытие абзаца
  <w:r>  // открытие блока текста
    <w:t xml:space="preserve">Текст</w:t>  
  </w:r>  
  <w:del w:author="Автор удаления">  
    <w:r>  
      <w:delText>Удаленный текст</w:delText>  
    </w:r>  
  </w:del>  
</w:p>  

Иными словами, если удалить абзац в режиме рецензирования, а потом поставить вид отображения в WORD как «Исходный документ», то отчет АП не будет соответствовать видимой части текста. Однако, такой метод имеет особенность – WORD по умолчанию всегда открывает документ в режиме «Все исправления», что позволяет при должной внимательности определить рецензируемые блоки.

Как отмечают на «Хабре» представители компании «Антиплагиат» все указанные выше способы не должны работать, но, к сожалению, практика показывает, что методы технического повышения не сильно видоизменяются (причин понять не могу) за исключением метода формулы (поле «EQ», который уже нигде не встречается) и пользуются популярностью у студентов. По отзывам и форумам (которым можно доверять), а также в общении с коллегами иногда приходится слышать слышать (не утверждаю истинность), что такие документы иногда проходят проверку и подозрительными не отмечаются.

Конечно, проверять применение указанных выше методов можно и в ручном режиме, самым простейшим методом из которых является «очистка форматирования текста».

Конечно, проверять применение указанных выше методов можно и в ручном режиме, самым простейшим методом из которых является «очистка форматирования текста».

Однако, хотелось автоматизации и для этих целей была выбрана библиотека PHP – «phpoffice/phpword», которая позволяет работать с документами Microsoft Word (создание и чтение). Нас в первую очередь, интересует возможность получения содержимого документа.

При чтении анализе документа нас, в первую очередь, интересует форматирование текста.

Библиотека предлагает широкие возможности для извлечения текста, однако в процессе реализации такого инструментария я столкнулся с ошибками извлечения отдельных блоков текста по причине наличия в документе мультимедиа-контента. Это приводило к тому, что библиотека работала некорректно и не получала все текстовое содержимое документа. Более того, в библиотеке не было возможности получить масштаб текста.

Решением проблемы оказалось следующее (пришлось вникать в ядро библиотеки):

// ПОЛУЧЕНИЕ МАСШТАБА
В классе vendor\phpoffice\phpword\src\PhpWord\Reader\2007\AbstractPart есть метод readFontStyle(), который формирует в переменной $styleDefs (массиве) дефолтные поля для чтения. В данных полях необходимо добавить
--------------
'spacing'=> array(self::READ_VALUE, 'w:spacing'),
'scale' => array(self::READ_VALUE, 'w:w')
--------------
Не совсем понятно, почему именно этих полей не было изначально. На скорость никак не влияет.

 // ИГНОРИРОВАНИЕ КАРТИНОК
Заменить в файле \vendor\phpoffice\phpword\src\PhpWord\Element\Image.php строки c 489 (версия 0.17.0)
---------------
        if ($zip->open($zipFilename) !== false) {
            if ($zip->locateName($imageFilename) !== false) {
                $imageContent = $zip->getFromName($imageFilename);
                if ($imageContent !== false) {
                    file_put_contents($tempFilename, $imageContent);

                    $imageData = getimagesize($tempFilename);
                    if ($imageData == false) {
                        $imageData = array("0"=> "900", "1"=> "900", "2"=> "2", "3"=> "24", "width"=>"900", "height"=>"900", "bits"=> "8", "channels"=> "3", "mime"=>"image/jpeg");
                    }
                    unlink($tempFilename);
                }
            }
            $zip->close();
        }
---------------

Меня интересовал только текст и формирование по нему отчета, поэтому исправления заключалось в следующих действиях.

Исходя из принципов работы библиотеки, которая перебирала блоки (секции) в документе была создана собственная проверка существования необходимых методов.

Так, при получении блока (секции) в цикле foreach проверялось наличие доступного метода обработки параграфа:

if ((method_exists($e, 'getParagraphStyle'))) {
…обработка текстовых блоков в параграфе…
}
….
Проверка возможности извлечения текста:
if ((method_exists($text, 'getText')) and (method_exists($text, 'getParagraphStyle'))) {
…анализ текста…
}

Финальный html отчет позволял видеть блоки, которые содержали нестандартное форматирование.

Полная функция обработки документа и создание html-представления.
public function ajaxAnalyze()
    {
        if ($this->isAjax()) {
//--------------------------------------
            $_SESSION['errors'] = [];
            $filename = $_POST['filename'];
            $source = ROOT . "/public/uploads/" . $filename . ".docx";
            //$source = ROOT . "/public/uploads/3.docx";

            $objReader = \PhpOffice\PhpWord\IOFactory::createReader('Word2007');
            $phpWord = $objReader->load($source);
            $body = '';


            foreach ($phpWord->getSections() as $section) {
                $arrays = $section->getElements();

                foreach ($arrays as $e) {
                    if (get_class($e) === 'PhpOffice\PhpWord\Element\TextRun') {
                        if ((method_exists($e, 'getParagraphStyle'))) {
                            $style = $e->getParagraphStyle();
                            $align = $style->getAlignment();
                        }
                        $body .= '<div style="text-align: ' . ($align ?: 'justify') . '">';
                        foreach ($e->getElements() as $text) {
                            $title = '';
                            if ((method_exists($text, 'getText')) and (method_exists($text, 'getParagraphStyle'))) {

                                $font = $text->getFontStyle();
                                $size = ($font->getSize());
                                $scale = $font->getScale();
                                $spacing = $font->getSpacing();
                                $styleName = $font->getStyleName();
                                $bold = $font->isBold() ? 'font-weight:700' : '';
                                $color = (($font->getColor() == '000000' or $font->getColor() == NULL) ? 'black' : $font->getColor() );
                                //echo 'Цвет текста: ' . $color . '<br>';
                                $fontFamily = $font->getName();
                                $x = '<span style="font-size:' . ($size + 10) . 'px; font-family:' . $fontFamily . '; ' . $bold . '; color:#' . $color . '">' . $text->getText() . '</span>';
                                if ($color !== 'black') {
                                    array_push($_SESSION['errors'], 'цвет шрифта в некоторых словах не "черный"');
                                    $title .= '<div>---цвет шрифта не черный</div>';
                                }

                                if ($styleName !== null) {
                                    array_push($_SESSION['errors'], 'подозрительный стиль');
                                    $title .= '<div>---изменен стиль текста</div>';
                                    
                                }

                                if ($scale !== null) {
                                    array_push($_SESSION['errors'], 'изменен масштаб текста');
                                    $title .= '<div>---изменен масштаб текста</div>';
                                }

                                if ($spacing !== null) {
                                    array_push($_SESSION['errors'], 'изменен интервал в словах');
                                    $title .= '<div>---изменен интервал в словах</div>';
                                }

                                if ($size < 10 or $size == null) {
                                    array_push($_SESSION['errors'], 'маленький размер текста');
                                    $title .= '<div>---маленький размер шрифта</div>';
                                }
                                if (!empty($title)) {
                                    $x = '<span data-toggle="tooltip" title="<div style=\'font-size:20px;\'>' . $title . '</div>" style="font-size:' . ($size + 10) . 'px; background-color: LightCoral; font-family:' . $fontFamily . '; ' . $bold . '; color:#' . $color . '">' . $text->getText() . '</span>';
                                }

                                $body .= $x;
                            }
                        }
                        $body .= '<br>';
                        $body .= '</div>';
                    } else if (get_class($e) === 'PhpOffice\PhpWord\Element\TextBreak') {
                        $body .= '<br>';
                    } else if (get_class($e) === 'PhpOffice\PhpWord\Element\Table') {
                        $body .= '<table border="2px">';
                        $rows = $e->getRows();
                        foreach ($rows as $row) {
                            $body .= '<tr>';
                            $cells = $row->getCells();
                            foreach ($cells as $cell) {
                                $body .= '<td style="width:' . $cell->getWidth() . '">';
                                $celements = $cell->getElements();
                                foreach ($celements as $celem) {
                                    if (get_class($celem) === 'PhpOffice\PhpWord\Element\Text') {
                                        $body .= $celem->getText();
                                    } else if (get_class($celem) === 'PhpOffice\PhpWord\Element\TextRun') {
                                        foreach ($celem->getElements() as $text) {
                                            $body .= $text->getText();
                                        }
                                    } else {
                                        $body .= get_class($celem);
                                    }
                                }
                                $body .= '</td>';
                            }

                            $body .= '</tr>';
                        }
                        $body .= '</table>';
                    } else if (get_class($e) === 'PhpOffice\PhpWord\Element\PageBreak') {


                        $body .= '<br>';


                    } else if (get_class($e) === 'PhpOffice\PhpWord\Element\PreserveText') {
                        if (method_exists($e, 'getText')) {
                            $body .= $e->getText();
                        }
                    } else {
                        if (method_exists($e, 'getText')) {
                            $body .= $e->getText();
                        }
                    }
                }
                break;
            }
//--------------------------------------
            $x = file_put_contents(ROOT . "/public/reports/" . $filename . ".html", $body);
            $_SESSION['errors'] = array_unique($_SESSION['errors']);
            echo json_encode($_SESSION['errors']);
            exit();
        }
    }

Конец 3 части.

Часть 1. Аналог Moodle или как преподаватель-юрист создавал собственную систему дистанционного обучения. Часть 1. Начало

Часть 2. Создание аналога Moodle. Реализация API для прототипа SPA. Межсайтовые запросы. Первые проблемы архитектуры

Часть 3. Выявление технических методов повышения уникальности текста с помощью PHP (в рамках создания собственной СДО)

Часть 4. Выбор фреймворка и переход на Laravel в рамках создания собственной СДО

Часть 5. Переход на ReactJs, внедрение flux, SOLID и интеграция в Laravel.

Часть 6. Внедрение нейронных сетей в работу СДО.