• Jetzt anmelden. Es dauert nur 2 Minuten und ist kostenlos!

stichworte aus einem text ermitteln

mustang

Mitglied
hallo leute,

ich habe eine komplexe Aufgabe, bei der ich allerdings keinen Lösungsansatz finde.
Ich habe eine Website, deren Seiten komplett in einer sql Datenbank gespeichert sind.
Ich möchte nun, dass auf jeder Seite eine Auswahl an anderen Seiten erscheint, die Themenverwandt sind. Am besten über eine Funktion, die aus den vorhandenen Texten Stichworte heraussucht und mit den Stichworten der aktuellen Seite abgleicht. Die Seiten mit den besten Übereinstimmungen sollen dann ausgegeben werden.

Es könnte auch eine Funktion sein, die ich selber immermal ausführe, die die Stichworte in der Tabelle der Seiten einfügt. Das abgleichen danach stelle ich mir nicht allzukompliziert vor.

Ich habe leider noch nicht einmal eine Ahnung mit was ich eine solche Funktion realisieren könnte, geschweige denn wie. Gibt es vielleicht schon etwas ähnliches Vorgefertigtes?
 
richtig. ohne die jetzt selber einzugeben :P

ps.: html tags, etc. sollten natl nicht ausgewertet werden...
 
Zuletzt bearbeitet:
Die Ergebnisse dürften die besten werden, wenn du die Stichworte/Tags von Hand eingibst.

Man müsste eine Funktion schreiben, die nicht nur die Anzahl an gleichen Worten zählt, sondern vor allem Wörter wie "und", "aber", "ich" (und hunderte andere) filtert, da diese die Ergebnisse verfälschen würden.

Gruß
 
richtig... eine suchfunktion habe ich schon hinbekommen. die wird ständig erweitert. immer wenn mir eine sache einfällt, die noch berücksichtigt werden soll, füge ich sie ein.
aber ber der stichwortsuche wars dann echt vorbei... das ist sozusagen der umgekehrte vorgang. ich wünsche mir eine funktion, die ein php-script von hinten nach vorne durchläuft.
nicht stichwort eingeben und textvorgeschlagen bekommen, sondern text eingeben und stichworte vorgeschlagen bekommen^^
 
hm schade. ich dachte da kann ich bisschen code klauen...
naja ich glaube ich werds wie bei der suchfunktion machen. mit der zeit immer wieder neue schritte hinzufügen, die worte aussortiert oder nach neuen sucht...
 
T!P-TOP schrieb:
Also du weißt nicht wie du die Stichwörter aus dem Text herausfiltern kannst?

mustang schrieb:

Das habe ich noch verstanden.

mustang schrieb:
ich wünsche mir eine funktion, die ein php-script von hinten nach vorne durchläuft.
nicht stichwort eingeben und textvorgeschlagen bekommen, sondern text eingeben und stichworte vorgeschlagen bekommen^^

Das hier nicht mehr.

Was genau versuchst du jetzt?

vitus37 schrieb:
Man müsste eine Funktion schreiben, die nicht nur die Anzahl an gleichen Worten zählt, sondern vor allem Wörter wie "und", "aber", "ich" (und hunderte andere) filtert, da diese die Ergebnisse verfälschen würden.

Die werden als „Stoppwörter“ bezeichnet.

- Stoppwort

* * *​

