Обзор алгоритмов обнаружения плагиата в исходных кодах программ1 10 мая 2006 г. Аннотация В этой статье описываются наиболее интересные алгортмы, с помощью которых можно обнаружить плагиат в исходных кодах программ за приемлемое время. Содержание 1. Введение 1 2. Виды представления исходных кодов программ 2 2.1. В виде элемента п-мерного пространства ........................................ 2.2. Исходный код ...................................................................................... 2.3. Токенизация ......................................................................................... 2 3 3 3. Алгоритмы поиска плагиата в исходных кодах программ 3.1. Алгоритм Хескела ................................................................................ 3.2. Выравнивание строк .......................................................................... 3.3. Жадное строковое замощение............................................................. 3.3.1. Базовая реализация алгоритма............................................. 3.3.2. Улучшения алгоритма ............................................................ 3.4. Колмогоровская сложность в задаче нахождения плагиата . . 3.5. Метод отпечатков ................................................................................. 3.5.1. Основная идея ......................................................................... 3.5.2. Метод просеивания .. 4 4 5 6 6 9 10 13 13 15 1. Введение Прежде всего стоит отметить, что за рамки этой сатьи выходят алгоритмы и представления, которые преобразуют программу в дерево (например, абстрактное синтаксическое) или ориентированный граф. Эти алгоритмы имеют достаточно высокую трудоемкость и пока на практике могут применяться только в крайне редких случаях. Прежде всего во втором параграфе мы рассмотрим представления программ, которые будем использовать в третьем параграфе, где будут описаны сами алгоритмы для нахождения плагиата. 1С полной версией данной статьи можно ознакомиться на [1]. 1 (с rain.ifmo.ru/cat ©Александр Красс Наиболее интересны алгоритмы приближения Колмогоровской сложности и метод отпечатков. Первый из-за того, что теоретически от него труднее всего сокрыть плагиат; а второй, потому что он, единственный из описанных здесь алгоритмов, может обслуживать достаточно большую базу данных 2 за приемлемое время. Отметим, что если две программы имеют существенную общую часть (на уровне языка программирования), то мы считаем, что одна из них содержит плагиат; причем плагиатор может изменить содержание оригинальной программы вставкой лишких операторов, переименованием переменных, изменением порядка следования независимых операторов, разбиением некоторых функций на две и так далее. 2. 2.1. Виды представления исходных кодов программ В виде элемента n-мерного пространства Ранние системы по обнаружению плагиата (например, [2]) представляли программу, как точку в n-мерном пространстве натуральных чисел с нулем, i-ая координата которой — количественная характеристика какого-либо свойства (атрибута) всей программы. Например, средняя длина строки кода, количество объявленных и используемых переменных, средняя длина имен переменных, количество операторов ветвления и так далее. Если точки двух программ лежат рядом, то одна из них предполагается плагиатом другой. Обычно такие системы называют "подсчитывающими отличительные черты" (attribute-counting systems), чтобы определить насколько близко лежат точки двух программ, обычно на них подсчитывается метрика (например, Евклидово расстояние) или же определяется их корреляция. Вычисляя i-ую характеристику (то есть координату) на протяжении всей программы, мы получаем ее усредненное значение, поэтому теряем слишком много информации о структуре программы. У таких систем два недостатка: они выдают очень много ложных совпадений и при крайне поверхностном изменении кода оргинала не находят плагиат. Если же мы используем только часть какой-либо программы в нашей, то найти плагиат с их помощью практически невозможно. В некоторых таких системах используются и более сложные характеристики, основанные на использовании графа потока управления (control-flow graph), но это более трудоемкие алгоритмы и, к тому же, небольшие изменения структуры программы могут вызвать сильные количественные изменения таких характеристик. 2.2. Исходный код Другие системы обнаружения плагиата рассматривают исходный код "как есть". Например, так поступают детекторы плагиата, которые работают с кодом так же, как и с обычными текстами. Но они крайне неэффективны для решения нашей задачи, так как переименование функций и переменных или 2Имеется в виду проверка один-против-всех. 2 (с rain.ifmo.ru/cat ©Александр Красс несущественные изменения в коде являются серьезными препятствиями для их правильной работы. Иногда используется параметризованное представление кода (см. [3]). Один из его вариантов таков: имена функций и переменных заменяются при первой встрече в коде на ноль, а при последующих на расстояние до предыдущей позиции. Обычно детекторы основанные на этих двух представлениях лучше находят плагиат, чем системы "подсчитывающие отличительные черты", а также способны находить плагиат в случаях, когда скопирована только часть программы. 2.3. Токенизация Пусть у нас есть две строки кода for(int i = 0; i <= n; i + +) и for(int j = 0; (j — 1) < n; j + +). Очевидно, что у них одинаковая функциональность, и плагиатор мог из одной получить другую без особых усилий, а для ранее описанных представлений они совершенно различны. Чтобы бороться с такого рода средствами сокрытия плагиата было придумано то-кенизированное представление кода. Основная идея этого представления — это сохранение существенных и игнорирование всех поверхностных (то есть легко модифицируемых) деталей кода программы. Процедура токенизации выглядит примерно так: • Каждому оператору языка (кроме пустого — его игнорируем), который не является операндом, приписываем код, назначенный заранее для каждого класса операторов. Также коды можно приписывать блочным операторам (наприме, begin/end, {}), подключениям библиотек и заголовочных файлов. • Строим строку из полученных кодов, сохраняя порядок следования их в исходном коде программы. Один символ строки (токен) — код одного оператора. Таким образом мы автоматически игнорируем названия функций и переменных (классов, объектов и так далее), разделительные символы, предотвращаем влияние мелких изменений кода программы (см. пример выше). Нужно отметить, что процесс токенизации и разбиение операторов на классы зависим от используемого в исходном коде языка программирования. К одному классу операторов обычно относят те, которым соответствует один идентификатор языка программирования, все вызовы функций, вызовы методов классов, объявления переменных элементарных типов, объявление экземпляров классов. Более подробную информацию можно посмотреть в приложении к [4]. 3. 3.1. Алгоритмы поиска плагиата в исходных кодах программ Алгоритм Хескела Пусть у нас есть две программы, представим их в виде строк токенов (см. 2.1) а и Ь соответственно. Одним из критериев сходства строк считается длина их наибольшей общей подпоследовательности. Мы всегда можем найти такой 3 (c rain.ifmo.ru/cat (c Александр Красс элемент строки а$, что НОП строк а1 = а|а|а|а|_і . .. а^а\ .. . а^_\ и Ь будет значительно меньше (максимум в два раза), чем НОП(а, Ь) (если НОП(а, Ь) > 1). Чтобы избежать этого явления можно воспользоваться алгоритмом сравнения строк Хескела [5], он требует нескольких проходов, но работает за линейное время. Разобьем строки а и Ь на к-граммы (подстроки длины к). Найдем те к-граммы, которые встречаются в а и Ь только по одному разу. Для каждой такой пары проверим совпадают ли элементы строк, непосредственно лежащие над ними; если это так, то проведем ту же проверку и для них и так далее, пока несовпадение не будет найдено. Аналогично для строк, лежащих ниже соответствующих к-граммов. Получаем набор общих непересекающиеся подстрок а и Ь. Их общая длина может служить мерой схожести программ соответвующих а и Ь. Достоинства. • Линейная трудоемкость алгоритма Недостатки. • Возможноть совпадения токенизированного представления программ, но отсутствия совпадения в исходных кодах программ. • Небольшое количество уникальных к-граммов в больших программах, соответственно многие совпадения, не содержащие в себе таких к-граммов будут проигнорированы. • Вставка в найденный блок или изменение на семантически эквивалентный опрератора во многих случаях будет приводить к игнорированию той части блока, в которой не содержится уникальный к-грамм. • Нельзя организовать базу данных, ускоряющую проверку один-против-всех. 3.2. Выравнивание строк Пусть у нас есть две программы, представим их в виде строк токенов (см. 2.1) s и t соответственно (возможно различной длины). Теперь мы можем воспользоваться методом локального выравнивания строк, разработанным для определения схожести строк ДНК [6]. Выравнивание двух строк получается с помощью вставки в них пробелов таким образом, чтобы их длины стали одинаковыми. Заметим, что существует большое количество различных выравниваний двух строк. Например, рассмотрим два выравнивания строк "masters" и "stars": masters masters sta rs stars Будем называть строки, полученные после выравнивания s и t, соответственно s' и t'. Рассмотрим sj и tj: стоимость их совпадения m, стоимость пропуска g, стоимость несовпадения d, где m, d и g — произвольные числа. Цена выравнивания — это сумма индивидуальных стоимостей всех пар sj и tj, наибольшее значение этой целевой функции для всех i и j (i < j < |s'| = |t'|) на строках s'[i..j] и t'[i..j] — величина выравнивания. Пусть m =1, d = —1 и g = 4 (с rain.ifmo.ru/cat ©Александр Красс —2, тогда "rs/rs" и "sters/strars— блоки с наибольшей целевой функцией в первом и втором выравнивании соответственно. Цена выравниваний — 2 и 3. Она родственна редакционному расстоянию и используется для измерения расстояния между почти идентичными объектами, также успешно применяется для определения неточного соответствия ДНК в вычислительной биологии [6]. Оптимальное выравнивание между двумя строками — максимальное значение целевой функции среди всех выравниваний. Это значение может быть вычислено с помощью динамического программирования. Пусть даны две строки s и t, определим D(i,j) как оптимальное выравнивание между строками s[1..i] и t[1..j]. Мы ищем max1<j<|sjjl<j<|t| D(i,j). Определим ( jm, если s[i] score(s[i],t[j ]) = i . d, иначе = t[j], Следующее рекуррентное соотношение позволяет нам получить требуемое: nr .) D(i, j) = max Л — 1,j — 1) + score(s[i],t[j]), I( D(i D(i — g, I D(i,j — 1)+ g, .0 Граничные условия задаются соотношениями D(0,i) = 0 и D(j, 0) = 0. Остальные элементы матрицы D могут быть вычислены при проходе слева направо и сверху вниз. Это возможно, потому что D(i, j) зависит только от D(i — 1, j — 1), D(i — 1, j) и D(i,j — 1). Время вычисления оптимального выравнивания составляет O(|s||t|); затраты памяти — O(max(|s|, |t|), потому что для вычисления нужны только две строки. Применение этого алгоритма в нашем случае выглядит примерно так: получаем токенизированное представление pi и p2 двух программ, делим вторую строку p2 на подстроки (секции), каждая из которых представляет модуль исходной программы. Для каждой секции и pi получаем значение оптимального локального выравнивания. Это позволяет алгоритму корректно обрабатывать перестановки модулей исходной программы. При построении оптимального локального выравнивания существуют некоторые тонкости. Например, в детекторе SIM [6] используется токенизи-рованное представление кода, под которой мы скорее понимаем специфическое параметризованное представление (см. 2.2). Игнорируются разделительные символы, содержание комментариев (то есть комментарию сопоставляется один код-токен), каждой переменной и функции вместо имени динамически ставится в соответствие некоторый код, каждому ключевому слову языка и специальному символу присваивается заранее определенный код, всем операндам также присваиваются коды. При построении оптимального локального выравнивания pi и подстроки p2 , несовпадениям и совпадениям разных токенов могут ставиться в соответствие разные цены. Например, при совпадении двух токенов-идентификаторов, мы имеем 2, другие совпадение 1; при несовпадении двух токенов-идентификаторов имеем 0, другие несовпадение -2. 5 (c rain.ifmo.ru/cat (c Александр Красс Достоинства и недостатки Если смотреть на реализацию алгоритма, используемую детектором SIM, то ничего хорошего мы не увидим, в том числе на базе этого алгоритма нельзя организовать базу данных, ускоряющую проверку один-против-всех. Но при использовании токенизации в нашем понимании, возможно алгоритм будет показывать достаточно хорошие результаты. Подробный анализ требует дополнительных исследований и проведения тестирования. 3.3. Жадное строковое замощение 3.3.1. Базовая реализация алгоритма Рассмотрим эвристический алгоритм получения жадного строкового замощения (The Greedy String Tiling, см. [7]) Он получает на вход две строки символов над определенным алфавитом (у нас это множество допустимых токенов), а на выходе дает набор их общих непересекающихся подстрок близкий к оптимальному. Подстроку, входящую в этот набор, мы будем называть тайлом (tile) . Пусть P и T — токенизированные представления сравниваемых программ (п. 2.1). Определение. MinimumMatchLength — минимальная длина наибольшего общего префикса строк Pp и Tt, при которой он учитывается алгоритмом. Определение. Длина самого большого из пока найденных на текущей итерации алгоритма общих префиксов строк Pp и Tt обозначается maxmatch. Определение. Набор тайлов мы будем обозначать как tiles. Определение. Множество, содержащие кандидатов на попадание в набор тайлов, обозначется mathes. Алгоритм можно разделить на две фазы: 1. Ищутся наибольшие общие подстроки P и T, состоящие только из непомеченных элементов (вначале алгоритма все элементы непомече-ны). Для этого используются три вложенных цикла: первые пробегает по всем возможным Pp, второй по всем Tt, а третий находит наибольший общий префикс Pp и Tt. Далее существует три варианта в зависимости от соотношения величин maxmatch и найденного префикса: a) если maxmatch меньше, то мы удаляем из списка общих подстрок matches все до этого добавленные и помещаем туда найденный префикс. b) если maxmatch больше, то ничего не меняем. c) если они равны, то добавляем наибольший общий префикс Pp и Tt к списку matches. 6 (с rain.ifmo.ru/cat ©Александр Красс 2. Проходим по списку, если текущий элемент списка — подстрока, не содержащая помеченных элементов, то помещаем ее в выходной набор tiles (теперь эту подстроку называют tile — отсюда и название алгоритма), помечаем все элементы рассматриваемой строки, входящие в P и T. Если длины строк в списке (maxmatch) больше чем MinimumMatchLength, то переходим к первой фазе. Приведем псевдокод этого алгоритма: Greedy-String-Tiling(String P, String T) { tiles = { } ; do { maxmatch = MinimumMatchLength; Forall unmarked tokens Pp in P { Forall unmarked tokens Tt in T { з = 0; while (Pp+j == Tt+j) && unmarked(P p+j) && unmarked(T t+j) з + +; if (j == maxmatch) { matches = matches U match(p,t, j); } else if (3 > maxmatch) { matches = {match(p,t,j)}; maxmatch = j; } } } Forall match(p, t, maxmatch) G matches { if (not occluded) { for j = 0 . . . (maxmatch — 1) { mark( P p+j); mark( T t+j); } tiles = tiles U matches(a, b, maxmatch); } } } while (maxmatch > MinimumMatchLength); return tiles; } Этот алгоритм использует две эвристики: 1. Вначале мы находим наибольшие общие подстроки, состоящие из непомеченных символов, потому что считаем, что длинные совпадения для нашей задачи наиболее содержательны. Понятно, что вследствие этой эвристики мы иногда можем не найти более оптимальное (в плане количества покрывающих подстрок) общее частичное покрытие, но это было бы нам абсолютно безразлично без следующей эвристики. 2. Если в алгоритме мы будем позволять учитывать слишком маленькие наибольшие общие префиксы, то случайные совпадения небольшого количества токенов будут влиять на определение схожести программ, чтобы избежать этого вводится константа MinimumMatchLength. 7 (c rain.ifmo.ru/cat (c Александр Красс Заметим, что "not occluded" (не пересекающиеся) означает, что ни один токен с Pp по Pp+maxmatch -1 и с Tt по Tt+ m a xmatch- 1 не был помечен на предыдущих итерациях. Как бы то ни было, меньшие тайлы не могут быть созданы до больших. Этого достаточно, чтобы утверждать, что нужно проверять только метки на концах предполагаемого тайла, чтобы узнать содержит ли он помеченные символы. Очевидно, это можно проделать за O(1), тогда асимптотическая сложность времени работы всего алгоритма в худшем случае O(n 3 ). 3.3.2. Улучшения алгоритма Существует достаточно большое количество оптимизаций алгоритма жадного строкового замощения, которые серьезно увеличивают его быстродействие, но не влияют на асимптотику [7]. Они основаны на представлении Р и Т в виде списков, где каждый непомеченный элемент ссылается на следующий непомеченный. Существует и более кардинальное улучшение с помощью алгоритма Карпа-Рабина поиска подстроки в строке [8]. Обычно оно комбинируется с другими для достижения еще большей эффективности. Алгоритм Карпа-Рабина поиска подстроки в строке находит все вхождения короткой строки (шаблона Р) в более длинную (текст Т) с использованием хэш-функции. Для этого мы вычисляем значения хэш-функции всех подстрок Т длины |Р |, это можно сделать за линейное время, если использовать хэш-функцию к, такую что к(ТТ^+і .Т^+р можно вычислить через производится посимвольное сравнение соответствующих подстрок для определения вхождения P в T. Сложность алгоритма на практике близка к линейной. Эта идея используется в [7] для модификации алгоритма GST: 1. Вычисляем значение хэш-фунции для всех подстрок длины s в P и T, где s произвольный параметр, больший или равный MinimumMatch-Length, который можно менять после второй фазы оригинального алгоритма (см. выше). Как было сказано ранее, это можно сделать за O(|P | + |T |). 2. Каждое значение хэш-функции, вычисленное от подстроки P, сравнивается с каждым значением, вычисленным от подстроки T, с помощью хэш-таблицы, за O(k), где к — количество совпавших значений для фиксированной подстроки P. Если два значения одинаковы, то возможно равенство соответствующих подстрок, которое затем проверяется посимвольно. При подтверждении совпадения делается попытка его продлить на длину большую, чем s, то есть ищется наибольший общий префикс соответствующих подстрок, далее действия производятся аналогично оригинальному алгоритму. Асимптотика худшего случая этой модификации алгоритма также составляет O(n3), но на практике она значительно ниже O(n2). 8 (с rain.ifmo.ru/cat ©Александр Красс Из очевидных соображений можно сделать вывод, что, чем больше нормализованная сумма длин общих непересекающихся подстрок, меньших определенной пороговой длины, двух строк, тем больше схожесть этих строк. Поэтому функцию схожести двух токенизированных программ можно определить так, как сделано в [4]: 2 • sim(P, T) |tiles| где |tiles| - длина всех найденных непересекающихся общих подстрок, причем, каждая длиной не меньше MinimumMatchLength (т.е. равна сумме длин всех тайлов). Достоинства. • Преимущества токенизированного представления (см. выше). • Общие подстроки меньшей длины, чем MinimumMatchLength игнорируются, поэтому алгоритм не принимает в расчет небольшие случайно совпавшие участки кода. • При разбиении совпавшего участка кода на две и более части вставкой одного-нескольких блоков или одиночных операторов, а также перестановкой небольшого количества независимых операторов, функция схожести слабо изменяется. (Длина совпадения должна быть значительно больше MinimumMatchLength, количество вставленных операторов мало по сравнению с длиной совпадения.) • Алгоритм нечувствителен к перестановкам больших фрагментов кода. Недостатки. • Возможноть совпадения токенизированного представления программ, но отсутствия совпадения в исходных кодах программ. • Разбиение совпадения на блоки, вставкой или заменой оператора на похожий (например, for на while), каждый длиной меньше MinimumMatchLength, ведет к полному игнорированию совпадения. • Из-за эвристик, используемых в алгоритме, совпадения, длиной меньшей чем MinimumMatchLength, будут проигнорированы. • Нельзя организовать базу данных, ускоряющую проверку один-противвсех. 3.4. Колмогоровская сложность в задаче нахождения плагиата В работе [9] используется расстояние между последовательностями, основанное на теории информации (an information based sequence distance): d(x,y) = 1 где К(х) — Колмогоровская сложность K (xy) последовательности х. Она показывает сколько информации содержит последовательность х. По определению, К(х) — длина самой короткой программы, которая на пустой ввод печатает х, К(х|у) — количество информации, полученной х от у, если пусто, то оно равно 9 (c rain.ifmo.ru/cat (c Александр Красс К(х); (К(х) — К(х|у)) — сколько у "знает" о х. По определению, К( х| у) — это длина самой короткой программы, которая на ввод у печатает х. Более подробно про Колмогоровскую сложность можно посмотреть в [10]. Основное отличие приведенной метрики от других, используемых в задачах определения плагиата, состоит в ее универсальности: две программы, близкие относительно любой другой метрики, будут близкими и относительно данной (см. [11]). Теоретически, детектор, основанный на такой метрике, невозможно обмануть. Чем ближе функция расстояния 0(х, у) к 0, тем больше общей инфорции содержат две программы х и у. Но, как хорошо известно, Колмогоровская сложность не вычислима [10]. В статье [9] приводится эвристическое приближение 0(х,у), основанное на приминении алгоритма сжатия к токени-зированному представлению программы: 0(х,у) « 1 где Сотр(.) (Сотр(.\.)) — длина Сотр(ху) сжатой (условно) строки, полученной из исходной с помощью какого-либо алгоритма сжатия. В работе [9] специально был разработан алгоритм сжатия, который удовлетворяет следующим специфическим требованиям: • От традиционных алгоритмов сжатия очень часто требуют возможность выполнения в реальном времени или с линейной асимптотикой. Для нас же главное качество сжатия, а скорость менее важна. Наш алгоритм может работать за сверхлинейное время, но должна достигать наилучшей степени сжатия для используемого типа данных. • Для получения а1(х,у) нужно вычислить Сотр(х), условное сжатие х по у (Сотр(х\у)) и Сотр(ху). Обычно программа, которая является плагиатом другой, содержит длинные приблизительно совпадающие с фрагментами оригинальной программой блоки. Они возникают, когда злоумышленник копирует часть кода оригинальной программы и делает в нем простые изменения. Требуемый алгоритм сжатия должен эффективно справляться с такими вещами, поэтому примененяется алгоритм семейства \1Ь (Ьепгре1^у, см. [12]), использующий найденные неточных совпадения. Рассмотрим непосредственно процесс сравнения двух программ. Сначала производится их токенизация, далее запускается алгоритм ТокепСопгргевв, основанный на алгоритме сжатия LZ (см. [12]). Первым делом он находит длиннейшую неточно повторяющуюся подстроку (approximately duplicated substring), заканчивающуюся в текущем символе; кодирует ее указателем на предыдущее размещение и сохраняет информацию о внесенных поправках. Реализация отличается от классического LZ-сжатия некоторыми особенностями, связанными со спецификой задачи: классический алгоритм семейства LZ с ограниченным буфером может пропустить некоторые длинные повторяющиеся подстроки из-за ограничения на размер словаря (или скользящего окна) во время кодирования. Это не страшно для кодирования обычных текстов, где нужно прежде всего экономить память и уменьшать время исполнения, ведь потери качества сжатия будут крайне малы. Но в силу специфики нашей задачи и выбранного алгоритма ее решения, использующего неточные совпадения, потеря даже небольшого количества длинных повторяющихся подстрок неприемлема. Приведем псевдокод алгоритма: 10 © rain.ifmo.ru/cat ©Александр Красс Algorithm TokenCompress Input: A token sequences Output: Comp(s) and matched repeat pairs i = 0; An empty buffer B; while (i < s ) { p = FindRepeatPair(i); if (p.compressProfit > 0) { EncodeRepeatPair(p, compFile); i = i + p.length; OutputRepeatPair(p, repFile); } else { AppendCharToBuffer(sj, B); i + +; } } EnodeLiteralZone( B , compFile); return Comp(s) = compFile.file_size; and repeat pairs stored in repFile. В оригинальной статье [9] не приводится алгоритм подсчета условного сжатия x по y (Comp(x^y)), но скорее всего предполагается, что мы вначале алгоритма инициализируем словарь (буфер B ) всеми подстроками строки y, а дальше алгоритм остается без изменений. Классические алгоритмы семейства LZ не поддерживают неточных совпадений. Данный же алгоритм ищет неточно повторяющиеся подстроки и кодирует их несовпадения. TokenCompress использует пороговую функцию, чтобы определить выгоднее ли нам кодировать несовпадения или лучше воспользоваться альтернативами (такими как кодирование частей отдельно или вообще не производить кодирования некоторых частей). Соответственно, некоторые неточные повторы могут быть объединены в один снебольшим количеством ошибок (несовпадений). Неточно совпавшие пары подстрок (т.е. неточные повторы) записываются в файл и позже могут быть просмотрены человеком. На основе работы этого алгоритма считается d(x, y), которая затем используется для определения насколько вероятно то, что одна из этих программ является плагиатом другой. Достоинства. • Преимущества токенизированного представления (см. выше). • Используется эвристическое приближение метрики, такой что, если две программы, близкие относительно любой другой функции расстояния, будут близкими и относительно данной (см. [11]). • Общие подстроки, меньше пороговой длины игнорируются, поэтому алгоритм не принимает в расчет малые случайно совпавшие участки кода. • При разбиении совпавшего участка кода на две и более части вставкой одного-нескольких блоков или одиночных операторов, а также перестановкой небольшого количества независимых операторов, функция схожести слабо изменяется. (Длина совпадения должна быть значительно больше некоторой пороговой длины.) • Крайне высокая устойчивость к вставке операторов. 11 © rain.ifmo.ru/cat ©Александр Красс • Алгоритм нечувствителен к перестановкам больших фрагментов кода. Недостатки. • Возможноть совпадения токенизированного представления программ, но отсутствия совпадения в исходных кодах программ. • Нельзя организовать базу данных, ускоряющую проверку один-против-всех. 3.5. 3.5.1. Метод отпечатков Основная идея В этом алгоритме мы представляем токенизированную программу в виде набора отпечатков (меток, fingerprints), так чтобы эти наборы для похожих программ пересекались. Этот метод позволяет организовать эффективную проверку по базе данных. Метод отпечатков можно представить в виде четырех нижеследующих шагов: 1. Последовательно хэшируем 3 подстроки токенизированной программы P длины k (фиксированный параметр). 2. Выделяем некоторое подмножество их хэш-значений, хорошо характеризующее P . Проделываем те же шаги для токенизированных программ T\,Ti .. .Tn и помещаем их выбранные хэш-значения в хэш-таблицу. 3. С помощью хэш-таблицы (базы) получаем набор участков строки P , подозрительных на плагиат. 4. Анализируем полученные на предыдущем шаге данные и делаем выводы. Очевидно, что содержательная часть этого алгоритмы выбор k и шаг 2. Что же задает число к? Оно назначает длину наименьшей подстроки, с которой может работать данный алгоритм. Если в двух программах есть общая подстрока длиной, например, два символа, то необязательно, что мы нашли плагиат - скорее всего это совпадение просто случайность и обу-словленно малой мощностью нашего алфавита. То есть значение к может способствовать игнорированию "шума", но при больших значениях к, мы можем проигнорировать настоящий случай плагиата, поэтому это число нужно выбирать с осторожностью. Пусть наша хэш-функция - это h, а hi , h-2 ... h|p|-fc+ i - последовательность значений хэш-функций, полученных на шаге 1. Рассмотрим некоторые возможные реализации шага 2: • Наивный подход заключается в выборе каждого i-ого из n хэш-значений, но он неустойчив к модификациям кода. (Если мы добавим вначало файла один лишний символ, то получим совершенно другое подмножество хэш-значений после выполнения алгоритма.) • Мы можем назначать меткамиp минимальных хеш-значений (см. [13]), их количество для всех документов будет постоянно. С помощью этого метода нельзя найти частичные копии, но он хорошо работает на файлах примерно одного размера, находит похожие файлы, может применяться для классификации документов. 3Обычно используют хэш-функцию из алгоритма Карпа-Рабина поиска подстроки в строке [8]. 12 © rain.ifmo.ru/cat ©Александр Красс • Манбер [14] предложил выбирать в качестве меток только те хеш-значения, для которых hi = 0 mod p, так останется только n/p меток (объем идентификационного набора для разных файлов будет отличаться, сами метки будут зависеть от содержимого файла). Однако, в этом случае расстояние между последовательно выбранными хеш-значениями не ограничено и может быть велико. В этом случае совпадения, оказавшиеся между метками, не будут учтены. • Метод просеивания (winnowing) [15] не имеет этого недостатка. Алгоритм гарантирует, что если в двух файлах есть хотя бы одна достаточно длинная общая подстрока, то как минимум одна метка в их наборах совпадет. 3.5.2. Метод просеивания Чтобы быть эффективнее других реализаций метода отпечатков, рассматриваемый алгоритм должен гарантировать: • Если у двух токенизированных программ есть общая подстрока, длиной как минимум £, то она будет найдена. • Общие подстроки короче шумового порога игнорируются. Последний пункт гарантируется величиной параметра к (см. выше). Теперь разберемся с первым пунктом. Очевидно, что он будет соблюдаться, если из каждых (4 — к +1) хэш-значаний от идущих непосредственно последовательно подстрок будет выбрана хотя бы одна в качестве метки. Будем продвигать окно размера т = (4 — к + 1) вдоль последовательности Ь\... Ьп, на каждом шаге перемещаем окно на одну позицию вправо. Назначаем меткой минимальное в окне; если таких элементов несколько, то назначается самый правый из них. Рассмотрим пример. Строке аЬгакаОаЬга соответствует последовательность хеш-значений: 12, 35, 78, 3, 26, 48, 55, 12, 35 Пусть к = 2,4 = 4 =Ф т = (£"к + 1) = 3 Выпишем последовательно содержание окон, полученных алгоритмом, жирным шрифтом выделены те хэш-значения, которые мы будем назначать метками: (12, 35, 78), (35, 78, 3), (78, 3, 26), (3, 26, 48), (26, 48, 55), (48, 55, 12), (55, 12, 35) Заметим, что если в двух последовательных окнах хэш-значения от одной и той же подстроки являются минимальными, то меткой назначается только одна из них (нет смысла хранить две последовательные абсолютно одинаковые метки), поэтому получим такой набор меток: 12, 3, 26, 12 Нужно отметить насколько компактен набор идентификационных меток строки. Показателем эффективности алгоритма может служить плотность 0 -доля хэш-значений, выбранных алгоритмом в качестве меток, среди всех хэш-значений документа. Для метода просеивания о 2 13 © rain.ifmo.ru/cat ©Александр Красс (см. [1] или оригинальную статью [15]). Некоторые другие реализации метода отпечатков тоже могут быть изменены так, чтобы соблюдались описанные выше требования, но их о в этом случае будет значительно выше, полученного для метода просеивания. Почему же данные алгоритм корректен? Как мы видим, выбор метки определяется только содержимым окна, такой алгоритм называется локальным. Любой локальный алгоритм выбора меток корректен. Действительно, если в двух файлах есть достаточно большая общая подстрока, то будут и одинаковые окна, а значит, будут назначены одинаковые метки. По ним определим, что в файлах есть совпадения. Достоинства. • Можно организовать один-против-всех. базу данных, ускоряющую проверку • Преимущества токенизированного представления (см. выше). • Общие подстроки, меньше пороговой длины игнорируются, поэтому алгоритм не принимает в расчет малые случайно совпавшие участки кода. • При разбиении совпавшего участка кода на две и более части вставкой одного-нескольких блоков или одиночных операторов, а также перестановкой небольшого количества независимых операторов, функция схожести слабо изменяется. (Длина совпадения должна быть значительно больше некоторой пороговой длины.) • Алгоритм нечувствителен к перестановкам больших фрагментов кода. Недостатки. • Возможноть совпадения токенизированного представления программ, но отсутствия совпадения в исходных кодах программ. • Разбиение совпадения на блоки, вставкой или заменой оператора на похожий (например, for на while), каждый длиной меньше k, ведет к полному игнорированию совпадения. Список литературы [1] Лифшиц Ю., Антипов Д., Ефтифеева О., Котов А., Красс А., Лакунин М., Лысенко Е., Семенников А., Счастливцев Р. Обзор автоматических детекторов плагиата в программах — http://detector.spb.su/bin/view/Sandbox/ProjectOutput, http://logic.pdmi.ras.ru/~yura/detector/. *, 3.5.2. [2] Faidhi, J. A. W. and S. K. Robinson. An Empirical Approach for Detecting Program Similarity within a University Programming Environment. Computers and Education 11(1): pp. 1119 (1987). 2.1. [3] B. S. Baker. On Finding Duplication and Near-Duplication in Large Software Systems. In Proceedings of the second IEEE Working Conference on Reverse Engineering (WCRE), July 1995, pp. 86-95. 2.2. [4] L. Prechelt, G. Malpohl, and M. Philippsen. JPlag: Finding plagiarisms among a set of programs. Technical Report No. 1/00, University of Karlsruhe, Department of Informatics, March 2000. 2.3., 3.3.2. 14 © rain.ifmo.ru/cat ©Александр Красс [5] Heckel, Paul. A Technique for Isolating Differences Between Communications of the ACM 21(4), pp. 264-268 (April 1978). 3.1. Files. [6] X. Huang, R. C. Hardison, AND W. Miller. A space-efficient algorithm for local similarities. Computer Applications in the Biosciences, 6 (1990), pp. 373-381. 3.2. [7] Michael J. Wise. String similarity via greedy string tiling and running Karp-Rabin matching. Dept. of CS, University of Sydney, December 1993. 3.3.1., 3.3.2. [8] R. M. Karp and M. O. Rabin. Efficient randomized pattern-matching algorithms. IBM J. of Research and Development, 31(2):249-260, March 1987. 3.3.2., 2 [9] X. Chen, B. Francia, M. Li, B. McKinnon, A. Seker. Shared Information and Program Plagiarism Detection. IEEE Trans. Information Theory, July 2004, 1545-1550. 3.4. [10] M. Li and P. Vitanyi. An introduction to Kolmogorov complexity and its applications. 2nd Ed., Springer, New York, 1997. 3.4. [11] M. Li, X. Chen, X. Li, B. Ma, P. Vitanyi. The similarity metric. In Proceedings of the 14th Annual ACM-SIAM Symposium on Discrete Algorithms, pp. 863-872, 2003. 3.4. [12] J. Ziv and A. Lempel. A universal algorithm for sequential data compression. IEEE Transactions on Information Theory, 23(1977), 337343. 3.4. [13] N. Heintze. Scalable document fingerprinting. In 1996 USENIX Workshop on Electronic Commerce, 1996. 3.5.1. [14] U. Manber. Finding similar files in a large file system. In Proceedings of the USENIX Winter 1994 Technical Conference, pages 1-10, San Francisco, CA, USA, 1994. 3.5.1. [15] A. Aiken, S. Schleimer, D. Wikerson. Winnowing: local algorithms for document fingerprinting. In Proceedings of ACMSIGMOD Int. Conference on Management of Data, San Diego, CA, June 9-12, pp. 76-85. ACM Press, New York, USA, 2003. 3.5.1., 3.5.2. 15 © rain.ifmo.ru/cat ©Александр Красс