/
bear
/
opencorpora
Обзор
Документация
Войти
/
bear
/
opencorpora
Код
Запросы
0
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
lib/lib_annot.php
724 строки
25 KB
Dmitry Granovsky
fix for issue #890
22 авг 2020, 16:53
22 авг 2020, 16:53
e2c8269
Код
Авторство
О чём код?
<?php require_once('lib_books.php'); class GramFilterType { const FLT_OR = 0; const FLT_AND = 1; } class GramFilter { public $match_type; public $grammemes = array(); public function __construct($gram_str) { if (strpos($gram_str, "|") !== false) { if (strpos($gram_str, "&") !== false) { throw new Exception("Gram filter must be either OR or AND"); } else { $this->match_type = GramFilterType::FLT_OR; $this->_fill_grammemes($gram_str, "|"); } } else { // either no operators (one grammeme) or AND $this->match_type = GramFilterType::FLT_AND; $this->_fill_grammemes($gram_str, "&"); } } private function _fill_grammemes($gram_str, $op) { $this->grammemes = array_map("trim", explode($op, $gram_str)); } } class GramFilterSet { public $filters; public function __construct($at_separated_str) { foreach (explode('@', $at_separated_str) as $item) { $this->filters[] = new GramFilter(trim($item)); } } } class MorphParse { public $lemma_id = 0; public $lemma_text; public $gramlist = array(); // $gramlist is array of arrays with keys 'inner', 'outer', 'descr' public function __construct($lemma_text = "", $gramlist = array(), $lemma_id = 0) { $this->lemma_id = $lemma_id; $this->lemma_text = $lemma_text; $this->gramlist = $gramlist; } public function from_xml_ary($xml_arr) { $lemma_grm = $xml_arr['_c']['l']['_c']['g']; $grm_arr = array(); if (isset ($lemma_grm['_a']) && is_array($lemma_grm['_a'])) { $grm_arr[] = array('inner' => $lemma_grm['_a']['v']); } elseif (is_array($lemma_grm)) { foreach ($lemma_grm as $t) { $grm_arr[] = array('inner' => $t['_a']['v']); } } $this->lemma_id = $xml_arr['_c']['l']['_a']['id']; $this->lemma_text = $xml_arr['_c']['l']['_a']['t']; $this->gramlist = $grm_arr; } public function replace_gram_subset($gram_find, $gram_replace) { // bad but quick $gram_find_str = join(':', $gram_find); $gram_replace_str = join(':', $gram_replace); $old_gram_str = join(':', $this->get_inner_gramlist()); $gramlist_inner = explode(':', str_replace($gram_find_str, $gram_replace_str, $old_gram_str)); // additional info (outer, descr) is lost, seems ok $this->gramlist = array(); foreach ($gramlist_inner as $gr) $this->gramlist[] = array('inner' => $gr); } public function to_xml() { $out = '<v><l id="'.$this->lemma_id.'" t="'.htmlspecialchars($this->lemma_text).'">'; foreach ($this->gramlist as $gram) $out .= '<g v="'.$gram['inner'].'"/>'; $out .= '</l></v>'; return $out; } public function is_unknown() { return sizeof($this->gramlist) == 1 && $this->gramlist[0]['inner'] == 'UNKN'; } public function get_inner_gramlist() { return array_column($this->gramlist, 'inner'); } public function match(GramFilter $filter) { $gramlist = $this->get_inner_gramlist(); foreach ($filter->grammemes as $gr) { $has = ($gr[0] == '!' && !in_array(substr($gr, 1), $gramlist)) || ($gr[0] != '!' && in_array($gr, $gramlist)); if ($has && $filter->match_type == GramFilterType::FLT_OR) { return true; } if (!$has && $filter->match_type == GramFilterType::FLT_AND) { return false; } } return $filter->match_type == GramFilterType::FLT_AND; } } class MorphParseUnknown extends MorphParse { public function __construct($lemma_text) { parent::__construct($lemma_text, array(array('inner' => 'UNKN'))); } } class MorphParseSet { public $token_text; public $parses; private static $gram_descr = array(); private static $RE_CYR_TOKEN = '/^[А-Яа-яЁё][А-Яа-яЁё\-\.\']*$/u'; private static $RE_MIXED_TOKEN = '/^[0-9]+\-[А-Яа-яЁё]+$/u'; // like '2-ого' public function __construct($xml="", $token_text="", $force_unknown=false, $force_include_init=false, $restrict_lemma_id=0) { if ($xml) $this->_from_xml($xml); elseif ($token_text !== "") $this->_from_token($token_text, $force_unknown, $force_include_init, $restrict_lemma_id); else throw new Exception(); } public function to_xml() { $out = '<tfr t="'.htmlspecialchars($this->token_text).'">'; foreach ($this->parses as $parse) $out .= $parse->to_xml(); $out .= '</tfr>'; return $out; } public function filter_by_lemma($lemma_id, $allow) { $newparses = array(); foreach ($this->parses as $parse) if (($parse->lemma_id == $lemma_id) == $allow) $newparses[] = $parse; $this->parses = $newparses; $this->_check_empty(); } public function filter_by_parse_index($index_array) { $newparses = array(); foreach ($this->parses as $i => $parse) if (in_array($i, $index_array)) $newparses[] = $parse; $this->parses = $newparses; $this->_check_empty(); } public function filter_by_gram_set(GramFilter $gram_filter) { $newparses = array(); foreach ($this->parses as $parse) if ($parse->match($gram_filter)) $newparses[] = $parse; $this->parses = $newparses; $this->_check_empty(); } public function match(GramFilter $filter) { foreach ($this->parses as $parse) { if (!$parse->match($filter)) { return false; } } return true; } public function match_set(GramFilterSet $flt_set) { $matched_filters = array(); foreach ($this->parses as $parse) { $matched = 0; foreach ($flt_set->filters as $i => $filter) { if ($parse->match($filter)) { ++$matched; $matched_filters[] = $i; } } if ($matched != 1) { return false; } } return sizeof(array_unique($matched_filters)) == sizeof($flt_set->filters); } public function remove_parse($lemma_id, $grams) { $new_parses = array(); foreach ($this->parses as $parse) { if ($parse->lemma_id != $lemma_id || $parse->get_inner_gramlist() != $grams) $new_parses[] = $parse; } $this->parses = $new_parses; $this->_check_empty(); } public function set_lemma_text($lemma_id, $lemma_text) { if (!$lemma_id || !$lemma_text) throw new Exception(); foreach ($this->parses as $parse) if ($parse->lemma_id == $lemma_id) $parse->lemma_text = $lemma_text; } public function replace_gram_subset($lemma_id, $gram_find, $gram_replace) { if (!$lemma_id || !is_array($gram_find) || !is_array($gram_replace)) throw new Exception(); foreach ($this->parses as $parse) if ($parse->lemma_id == $lemma_id) $parse->replace_gram_subset($gram_find, $gram_replace); $this->_make_parses_unique(); // non-uniqueness can occur here in smart updates } public function merge_from(MorphParseSet $other) { if (sizeof($other->parses) > 0) { if ($this->is_unknown()) $this->parses = array(); } foreach ($other->parses as $parse) { $this->parses[] = $parse; } $this->_make_parses_unique(); } public function is_unknown() { return sizeof($this->parses) == 1 && $this->parses[0]->is_unknown(); } private static function _fill_gram_info($gram_list) { // TODO preload all the grammemes at once and cache $t = array(); foreach ($gram_list as $gr) { if (!isset(self::$gram_descr[$gr['inner']])) { $r = sql_fetch_array(sql_query("SELECT outer_id, gram_descr FROM gram WHERE inner_id='".$gr['inner']."' COLLATE utf8_bin LIMIT 1")); self::$gram_descr[$gr['inner']] = array($r[0], $r[1]); } $t[] = array( 'inner' => $gr['inner'], 'outer' => self::$gram_descr[$gr['inner']][0], 'descr' => self::$gram_descr[$gr['inner']][1] ); } return $t; } private static function _yo_filter($token, $arr) { $token = mb_strtolower($token); if (!preg_match('/ё/u', $token)) return $arr; // so there is a 'ё' $res = sql_pe(" SELECT lemma_id, lemma_text, grammems FROM form2lemma WHERE form_text COLLATE 'utf8_bin' = ? ORDER BY lemma_id, grammems ", array($token)); // return if no difference if (sizeof($res) == sizeof($arr) || !sizeof($res)) return $arr; // otherwise the difference is what we need to omit $out = array(); foreach ($res as $r) $out[] = $r; return $out; } private function _from_xml($xml) { $arr = xml2ary($xml); $this->token_text = $arr['tfr']['_a']['t']; $xml_arr = $arr['tfr']['_c']['v']; if (isset($xml_arr['_c']) && is_array($xml_arr['_c'])) { //the only variant $this->parses[] = new MorphParse(); $this->parses[0]->from_xml_ary($xml_arr); $this->parses[0]->gramlist = self::_fill_gram_info($this->parses[0]->gramlist); } elseif (is_array($xml_arr)) { //multiple variants foreach ($xml_arr as $i => $xml_var_arr) { $this->parses[] = new MorphParse(); $this->parses[$i]->from_xml_ary($xml_var_arr); $this->parses[$i]->gramlist = self::_fill_gram_info($this->parses[$i]->gramlist); } } else throw new Exception(); } private function _from_token($token, $force_unknown, $force_include_init, $restrict_lemma_id=0) { $this->token_text = $token; $cyrillic = false; if ($force_unknown) { $this->parses[] = new MorphParseUnknown($token); } elseif (preg_match(self::$RE_CYR_TOKEN, $token) || preg_match(self::$RE_MIXED_TOKEN, $token)) { $cyrillic = true; $res = sql_pe(" SELECT lemma_id, lemma_text, grammems FROM form2lemma WHERE form_text=? ". ($restrict_lemma_id ? "AND lemma_id = $restrict_lemma_id" : "") ." ORDER BY lemma_id, grammems ", array($token)); if (sizeof($res) > 0) { $var = array(); foreach ($res as $r) { $var[] = $r; } if (sizeof($var) > 1) { $var = self::_yo_filter($token, $var); } foreach ($var as $r) { $gramlist = array(); if (preg_match_all('/g v="([^"]+)"/', $r['grammems'], $matches) > 0) { $require_uc = false; foreach ($matches[1] as $gr) { $gramlist[] = array('inner' => $gr); if ($gr == 'Init') { $require_uc = true; } } } if (!$require_uc || $force_include_init || preg_match('/^[А-ЯЁ]+$/u', $token)) $this->parses[] = new MorphParse($r['lemma_text'], $gramlist, $r['lemma_id']); } } } elseif (preg_match('/^(\p{P}+|=\)+|=\(+)$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'PNCT'))); } elseif (preg_match('/^\p{Nd}+[\.,]?\p{Nd}*$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'NUMB'))); } elseif (preg_match('/^([01][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'TIME'))); } elseif (preg_match('/^(0?[1-9]|[12][0-9]|3[01])[-.](0[1-9]|1[12])[-.]([12][0-9])?[0-9]{2}$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'DATE'))); } elseif (preg_match('/^[\p{Latin}\.-]+$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'LATN'))); } elseif (preg_match('/^[\p{Han}]+$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'HANI'))); } elseif (preg_match('/^[\p{Greek}]+$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'GREK'))); } elseif (preg_match('/^\p{S}+$/u', $token)) { $this->parses[] = new MorphParse($token, array(array('inner' => 'SYMB'))); } if (!$force_unknown && preg_match('/^[IVXLCMDivxlcmdХх]+$/u', $token)) $this->parses[] = new MorphParse($token, array(array('inner' => 'ROMN'))); if (sizeof($this->parses) == 0) { if ($cyrillic) $token = mb_strtolower($token); $this->parses[] = new MorphParseUnknown($token); } foreach ($this->parses as $parse) $parse->gramlist = self::_fill_gram_info($parse->gramlist); } private function _make_parses_unique() { // yes, this looks terrible // but array_unique doesn't work here, both with map(serialize) or without $seen_str = array(); $uniq = array(); foreach ($this->parses as $parse) { $as_xml = $parse->to_xml(); if (!in_array($as_xml, $seen_str)) { $seen_str[] = $as_xml; $uniq[] = $parse; } } $this->parses = $uniq; } private function _check_empty() { if (sizeof($this->parses) == 0) $this->_from_token($this->token_text, true, false); } } function get_sentence($sent_id) { global $config; $r = sql_fetch_array(sql_query("SELECT `check_status`, source FROM sentences WHERE sent_id=$sent_id LIMIT 1")); $out = array( 'id' => $sent_id, 'next_id' => get_next_sentence_id($sent_id), 'prev_id' => get_previous_sentence_id($sent_id), 'status' => $r['check_status'], 'source' => $r['source'] ); //counting comments $r = sql_fetch_array(sql_query("SELECT COUNT(comment_id) comm_cnt FROM sentence_comments WHERE sent_id=$sent_id")); $out['comment_count'] = $r['comm_cnt']; //looking for source name $r = sql_fetch_array(sql_query(" SELECT book_id, old_syntax_moder_id FROM books WHERE book_id = ( SELECT book_id FROM paragraphs WHERE par_id = ( SELECT par_id FROM sentences WHERE sent_id=$sent_id LIMIT 1 ) ) ")); $out['book_id'] = $book_id = $r['book_id']; $out['syntax_moder_id'] = $r['old_syntax_moder_id']; $r = sql_fetch_array(sql_query(" SELECT book_name FROM books WHERE book_id = ( SELECT parent_id FROM books WHERE book_id = $book_id LIMIT 1 ) ")); $out['book_name'] = $r['book_name']; //looking for url $res = sql_query(" SELECT tag_name FROM book_tags WHERE book_id = ".$book_id ); while ($r = sql_fetch_array($res)) { if (substr($r['tag_name'], 0, 4) == 'url:') { $out['url'] = substr($r['tag_name'], 4); break; } } $tf_text = array(); // TODO we'd better preload all grammemes info to save queries $res = sql_query(" SELECT tf_id, tf_text, rev_text FROM tokens LEFT JOIN tf_revisions USING (tf_id) WHERE sent_id=$sent_id AND is_last = 1 ORDER BY `pos` "); $j = 0; //token position, for further highlighting while ($r = sql_fetch_array($res)) { array_push($tf_text, '<span id="src_token_'.($j++).'">'.htmlspecialchars($r['tf_text']).'</span>'); $parse = new MorphParseSet($r['rev_text']); $out['tokens'][] = array( 'tf_id' => $r['tf_id'], 'tf_text' => $r['tf_text'], 'variants' => $parse->parses ); } $out['fulltext'] = typo_spaces(implode(' ', $tf_text), 1); return $out; } function get_previous_sentence_id($sent_id) { return get_adjacent_sentence_id($sent_id, false); } function get_next_sentence_id($sent_id) { return get_adjacent_sentence_id($sent_id, true); } function get_adjacent_sentence_id($sent_id, $next) { // same paragraph $r = sql_fetch_array(sql_query(" SELECT par_id, pos FROM sentences WHERE sent_id = $sent_id LIMIT 1 ")); $par_id = $r['par_id']; $sent_pos = $r['pos']; if (!$par_id) throw new Exception(); $res = sql_query(" SELECT sent_id FROM sentences WHERE par_id = $par_id AND pos ".($next ? ">" : "<")." $sent_pos ORDER BY pos ".($next ? "ASC" : "DESC")." LIMIT 1 "); if (sql_num_rows($res) == 1) { $r = sql_fetch_array($res); return $r['sent_id']; } // next/previous paragraph $r = sql_fetch_array(sql_query(" SELECT book_id, pos FROM paragraphs WHERE par_id = $par_id LIMIT 1 ")); $book_id = $r['book_id']; $par_pos = $r['pos']; if (!$book_id) throw new Exception(); $res = sql_query(" SELECT sent_id FROM sentences s JOIN paragraphs p USING (par_id) JOIN books b USING (book_id) WHERE book_id = $book_id AND p.pos ".($next ? ">" : "<")." $par_pos ORDER BY p.pos ".($next ? "ASC" : "DESC").", s.pos ".($next ? "ASC" : "DESC")." LIMIT 1 "); if (sql_num_rows($res) == 1) { $r = sql_fetch_array($res); return $r['sent_id']; } return 0; } function prepare_parse_indices($flag_array) { // note: $flag_array is 1-based, return values are 0-based $ret = array(); foreach ($flag_array as $i => $val) { if ($val) $ret[] = $i-1; } return $ret; } function sentence_save($sent_id, $comment, $flag, $dict) { // $flag = what morphovariants are checked as possible (array of arrays) // $dict = whether this token has been reloaded from the dictionary (array) check_permission(PERM_DISAMB); if (!$sent_id) throw new UnexpectedValueException(); $res = sql_query(" SELECT tf_id, tf_text, rev_text FROM tokens LEFT JOIN tf_revisions USING (tf_id) WHERE sent_id=$sent_id AND is_last = 1 ORDER BY `pos` "); while ($r = sql_fetch_array($res)) { $tokens[$r['tf_id']] = array($r['tf_text'], $r['rev_text']); } $matches = array(); $all_changes = array(); if (count($flag) != count($tokens)) throw new Exception(); sql_begin(); foreach ($tokens as $tf_id=>$v) { list($tf_text, $base_xml) = $v; //substitute the last revision's xml for one from dictionary if relevant if (isset($dict[$tf_id]) && $dict[$tf_id] == 1) { $parse = new MorphParseSet(false, $tf_text, false, true); } else { $parse = new MorphParseSet($base_xml); } if (sizeof($parse->parses) == 0) throw new Exception(); // flags quantity check if (sizeof($parse->parses) != sizeof($flag[$tf_id])) throw new Exception(); // XXX this is bad since order of parse selection from db // is not guaranteed to be consistent $parse->filter_by_parse_index(prepare_parse_indices($flag[$tf_id])); $new_xml = $parse->to_xml(); if ($base_xml != $new_xml) { //something's changed array_push($all_changes, array($tf_id, $new_xml)); } } if (count($all_changes) > 0) { $revset_id = current_revset($comment); foreach ($all_changes as $v) create_tf_revision($revset_id, $v[0], $v[1]); } sql_query("UPDATE sentences SET check_status='1' WHERE sent_id=$sent_id LIMIT 1"); sql_commit(); } function sentence_save_source($sent_id, $text) { check_permission(PERM_ADMIN); sql_pe("UPDATE sentences SET source = ? WHERE sent_id=? LIMIT 1", array($text, $sent_id)); } function create_tf_revision($revset_id, $token_id, $rev_xml) { $res = sql_pe("SELECT rev_text FROM tf_revisions WHERE tf_id=? ORDER BY rev_id DESC LIMIT 1", array($token_id)); if (sizeof($res) > 0 && $res[0]['rev_text'] === $rev_xml) // revisions are identical, do nothing return true; sql_begin(); sql_pe("UPDATE tf_revisions SET is_last=0 WHERE tf_id=?", array($token_id)); sql_pe("INSERT INTO `tf_revisions` VALUES(NULL, ?, ?, ?, 1)", array($revset_id, $token_id, $rev_xml)); sql_commit(); } function get_context_for_word($tf_id, $delta, $dir=0, $include_self=1) { // dir stands for direction (-1 => left, 1 => right, 0 => both) // delta <= 0 stands for infinity $t = array(); $tw = 0; $left_c = -1; //if there is left context to be added $right_c = 0; //same for right context $mw_pos = 0; static $query1 = NULL; // prepare the 1st query if ($query1 == NULL) $query1 = sql_prepare(" SELECT MAX(tokens.pos) AS maxpos, MIN(tokens.pos) AS minpos, sent_id, source, book_id FROM tokens JOIN sentences USING (sent_id) JOIN paragraphs USING (par_id) WHERE sent_id = ( SELECT sent_id FROM tokens WHERE tf_id=? LIMIT 1 ) "); sql_execute($query1, array($tf_id)); $res = sql_fetchall($query1); $r = $res[0]; $sent_id = $r['sent_id']; $sentence_text = $r['source']; $book_id = $r['book_id']; $maxpos = $r['maxpos']; $minpos = $r['minpos']; // prepare the 2nd query // this is really bad unreadable code, sorry static $query2 = NULL; if ($query2 == NULL) { $q = "SELECT tf_id, tf_text, pos FROM tokens WHERE sent_id = ?"; if ($dir != 0 || $delta > 0) { $q_left = $dir <= 0 ? ($delta > 0 ? "(SELECT IF(pos > $delta, pos - $delta, 0) FROM tokens WHERE tf_id=? LIMIT 1)" : "0") : "(SELECT pos FROM tokens WHERE tf_id=? LIMIT 1)"; $q_right = $dir >= 0 ? ($delta > 0 ? "(SELECT pos+$delta FROM tokens WHERE tf_id=? LIMIT 1)" : "1000") : "(SELECT pos FROM tokens WHERE tf_id=? LIMIT 1)"; $q .= " AND pos BETWEEN $q_left AND $q_right"; } $q .= " ORDER BY pos"; $query2 = sql_prepare($q); } // how many values should we provide? $bound = array($tf_id, $tf_id); if ($delta <= 0) { if ($dir == 0) $bound = array(); else $bound = array($tf_id); } sql_execute($query2, array_merge(array($sent_id), $bound)); foreach (sql_fetchall($query2) as $r) { if ($delta > 0) { if ($left_c == -1) { $left_c = ($r['pos'] == $minpos) ? 0 : $r['tf_id']; } if ($mw_pos) { if ($r['pos'] > $mw_pos) $right_c = $r['tf_id']; if ($right_c && $r['pos'] == $maxpos) $right_c = 0; } } if ($include_self || $r['tf_id'] != $tf_id) $t[$r['tf_id']] = $r['tf_text']; if ($include_self && $r['tf_id'] == $tf_id) { $mw_pos = $r['pos']; } } return array( 'context' => $t, 'mainword' => $tf_id, 'has_left_context' => $left_c, 'has_right_context' => $right_c, 'sentence_id' => $sent_id, 'sentence_text' => $sentence_text, 'book_id' => $book_id ); } function get_search_results($query, $exact_form=true) { $forms = array($query); if (!$exact_form) { include_once('lib_dict.php'); $forms = get_all_forms_by_lemma_text($query); } $r = sql_fetch_array(sql_query(" SELECT COUNT(*) FROM form2tf WHERE form_text IN (".join(",", array_map('sql_quote', $forms)).") ")); $out = array('total' => $r[0], 'results' => array()); $res = sql_query(" SELECT tf_id FROM form2tf WHERE form_text IN (".join(",", array_map('sql_quote', $forms)).") LIMIT 100 "); while ($r = sql_fetch_array($res)) $out['results'][] = get_context_for_word($r['tf_id'], 20); return $out; } ?>