Wenn es darum geht, die „Nähe“ zweier Texte anhand der Anzahl/Verteilung enthaltener Wörter zu ermitteln, kann ich erstens nicht ausschließen, dass entsprechende Methoden bereits existieren und würde zweitens diesen Ansatz verfolgen:

  1. Wortverteilung ermitteln. Etwa: strip_tags + Frequency of words in a text + Stoppwörter eliminieren
  2. „Nähe“ ermitteln. Das ist der komplizierte Teil und ich tendiere spontan dazu, zu behaupten, dass du über das schlaue Setzen und den Vergleich von Tags ein besseres Ergebnis bekommen wirst. Du brauchst zwei Tabellen, um die Infos zu halten:
    Code:
    # Tabelle mit den Originaltexten
    
    [text]
    id content
    
    # Fremdschlüssel auf text, frequencies hält serialisiertes Array mit
    # Wort-Häufigkeiten dieses Texts (quasi ein Cache)
    
    [text_word_frequency] 
    text_id frequencies
    
    # Zwei Fremdschlüssel auf text, conformity hält die „Nähe“ der beiden Texte
    
    [text_conformity]
    text1_id text2_id conformity

    Dazu eine Funktion, die die Übereinstimmung, also die „Nähe“, berechnet. Ansatz:
    1. Vergebe a Punkte für jedes Wort, das in beiden Texten vorkommt.
    2. Vergebe pro übereinstimmendem Wort b Punkte für die Anzahl an Übereinstimmungen.
    So erhältst du eine Gesamtpunktzahl. Je höher diese ist, desto höher die Übereinstimmung.

    Diesen Algorithmus führst du dann für jeden Text mit jedem Text durch und speicherst das Ergebnis in text_conformity.
 
jap so hab ich mir das inzwischen auch gedacht. also häufigkeit der wörter im text ermitteln - mit unterschiedlichen funktionen natürlich und jenachdem punkte vergebenen.
danach die 10 wichtigsten abspeichern und bei einer abfrage abgleichen.
naja wird ein bisschen arbeit.. aber danke :-) wenns irgendwann mal fertig ist, poste ich es mal :-)

ps.:
ich wünsche mir eine funktion, die ein php-script von hinten nach vorne durchläuft.
nicht stichwort eingeben und textvorgeschlagen bekommen, sondern text eingeben und stichworte vorgeschlagen bekommen^^
das war spaß^^
zur erklärung: ich hab doch schon eine suchfunktion programmiert, die texte nach stichwörtern durchsucht und im prinzip brauche ich jetzt für das ermitteln von stichwörtern genau das gegenteil. also aus einem text stichwörter herausfiltern. wäre halt cool, wenn man ein php script umkehren könnte^^ wie gesagt war nur spaß.
 
Mein Script („Frequency of words in a text“) hat übrigens einen kleinen Fehler. Der reguläre Ausdruck findet das erste Wort des Texts nicht. Richtig ist er (hoffentlich) so:

Code:
/(?:^|(.+?))(?:(\p{L}{3,})|$)/su

Was zudem eigentlich noch notwendig wäre, ist eine Form von Stemming, damit beispielsweise „König“ und „Königs“ nicht zwei Einzelwörter ergeben. Das ist eine recht harte Nuss, fürchte ich. Eine erste Idee wäre es, nicht komplette Wörter abzugleichen, sondern nur die ersten 80 % oder so der Zeichen des Begriffs.

- Stemming
 
