Beliebteste Beiträge

Ich weiß nicht, ob’s jemand schon bemerkt hat^^, aber seit letzter Woche findet sich in meiner Sidebar ein Abschnitt mit den beliebtesten Beiträgen, und wie das bei mir so üblich ist, bemühe ich da nicht groß ein Plugin, sondern hab das selber gebastelt. Und da es vielleicht den einen oder anderen auch interessieren könnte, gibt’s diesen Beitrag… Verbesserungsvorschläge sind natürlich willkommen.

Die Frage war also: Welche Kriterien sollen für die Beliebtheit gelten, und wie krieg ich das mit einer Datenbankabfrage hin? Die Kriterien, an die ich gedacht habe, sind:

  • Aufrufzahlen der Beiträge, gezählt mit WP-PostViews1;
  • Anzahl der Kommentare;
  • Länge der Kommentare2 – kurze à la „toller Beitrag“ weniger stark gewichten;
  • Alter der Beiträge – neue Beiträge mit vielen Aufrufen oder Kommentaren sollen bevorzugt werden – bzw. Aufrufe pro Tag.

Wer ein Plugin zur Beitragsbewertung einsetzt, mag dessen Werte natürlich auch noch mit einschließen; entsprechendes gilt für ein anderes Statistik-Plugin als WP-PostViews. Und natürlich gibt’s auch etliche „Popular Posts“-Plugins, doch die wenigsten davon berücksichtigen mehr als die Kommentaranzahl – und eine maßgeschneiderte Lösung ist mir eben auch lieber…

Generell stellt sich noch die Frage nach der Gewichtung – wenn bei euch fast jeder Besucher kommentiert, müsst ihr die Kommentaranzahl nicht deutlich stärker als die Aufrufe gewichten, und man kann natürlich eigene Schwerpunkte setzen. Und man muss die einzelnen Kriterien auch nicht einfach mit einem Faktor addieren, sondern kann etwa alle anderen durch das Alter teilen u.v.a.m. Meine Gewichtung seht ihr unten im/nach dem Code.

Nun könnte man dazu mehrere Datenbankabfragen machen, die alle der Post-ID, also der eindeutigen Nummer eines Beitrags, ihren jeweiligen Wert zuordnen, und diese Arrays dann mittels PHP verrechnen und gewichten – ich hab’s hingegen in eine einzige MySQL-Abfrage gequetscht (war auch ’ne kleine Übung in Sachen MySQL… und ist sicher nicht perfekt…); mag sein, dass einzelne Abfragen schneller wären, aber nach etwas Optimierung hatte ich meine Query so weit, dass sie nicht viel länger dauert als das „Einsammeln“ der Kommentarlängen (die bei >10000 Kommentaren/Trackbacks schon ein bisschen dauert), und vermutlich geht’s nicht viel schneller – konkret: von ca. 1½ auf 0,3 Sekunden.

(Aus der Zeit vor der Optimierung stammt auch die kleine Cache-Funktionalität, die ihr gleich im Code seht; bei Verwendung eines Datenbank-Caches oder Verlassen auf den Query Cache der Datenbank selbst wäre die auch weniger wichtig.)

Hier also der Code3:

function ag_popular_posts($num=10) {
    global $wpdb;
    $lastupdate = (int) get_option("ag_popular_lastupdate",0);
    $output = '';
    if (time()-$lastupdate > 1800) {
        $exclude = "
            AND ID NOT IN (78,70)
            AND ID NOT IN (
                SELECT object_id FROM $wpdb->term_relationships
                WHERE $wpdb->term_relationships.term_taxonomy_id IN (92,384)
            )"
;
        $posts = $wpdb->get_results("
            SELECT ID,post_date,post_title,post_name,comment_count,
                $wpdb->postmeta.meta_value AS views,
                TO_DAYS(NOW())-TO_DAYS(post_date) AS age,
                comment_length,
                ($wpdb->postmeta.meta_value/2
                 +comment_count*50
                 +comment_length/comment_count*5
                 +(TO_DAYS(post_date)-TO_DAYS('2006-09-06'))*5) AS popularity
            FROM $wpdb->posts,$wpdb->postmeta,(
                SELECT comment_post_id, SUM(LENGTH(comment_content)) AS comment_length
                FROM $wpdb->comments
                WHERE comment_approved='1'
                GROUP BY comment_post_id) AS thecomm
            WHERE $wpdb->posts.ID = $wpdb->postmeta.post_id
            AND $wpdb->posts.ID = thecomm.comment_post_id
            AND $wpdb->postmeta.meta_key = 'views'
            AND comment_count > 0
            AND post_status = 'publish'
            AND post_type = 'post'
            AND post_password = ''
            $exclude
            ORDER BY popularity DESC, post_date
            LIMIT $num"
);
        if ($posts) {
            $output="<ul>";
            $pop0=0;
            foreach ($posts as $p) {
                if ($pop0==0) $pop0=$p->popularity;
                $output.='<li><a href="'.get_permalink($p->ID).'" title="'.
                        $p->comment_count.' Kommentare mit durchschnittlich '.
                        ($p->comment_count==0?'0':number_format($p->comment_length/$p->comment_count,1)).' Zeichen, '.
                        number_format($p->views).'x aufgerufen, '.
                        number_format($p->age).' Tage alt'.
                    '">'.
                    $p->post_title."</a> ".
                    "<small>(".number_format($p->popularity/$pop0*100,0).'%'.")</small>".
                    "</li>\n";
            }
            $output.="</ul>\n";
        }
        update_option("ag_popular_lastupdate",time());
        update_option("ag_popular_cached",$output);
    } else {
        $output = get_option("ag_popular_cached",'');
    }
    if ($output!='') {
        echo "<li id=\"side-popular\">\n".
            "<h2>Beliebteste Beitr&auml;ge</h2>\n".
            $output.
            "</li>\n";
    }
}
 