Setup:

  1. Unterverzeichnisse data und texts anlegen.
  2. Stoppwörter von hier (http://solariz.de/649/deutsche-stopwords.htm) in data/stopwords.txt kopieren, dabei mit Semikolon beginnende Zeilen entfernen.
  3. Diverse Texte mit Endung *.txt in texts anlegen. Als Quellen können etwa Nachrichtenseiten dienen.

Beispielausgabe: http://people.ermshaus.org/marc/tmp/html.de/textcomp-example.html

PHP:
<?php
/**
 * Copyright 2011 Marc Ermshaus
 *
 * This code is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this code. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * 
 * @version 2011-Feb-13
 * @author Marc Ermshaus <http://www.ermshaus.org/>
 */
class TextComparator_Item
{
    const FROM_FILE  = 0x01;
    const STRIP_HTML = 0x02;

    /** @var array */
    protected $stopwords = array();

    /** @var int */
    protected $minimumTokenLength = 3;

    /** @var array */
    protected $tokens = array();

    /**
     *
     * @param string $input
     * @param int $flags
     */
    public function loadText($input, $flags = null)
    {
        if ($flags === null) {
            $flags = self::FROM_FILE | self::STRIP_HTML;
        }

        if ($flags & self::FROM_FILE) {
            $input = file_get_contents($input);
        }

        if ($flags & self::STRIP_HTML) {
            $input = strip_tags($input);
        }

        $this->tokenize($input);
        $this->groupByToken();
    }

    /**
     *
     * @param string $input
     */
    protected function tokenize($input)
    {
        // Everything consisting of x+ letters shall be indexed as a word
        $input = mb_strtolower($input);
        $cleaned = trim(preg_replace('/(?:^|(.+?))(?:(\p{L}{'
                . $this->minimumTokenLength . ',})|$)/su', ' $2', $input));

        $this->tokens = explode(' ', $cleaned);

        $this->tokens = array_flip($this->tokens);

        // Remove stopwords
        foreach ($this->tokens as $key => $unused) {
            if (isset($this->stopwords[$key])) {
                unset($this->tokens[$key]);
            }
        }
    }

    /**
     * 
     */
    protected function groupByToken()
    {
        $grouped = array();

        foreach ($this->tokens as $token => $unused) {
            if (isset($grouped[$token])) {
                $grouped[$token]++;
            } else {
                $grouped[$token] = 1;
            }
        }

        $this->tokens = $grouped;
    }

    /**
     *
     * @return array
     */
    public function getStopwords()
    {
        return $this->stopwords;
    }

    /**
     *
     * @param array $stopwords
     */
    public function setStopwords(array $stopwords)
    {
        $this->stopwords = $stopwords;
    }

    /**
     *
     * @return int
     */
    public function getMinimumTokenLength()
    {
        return $this->minimumTokenLength;
    }

    /**
     *
     * @param int $minimumTokenLength
     */
    public function setMinimumTokenLength($minimumTokenLength)
    {
        $this->minimumTokenLength = (int) $minimumTokenLength;
    }

    /**
     *
     * @return array
     */
    public function getTokens()
    {
        return $this->tokens;
    }
}

/**
 * 
 * @version 2011-Feb-13
 * @author Marc Ermshaus <http://www.ermshaus.org/>
 */
class TextComparator
{
    /** @var array */
    protected $stopwords = array();

    /** @var array */
    protected $pointCache = array();

    /**
     *
     * @param array $stopwords 
     */
    public function __construct(array $stopwords = array())
    {
        $this->stopwords = $stopwords;
    }

    /**
     *
     * @param TextComparator_Item $original
     * @param TextComparator_Item $compareTo
     * @return int
     */
    public function compare(TextComparator_Item $original,
            TextComparator_Item $compareTo)
    {
        $points = 0;        

        // Calculated points for two texts will be cached internally because the
        // cache uses hardly any space, so it does no harm to do so
        $cacheHit = false;
        foreach ($this->pointCache as $cacheEntry) {
            if ($cacheEntry['original'] === $original
                    && $cacheEntry['compareTo'] === $compareTo
            ) {
                $points = $cacheEntry['points'];
                $cacheHit = true;
                break;
            }
        }

        if (!$cacheHit) {
            $originalTokens = $original->getTokens();
            $compareToTokens = $compareTo->getTokens();

            foreach ($originalTokens as $token => $amount) {
                if (isset($compareToTokens[$token])) {
                    /*$points += 3;*/
                    //$points += (($amount < $textCompareTo[$token]) ? $amount : $textCompareTo[$token]);
                    $points += $compareToTokens[$token];
                }
            }

            $points = (int) round($points * 1000 / count($originalTokens));

            $this->pointCache[] = array(
                'original'  => $original,
                'compareTo' => $compareTo,
                'points'    => $points
            );
        }

        return $points;
    }

    /**
     * Factory method to return a TextComparator_Item instance with sane
     * defaults set
     *
     * @param string $input
     * @param int $flags Combination of TextComparator_Item flags
     * @return TextComparator_Item
     */
    public function createNewItem($input, $flags = null)
    {
        $item = new TextComparator_Item();
        $item->setStopwords($this->stopwords);
        $item->loadText($input, $flags);
        
        return $item;
    }
}



// Setup
error_reporting(-1);
mb_internal_encoding('UTF-8');
header('Content-Type: text/html; charset=UTF-8');

// Load stopwords. We are using hashed arrays for all expensive calculations
// as they allow for fast lookup (O(1)). This should be a classic time-memory
// tradeoff
$stopwords = array_flip(file('./data/stopwords.txt',
        FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));

// Initialize comparator instance
$tc = new TextComparator($stopwords);

// Holds all loaded texts
$allTexts = array();

// Load and tokenize all texts in subdirectory
$texts = glob('./texts/*.txt');
$matrix = array();

foreach ($texts as $file) {
    $allTexts[pathinfo($file, PATHINFO_FILENAME)] = $tc->createNewItem($file);
}

// Compare all texts in subdirectory with each other
foreach ($allTexts as $file1 => $t1) {
    foreach ($allTexts as $file2 => $t2) {
        if ($file1 !== $file2) {
            $matrix[$file1][$file2] = $tc->compare($t1, $t2);
        } else {
            $matrix[$file1][$file2] = '-';
        }
    }
}



// Render the results
echo '<table border="1">';

echo '<tr>';
echo '<th>Text/CompareTo</th>';
foreach ($matrix as $file1 => $cols) {
    foreach ($cols as $file2 => $unused) {
        echo '<th style="font-size: 10px;font-weight:normal;width: 60px;">'
                . pathinfo($file2, PATHINFO_FILENAME) . '</th>';
    }
    break;
}
echo '</tr>';

foreach ($matrix as $file1 => $cols) {
    echo '<tr>';
    echo '<th>' . $file1 . '</th>';
    foreach ($cols as $file2 => $points) {
        echo '<td style="width: 60px;">' . $points . '</td>';
    }

    echo '</tr>';
}

echo '</table>';

if (function_exists('xdebug_time_index')) {
    echo '<p>Current execution time: ' . round(xdebug_time_index(), 2) . ' s</p>';
}



// Show top 3 similar texts for each text
foreach ($allTexts as $file1 => $text1) {
    $points = array();

    foreach ($allTexts as $file2 => $text2) {
        if ($file1 !== $file2) {
            $points[$file2] = $tc->compare($text1, $text2);
        }
    }

    arsort($points);

    $tmp = array_slice($points, 0, 3);
    echo '<p>Similar texts for ' . $file1 . ':</p>';
    echo '<ol>';
    foreach ($tmp as $file => $points) {
        echo '<li>' . $file . ' (' . $points . ')</li>';
    }
    echo '</ol>';
}

if (function_exists('xdebug_time_index')) {
    echo '<p>Current execution time: ' . round(xdebug_time_index(), 2) . ' s</p>';
}

Aktueller Algorithmus zur Berechnung der Punktzahl:

Code:
Token := Wort aus einem Text, das notwendige Eigenschaften
    erfüllt, um berücksichtigt zu werden (zum Beispiel: mindestens 3 Zeichen lang)
t1 := Originaltext
t2 := Text, dessen Ähnlichkeit zu t1 ermittelt werden soll

1. Vergebe für jedes Token aus t1, das in t2 vorkommt, n Punkte.
    n ist die Anzahl der Vorkommen dieses Tokens in t2.
2. Teile die Gesamtpunktzahl durch die Anzahl der Tokens in t1

In Schritt 2 wird noch mit 1000 multipliziert, um Ganzzahlen zurückzubekommen.

Effekte:

  • Ein langer t2 erhält eine tendentiell höhere Punktzahl, da die Chance auf Entsprechungen größer ist.
  • Ein kurzer t2 erhält generell niedrige Punktzahlen, da er nur wenig Tokens enthält.

Verbesserungsvorschläge zum Algorithmus sind sehr willkommen.

Edit: Irgendwo steckt übrigens offenbar ein Bug.
 
Zuletzt bearbeitet:
Zurück
Oben