Also gehen wir den Code mal durch:

Wir beginnen mit der Abfrage, ob die Liste aktualisiert werden soll – das Intervall liegt hier bei 1800 Sekunden, also einer halben Stunde.

Um bestimmte Beiträge auszuschließen, kann man ihre ID wie in der ersten Zeile des ausgelagerten $exclude-Statements angeben – bei mir ist es 78 = die Liste der Musik-Zitate, die aufgrund ihrer hohen Abrufzahlen unangefochten an Platz 1 wäre, aber nicht so interessant ist, dass ich sie hier in der Beliebtheitsliste hervorheben möchte, und die 70 = die damalige Technorati4-Ketten-Aktion, die nur bei manchen Gewichtungsversuchen in der Top 10 wäre –, oder alternativ z.B. auch post_name.

Eine ganze Kategorie oder ein ganzes Tag kann man mit dem zweiten AND ID NOT IN-Statement ausschließen. Dazu muss man die term_taxonomy_id der/des fraglichen Kategorie/Tag herausfinden – indem man in der Tabelle wp_terms anhand des Namens die term_id findet und mit dieser in wp_term_taxonomy nachschlägt (oder irgendwelche WP-Funktionen verwendet, mit get_term_by() könnt’s vielleicht gehen) – und am Ende einsetzen; bei mir will ich alle Quiz-Beiträge (92) und nur englische (384) ausschließen (bzw. in englischer Ansicht alle deutschen, da steht dann 385).

Weiter geht’s mit der eigentlichen SELECT-Abfrage. In deren ersten vier Zeilen stehen ein paar für die Ausgabe benötigte Angaben, danach in Klammern (...) AS popularity der eigentliche Beliebtheitswert.

Wie ihr (vielleicht) seht, gewichte ich die Seitenaufrufe mit 0,5, die Kommentaranzahl mit 50, die durchschnittliche Kommentarlänge ebenso wie die Neuheit des Beitrags (Tage seit Blog-Start, was ihr für euch natürlich ändern müsst, siehe gelb hinterlegtes Datum – wenn ihr stattdessen das Alter, also die Tage zwischen Beitrag und heute, verwenden wollt, nehmt TO_DAYS(NOW())-TO_DAYS(post_date)) mit 5, was sich nach etwas Herumspielen als recht passend für mein Blog gezeigt hat.

(Die kurz darüber deklarierten Felder (AS views, AS age) kann man hier übrigens nicht mit den neuen Namen nehmen, da meckert MySQL – also eben nochmal komplett einsetzen.)

Die FROM-Zeile enthält neben der wp_posts-Tabelle noch wp_postmeta, weil dort die Aufrufzahlen von WP-PostViews liegen – braucht ihr nicht, wenn ihr diese Werte nicht verwendet – und das untergeordnete SELECT für die Kommentarlänge, genauer: die Summe der Länge aller Kommentare mit derselben post_id (inkl. Trackbacks; diese könnte man mit AND comment_type='' vor GROUP ausschließen).

Die WHERE– und die erste AND-Zeile verknüpfen die beteiligten Tabellen über die Post-ID, die nächste wählt die WP-PostViews-Werte aus (die eben „views“ heißen) und der Rest beschränkt das ganze auf geeignete Beiträge.

Danach wird die Ausgabe zusammengebastelt, insb. in der foreach-Schleife, die über die Beiträge läuft, wo der Link samt Tooltip (title) mit statistischen Infos und einem Prozentwert der Beliebtheit mit 100% für den 1. Platz zusammengeklebt wird.

Wenn diese Ausgabe zusammengeklöppelt ist, wird sie noch schnell in die Datenbank für unseren kleinen Cache geschrieben, von wo aus sie (im else-Zweig) innerhalb der nächsten halben Stunde gelesen wird. Und zu guter Letzt muss das ganze natürlich noch (mit Überschrift) ausgegeben werden.

So, das war jetzt jede Menge Code. Wer Verständnis- oder sonstige Fragen, Anmerkungen, Stellungnahmen, Lob, Kritik, Verbesserungsvorschläge, Beleidigungen, Ideen oder Anregungen hat, nur raus damit – Moment, auf eine dieser Arten von Äußerungen solltet ihr besser verzichten.^^

 

  1. mit dem Nachteil, dass ich dieses anfangs alle Zugriffe inkl. Suchmaschinen zählen ließ, aber seitdem ich auch WP Super Cache nutze nur mittels JavaScript – wodurch die Zähler mittlerweile weit weniger stark steigen… []
  2. auf Anregung von Julia, dankeschön []
  3. ich hoffe, ich hab beim Wieder-Entfernen meiner Sprachumschaltung keinen Fehler eingebaut… []
  4. Berners-Lee hab es selig []

8 Kommentare
1 Trackback

  1. V

    Kommentar: Ich bin beeindruckt, weil ich keine Ahnung habe, was der Code macht. Aber Gratulation zu solch multibler Begabung (texten, bildern, coden und überhaupt)!

  2. U

    ob ich mich trauen sollte, den mal testweise in ein Sidebar-Widget einzubauen..? :)

  3. R

    Wirklich interessanter Beitrag, hinter dem sicherlich viel Arbeit steckt. Auch ich suche schon seit längerem nach ein Plugin-Freien Lösung und bin hier über dein Beispiel gestoßen. Ein paar Fragen und Anregungen hätte ich doch noch:

    1.) Könnte man die Funktion ag_popular_posts nicht in die functions.php des Themes auslagern und dann in der sidebar nur noch die funktion selbst auszurufen? Welche Teil müsste man auslagern und welchen Teil dann in der Sidebar unterbringen?

    2.) Ist es nicht möglich auf das Plugin WP-Postviews zu verzichten? Ich denke da an die Möglichkeit die API des von den meisten verwendeten WordPress.com Stats Plugins zurückzugreifen. Über stats_get_csv kann man da IMHO die meisten angeklickten Beiträge auslesen. Vielleicht guckst du es Dir hier einfach mal an. So könnte ich mir ein weiteres Plugin sparen. (Und du vielleicht auch)

    • c

      zu 1.) Ich hab’s offenbar vergessen, oben zu erwähnen: Die ganze Funktion ist dazu gedacht, in der functions.php zu landen (auch wenn sie natürlich auch in die sidebar.php passt). Aufgerufen wird sie so oder so z.B. mit ag_popular_posts(12); für 12 Beiträge.

      zu 2.) Ich benutze WP-PowtViews halt schon lange, deswegen verwende ich’s auch hier. (Jetzt erst damit zu beginnen, es wegen der beliebten Beiträge zu nutzen, würde den älteren Beiträgen eh nicht gerecht.) Natürlich könnte man auch WP.com-Stats „anzapfen“ – ('postviews','days=-1') dürfte die gewünschten Zahlen liefern –, aber dann kann man natürlich nicht im Rahmen einer einzelnen Datenbankabfrage die Popularität errechen, sondern müsste alles nachträglich miteinander verrechnen. Dürfte auch nicht besonders schwer sein, ist halt ein bisschen mehr Code als oben.

      Wäre durchaus auch für mich eine Überlegung wert, WP-PostViews rauszuschmeißen… nur ist das dann ein (wenn auch zeitweise gecacheter) Zugriff auf einen (möglicherweise langsamen) externen Server, wenn ich unter den Beiträgen die jeweilige Views-Zahl ausgeben will, und kein Griff in die eigene Datenbank. Andererseits würde es etwas Serverlast sparen, weil der separate JavaScript-Aufruf beim Anschauen (innerhalb der WP-Super-Cache-Datei) wegfiele… hmm, mal sehen…

      • R

        Stimmt, darüber habe ich auch noch nicht nachgedacht, das die Zählung natürlich erst mit Installation des Plugins beginnt. Ich würde mich freuen wenn du bezüglich der Integration von wordpress.com Stats etwas Zeit erübrigen könntest. Ich bleibe bis dahin weiter neugierig und danke schonmal für die schnellen Antworten.

        • c

          Wobei man die WP.com-Stats-Werte sicher auch WP-PostViews-kompatibel importieren könnte. :)

          Nun, mal sehen. Dass das Plugin eine schöne stats_get_csv-Funktion anbietet (die sogar selbst cacht), wusste ich vorher noch gar nicht – somit eignet es sich wirklich, um WP-PostViews zu ersetzen (wenn die WP-Server schnell genug sind), und ich werd mich demnächst mal drum kümmern…

Schreib einen Kommentar

Alle Angaben sind freiwillig. Die E-Mail-Adresse wird nicht veröffentlicht oder weitergegeben.

  • Moderation: Wer zum ersten Mal kommentiert, dessen Kommentar muss manuell von mir freigeschaltet werden.
  • Benimm dich! Keine Beleidigungen, keine rechtswidrigen Inhalte u.s.w.! Sollte eigentlich selbst­verständlich sein, oder...?
  • Webseite: Nichts gegen Blogs mit Werbung, aber rein kommerzielle Links sind unerwünscht und werden gelöscht. Reine Spam-Kommentare natürlich auch.
  • Erlaubte HTML-Tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <sub> <sup> <big> <small> <u>