<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Development auf Nicky Reinert</title><link>https://nickyreinert.de/tags/development/</link><description>Blog &amp; Projekte von Nicky Reinert (Institut für digitale Herausforderungen): Webentwicklung &amp; Software Development, KI, SEO &amp; Analytics, Hosting &amp; DevOps, WordPress &amp; Hugo, Tools &amp; Projekte, Datenschutz und digitale Kultur</description><generator>Hugo 0.167.0</generator><language>de</language><managingEditor>Nicky Reinert</managingEditor><webMaster>Nicky Reinert</webMaster><copyright/><lastBuildDate>Thu, 08 Oct 2020 00:00:00 +0000</lastBuildDate><atom:link href="https://nickyreinert.de/tags/development/index.xml" rel="self" type="application/rss+xml"/><item><title>Automatisierte WordPress Installation</title><link>https://nickyreinert.de/2020/2020-10-08-automatisierte-wordpress-installation/</link><pubDate>Thu, 08 Oct 2020 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2020/2020-10-08-automatisierte-wordpress-installation/</guid><description>Et voilá. Ein BASH Script, um WordPress automatisiert über die Kommandozeile zu installieren, inklusive Theme und beliebigen Plugins sowie - wenn benötigt - zufälligen Beiträgen, Kategorien, Tags und Bildern.
Warnung! Du solltest geübt im Umgang mit der Shell, Bash und WordPress sein und unbedingt …</description><content:encoded><![CDATA[<p>Et voilá. Ein BASH Script, um WordPress automatisiert über die Kommandozeile zu installieren, inklusive Theme und beliebigen Plugins sowie - wenn benötigt - zufälligen Beiträgen, Kategorien, Tags und Bildern.</p>
<h2 id="warnung">Warnung!</h2>
<p>Du solltest geübt im Umgang mit der Shell, Bash und WordPress sein und unbedingt <strong>Backups</strong> nutzen. Das Script verwendet, wenn der entsprechende Parameter gesetzt ist, den Befehl &ldquo;rm -rf&rdquo;, um einen Ordner komplett zu <strong>löschen</strong>. Dieser Schritt lässt sich nicht widerrufen. Außerdem wird, sofern per Parameter gefordert, eine Datenbank mit &ldquo;DROP DATABASE&rdquo; komplett entfernt. Wenn du das Script auf eine existierende WordPress-Installation oder Datenbank anwendest, können existierende Daten <strong>überschrieben</strong> werden!</p>
<h2 id="funktionsweise">Funktionsweise</h2>
<p>Das Script wird über die Kommandozeile aufgerufen und nutzt die [WordPress Kommandozeilen-Benutzeroberfläche,](um <strong>WordPress</strong> zu installieren sowie samt <strong>Themes</strong> und <strong>Plugins</strong> einzurichten. Du kannst außerdem beliebig viele <strong>Beiträge</strong> anlegen. Die Inhalte für die Beiträge stammen von einem Online-Dienst für zufällige englische Texte: [metaphorpsum.com/paragraphs](Außerdem werden zufällige <strong>Textauszüge</strong> (Excerpts) sowie <strong>Schlagwörter</strong> (Tags) und sogar <strong>Bilder</strong> jedem Beitrag hinzugefügt. Beiträge werden außerdem Kategorien zugeordnet. Diese werden ebenfalls <strong>zufällig</strong> bis zu einer Tiefe von 2 Ebenen erzeugt.</p>
<h2 id="installation">Installation</h2>
<p>Du findest den Sourcecode auf github: 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/nickyreinert/AutomatedWordPressInstallation?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://github.com/nickyreinert/AutomatedWordPressInstallation</a>


.</p>
<p>Nach der Installation kopierst du die Datei settings-template zu .settings, um in dieser Datei die Einstellungen für deine WordPress-Installation vorzunehmen. Du startest den Prozess mit <strong>bash start.sh</strong> oder setzt den Ausführen-Flag für bash.sh um diese direkt auszuführen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">chmod u+x bash.sh
</span></span><span class="line"><span class="ln">2</span><span class="cl">./bash.sh
</span></span></code></pre></div><h2 id="zufällige-inhalte">Zufällige Inhalte</h2>
<p>Das Script ermöglicht das Anlegen von beliebig vielen zufälligen Beiträgen in beliebig vielen zufälligen Kategorien. Was genau dabei passiert, beschreibe ich hier kurz:</p>
<p>Zunächst wird eine Kategorie erzeugt. Dazu wird von der folgenden API ein zufälliges Wort abgerufen: 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://random-word-api.herokuapp.com/word?swear=0&amp;number=&amp;utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://random-word-api.herokuapp.com/word?swear=0&amp;number=</a>


</p>
<p>Dieser Kategorie wird dann ein Beitrag wird mit einer beliebigen Anzahl von <strong>Absätzen</strong> von dieser API zugeordnet: 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://metaphorpsum.com/paragraphs/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">http://metaphorpsum.com/paragraphs/</a>


. Der Beitrag erhält einen zufälligen Titel (von dort: 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://metaphorpsum.com/sentences/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">http://metaphorpsum.com/sentences</a>


), einen Textauszug (gleiche API), sowie eine beliebige Anzahl zufälliger Tags (Schlagwörter), ebenfalls von 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://random-word-api.herokuapp.com/word?swear=0&amp;number=&amp;utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">random-word-api.herokuapp.com</a>


. Dann werden für diesen Beitrag Revisionen erzeugt - auch diese in beliebiger Höhe. Abschließend erhält jeder Beitrag ein zufälliges Featured Image sowie ein weiteres zufälliges Bild am Ende des Fließtextes.</p>
<p>Je nachdem, wieviele Beiträge und Kategorien bzw. Sub-Kategorien eingerichtet werden soll, wird dieser Prozess natürlich wiederholt.</p>
<p><em>(Bisher werden die Inhalte leider nur auf Englisch eingefügt)</em></p>
<h2 id="einstellungen">Einstellungen</h2>
<p>Alle Einstellungen befinden sich in der Datei <strong>.settings</strong> und werden im Folgenden beschrieben. Der PROJECT-Parameter kann im weiteren Verlauf als Platzhalter verwendet werden und sollte keine Leerzeichen oder Sonderzeichen enthalten:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">PROJECT=myWordPressBlog
</span></span></code></pre></div><h3 id="datenbank">Datenbank</h3>
<p>Du kannst eine neue Datenbank anlegen (CREATE_DB) oder auf eine existierende Datenbank zurückgreifen.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">DB_DROP=yes|no
</span></span><span class="line"><span class="ln">2</span><span class="cl">CREATE_DB=yes|no
</span></span><span class="line"><span class="ln">3</span><span class="cl">DB_ROOT=root
</span></span><span class="line"><span class="ln">4</span><span class="cl">DB_ROOT_PASSWORD=secret
</span></span></code></pre></div><p>Willst du eine frische Datenbank anlegen, solltest du die alte Datenbank mit DB_DROP=yes löschen. Soll eine neue Datenbank angelegt werden, müssen die Zugangsdaten für einen privilegierten Nutzer angegeben werden: Mit den folgenden Parameter legst du fest, wie WordPress auf die Datenbank zugreift:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">DB_HOST=127.0.0.1
</span></span><span class="line"><span class="ln">2</span><span class="cl">DB_NAME=&#34;${PROJECT}&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">DB_PREFIX=&#34;wp_&#34;
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">ADD_DB_USER=yes|no
</span></span><span class="line"><span class="ln">6</span><span class="cl">DB_USER=&#34;${PROJECT}&#34;
</span></span><span class="line"><span class="ln">7</span><span class="cl">DB_USER_PASSWORD=$(cat /dev/urandom | tr -dc &#39;a-zA-Z0-9&#39; | fold -w 32 | head -n 1)
</span></span></code></pre></div><p>Das Passwort wird im Beispiel zufällig erzeugt, willst du ein eigenes Passwort verwenden, kannst du den Parameter überschreiben, z.B:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">DB_USER_PASSWORT=secret
</span></span></code></pre></div><p>Der DB-Nutzer wird natürlich nur angelegt, wenn ADD_DB_USER=yes gesetzt ist. Andernfalls musst du hier die passenden Zugangsdaten angeben.</p>
<h3 id="wordpress">WordPress</h3>
<p>Du kannst auf eine existierende Installation zurückgreifen, um z.B. dort Beiträge hinzuzufügen, oder WordPress komplett neu installieren. Hier kannst du z.B. auch den oben festgelegten PROJECT-Parameter nutzen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">WP_PATH</span><span class="o">=</span><span class="s2">&#34;/var/nginx/htdocs/${PROJECT}/&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">INSTALL_CORE</span><span class="o">=</span><span class="n">yes</span><span class="o">|</span><span class="n">no</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">WP_DROP</span><span class="o">=</span><span class="n">yes</span><span class="o">|</span><span class="n">no</span>
</span></span></code></pre></div><p>Mit WP_DROP kannst du festlegen, dass der Ordner WP_PATH komplett gelöscht wird. <strong>Vorsicht</strong> ist hier also geboten! Die folgenden Parameter betreffen deine allgemeinen WordPress-Einstellungen. Bei der URL kannst du wieder den PROJECT-Platzhalter nutzen.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">WP_TITLE=&#39;WordPress Test&#39;
</span></span><span class="line"><span class="ln">2</span><span class="cl">URL=&#34;https://blog.example.org/${PROJECT}&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">WP_ADMIN_NAME=admin
</span></span><span class="line"><span class="ln">4</span><span class="cl">WP_ADMIN_EMAIL=mail@example.org
</span></span><span class="line"><span class="ln">5</span><span class="cl">WP_ADMIN_PASSWORD=secret
</span></span></code></pre></div><p>Willst du ein eigene Passwort vergeben, kannst du auch hier ein zufälliges Passwort erzeugen lassen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">WP_ADMIN_PASSWORD=$(cat /dev/urandom | tr -dc &#39;a-zA-Z0-9&#39; | fold -w 32 | head -n 1)
</span></span></code></pre></div><h3 id="theme-und-plugins">Theme und Plugins</h3>
<p>Mit den folgenden Parametern legst du fest, welches Theme eingerichtet und welche Plugins installiert werden sollen. Der Theme-Name ist der Slug des Themes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">INSTALL_THEME=yes|no
</span></span><span class="line"><span class="ln">2</span><span class="cl">THEME=&#34;elementor&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">INSTALL_PLUGINS=no
</span></span><span class="line"><span class="ln">5</span><span class="cl">PLUGINS=&#34;elementor wordpress-seo tinymce-advanced ninja-forms shortcodes-ultimate instagram-feed ml-slider the-events-calendar amp contact-widgets coblocks woocommerce&#34;
</span></span></code></pre></div><p>Du kannst mehrere Plugins angeben, wenn diese per Leerzeichen voneinander getrennt sind.</p>
<h3 id="inhalte">Inhalte</h3>
<p>Der nächste Abschnitt regelt das (zufällige) Anlegen von Beiträgen, Kategorien usw. Natürlich musst du keine Inhalte anlegen. Ist <strong>RANDOMIZE_EVERY_ITEM</strong> auf <strong>yes</strong> gesetzt, wird jeder Beitrag wie oben zufällig erzeugt. Da das eine Menge HTTP-Requests erfordert, kannst du den Parameter auch auf no setzen. In dem Fall wird jede Zufalls-API genau einmal abgerufen. Der ganze Prozess wird dadurch maßgeblich beschleunigt.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">ADD_CONTENT=yes|no
</span></span><span class="line"><span class="ln">2</span><span class="cl">META_INPUT=&#34;&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">RANDOMIZE_EVERY_ITEM=yes|no
</span></span></code></pre></div><p>Manche Themes, wie z.B. <strong>Elementor</strong>, fügen den Inhalten, also Beiträgen, bestimmte Meta-Informationen hinzu. Beim automatisierten Anlegen von Beiträgen passiert das allerdings nicht automatisch. Du kannst das hier also entsprechend einrichten. Der Wert wird als JSON übergeben. Achte auf das einfache Anführungszeichen.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">META_INPUT=&#39;{&#34;_elementor_edit_mode&#34;:&#34;&lt;![CDATA[builder]]&gt;&#34;,&#34;_elementor_template_type&#34;:&#34;&lt;![CDATA[kit]]&gt;&#34;,&#34;_elementor_version&#34;:&#34;&lt;![CDATA[2.9.13]]&gt;&#34;}&#39;
</span></span></code></pre></div><p>Die folgenden Paremeter legen fest, wieviele Inhalte du erzeugen möchtest:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">COUNT_CATEGORIES_LEVEL1=10
</span></span><span class="line"><span class="ln">2</span><span class="cl">COUNT_CATEGORIES_LEVEL2=5
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">COUNT_POSTS=100
</span></span><span class="line"><span class="ln">5</span><span class="cl">COUNT_PARAGRAPHS=20
</span></span><span class="line"><span class="ln">6</span><span class="cl">COUNT_POST_REVISIONS=5
</span></span><span class="line"><span class="ln">7</span><span class="cl">COUNT_TAGS=20
</span></span></code></pre></div><p>Im Beispiel werden 10 Haupt-Kategorien mit jeweils 5 Unter-Kategorien angelegt. Jede Kategorie wird mit 100 Posts befüllt und jeder Post erhält 20 Tags und 5 Revisionen. Das ergibt also 10 * 5 * 100 * 5 Beiträge = 25.000 Beiträge! In diesem Fall macht es Sinn RANDOMIZE_EVERY_ITEM auf no zu setzen, da du sonst mehre tausende Anfragen an die Zufalls-APIs generieren würdest. COUNT_PARAGRAPHS legt die Länge jedes Beitrags in Absätzen fest.</p>
<h3 id="standard-parameter">Standard-Parameter</h3>
<p>Die folgenden Parameter musst du in der Regel nicht anpassen. WP_CLI legt den Pfad zur WordPress Kommandozeilen Oberfläche fest. Wird die Datei nicht gefunden, muss WP CLI installiert werden. Dazu dient der Parameter WP_CLI_SOURCE.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">WP_CLI_EXEC=~/wp-cli.phar
</span></span><span class="line"><span class="ln">2</span><span class="cl">WP_CLI_SOURCE=https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
</span></span></code></pre></div><p>Im folgenden werden die Quellen für die APIs festgelegt. Hier solltest du nichts ändern, da das Script für die Zusammenarbeit mit diesen Quellen eingerichtet ist. Einzig beim Parameter RANDOM_IMAGE_API_URI kannst du bei Bedarf in der URL die Auflösung der abgefragten Bilder festlegen.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">RANDOM_WORD_API_URL=&#39;https://random-word-api.herokuapp.com/word?swear=0&amp;number=&#39;
</span></span><span class="line"><span class="ln">2</span><span class="cl">RANDOM_SENTENCE_API_URL=&#39;http://metaphorpsum.com/sentences/&#39;
</span></span><span class="line"><span class="ln">3</span><span class="cl">RANDOM_TEXT_API_URL=&#39;http://metaphorpsum.com/paragraphs/&#39;
</span></span><span class="line"><span class="ln">4</span><span class="cl">RANDOM_IMAGE_API_URI=&#39;https://picsum.photos/200/300.jpg&#39;
</span></span></code></pre></div>]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>project_documentation</dc:type></item><item><title>WordPress Word Cloud 2.0</title><link>https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/</link><pubDate>Tue, 09 Jun 2020 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/</guid><description>Mein Word Cloud Plugin für WordPress liegt in einer neuen Version vor! Die Word Cloud unterstützt nun OCR, Tags, interne und externe Quellen und vieles mehr. Da ich also sehr viele grundlegende Änderungen vorgenommen habe, gibt es an dieser Stelle eine komplett neue Dokumentation. Viel Spass.
Was …</description><content:encoded><![CDATA[<p>Mein Word Cloud Plugin für WordPress liegt in einer neuen Version vor! Die Word Cloud unterstützt nun <strong>OCR</strong>, <strong>Tags</strong>, <strong>interne</strong> und <strong>externe Quellen</strong> und vieles mehr. Da ich also sehr viele grundlegende Änderungen vorgenommen habe, gibt es an dieser Stelle eine komplett neue Dokumentation. Viel Spass.</p>
<h2 id="was-ist-eine-word-cloud">Was ist eine Word Cloud?</h2>
<p>Eine <strong>Word Cloud</strong>, auch <strong>Schlagwortwolke</strong>, <strong>Wortwolke</strong> usw., dient der besseren Visualisierung von Texten. Um eine Word Cloud zu erstellen, werden die Vorkommen von Wörtern eines Textes gezählt. Die Wörter mit der größten <strong>Häufigkeit</strong> werden groß dargestellt, Wörter mit geringerer Häufigkeit weniger groß. Und das ganze wird dann in einer <strong>Wolkenform</strong> dargestellt, mit den größten Wörtern in der Mitte.</p>
<p>So ist es möglich auf einen Blick zu erkennen, welche Wörter in einem Text am häufigsten vorkommen. Das ist nicht nur bei der Analyse von Texten sondern auch von Webseiten nützlich. So kannst du z.B. bei WordPress eine Word Cloud aus Tags erstellen (<strong>Tag Cloud</strong>), anhand der man sehen kann, welche Tags auf einer Seite am meisten verwendet wurden.</p>
<p>Die folgende Word Cloud zeigt auf einen Artikel, 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/wie-erstelle-ich-ein-labyrinth-mit-javascript/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">in dem ich einen Algorithmus beschreibe, um in JavaScript ein Labyrinth zu erstellen</a>


:</p>
<p>[wp-word-cloud source-type=&ldquo;id&rdquo; id=&ldquo;demo1&rdquo; size-factor=50 min-alpha=0.5 count-words=1]3334[/wp-word-cloud]</p>
<h2 id="was-kann-dieses-plugin">Was kann dieses Plugin?</h2>
<p><strong>WP Word Cloud</strong> stellt einen Shortcode zur Verfügung, mit dem du beliebig viele <strong>Word Clouds</strong> auf deinen Seiten einfügen kannst. Du kannst verschiedene Quellen nutzen, um dort die Wörter zu zählen und in einer <strong>Word Cloud</strong> darstellen zu lassen.</p>
<ul>
<li>inline - du gibst den Text direkt im Shortcode im Beitrag an</li>
<li>url - ein Verweis auf eine Internetadresse</li>
<li>id - der Verweis auf eine Seite / einen Beitrag</li>
<li>sql - eine SQL-Abfrage um Text aus der Datenbank zu lesen</li>
<li>custom-field - Text aus einem custom field des Beitrages / der Seite</li>
<li>Text, den der Besucher der Webseite selber eingeben kann</li>
<li>Text von einem Bild, das der Besucher der Website hochlädt</li>
</ul>
<p>Das Plugin greift auf zwei sehr großartige Bibliotheken zurück: WodCloud2.js von TimDream (





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/timdream/wordcloud2.js/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">github.com</a>


). Sowie die Offline-Implementierung von Tesseract in JavaScript (<strong>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/jeromewu/tesseract.js-offline?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">github.com</a>


</strong>).</p>
<h2 id="installation">Installation</h2>
<h3 id="über-die-kommandozeile">Über die Kommandozeile</h3>
<p>Wenn du Zugriff auf deinen Server über SSH hast, kannst du das Plugin folgendermaßen installieren:</p>
<p>Wechsle in den Plugin-Ordner deiner WordPress-Installation und lege dort einen neuen Ordner an: WP-Word-Cloud, wechsle danach in diesen Ordner:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">mkdir /htdocs/wp-content/plugins/WP-Word-Cloud
</span></span><span class="line"><span class="ln">2</span><span class="cl">cd /htdocs/wp-content/plugins/WP-Word-Cloud
</span></span></code></pre></div><p>Danach kannst du das Plugin-Repository mit git klonen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">git clone https://github.com/nickyreinert/wordCloud-for-Wordpress.git .
</span></span></code></pre></div><p>Wenn du git nicht benutzen möchtest oder kannst, kannst du das Plugin auch mit wget herunterladen und dann entpacken. Dann musst du den Plugin-Ordner allerdings nicht selber erstellen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">cd /htdocs/wp-content/plugins/
</span></span><span class="line"><span class="ln">2</span><span class="cl">wget https://github.com/nickyreinert/wordCloud-for-Wordpress/archive/wordCloud-for-wordPress-2.zip --no-check-certificate
</span></span><span class="line"><span class="ln">3</span><span class="cl">unzip wordCloud-for-wordPress-2.zip -d WP-Word-Cloud
</span></span></code></pre></div><h3 id="mit-einem-ftp-programm">Mit einem FTP-Programm</h3>
<p>Wenn du keinen Zugriff auf die Kommandozeile hast, kannst du das Plugin von dieser Adresse herunterladen:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/nickyreinert/wordCloud-for-Wordpress/archive/wordCloud-for-wordPress-2.zip?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://github.com/nickyreinert/wordCloud-for-Wordpress/archive/wordCloud-for-wordPress-2.zip</a>


</p>
<p>Danach entpackst du das Archiv und lädst den kompletten Ordner in das Plugin-Verzeichnis deiner WordPress-Installation. Du kannst den Ordner mit dem FTP-Programm dann umbenennen:</p>
<img src="https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/images/grafik.png" alt=""><p>Der Order für das Word Cloud Plugin</p>
<h2 id="abschluss-der-installation">Abschluss der Installation</h2>
<p>Abschließend muss das Plugin noch im Backend von WordPress aktiviert werden. Dazu suchst du im Plugin-Bereich nach dem Eintrag &ldquo;WP Word Cloud&rdquo; und klickst auf &ldquo;aktivieren&rdquo;. Fertig.</p>
<img src="https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/images/grafik-1-700x70.png" alt=""><p>Aktiviere das Plugin in der Plugin-Übersicht von WordPress</p>
<h2 id="erste-verwendung">Erste Verwendung</h2>
<p>Das Plugin aktiviert für deine WordPress-Installation einen neuen Shortcode: <strong>[ wp-word-cloud ]</strong>. Diesen kannst du nun auf jeder Seite und in jedem Beitrag beliebig oft einfügen. Wenndu den <strong>Gutenberg-Editor</strong> verwendest, benötigst du dafür den Block &ldquo;<strong>Shortcode</strong>&rdquo;. Im konventionellen Editor kannst du den Shortcode einfach in deinen Text einfügen.<br>
Eine einfache Implementierung sieht z.B. so aus:</p>
<p><code>[ wp-word-cloud source-type=&quot;url&quot; id=&quot;meine-word-cloud&quot; ]https://www.spiegel.de[ /wp-word-cloud ]</code></p>
<p>Es gibt <strong>drei Parameter</strong>, die du <strong>immer</strong> angeben musst:</p>
<ul>
<li><strong>id</strong>, eine eindeutige Id, mit der die <strong>Word Cloud</strong> auf deiner Seite angesprochen wird, z.B. <strong>meine-word-cloud</strong> oder einfach nur <strong>test</strong></li>
<li><strong>source-type</strong>, welcher Art die Quelle für die Word Cloud ist, also z.B. von einer <strong>url</strong> oder den <strong>tags</strong></li>
<li>sowie Inline-Text, als Verweis auf die Quelle, von wo der Inhalt für die Word Cloud kommt, also z.B. die URL <strong>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.spiegel.de?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://www.spiegel.de</a>


</strong> oder die Id eines Beitrages (123)</li>
</ul>
<h2 id="zusätzliche-einstellungen">Zusätzliche Einstellungen</h2>
<p>Natürlich gibt es noch <strong>eine Menge weiterer Parameter</strong>, mit denen du das Erscheinungsbild der Word Cloud steuern kannst. Das Plugin setzt bei der ersten Installation für all diese Parameter <strong>Standard-Einstellungen</strong> fest. Diese sind grundsätzlich <strong>global gültig</strong> und können auf der Einstellungseite angepasst werden:</p>
<img src="https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/images/grafik-2.png" alt=""><p>In den Einstellungen findest du eine eigene Seite für das WP Word Cloud Plugin - wunderbar!</p>
<p>Jeden dieser globalen Parameter kannst du auch in deinem Shortcode verwenden. So wir die globale Einstellung überschrieben. So ist die <strong>Hintergrundfarbe</strong> z.B. standardmäßig <strong>transparent</strong> eingestellt. Möchtest du sie für die aktuelle Word Cloud auf <strong>grün und halbtransparent</strong> ändern, übergibst du einfach den Parameter <strong>background-color</strong> folgendermaßen:</p>
<p><code>[ wp-word-cloud source-type=&quot;url&quot; id=&quot;meine-word-cloud&quot; background-color=&quot;rgba(0,255,255,0.5)&quot; ]https://www.spiegel.de[ /wp-word-cloud ]</code></p>
<p>Eine genaue Beschreibung aller Parameter folgt jetzt:</p>
<h2 id="parameter">Parameter</h2>
<h3 id="id">id</h3>
<p>Dieser Parameter ist für jeden Shortcode, den du einfügst, <strong>verpflichtend</strong>! Die Id adressiert jede Word Cloud auf einer Seite oder in einem Beitrag. Damit ist es möglich, mehrere Word Clouds gleichzeitig einzubinden. Mit der Id kannst du außerdem das Aussehen der Word Cloud mit CSS steuern. Beispiel:</p>
<p><code>[ wp-word-cloud source-type=&quot;inline&quot; id=&quot;wordcloud1&quot; ]Lorem Ipsum[ /wp-word-cloud ]</code></p>
<p><code>[ wp-word-cloud source-type=&quot;inline&quot; id=&quot;wordcloud2&quot; ]Lorem Ipsum[ /wp-word-cloud ]</code></p>
<h3 id="source-type"><strong>source-type</strong></h3>
<p>Damit legst du fest, um was für eine Quelle es sich handelt. Dieser Parameter ist <strong>verpflichtend</strong>. Die folgenden Quellen werden unterstützt:</p>
<h4 id="source-typeinline">source-type=inline</h4>
<p>Der Text wird innerhalb des Shortcodes übermittelt, z.B:</p>
<p><code>[ wp-word-cloud source-type=inline id=&quot;demo&quot; ]Lorem Ipsum[ /wp-word-cloud ]</code></p>
<h4 id="source-typeurl">source-type=<strong>url</strong></h4>
<p>Im Shortcode wird eine URL angegeben, von der Text abgerufen wird, z.B:</p>
<p><code>[ wp-word-cloud source-type=url id=&quot;demo&quot; ]https://www.spiegel.de[ /wp-word-cloud ]</code></p>
<h4 id="source-typeid">**source-type=**id</h4>
<p>Im Shortcode wird eine Id übergeben, die auf eine Seite oder einen Beitrag deiner Webseite verweist, z.B:</p>
<p><code>[ wp-word-cloud source-type=id=&quot;demo&quot; ]123[ /wp-word-cloud ]</code></p>
<h4 id="source-typesql">source-type=sql</h4>
<p>Im Shortcode wird eine SQL-Abfrage übergeben, die eine bereits gezählte Wortliste zurück gibt, z.B:</p>
<p><code>[ wp-word-cloud source-type=sql id=&quot;demo&quot;]SELECT word,count FROM table;[ /wp-word-cloud]</code></p>
<h4 id="source-typetags">source-type=tags</h4>
<p>Die Word Cloud zeigt alle Tags deiner Seite an, hier musst du im Shortcode nichts weiter übergeben:</p>
<p><code>[ wp-word-cloud source-type=tags id=&quot;demo&quot;]SELECT word,count FROM table;[ /wp-word-cloud]</code></p>
<h4 id="source-typecustom-field">source-type=custom-field</h4>
<p>Im Shortcode wird auf ein custom field, also benutzerdefiniertes bzw. eigenes Feld verwiesen, z.B:</p>
<p><code>[ wp-word-cloud source-type=custom-field id=&quot;demo&quot;]my-custom-field[ /wp-word-cloud]</code></p>
<p>Die Darstellung der <strong>eigenen Felder</strong> musst du im Gutenberg-Edito ggf. erst aktivieren. Klicke dazu das Punkt-Menü und dann ganz unten auf &ldquo;Ansicht anpassen&rdquo;. Es öffnet sich nun eine Liste, in der du die &ldquo;eigenen Felder&rdquo; aktivieren kannst:</p>
<ul>
<li>
<img src="https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/images/grafik-10.png" alt=""></li>
<li>
<img src="https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/images/grafik-11-700x1059.png" alt=""></li>
</ul>
<p>Anzeigen der benutzerdefinierten Felder im Gutenberg-Editor</p>
<h3 id="count-words"><strong>count-words</strong></h3>
<p>Wenn die im Shortcode angegebene Quelle einen kompletten Text enthält, müssen die Wörter darin erst gezählt werden. Dazu muss <strong>count-words</strong> auf <strong>1</strong> gesetzt werden. Ist <strong>count-words=0</strong> gesetzt, erwartet das Plugin eine List mit gezählten Wörtern.</p>
<h3 id="enable-frontend-edit"><strong>enable-frontend-edit</strong></h3>
<p>Ist dieser Parameter auf 1 gesetzt, wird auf der Seite ein Textfeld angezeigt. Das Textfeld enthält den Text, der mit dem Shortcode definiert wurde. Der Besucher kann den Text dann beliebig verändern oder eigenen Text hinzufügen, um so die <strong>Word Cloud</strong> selber zu steuern. Die Berechnung der <strong>Word Cloud</strong> findet im Browser statt. Im folgenden Beispiel kommt der Text von einem benutzerdefiniertem Feld im Backend. Er wird an das Frontend ausgeliefert und dort in der Word Cloud dargestellt. Gleichzeitig kann man den Text aber auch anpassen und die Wolke neu zeichnen lassen:</p>
<p><code>[ wp-word-cloud source-type=&quot;custom-field&quot; enable-frontend-edit</code>=1 <code>id=&quot;demo&quot;]my-custom-field[ /wp-word-cloud]</code></p>
<img src="https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/images/grafik-8-700x755.png" alt=""><p>Ein Textfeld für die Besucher deiner Webseite</p>
<h3 id="ocr---texterkennung">OCR - Texterkennung</h3>
<h4 id="enable-ocr"><strong>enable-ocr</strong></h4>
<p>Ist dieser Parameter auf 1 gesetzt, kann der Benutzer auf der Webseite ein Bild hochladen oder mit der Kamera aufnehmen. Im Browser findet dann eine Text-Erkennung statt. Die Wörter werden dann gezählt und die Word Cloud darauf basierend gezeichnet. Wenn <strong>enable-frontend-edit</strong> nicht aktiv ist, hat dieser Parameter keine Wirkung.</p>
<p><code>[ wp-word-cloud source-type=&quot;custom-field&quot; enable-frontend-edit=1 enable-ocr=1 id=&quot;demo&quot;]my-custom-field[ /wp-word-cloud]</code></p>
<p>Für diese Funktionalität greift das Plugin auf die 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://tesseract.projectnaptha.com/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Tesseract-Bibliothek</a>


 zurück. Diese ermöglicht die Texterkennung von abfotografierten Dokumenten in verschiedenen Sprachen. Die Texterkennung erfolgt komplett im Browser, also beim Besucher der Webseite. Das Bild wird <strong>nicht zum Server hochgeladen</strong>.</p>
<h4 id="ocr-language">ocr-language</h4>
<p>Mit diesem Parameter legst du fest, welche Sprache(n) das ORC-Modul unterstützen soll. Standardmäßig ist hier deu eingestellt. Du kannst mehre Sprachen angeben, wenn du sie mit einem + trennst, z.B. Deutsch + Englisch:</p>
<p><code>`[ wp-word-cloud source-type=&quot;custom-field&quot; enable-frontend-edit=1 enable-ocr=1 ocr-language=&quot;deu+eng&quot; id=&quot;demo&quot;]my-custom-field[ /wp-word-cloud]`</code></p>
<p>Eine Liste aller unterstützten Sprachpakete findest du hier:</p>
<p>





  
  

  
  
    
    
    
  

  
  
  

  
    
      
    
  

  
  <a href="https://tesseract-ocr.github.io/tessdoc/Data-Files?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous#data-files-for-version-400-november-29-2016" target="_blank" rel="noopener noreferrer">https://tesseract-ocr.github.io/tessdoc/Data-Files#data-files-for-version-400-november-29-2016</a>


</p>
<h4 id="ocr-local-libraries">ocr-local-libraries</h4>
<p>Das OCR-Modul lädt einige, größere Dateien von einem anderen Server nach, unter anderem auch die Sprachpakete. Du kannst diese Dateien auch auf deinem Server anbieten. Dazu musst du die folgenden Dateien herunterladen und im Unterordner &ldquo;<strong>lib</strong>&rdquo; innerhalb des Plugin-Ordners ablegen:</p>
<ul>
<li>[https://unpkg.com/tesseract.js@v2.0.0/dist/worker.min.js](





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://unpkg.com/tesseract.js@v2.0.0/dist/worker.min.js%27?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://unpkg.com/tesseract.js@v2.0.0/dist/worker.min.js'</a>


 defer=&lsquo;defer)</li>
<li>[https://unpkg.com/tesseract.js-core@v2.0.0/tesseract-core.wasm.js](





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://unpkg.com/tesseract.js-core@v2.0.0/tesseract-core.wasm.js%27?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://unpkg.com/tesseract.js-core@v2.0.0/tesseract-core.wasm.js'</a>


 defer=&lsquo;defer)</li>
</ul>
<p>Außerdem benötigst du die von dir konfigurierten Sprachpakete. Diese kannst 





  
  

  
  
    
    
    
  

  
  
  

  
    
      
    
  

  
  <a href="https://tesseract-ocr.github.io/tessdoc/Data-Files?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous#data-files-for-version-400-november-29-2016" target="_blank" rel="noopener noreferrer">du hier für die Version 4 herunterladen</a>


. Die Sprachpakete gehören ebenfalls in den Unterordner &ldquo;lib&rdquo;. Außerdem <strong>musst du manuell</strong> die Dateiendung &ldquo;.gz&rdquo; anhängen.</p>
<h3 id="eingrenzen-der-wortliste">Eingrenzen der Wortliste</h3>
<h4 id="min-word-length">min-word-length</h4>
<p>Hiermit legst du fest, wie lang ein Wort mindestens sein soll, um beim Zählen berücksichtigt zu werden. Diese Einstellung wird <strong>nicht</strong> berücksichtigt, wenn du eine fertige Wortliste als Quelle angibst. In diesem Beispiel wird das Wort Foo beim Zählen ignoriert.</p>
<p><code>`[ wp-word-cloud source-type=&quot;inline&quot; count-words=1 min-word-lenght=4 id=&quot;demo&quot;]Lorem Ipsum Foo[ /wp-word-cloud]`</code></p>
<h4 id="min-word-occurence">min-word-occurence</h4>
<p>Mit diesem Parameter legst du fest, wie oft ein Wort mindestens vorkommen muss, um in der word cloud dargestellt zu werden. Diese Einstellung wird <strong>immer</strong> berücksichtigt, auch wenn du eine fertige Wortliste als Quelle angibst. Im Beispiel werden nur Wörter in der word cloud dargestellt, die mindestens 2 mal vorkommen:</p>
<p><code>`[ wp-word-cloud source-type=&quot;inline&quot; count-words=1 min-word-occurence=2 id=&quot;demo&quot;]Lorem Ipsum Foo[ /wp-word-cloud]`</code></p>
<h4 id="black-list-und-enable-black-list">black-list und enable-black-list</h4>
<p>Die Blacklist enthält Wörter, die beim Zählen nicht berücksichtigt werden dürfen. Das können z.B. Funktionswörter oder Fremdwörter sein, die du nicht in der word cloud sehen möchtest. Die Wörter werden mit einem Leerzeichen getrennt angegeben. Wenn du den Parameter im Shortcode angibst, wird die globale Einstellung komplett ignoriert. Die Blacklist wird nur verwendet, wenn der Parameter enable-black-list auf 1 gesetzt ist. Im Beispiel wird das Wort &ldquo;Lorem&rdquo; beim Zählen ignoriert, das es sich auf der Blacklist befindet.</p>
<p><code>`[ wp-word-cloud source-type=&quot;inline&quot; count-words=1 enable-black-list`</code>=1 <code>`black-list=&quot;Lorem&quot; id=&quot;demo&quot;]Lorem Ipsum Foo[ /wp-word-cloud]`</code></p>
<h4 id="enable-custom-black-list">enable-custom-black-list</h4>
<p>Ist dieser Parameter aktiviert, kann der Besucher deiner Webseite Wörter in der word cloud anklicken, um sie aus der word cloud zu entfernen. Die Wörter erscheinen dann unter der word cloud in einer &ldquo;benutzerdefinierten&rdquo; Blacklist. Wenn sie dort erneut angeklickt werden, erscheinen sie wieder in der word cloud.</p>
<img src="https://nickyreinert.de/2020/2020-06-09-wordpress-word-cloud-2-0/images/grafik-9-700x569.png" alt=""><p>Eine word cloud mit benutzerdefinierter Blacklist, so schön!</p>
<h4 id="persistent-custom-black-list">persistent-custom-black-list</h4>
<p>Hiermit kannst du festlegen, dass die Blacklist erhalten bleibt, wenn der Besucher einen neuen Text in das Eingabefeld eingibt. Ist der Parameter deaktiviert, wird die benutzerdefnierte Blacklist gelöscht, sobald der Besucher einen neuen Text eingibt. Dieser Parameter ist nur wirksam, wenn auch das Textfeld aktiviert ist.</p>
<h4 id="ignore-chars">ignore-chars</h4>
<p>Mit diesem Parameter legst du fest, welche Zeichen aus dem Text entfernt werden, bevor er gezählt wird. Hier wird ein regulärer Ausdruck dieser Form erwartet:</p>
<p><code>ignore-chars=&quot;\(\)\[\]\,\.;&quot;</code></p>
<p>Auf diese Art kannst du ungewollte Satzzeichen, Sonderzeichen oder Steuerzeichen entfernen.</p>
<h3 id="erscheinungsbild-anpassen">Erscheinungsbild anpassen</h3>
<h4 id="style">style</h4>
<h4 id="text-transform">text-transform</h4>
<p>Hiermit kannst du festlegen, dass der Text beim Darstellen in der word cloud groß oder kleingeschrieben werden soll. Dieser Parameter kann dementsprechend drei Werte annehmen:</p>
<ul>
<li>uppercas (alle Wörter groß schreiben)</li>
<li>lowercase (alle Wörter klein schreiben)</li>
<li>none (keine Änderung)</li>
</ul>
<p>Diese word cloud nutzt als Ressource die aktuellen Mitteilungen von 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://blog.fefe.de?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">fefe</a>


:</p>
<p>[wp-word-cloud source-type=&ldquo;url&rdquo; id=&ldquo;demo2&rdquo; count-words=1 min-word-occurence=1 min-word-length=3 size-factor=100 min-alpha=0.5 count-words=1]https://blog.fefe.de[/wp-word-cloud]</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>tools</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>project_documentation</dc:type></item><item><title>Fokussierter Drill-Down mit Tableau</title><link>https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/</link><pubDate>Mon, 15 Jul 2019 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/</guid><description>In Tableau gibt es die eigentlich sehr nützliche Möglichkeit, Dimensionen beliebig zu kombinieren und in einer Hierarchie zusammenzufassen. Diese Funktion ist sehr intuitiv hat aber einen Haken: Wenn man bei großen Datenmengen und Dimensionen mit hoher Kardinalität einen Drill Down macht, werden die …</description><content:encoded><![CDATA[<p>In Tableau gibt es die eigentlich sehr nützliche Möglichkeit, Dimensionen beliebig zu kombinieren und in einer Hierarchie zusammenzufassen. Diese Funktion ist sehr intuitiv hat aber einen Haken: Wenn man bei großen Datenmengen und Dimensionen mit hoher Kardinalität einen Drill Down macht, werden die Abfragen nicht nur irrsinnig langsam, je tiefer man kommt. Die Übersicht geht auch komplett verloren.</p>
<p>Um das zu demonstrieren habe ich eine Datenquelle mit 1 Mio. Zeilen und 10 Dimensionen erzeugt. Der Drill-Down auf Ebene 8 dauert hier auf normaler Hardware über 20 Sekunden und das Ergebnis ist… nun ja: Für eine schnellen Überblick kaum zu gebrauchen:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-700x380.png" alt=" Drill-Down auf die 8. Ebene einer Hierarchie mit hoher Kardinalität: Übersicht ade "></a>


</p>
<p>Drill-Down auf die 8. Ebene einer Hierarchie mit hoher Kardinalität: Übersicht ade</p>
<p>Im Folgenden beschreibe ich einen Weg, wie man einen fokussierten Drill-Down ermöglicht, der weitaus performanter und vor allem übersichtlicher ist. Man könnte zwar die eingebaute Quick-Filter-Funktion nutzen, das ist dann aber relativ umständlich, da man je Ebene mindestens zwei Klicks benötigt und vor allem immer noch nicht übersichtlich:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-2.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-2.png" alt="Die Quick-Filter-Funktion bei mehreren Dimensionen in einer Hierarchie"></a>


</p>
<p>Drill-Down bei mehreren Dimensionen einer Hierarchie unter Verwendung des Quick-Filters: Unpraktisch</p>
<p>Bevor ich die Schritte im einzelnen erkläre, möchte ich zeigen, was das Ziel der ganzen Übung sein soll:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-1.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-1-700x526.png" alt="Schematische Darstellung der Logik"></a>


</p>
<p>Schematische Darstellung der Logik</p>
<p>Die Namen der einzelnen Dimensonen werden für jedes Level zusammengefasst. Jede Dimension in der Hierarchie wird außerdem durch einen <strong>Index</strong> repräsentiert, der die Tiefe wiederspiegelt. Dadurch entsteht ein Pfad nach dem Schema <strong>nameA.nameB.NameC.nameD.nameX</strong> usw. Diese Pfad soll als Filter dienen. So kann ich mit einem einzigen Filter mehrere Dimensionen abdecken.</p>
<p>Ein Klick auf eine Zeile im Arbeitsblatt &ldquo;<strong>main</strong>&rdquo; soll dafür sorgen, dass der Index für die aktuelle Ebene um 1 hochgezählt wird - man also tiefer in den Baum hineingeht. Außerdem wird der Pfad für die entsprechende Ebene als Filter genutzt. Dadurch erhalte ich eine Ansicht, die einem kombinierten Filter gleicht, immer nur die Zeilen anzeigt, die ich beim Drill-Down ausgewählt habe. Klicke ich auf das zweite Arbeitsblatt, wird der Index heruntergezählt, also immer die nächsthöhere Ebene angezeigt. Außerdem wird bei dem zusammengesetzten Pfad das letzte Element entfernt, wodurch sich gleichzeitig die Filterbedingung ändert.<br>
Um diese Mechanik umzusetzen, benötigen wir also zwei Felder, eines enthält immer den aktuellen <strong>Index + 1</strong> für die nächste Ebene und <strong>Index - 1</strong> für die nächst höhere Ebene. Außerdem gibt es ein Feld, dass den aktuellen Pfad enthält, also z.B. <strong>nameA.nameB.nameC</strong>. Ein weiteres Feld enthält den nächsten Pfad, je nachdem wo der Nutzer klickt, also z.B. <strong>nameA.nameB.nameC.nameD</strong> und ein Feld enthält den Pfad für die zuvor ausgewählte Ebene, also z.B. <strong>nameA.nameB</strong>. Das ganze wird mit ein paar Aktionen und Filtern so miteinander kombiniert, dass der Benutzer interaktiv durch die Hierarchie reisen kann.<br>
Klar soweit? Los geht&rsquo;s:</p>
<p>Du benötigst zwei Arbeitsblätter und ein Dashboard, das die beiden Arbeitsbelätter beherbergt. Als Namen wähle ich &ldquo;<strong>main</strong>&rdquo; für die eigentliche Darstellung der KPIs und &ldquo;<strong>go back</strong>&rdquo; für die Navigation. Wir beginnen mit der Erstellung der beiden Parameter.<br>
Der Parameter &ldquo;<strong>current level index</strong>&rdquo; enthält, entsprechend der Anzahl der Dimensionen, die Ziffern 1 bis 10. Der Parameter &ldquo;<strong>level name concatenated</strong>&rdquo; dient später als Filter-Element und enthält den oben erwähnten Pfad:</p>
<ul>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-breadcrumb-navigation-in-tableau/parameter_level_name_concatenated.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/parameter_level_name_concatenated.png" alt="Parameter level name concatenated"></a>


</p>
<p>Parameter level name concatenated</p>
</li>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-breadcrumb-navigation-in-tableau/parameter_current_level_index.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/parameter_current_level_index.png" alt="Parameter current level index"></a>


</p>
<p>Parameter current level index</p>
</li>
</ul>
<p>Nun erstellst du einen Filter, der sich auf den Parameter &ldquo;<strong>level name concatenated</strong>&rdquo; bezieht. Das vereinfacht den weiteren Prozess ungemein, da du kein Filter-Aktion anlegen musst, sondern Tableau den Filter immer dynamisch anpasst. Dazu legst du einen Filter für das Arbeitsblatt &ldquo;<strong>main</strong>&rdquo; an und wählst im Reiter &ldquo;Bedingung&rdquo; die Option &ldquo;nach Formel&rdquo; aus. Als Formel verwendest du diese:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">[level name concatenated] = [current level filter]
</span></span></code></pre></div><p>So sollte der Filter dann in Tableau aussehen:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-4.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-4.png" alt="Filter mit einer Formel als Bedingung"></a>


</p>
<p>Filter mit einer Formel als Bedingung</p>
<p>Weiter geht es mit ein paar berechneten Feldern:</p>
<p>Das erste Feld nennt sich &ldquo;<strong>current level</strong>&rdquo;, dass den Parameter &ldquo;<strong>current level index</strong>&rdquo; nutzt um die entsprechende Dimension darzustellen. Die Formel dazu ist simpel:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">IF     [current level index] = 1 THEN [Dimension1] 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ELSEIF [current level index] = 2 THEN [Dimension2]
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ELSEIF [current level index] = 3 THEN [Dimension3]
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">ELSEIF [current level index] = 4 THEN [Dimension4] 
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ELSEIF [current level index] = 5 THEN [Dimension5] 
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">ELSEIF [current level index] = 6 THEN [Dimension6] 
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ELSEIF [current level index] = 7 THEN [Dimension7] 
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ELSEIF [current level index] = 8 THEN [Dimension8] 
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">ELSEIF [current level index] = 9 THEN [Dimension9] 
</span></span><span class="line"><span class="ln">10</span><span class="cl">ELSEIF [current level index] = 10 THEN [Dimension10] 
</span></span><span class="line"><span class="ln">11</span><span class="cl">END
</span></span></code></pre></div><p>Als nächstes gibt es zwei Felder, die auch mit einer einfachen Formel auskommen: &ldquo;<strong>previous level index</strong>&rdquo; und &ldquo;<strong>next level index</strong>&rdquo;. Previous level index dient dazu, den Index herunter zu zählen. Hier könnte man sicherlich eine eleganter Lösung nutzen, um das Prinzip zu verdeutlichen, habe ich das mit einer einfachen Wenn-Dann-Bedingung realisiert:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">IF [current level index] = 1 then 1
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ELSEIF [current level index] = 2 then 1
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ELSEIF [current level index] = 3 then 2
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">ELSEIF [current level index] = 4 then 3
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ELSEIF [current level index] = 5 then 4
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">ELSEIF [current level index] = 6 then 5
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ELSEIF [current level index] = 7 then 6
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ELSEIF [current level index] = 8 then 7
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">ELSEIF [current level index] = 9 then 8
</span></span><span class="line"><span class="ln">10</span><span class="cl">ELSEIF [current level index] = 10 then 9
</span></span><span class="line"><span class="ln">11</span><span class="cl">END
</span></span></code></pre></div><p>Bei &ldquo;<strong>next level index</strong>&ldquo;läuft es genau anders rum:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">IF     [current level index] = 1 THEN 2
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ELSEIF [current level index] = 2 THEN 3
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ELSEIF [current level index] = 3 THEN 4
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">ELSEIF [current level index] = 4 THEN 5
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ELSEIF [current level index] = 5 THEN 6
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">ELSEIF [current level index] = 6 THEN 7
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ELSEIF [current level index] = 7 THEN 8
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ELSEIF [current level index] = 8 THEN 9
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">ELSEIF [current level index] = 9 THEN 10
</span></span><span class="line"><span class="ln">10</span><span class="cl">END
</span></span></code></pre></div><p>Weiter geht es mit den Pfaden, die wir für den Filter benötigen. Jetzt wird es etwas komplizierter: Das Feld &ldquo;<strong>current level concat</strong>&rdquo; fasst zunächst, entsprechen der aktuell ausgewählten Ebene, die Namen der vorherigen Ebenen zusammen. So entsteht der Pfad:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">IF     [current level index] = 1  THEN [Dimension1] 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ELSEIF [current level index] = 2  THEN [Dimension1] + &#34;.&#34; + [Dimension2]
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ELSEIF [current level index] = 3  THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3]
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">ELSEIF [current level index] = 4  THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4]
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ELSEIF [current level index] = 5  THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5]
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">ELSEIF [current level index] = 6  THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6]
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ELSEIF [current level index] = 7  THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6] + &#34;.&#34; + [Dimension7]
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ELSEIF [current level index] = 8  THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6] + &#34;.&#34; + [Dimension7] + &#34;.&#34; + [Dimension8]
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">ELSEIF [current level index] = 9  THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6] + &#34;.&#34; + [Dimension7] + &#34;.&#34; + [Dimension8] + &#34;.&#34; + [Dimension9]
</span></span><span class="line"><span class="ln">10</span><span class="cl">ELSEIF [current level index] = 10 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6] + &#34;.&#34; + [Dimension7] + &#34;.&#34; + [Dimension8] + &#34;.&#34; + [Dimension9] + &#34;.&#34; + [Dimension10]
</span></span><span class="line"><span class="ln">11</span><span class="cl">END
</span></span></code></pre></div><p>Das Feld &ldquo;<strong>current level filter</strong>&rdquo; ist ähnlich aufgebaut. Da der Filter sich aber strenggenommen auf die vorherige Eben bezieht, sieht der Pfad etwas anders aus:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">IF     [current level index] = 1 THEN &#34;.&#34; 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ELSEIF [current level index] = 2 THEN [Dimension1] 
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ELSEIF [current level index] = 3 THEN [Dimension1] + &#34;.&#34; + [Dimension2]
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">ELSEIF [current level index] = 4 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3]
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ELSEIF [current level index] = 5 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4]
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">ELSEIF [current level index] = 6 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5]
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ELSEIF [current level index] = 7 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6]
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ELSEIF [current level index] = 8 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6] + &#34;.&#34; + [Dimension7]
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">ELSEIF [current level index] = 9 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6] + &#34;.&#34; + [Dimension7] + &#34;.&#34; + [Dimension8]
</span></span><span class="line"><span class="ln">10</span><span class="cl">ELSEIF [current level index] = 10 THEN [Dimension1] + &#34;.&#34; + [Dimension2] + &#34;.&#34; + [Dimension3] + &#34;.&#34; + [Dimension4] + &#34;.&#34; + [Dimension5] + &#34;.&#34; + [Dimension6] + &#34;.&#34; + [Dimension7] + &#34;.&#34; + [Dimension8] + &#34;.&#34; + [Dimension9]
</span></span><span class="line"><span class="ln">11</span><span class="cl">END
</span></span></code></pre></div><p>Beim Feld &ldquo;<strong>previous level concat</strong>&rdquo; wird es schon etwas schwieriger, da ich hier immer das letzte Element des Pfades entfernen muss. Das erinnert ein wenig an eine aus dem Ruder gelaufene Excel-Funktion, ist leider aber - nach meinem aktuellen Kenntnisstand - nicht einfacher zu realisieren, da die Behandlung von Strings in Tableau eben eingeschränkt sit. Sachdienliche Hinweise werden dankbar entgegen genommen.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">IF [current level index] = 1 THEN 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    &#34;.&#34;
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ELSEIF [current level index] = 2 THEN
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    &#34;.&#34;
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ELSEIF [current level index] = 3 THEN
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, 1) - 1)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ELSEIF [current level index] = 4 THEN
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, 1) + 1) - 1)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">ELSEIF [current level index] = 5 THEN
</span></span><span class="line"><span class="ln">10</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, 1) + 1) + 1) - 1)
</span></span><span class="line"><span class="ln">11</span><span class="cl">ELSEIF [current level index] = 6 THEN
</span></span><span class="line"><span class="ln">12</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, 1) + 1)) + 1) + 1) - 1)
</span></span><span class="line"><span class="ln">13</span><span class="cl">ELSEIF [current level index] = 7 THEN
</span></span><span class="line"><span class="ln">14</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, 1) + 1) + 1) + 1) + 1) + 1) - 1)
</span></span><span class="line"><span class="ln">15</span><span class="cl">ELSEIF [current level index] = 8 THEN
</span></span><span class="line"><span class="ln">16</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, 1) + 1) + 1) + 1) + 1) + 1) + 1) - 1)
</span></span><span class="line"><span class="ln">17</span><span class="cl">ELSEIF [current level index] = 9 THEN
</span></span><span class="line"><span class="ln">18</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) - 1)
</span></span><span class="line"><span class="ln">19</span><span class="cl">ELSEIF [current level index] = 10 THEN
</span></span><span class="line"><span class="ln">20</span><span class="cl">    LEFT([level name concatenated], FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, FIND([level name concatenated], &#39;.&#39;, 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) - 1)
</span></span><span class="line"><span class="ln">21</span><span class="cl">END
</span></span></code></pre></div><p>Das gröbste ist somit erledigt, du solltest nun die folgenden Felder und Parameter haben. Achte darauf, dass du die Felder als Dimension nutzt:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-3.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-3.png" alt="Alle berechneten Felder und Parameter auf einen Blick"></a>


</p>
<p>Alle berechneten Felder und Parameter auf einen Blick</p>
<p>Nun ziehst du die Felder &ldquo;<strong>next level index</strong>&rdquo;, &ldquo;<strong>current level</strong>&rdquo; und &ldquo;<strong>current level concat</strong>&rdquo; auf das Arbeitsblatt &ldquo;<strong>main</strong>&rdquo;, die Felder &ldquo;<strong>previous level index</strong>&rdquo; und &ldquo;<strong>previous level concat</strong>&rdquo; gehören auf das Arbeitsblatt &ldquo;<strong>go back</strong>&rdquo;.</p>
<ul>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-5-700x172.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-5-700x172.png" alt="Arbeitsblatt main mit allen nötigen Feldern"></a>


</p>
<p>Arbeitsblatt main mit allen nötigen Feldern</p>
</li>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-6.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-6.png" alt="Arbeitsblatt “go back” mit allen nötigen Feldern"></a>


</p>
<p>Arbeitsblatt &ldquo;go back&rdquo; mit allen nötigen Feldern</p>
</li>
</ul>
<h3 id="das-dashboard-mit-leben-füllen">Das Dashboard mit Leben füllen</h3>
<p>Abschließend geht es an die Aktionen um das Dashboard mit Leben zu füllen. Insgesamt benötigen wir vier Aktionen zur Änderung eines Parameters. Die ersten beiden Aktionen sorgen dafür, dass der aktuelle Index entsprechen hoch- und runtergezählt wird. Die Aktion &ldquo;<strong>increase level index</strong>&rdquo; nutzt als Quellblatt &ldquo;<strong>main</strong>&rdquo; sowie das Feld &ldquo;<strong>next level index</strong>&rdquo; und verweist auf den Parameter &ldquo;<strong>current level index</strong>&rdquo;. Und vice versa die Aktion &ldquo;<strong>decrease level index</strong>&rdquo; mit dem Quellblatt &ldquo;<strong>go back</strong>&rdquo;, dem Feld &ldquo;<strong>previous level index&rdquo;</strong> und ebenfalls dem Ziel-Parameter &ldquo;<strong>current level index</strong>&rdquo;:</p>
<ul>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-7.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-7.png" alt="Aktion zur Parameter-Steuerng: Den Level-Index um 1 erhöhen…"></a>


</p>
<p>Aktion zur Parameter-Steuerng: Den Level-Index um 1 erhöhen…</p>
</li>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-8.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-8.png" alt="…und um eins verringern"></a>


</p>
<p>…und um eins verringern</p>
</li>
</ul>
<p>Danach benötigen wir eine Aktion, um den Filter-Parameter &ldquo;<strong>level name concatenated</strong>&rdquo; zu setzen: Dieser erhält den Wert aus dem berechneten Feld &ldquo;<strong>current level concat</strong>&rdquo;, wenn man auf das Arbeitsblatt &ldquo;<strong>main</strong>&rdquo; klickt. Und beim Arbeitsblatt &ldquo;<strong>go back</strong>&rdquo; ist es wieder genau andersrum: Hier kommt der Wert aus dem Feld &ldquo;<strong>previous level concat</strong>&rdquo;:</p>
<ul>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-9.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-9.png" alt="Die Aktion um den Filter für die nächsttiefere Eben zu setzen…"></a>


</p>
<p>Die Aktion um den Filter für die nächsttiefere Eben zu setzen…</p>
</li>
<li>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-10.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2019/2019-07-15-fokussierter-drill-down-mit-tableau/images/grafik-10.png" alt="… und für die darüberliegende Ebene"></a>


</p>
<p>… und für die darüberliegende Ebene</p>
</li>
</ul>
<h3 id="et-voila">Et voila</h3>
<p>Das war es. Du kannst nun das Dashboard nutzen, um beliebig durch deine Dimensionen zu klicken. Hier und da lässt sich das ganze sicherlich noch etwas optimieren. Zuerst kannst du z.B. die &ldquo;Steuerfelder&rdquo; ausblenden, um nur die relevanten Informationen zu präsentieren (in der Regel über rechte Maustaste &ldquo;<strong>Kopfzeile ausblenden</strong>&rdquo;). Außerdem kann man die Formeln optimieren und z.B. verhindern, dass der Nutzer tiefer klickt, als die verwendete Anzahl von Dimensionen. Das sind aber nur kleinere Baustellen. Das wichtigste Ziel sollte erreicht sein: Die Darstellung ist weitaus übersichtlicher und vor allem kann man nun in Sekundenbruchteilen bis zur letzten Eben navigieren.<br>
Natürlich kann man das ganze auch mit einem Chart kombinieren, was der Sache noch etwas mehr Glanz verleiht. Auf Tableau Public habe ich eine Dashboard veröffentlicht, dass das ganze in Aktion zeigt. Aus Gründen ist die Datenquelle dort allerdings nur 100.000 Zeilen groß:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/fokussierter-drill-down-mit-tableau/grafik-10.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://public.tableau.com/profile/nr1871#!/vizhome/focussedDrillDown/dashboard</a>


</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>tutorial</dc:type></item><item><title>NGinx mit PHP-FPM, MySQL und Xdebug mit Docker auf Mac OS einrichten</title><link>https://nickyreinert.de/2018/2018-09-16-nginx-mit-php-fpm-mysql-und-xdebug-mit-docker-auf-mac-os-einrichten/</link><pubDate>Sun, 16 Sep 2018 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2018/2018-09-16-nginx-mit-php-fpm-mysql-und-xdebug-mit-docker-auf-mac-os-einrichten/</guid><description>Ich habe mich eine ganze weile erfolgreich vor Docker als lokale Entwicklungsumgebung gedrückt. Der Grund: Ich nutze eine kommerzielle Parallels Lizenz, über die ich Ubuntu virtualisiert laufen lasse und bin damit bisher ganz gut gefahren. Bisher. Paralles hat nämlich immer wieder Problem gemacht. …</description><content:encoded><![CDATA[<p>Ich habe mich eine ganze weile erfolgreich vor <strong>Docker</strong> als lokale Entwicklungsumgebung gedrückt. Der Grund: Ich nutze eine kommerzielle Parallels Lizenz, über die ich Ubuntu virtualisiert laufen lasse und bin damit bisher ganz gut gefahren. Bisher. Paralles hat nämlich immer wieder Problem gemacht. So konnte ich nach manchen Updates von Parallels oder Ubuntu die <em>Parallels Tools</em> nicht mehr nutzen und musste diese umständlich neu installieren. Da die Doku dazu auch nur unvollständig vorliegt, war das regelmäßig eine ziemliche Zeitverschwendung. Die <em>Parallels Tools</em> sind aber notwendig, um von Ubuntu aus auf die Dateien des Gastsystems, Mac OS X, zuzugreifen.</p>
<p>Beim letzten Update auf Ubuntu 18.04 ließen sich die <em>Parallels Tools</em> gar nicht mehr installieren. Die Ursache dafür ist wohl irgendeine Inkompatibilität eines abhängigen Paketes mit dem neuen Linux-Kernel. Wie auch immer: Ich war die Sorgen leid und auf der Suche nach einer Alternative. Weshalb ich <strong>Docker</strong> stieß. Der Vorteil: Docker ist weitaus performanter und portabler. Der Nachteil: Um damit eine funktionierende Entwicklungsumgebung zu schaffen, kommt man um die Shell nicht herum. Das mitgelieferte UI Kitematic liegt noch als Beta vor und lässt einige Funktionen einfach vermissen. Es gibt also einige kleinere Fallstricke, die es zu überwinde galt. Was mir gelungen ist. Wie, das werde ich nun genauer erläutern. <em>Viel Spass</em>.</p>
<p>[caption id=&ldquo;attachment_2166&rdquo; align=&ldquo;aligncenter&rdquo; width=&ldquo;300&rdquo;]





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/nginx-mit-php-fpm-und-mysql-mit-docker-auf-mac-os-einrichten/kitematic-scho%cc%88n-aber-noch-nutzlos.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/tags/development/images/kitematic-scho%CC%88n-aber-noch-nutzlos-300x181.png" alt="Kitematic - schön aber (noch) weitestgehend nutzlos"></a>


 Kitematic - schön aber (noch) weitestgehend nutzlos[/caption]</p>
<h2 id="erste-schritte">Erste Schritte</h2>
<p>Grundsätzlich kann man bei der  Installation der Docker-Anwendung nicht viel falsch machen: Account anlegen, Docker herunterladen, installieren, anmelden - fertig. Auf die zugrunde liegende Technologie will ich hier nicht weiter eingehen, das machen andere weitaus besser (z.B. 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.ab-heute-programmieren.de/docker-teil-1-was-ist-docker/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">ist diese Anleitung sehr zu empfehlen</a>


).</p>
<p>Nur soviel soll gesagt sein: Du lädst ein <strong>Image</strong> herunter, dass du dann <em>starten</em> kannst. Dadurch erhältst du einen <strong>Container</strong>, der bestimmte Dienste bereitstellt. Dieser Container ist kein vollständiges Betriebssystem mit all seinem Ballast. So kannst du z.B. nicht ohne weiteres per SSH darauf zugreifen. Das funktioniert nur, wenn der entsprechende SSH-Dienst auch im Image vorgesehen ist. Das schöne an Docker ist aber, dass du das Image mit beliebigen Funktionalitäten über ein sogenanntes <strong>Dockerfile</strong> relativ unkompliziert nachrüsten kannst. Das erfordert zwar eine gewisse Umgewöhnung im Arbeitsablauf ab, bringt aber auch viele Vorteile mit sich.</p>
<p>Wer will kann sich mit der <strong>Kitematic UI</strong> durch die vorhandenen Docker-Images wühlen und auch direkt herunterladen. Diese stehen dann natürlich auch auf der Kommandozeile zur Verfügung. Mit <strong>docker image ls</strong> zeigst du alle verfügbaren Images an. Analog dazu listet <strong>docker container ls</strong> alle erstellten Container auf. Außerdem gibt es noch ein paar andere, für den Anfang ganz brauchbare Befehle:</p>
<p># alle lokal verfügbaren Images auflisten
docker image ls</p>
<h1 id="alle-gestarteten-bzw-erzeugten-container-auflisten">alle gestarteten bzw. erzeugten Container auflisten</h1>
<p>docker container ls</p>
<h1 id="die-kommandozeile-für-den-container-mit-dem-namen-mysql-die-kommandozeile-starten">die Kommandozeile für den Container mit dem Namen &ldquo;mysql&rdquo; die Kommandozeile starten</h1>
<p>docker exec -it mysql /bin/bash</p>
<h1 id="für-den-container-mit-dem-namen-mysql-das-setup-anzeigen">für den Container mit dem Namen &ldquo;mysql&rdquo; das Setup anzeigen</h1>
<p>docker inspect mysql</p>
<p>Für eine lokale Entwicklungsumgebung benötigt man zunächst einen <strong>HTTP- und einen MySQL-Server.</strong> Natürlich gehört zu jeder guten Entwicklungsumgebung auch ein Debugger - für PHP wäre das wohl <strong>xdebug</strong>. Ich nutze hier die beiden Images: 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://hub.docker.com/r/richarvey/nginx-php-fpm/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">nginx-php-fpm von Ric Harvey</a>


 und 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://hub.docker.com/_/mysql/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">das sehr aktuelle und offizielle MySQL-Image</a>


, die ich erstmal ganz unkompliziert über Kitematic herunterlade. Das nginx-Image werde ich schließlich mit einem <strong>Dockerfile</strong> anpassen um auch <strong>xdebug</strong> nutzen zu können.</p>
<p>[caption id=&ldquo;attachment_2167&rdquo; align=&ldquo;aligncenter&rdquo; width=&ldquo;300&rdquo;]





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/nginx-mit-php-fpm-und-mysql-mit-docker-auf-mac-os-einrichten/kitematic-auswahl-von-images.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2018/2018-09-16-nginx-mit-php-fpm-mysql-und-xdebug-mit-docker-auf-mac-os-einrichten/images/kitematic-auswahl-von-images-300x152.png" alt="Die Auswahl neuer Images über Kitematic ist sehr bequem"></a>


 Die Auswahl neuer Images über Kitematic ist sehr bequem[/caption]</p>
<p>Ab hier verlasse ich Kitematic allerdings wieder und werde Docker nur noch über die Kommandozeile und ein paar selbst geschriebene Scripte nutzen. Los gehts…</p>
<h2 id="den-mysql-container-starten">Den MySQL-Container starten</h2>
<p>… es mit dem MySQL-Container. Da der nginx-Container auf MySQL zugreift, muss der MySQL-Container auch zuerst dasein. Der Aufruf dafür sieht folgendermaßen aus:</p>
<p>docker run \
&ndash;name mysql \
&ndash;publish 3306:3306 \
&ndash;volume /Users/nicky/Development/MySQL:/var/lib/mysql \
&ndash;env MYSQL_ALLOW_EMPTY_PASSWORD=yes \
&ndash;env MYSQL_ROOT_HOST=% \
&ndash;detach \
&ndash;default-authentication-plugin=mysql_native_password \
mysql</p>
<p>Mit &ndash;name vergebe ich einen festen und vor allem lesbaren Namen. Ohne diesen Parameter würde Docker eine Id anlegen, die den Zugriff später aber unnötig kompliziert macht. Der Parameter <em>--publish</em> legt fest, welcher Port &ldquo;von draußen&rdquo; auf einen Port im Docker-Container gemappt wird. Da sich die Dateien für die Datenbank physikalisch natürlich nicht im Docker-Container, sondern im Dateisystem vom Host befinden, muss ich dem Container mitteilen, wie er darauf zugreifen kann. Das passiert mit dem Parameter <em>--volume</em>. Damit kann ich nich nur Ordner im Container verfügbar machen, sondern auch Dateien. Das ist ganz praktisch, um z.B. Configurations-Dateien von außerhalb beim Start des Containers mitzuliefern.</p>
<p>Der Parameter <em>--env</em> dient dazu, Umgebungsvariablen zu setzen. Diese sind natürlich abhängig vom verwendeten Container. Für den MySQL-Container möchte ich hier zwei Parameter übergeben: Ich will eine Root-Benutzer ohne Passwort anlegen (<em>MYSQL_ALLOW_EMPTY_PASSWORD</em>) (das mag unsicher erscheinen, da ich hier aber lokal nur mit Testdaten arbeite, ist das erstmal einfach nur pragmatisch). Außerdem soll sich jeder Client verbinden können, also setzte ich <em>MYSQL_ROOT_HOST</em> auf %.</p>
<p>Eine Besonderheit ist der Parameter <em>default-authentication-plugin</em>. Wenn du ein Root-Passwort vergibst, solltest du zusätzlich diesen Parameter setzen. Zur Erklärung: Es handelt sich hier um ein MySQL 8-Image. Dort wird als Authentifizierungs-Methode <em>caching_sha2_password</em> verwendet, was sich mit Docker leider nicht verträgt und mit dieser Fehlermeldung quittiert wird:</p>
<blockquote>
<p>Unable to load authentication plugin &lsquo;caching_sha2_password</p>
</blockquote>
<p>Und schließlich gibt es noch den Parameter &ndash;detach, der einfach dafür sorgt, dass der Container im Hintergrund gestartet wird.</p>
<p>Das war es fast. Was jetzt noch fehlt, ist der Name des Images, dass die Grundlage für deinen neuen Container bilden soll: <em>mysql</em>. Wenn das Image lokal nicht vorhanden ist, lädt Docker es hilfsbereiterweise einfach herunter.</p>
<p>Es empfiehlt sich, den Aufruf in ein Shell-Script zu packen und diese Zeilen voranzustellen. Beim Aufruf wird also ein vorhandener Container erst gestoppt und gelöscht und dann neu gestartet:</p>
<p>#!/bin/bash
docker stop mysql
docker rm mysql
docker run \
&ndash;name mysql \
-p 3306:3306 \
-v /Users/nicky/Development/MySQL:/var/lib/mysql \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
-e MYSQL_ROOT_HOST=% \
-d \
&ndash;default-authentication-plugin=mysql_native_password \
mysql</p>
<p>Et voilà: Ein MySQL-Server im Docker-Container auf Knopfdruck!</p>
<h2 id="den-http-container-starten">Den HTTP-Container starten</h2>
<p>Am Vorgehen ändert sich nicht viel. Auch für den <strong>nginx-Server</strong> erstelle ich mir ein kleines Script. Dieses befindet sich aber in einem Unterordner meiner Wordpress-Installation. Mit dem Platzhalter <strong>${PWD}</strong> im Parameter <em>--volume</em> kann ich Docker das aktuelle Verzeichnis mitgeben. Dadurch kann  ich das Script auch innerhalb andere Wordpress-Installationen verwenden (und muss lediglich den Namen des Containers anpassen). Hier sorge ich also dafür, dass Docker das aktuelle Verzeichnis als Root-Verzeichnis für den HTTP-Server nutzt. Außerdem liefere ich eine eigene ini-Datei für PHP mit. Wichtig ist außerdem die Verknüpfung zu meinem zuvor erstellen MySQL-Container über den Parameter <em>--link</em>. Der Rest wird analog des ersten Containers vorgegeben: Port, Name, Image, usw.</p>
<p>#!/bin/bash
docker stop nickyreinert-de
docker rm nickyreinert-de
docker run \
&ndash;link mysql \
&ndash;name nickyreinert-de \
&ndash;volume ${PWD}/dev/wordpress.ini:/usr/local/etc/php/conf.d/uploads.ini \
&ndash;volume ${PWD}:/var/www/html \
&ndash;publish 80:80 \
&ndash;detach \
richarvey/nginx-php-fpm</p>
<h2 id="xdebug-installieren">xdebug installieren</h2>
<p>Leider liefert das nginx-Image kein <strong>xdebug</strong> mit. Bzw: Zurecht - der Sinn des ganzen Konzeptes ist es ja, schlanke Container nutzen zu können. Eine All-In-One-Lösung entspräche letztlich ja wieder einer kompletten virtuellen Maschine. Für mich jedenfalls heißt das, dass ich das nginx-Image nun irgendwie mit <strong>xdebug</strong> füttern muss. Das passiert mit <strong>docker build</strong>. Dazu benötige ich zunächst ein Dockerfile, dass auch genau so heißt und folgendermaßen aufgebaut ist - Erklärung folgt darunter:</p>
<p>FROM richarvey/nginx-php-fpm</p>
<p>RUN apk add &ndash;no-cache &ndash;virtual .phpize-deps $PHPIZE_DEPS
RUN apk add &ndash;no-cache nano</p>
<p>RUN pecl install xdebug
RUN echo &lsquo;zend_extension = /usr/local/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so&rsquo; &raquo; /usr/local/etc/php/php.ini
RUN touch /usr/local/etc/php/conf.d/xdebug.ini; \
echo xdebug.remote_enable=1 &raquo; /usr/local/etc/php/conf.d/xdebug.ini; \
echo xdebug.remote_autostart=1 &raquo; /usr/local/etc/php/conf.d/xdebug.ini; \
echo xdebug.remote_connect_back=0 &raquo; /usr/local/etc/php/conf.d/xdebug.ini; \
echo xdebug.remote_host=192.168.0.11  &raquo; /usr/local/etc/php/conf.d/xdebug.ini; \
echo xdebug.remote_port=9000 &raquo; /usr/local/etc/php/conf.d/xdebug.ini; \
echo xdebug.remote_log=/tmp/php-xdebug.log &raquo; /usr/local/etc/php/conf.d/xdebug.ini;</p>
<p>Zunächst einmal lege ich mit <em>FROM</em> fest, welches Images als Grundlage genutzt werden soll - in meinem Fall also nginx-php-fpm von richarvey. Mit <em>RUN</em> übergebe ich dann, zum Image passende, Befehle. So könnte ich xdebug zum Beispiel einfach mit pecl install xdebug installieren lassen. Du wirst dann aber recht schnell feststellen, dass das nicht ohne weiteres funktioniert. PHP ist in diesem Docker-Image natürlich nur mit den wichtigsten Paketen eingerichtet, <em>phpize</em> gehört so z.B. nicht dazu und der Aufruf würde mit folgender Fehlermeldung quittiert werden:</p>
<p>Cannot find autoconf. Please check your autoconf installation and the # $PHP_AUTOCONF environment variable. Then, rerun this script.</p>
<p>Die Lösung ist, <em>phpize</em> im Voraus mit allen notwendigen Abhängigkeiten zu installieren. Dazu dient der Aufruf</p>
<p>RUN apk add &ndash;no-cache &ndash;virtual .phpize-deps $PHPIZE_DEPS</p>
<p>Das das nicht immer so umfangreich vonstatten gehen muss, soll die Installation des Texteditors <em>nano</em> zeigen: Man kann gewünschte Pakete auch einfach mit <em>apk add <paketname></em> zum Docker-Image hinzufügen. Der apk-Parameter <em>--virtual</em> besagt, dass die genannten Pakete zu einem &ldquo;virtuellen Paket&rdquo; hinzugefügt werden, dass sich dann mit <em>apk del <paketname></em> leicht wieder entfernen lässt. Danach schließlich kann <strong>xdebug</strong> mit <em>pecl install</em> installiert werden. Die letzten beiden RUN-Aufrufe sorgen dafür, dass eine Standard-Konfiguration für xdebug eingerichtet wird. Hier wird es tatsächlich noch etwas tricky, wenn es um den <em>remote_host</em> geht. Docker lässt den Container später in einem eigenen Netzwerk laufen. Der Docker-Host, also in meinem Fall OS X,erhält dafür eine eigene IP-Adresse, wie z.B. 172.17.0.1. Das Problem: Das ist nicht die IP-Adresse, unter der OS X bzw. der Debug-Client (z.B. Visual Studio Code) erreichbar ist. Jeder Debug-Versuch wurde (in meinem Fall) in <em>/tmp/php-xdebug.log</em> mit folgender Fehlermeldung quittiert:</p>
<p>W: Creating socket for &lsquo;127.0.0.1:9000&rsquo;, poll success, but error: Operation in progress (29).</p>
<p>Der Remote-Host ist also die tatsächliche IP-Adresse deines Hosts, unabhängig von Docker. Hier demnach die IP-Adresse <em>192.168.0.11</em>.</p>
<p>Zum Abschluss muss das Dockerfile nur noch verarbeitet werden. Das passiert mit folgendem Aufruf:</p>
<p>docker build &ndash;tag nginx-php-fpm-xdebug .</p>
<p>Mit dem Parameter <em>--tag</em> gibst du dem modifizierten Image einen eigenen Namen. Der letzte Parameter - ein Punkt - zeigt docker, in welcher Datei sich die Build-Anweisung befindet. Docker sucht standardmäßig nach einer Datei mit dem Namen Dockerfile. Deshalb verweise ich mit dem Punkt einfach nur auf den aktuellen Ordner, in dem sich diese Datei auch befindet.</p>
<p>Docker wird nun die Anweisungen aus dem Dockerfile verarbeiten und ein neues Image erzeugen. Der erste Aufruf wird etwas länger dauern. Wenn du an dem Dockerfile nur geringfügige Änderungen vornimmst und den build-Prozesse erneut startest, ist Docker so clever und nimmt nur die notwendigen Änderungen vor - alles andere befindet sich bereits in einer Art &ldquo;Zwischenspeicher&rdquo;.</p>
<p>Denke nun daran, den oben bereits zusammengebauten Container-Aufruf des HTTP-Servers den Image-Name anzupassen: <em>nginx-php-fpm-xdebug</em>!</p>
<h2 id="visual-studio-code">Visual Studio Code</h2>
<p>Der Vollständigkeit halber möchte ich nun noch die Schritte dokumentieren, die bei Microsofts Visual Studio Code (VSC) notwendig sind. Hier installiert man zunächst das Paket <strong>PHP Debug</strong>. Im Debug-Bereich fügt man dann eine neue Konfiguration hinzu, die VSC mitteilt, unter welcher IP-Adresse xdebug erreichbar ist - nämlich 127.0.0.1 und dem üblichen Port: 9000:</p>
<p>{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://go.microsoft.com/fwlink/?linkid=830387&amp;utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://go.microsoft.com/fwlink/?linkid=830387</a>



&ldquo;version&rdquo;: &ldquo;0.2.0&rdquo;,
&ldquo;configurations&rdquo;: [
{
&ldquo;name&rdquo;: &ldquo;Listen for XDebug&rdquo;,
&ldquo;type&rdquo;: &ldquo;php&rdquo;,
&ldquo;request&rdquo;: &ldquo;launch&rdquo;,
&ldquo;port&rdquo;: 9000,
&ldquo;host&rdquo;: &ldquo;127.0.0.1&rdquo;
}
]
}</p>
<p>Fertig. Mit einem Klick auf den grünen Playbutton wird VSC sich nun mit xdebug verbinden.</p>
<p>[caption id=&ldquo;attachment_2199&rdquo; align=&ldquo;aligncenter&rdquo; width=&ldquo;300&rdquo;]





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/nginx-mit-php-fpm-und-mysql-mit-docker-auf-mac-os-einrichten/visual-studio-code-den-debugger-starten.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2018/2018-09-16-nginx-mit-php-fpm-mysql-und-xdebug-mit-docker-auf-mac-os-einrichten/images/visual-studio-code-den-debugger-starten-300x162.png" alt="Visual Studio Code - den Debugger starten"></a>


 Visual Studio Code - den Debugger starten[/caption]</p>
<h2 id="fazit">Fazit</h2>
<p>Docker hat mich voll überzeugt. Ich bereue es, dass ich nicht schon früher umgestiegen bin. Es läuft sauber und wenn man das Konzept erstmal verstanden hat, ist es auch sehr intuitiv zu bedienen und lässt vor allem keine Wünsche auf. Im Nachhinein betrachtet hat mich die komplette Einrichtung der virtuellen Maschine mit Ubuntu unter Parallels auch weitaus mehr Zeit - und Nerven! - gekostet. Was jetzt noch fehlt, ist das etwas dynamischere Verwalten mehrerer lokaler Websiten mit Docker. Dazu komme ich später - <em>stay tuned</em>.</p>
<h2 id="referenzen">Referenzen</h2>
<p>Mein Dank gilt 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://paislee.io/a-minimal-development-server-with-nginx-and-docker/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Caleb Sotelo</a>


, von wo ich einen Großteil der Scripte habe. Ein Teil des Dockerfiles stammt von 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://blog.philipphauer.de/debug-php-docker-container-idea-phpstorm/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">philipphauer.de</a>


. Danke!</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>tutorial</dc:type></item><item><title>Die OS X - Bash</title><link>https://nickyreinert.de/2018/2018-08-24-die-os-x-bash/</link><pubDate>Fri, 24 Aug 2018 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2018/2018-08-24-die-os-x-bash/</guid><description>Da ich mit meinem Mac sehr oft auf der Kommandozeile unterwegs bin (dazu empfehle ich übrigens [iTerm2](- ein Terminal-Ersatz der alle anderen Anwendungen dieser Art auch auf Linux und Windows aussticht) habe ich mir dafür ein paar kleine Einstellungen zusammengesammelt, die das Leben und Arbeiten …</description><content:encoded><![CDATA[<p>Da ich mit meinem Mac sehr oft auf der Kommandozeile unterwegs bin (dazu empfehle ich übrigens [iTerm2](- ein Terminal-Ersatz der alle anderen Anwendungen dieser Art auch auf Linux und Windows aussticht) habe ich mir dafür ein paar kleine Einstellungen zusammengesammelt, die das Leben und Arbeiten auf der Kommandozeile etwas erleichtern.</p>
<h2 id="was-ist-bash">Was ist Bash?</h2>
<p>Bash steht für &ldquo;Bourn again shell&rdquo; und ist eine Erweiterung der älteren Bourne-Shell, eine Shell die auf Unix-(ähnlichen) Systemen als Benutzerschnittstelle dient. Und diese ist vor allem daran erkennbar, dass die Steuerung des Systems nur über die Tastatur, ohne Maus und Fenster funktioniert.</p>
<p>Seit der Version 10.3 ist Bash auch Bestandteil von Mac OS X und bringt damit eine Menge Vorteile für die Arbeit auf der Kommandozeile mit sich.</p>
<h2 id="die-bash-konfigurationsdateien">Die Bash Konfigurationsdateien</h2>
<p>Die globalen Einstellungen für die Bash befinden sich in der Datei <strong>/etc/bashrc</strong> und der <strong>/etc/profile</strong>. Wer Einstellungen auf bestimmte Benutzer beschränken will, kann im Benutzerverzeichnis (du gelangst mit <em>cd ~</em> dorthin) eine Datei mit dem Name <strong>.profile</strong>, <strong>.bash_login</strong> oder <strong>.bash_profile</strong> anlegen. Beachte, dass <strong>.profile</strong> nur geladen wird, wenn <strong>.bash_login</strong> nicht existiert, welche wiederum durch <strong>.bash_profile</strong> überschrieben wird. Das gilt allerdings nicht, wenn du dich als <strong>root-Benutzer</strong> anmeldest - dann wird nur die Datei <strong>.profile</strong> gelesen. Der Grund ist, dass der Root-Benutzer nicht <strong>bash</strong> sondern <strong>sh</strong> ausführt, wodurch nur die <strong>~/.profile</strong>-Datei berücksichtigt wird.</p>
<img src="https://nickyreinert.de/2018/2018-08-24-die-os-x-bash/images/image-8.png" alt=""><p>Hierarchie der Bash-Konfigurations-Dateien</p>
<p>Daneben gibt es theoretisch auch die Möglichkeit, im Benutzerverzeichnis eine Datei mit dem Namen <strong>.bashrc</strong> anzulegen. Diese wird jedoch nur dann geladen, wenn Bash ausgeführt wird, ohne dass sich ein Benutzer dazu anmeldet (die sogenannte <em>non-login-shell</em>). Im OS X-Umfeld wird das aber nicht benötigt. Du kannst das erzwingen, indem du auf der Kommandozeile <strong>/bin/bash</strong> ausführst - wir wollen die jetzt schon komplizierte Geschichte aber nicht unnötigen verkomplizieren. ;)</p>
<h2 id="die-bash-aufhübschen">Die Bash aufhübschen</h2>
<p>Nach der langweiligen aber notwendige Theorie, gibts jetzt ein paar Zeilen Code, mit denen die Kommandozeile gleich viel besser aussieht. Ich hab aus oben genannten Gründen das Ganze in die Datei <strong>~/.profile</strong> gepackt. Zunächst einmal ein paar Aliase um die Verzeichnislisten etwas übersichtlicher zu gestalten oder auch das oft benutzte <strong>cd</strong> (<em>change dir</em>) zu vereinfachen.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">alias l=&#39;ls -alCFGA&#39;
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">alias ll=&#34;ls -CFGla&#34;
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">alias h=&#34;history&#34;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">alias .=&#34;cd ~&#34;
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">alias ..=&#34;cd ..&#34;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">alias ...=&#34;cd ../..&#34;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"># -p: erzeuge Unterverzeichnis, falls sie nicht existieren
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"># -v: gebe erzeugtes Verzeichnis zurück
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">alias mkdir=&#34;mkdir -pv&#34;
</span></span><span class="line"><span class="ln">10</span><span class="cl"># grep Ausgabe farbig gestalten
</span></span><span class="line"><span class="ln">11</span><span class="cl">alias grep=&#39;grep --color=auto&#39;
</span></span><span class="line"><span class="ln">12</span><span class="cl">alias fgrep=&#39;fgrep --color=auto&#39;
</span></span><span class="line"><span class="ln">13</span><span class="cl">alias egrep=&#39;egrep --color=auto&#39;
</span></span><span class="line"><span class="ln">14</span><span class="cl"># das aktuelle Verzeichnis im Finder öffnen
</span></span><span class="line"><span class="ln">15</span><span class="cl">alias f=&#39;open -a Finder ./&#39;
</span></span></code></pre></div><p>Die folgende Zeile ist kein Alias sondern eine Funktion. Damit wird ein Verzeichnis (mit allen benötigten Unterverzeichnissen erstellt) und dann mit cd direkt in das Verzeichnis gewechselt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">mcd () { mkdir -p &#34;$1&#34; &amp;&amp; cd &#34;$1&#34;; }
</span></span></code></pre></div><p>Als nächstes will ich die Darstellung der zuletzt benutzten Befehle (<strong>history</strong> bzw. jetzt <strong>h</strong>) etwas anpassen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl"># Duplikate ignorieren
</span></span><span class="line"><span class="ln">2</span><span class="cl">HISTCONTROL=ignoreboth
</span></span><span class="line"><span class="ln">3</span><span class="cl"># Befehle anhängen und die Historie nicht jedes mal neu überschreiben
</span></span><span class="line"><span class="ln">4</span><span class="cl">shopt -s histappend
</span></span><span class="line"><span class="ln">5</span><span class="cl"># die maximale Größe bzw. Länge der Historie erhöhen
</span></span><span class="line"><span class="ln">6</span><span class="cl">HISTSIZE=1000
</span></span><span class="line"><span class="ln">7</span><span class="cl">HISTFILESIZE=2000
</span></span></code></pre></div><p>Der folgende Befehl sorgt dafür, dass sich die Anzahl der dargestellten <strong>Spalten und Zeilen</strong> an die Fenstergröße anpasst. Die Funktion sollte per default aktiviert sein, zur Sicherheit legen wir das hier trotzdem fest! Mit dem Parameter -u kann die Option übrigens wieder deaktiviert werden.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">shopt -s checkwinsize
</span></span></code></pre></div><p>Hiermit wird das Aussehen und die Farbe der Kommandozeile geändert. Ich hab mich für eine relativ bunte Variante entschieden. Die Einstellung für den sog. Prompt wird in der globalen Variable <strong>PS1</strong> (<em>prompt statement 1</em>) gespeichert. Die folgenden Parameter sind hier geläufig:</p>
<ul>
<li>\u - Benutzername</li>
<li>\h - Name des Hosts / Computers</li>
<li>\w - der komplette aktuelle Pfad</li>
<li>\n  - Ein Zeilenumbruch</li>
</ul>
<p>Mit <strong>PS2</strong>, <strong>PS3</strong>, <strong>PS4</strong> lassen sich noch andere Prompts konfigurieren, wie z.B. der für interaktive Eingaben in Bash-Scripten. Die Variable <strong>PROMPT_COMMAND</strong> wird sogar noch vor <strong>PS1</strong> dargestellt. Wer mehr dazu erfahren möchte, 





  
  

  
  
    
    
    
  

  
  
  

  
    
      
    
  

  
  <a href="https://wiki.ubuntuusers.de/Bash/Prompt/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous#source-2" target="_blank" rel="noopener noreferrer">schaut einfach mal dort vorbei</a>


.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">export</span> <span class="n">PS1</span><span class="o">=</span><span class="s2">&#34;$(date +%k:%m:%S) \[</span><span class="se">\033</span><span class="s2">[36m\]\u\[</span><span class="se">\033</span><span class="s2">[m\]@\[</span><span class="se">\033</span><span class="s2">[32m\]\h:\[</span><span class="se">\033</span><span class="s2">[33;1m\]\w\[</span><span class="se">\033</span><span class="s2">[m\]\$ &#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Diese Konstanten steuern die Farben für die Ausgabe von cd.</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">export</span> <span class="n">CLICOLOR</span><span class="o">=</span><span class="mi">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">export</span> <span class="n">LSCOLORS</span><span class="o">=</span><span class="n">ExFxBxDxCxegedabagacad</span>
</span></span></code></pre></div><p>Das ganze sieht dann so aus:</p>
<img src="https://nickyreinert.de/2018/2018-08-24-die-os-x-bash/images/image-9.png" alt=""><p>Eine bunte aber informative Kommandozeile</p>
<p>Diese Funktion zeigt alle Prozesse, die unter dem aktuellen Benutzer aktiv sind:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">me() { ps $@ -u $USER -o pid,%cpu,%mem,start,time,bsdtime,command ; }
</span></span></code></pre></div><p>Als nächstes ein paar Aliase um das Netzwerk ein bisschen einfacher im Griff zu behalten:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl"># alle Verbindungen, die gerade &#34;Lauschen&#34;
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">alias cons=&#39;sudo lsof -i | grep LISTEN&#39;
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"># alle offenen Sockets
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">alias socks=&#39;sudo /usr/sbin/lsof -i -P&#39;
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"># alle offenen TCP/IP-Sockets
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">alias tsocks=&#39;lsof -i&#39;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"># alle offenen UDP-Sockets
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">alias usocks=&#39;sudo /usr/sbin/lsof -nP | grep UDP&#39;
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"># alle offenen TCP-Sockets
</span></span><span class="line"><span class="ln">10</span><span class="cl">alias lsocks=&#39;sudo /usr/sbin/lsof -nP | grep TCP&#39; 
</span></span><span class="line"><span class="ln">11</span><span class="cl"># grundlegende Netzwerkeinstellungen
</span></span><span class="line"><span class="ln">12</span><span class="cl">alias net=&#39;ipconfig getpacket en0&#39;
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"># den DNS-Cache leeren
</span></span><span class="line"><span class="ln">15</span><span class="cl">alias cleardns=&#39;dscacheutil -flushcache&#39;
</span></span></code></pre></div><p>Die ganzen Inhalte gibt es hier noch einmal in kompakter Version:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">case</span> <span class="o">$-</span> <span class="ow">in</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="o">*</span><span class="n">i</span><span class="o">*</span><span class="p">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">      <span class="o">*</span><span class="p">)</span> <span class="k">return</span><span class="p">;;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">esac</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">alias</span> <span class="n">l</span><span class="o">=</span><span class="s1">&#39;ls -alCFGA&#39;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">alias</span> <span class="n">ll</span><span class="o">=</span><span class="s2">&#34;ls -CFGla&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">alias</span> <span class="n">h</span><span class="o">=</span><span class="s2">&#34;history&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">alias</span> <span class="o">.=</span><span class="s2">&#34;cd ~&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">alias</span> <span class="o">..=</span><span class="s2">&#34;cd ..; pwd&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">alias</span> <span class="o">...=</span><span class="s2">&#34;cd ../..; pwd&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">alias</span> <span class="n">mkdir</span><span class="o">=</span><span class="s2">&#34;mkdir -pv&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">alias</span> <span class="n">grep</span><span class="o">=</span><span class="s1">&#39;grep --color=auto&#39;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">alias</span> <span class="n">fgrep</span><span class="o">=</span><span class="s1">&#39;fgrep --color=auto&#39;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">alias</span> <span class="n">egrep</span><span class="o">=</span><span class="s1">&#39;egrep --color=auto&#39;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">alias</span> <span class="n">f</span><span class="o">=</span><span class="s1">&#39;open -a Finder ./&#39;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">HISTCONTROL</span><span class="o">=</span><span class="n">ignoreboth</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">shopt</span> <span class="o">-</span><span class="n">s</span> <span class="n">histappend</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">HISTSIZE</span><span class="o">=</span><span class="mi">1000</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">HISTFILESIZE</span><span class="o">=</span><span class="mi">2000</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">shopt</span> <span class="o">-</span><span class="n">s</span> <span class="n">checkwinsize</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="k">case</span> <span class="s2">&#34;$TERM&#34;</span> <span class="ow">in</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="n">xterm</span><span class="o">-</span><span class="n">color</span><span class="o">|*-</span><span class="mi">256</span><span class="n">color</span><span class="p">)</span> <span class="n">color_prompt</span><span class="o">=</span><span class="n">yes</span><span class="p">;;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="n">esac</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="k">export</span> <span class="n">PS1</span><span class="o">=</span><span class="s2">&#34;$(date +%k:%m:%S) \[</span><span class="se">\033</span><span class="s2">[36m\]\u\[</span><span class="se">\033</span><span class="s2">[m\]@\[</span><span class="se">\033</span><span class="s2">[32m\]\h:\[</span><span class="se">\033</span><span class="s2">[33;1m\]\w\[</span><span class="se">\033</span><span class="s2">[m\]\$ &#34;</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="k">export</span> <span class="n">CLICOLOR</span><span class="o">=</span><span class="mi">1</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="k">export</span> <span class="n">LSCOLORS</span><span class="o">=</span><span class="n">ExFxBxDxCxegedabagacad</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="n">mcd</span> <span class="p">()</span> <span class="p">{</span> <span class="n">mkdir</span> <span class="o">-</span><span class="n">p</span> <span class="s2">&#34;$1&#34;</span> <span class="o">&amp;&amp;</span> <span class="n">cd</span> <span class="s2">&#34;$1&#34;</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="n">me</span><span class="p">()</span> <span class="p">{</span> <span class="n">ps</span> <span class="o">$</span><span class="err">@</span> <span class="o">-</span><span class="n">u</span> <span class="o">$</span><span class="n">USER</span> <span class="o">-</span><span class="n">o</span> <span class="n">pid</span><span class="p">,</span><span class="o">%</span><span class="n">cpu</span><span class="p">,</span><span class="o">%</span><span class="n">mem</span><span class="p">,</span><span class="n">start</span><span class="p">,</span><span class="n">time</span><span class="p">,</span><span class="n">bsdtime</span><span class="p">,</span><span class="n">command</span> <span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="n">alias</span> <span class="n">cons</span><span class="o">=</span><span class="s1">&#39;sudo lsof -i | grep LISTEN&#39;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="n">alias</span> <span class="n">socks</span><span class="o">=</span><span class="s1">&#39;sudo /usr/sbin/lsof -i -P&#39;</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="n">alias</span> <span class="n">tsocks</span><span class="o">=</span><span class="s1">&#39;lsof -i&#39;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="n">alias</span> <span class="n">usocks</span><span class="o">=</span><span class="s1">&#39;sudo /usr/sbin/lsof -nP | grep UDP&#39;</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="n">alias</span> <span class="n">lsocks</span><span class="o">=</span><span class="s1">&#39;sudo /usr/sbin/lsof -nP | grep TCP&#39;</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="n">alias</span> <span class="n">net</span><span class="o">=</span><span class="s1">&#39;ipconfig getpacket en0&#39;</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="n">alias</span> <span class="n">cleardns</span><span class="o">=</span><span class="s1">&#39;dscacheutil -flushcache&#39;</span>
</span></span></code></pre></div>]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>guide</dc:type></item><item><title>Wie entwickelt man einen Amazon-Bot für Telegramm?</title><link>https://nickyreinert.de/2018/2018-03-02-wie-entwickelt-man-einen-amazon-bot-fuer-telegramm/</link><pubDate>Fri, 02 Mar 2018 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2018/2018-03-02-wie-entwickelt-man-einen-amazon-bot-fuer-telegramm/</guid><description>Was ist ein Telegramm-Bot? Schon mal was von Telegramm gehört oder gar einem Telegramm-Bot gehört? Nein? Macht nichts. Telegramm ist ein Messenger, wie Whatsapp oder Threema. Der große Vorteil von Telegramm gegenüber Whatsapp ist die Plattforum-Unabhängigkeit. Es gibt nicht nur Apps für Android und …</description><content:encoded><![CDATA[<h2 id="was-ist-ein-telegramm-bot">Was ist ein Telegramm-Bot?</h2>
<p>Schon mal was von Telegramm gehört oder gar einem Telegramm-Bot gehört? Nein? Macht nichts. Telegramm ist ein Messenger, wie Whatsapp oder Threema. Der große Vorteil von Telegramm gegenüber Whatsapp ist die Plattforum-Unabhängigkeit. Es gibt nicht nur Apps für Android und iOs, auch unter OS X (Mac) und Windows lässt sich Telegramm unkompliziert nutzen. Bei Whatsapp war das bisher nur über den Browser möglich, Threema bietet diese Möglichkeit gar nicht an.</p>
<p>Außerdem bietet Telegramm ein sehr umfangreiche API an, die noch dazu sehr gut dokumentiert ist. Damit lassen sich sogenannte Telegramm-Bots entwickeln. Diese Bots können dann von den Nutzern mit @einBot angesprochen werden, um alle Möglichen Aufgaben auszuführen. Einer der bekanntesten ist vermutlich der @gif-Bot, der die Plattform giphy.com durchsuchen kann und die Ergebnisse in den Chat-Verlauf schicken kann. Und diese Art von Bots lassen sich auch selber entwickeln und anbieten. Unter anderem auch mit PHP.</p>
<p>Wer die Kommunikation mit Telegramm nicht komplett von vorne aufbauen will, kann auf eine Bibliothek [von Longman auf github.com]( zugreifen. Für meinen Geschmack ist diese leider etwas zu schlecht dokumentiert und mitunter etwas durcheinander organisiert. Noch dazu wird sie offenbar nur sporadisch betreut. Dennoch gibt es ein relativ großes Repository mit Beispielen, was definitiv sehr hilfreich  ist. Etwas verwirrend sind die beiden Repositories, die da <em>Core</em> und <em>Manager</em> heißen. Der <em>Manager</em> ist nur eine Art Wrapper für das Core-Repository. Dessen Nutzung erleichtert die Einrichtung und Entwicklung des Bots um ein vielfaches und bietet z.B. eine zentrale Datei (manager.php) an, um alle Funktionen zu steuern. Beim Core werden die jeweiligen Bot-Funktionen über verschiedene Dateien gesteuert.</p>
<h3 id="was-ist-das-ziel">Was ist das Ziel?</h3>
<p>Was ist also der Plan? Wir bauen uns einen Telegramm-Bot, mit dem sich Amazon nach Produkten durchsuchen lässt. Das Ergebnis wird in einem Grid dargestellt. Wählt man ein Element aus, erscheint es mit Link zum Produkt im Chat-Verlauf. Und mit einer Analytics-Plattform soll das ganze</p>
<p>Um deinen eigenen Bot betreiben zu können brauchst du natürlich erstmal Telegramm und außerdem einen Web-Server und eine Domain für die du ein SSL-Zertifikat einrichten musst. Außerdem solltest du 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://getcomposer.org/download/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">composer</a>


 installiert haben und so ungefähr wissen, wie man damit umgeht.</p>
<h2 id="anmelden-eines-bots">Anmelden eines Bots</h2>
<p>Zunächst musst du deinen neuen Telegramm-Bot anmelden. Dazu nutzt du den BotFather (





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://telegram.me/botfather?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">BotFather)</a>


. dem du mit dem Befehl /<em>newbot</em> dazu bringen kannst, einen neuen Bot zu registrieren. Zuerst wird ein Name für den Bot verlangt - das ist allerdings noch <em>nicht</em> der Name, mit dem der Bot später auch angesprochen wird. Das ist erst der sogenannte <em>username</em>, der im 2. Schritt verlangt wird. Ich nenne meinen Bot <em>@buecherBot</em>.</p>
<p>Leider gibt es hier eine Restriktion: Der Name muss mit <em>bot</em> enden. Wer einen kurzen Namen wie @gif oder @youtube nutzen möchte, muss dazu vermutlich etwas mehr Aufwand betreiben. Als nächstes rufst du die Einstellungen des Bots auf, indem du das Inline-Menü mit /mybots öffnest.</p>
<p>Für unseren Bot solltest du zunächst den Inline-Mode aktivieren. Damit kannst du den Bot im Textfeld direkt ansprechen. Das gehst du über den Punkt Bot-Settings zum Inline-Mode und aktivierst diesen.</p>
<p>[caption id=&ldquo;attachment_1655&rdquo; align=&ldquo;aligncenter&rdquo; width=&ldquo;300&rdquo;]





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/wie-entwickelt-man-einen-bot-fuer-telegramm/telegramm-bot-father.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2018/2018-03-02-wie-entwickelt-man-einen-amazon-bot-fuer-telegramm/images/telegramm-bot-father-300x200.png" alt="Inline-Mode für den Telegramm-Bot aktivieren"></a>


 Inline-Mode für den Telegramm-Bot aktivieren[/caption]</p>
<p> </p>
<p>Schließlich kannst du <em>Allow Groups</em> auf Nein setzen und die <em>Group Privacy</em> aktivieren. Da der Bot noch nicht in Gruppen aktiv sein soll, muss er weder Mitglied von Gruppen sein noch deren Nachrichten mitlesen.</p>
<p>Als nächstes lässt du dir den API-Schlüssel anzeigen, damit du auch von außen mit dem Bot kommunizieren kannst. Der API-Schlüssel befindet sich im obersten Menü und ist folgendermaßen aufgebaut:</p>
<p>123123123:128390123jKF19082_1293123123</p>
<p>Das war es, was die Vorbereitung angeht. Weiter geht es mit PHP und einer kleinen Bot-Logik.</p>
<h2 id="dem-bot-mit-php-leben-einhauchen">Dem Bot mit PHP Leben einhauchen</h2>
<p>Als Library bzw. Framework nutzen wir das 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/php-telegram-bot/core?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Telegram-Bot-Repository</a>


 von Longman bzw. den dazugehörigen 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/php-telegram-bot/telegram-bot-manager?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Manager</a>


, eine Art Wrapper, der die Steuerung etwas erleichtert.Damit du für den ersten Start bereits eine gute Grundlage hast, solltest du dir 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/php-telegram-bot/example-bot?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">die Beispiel-Dateien von github</a>


 herunterladen. Auf der github-Seite wird zwar die Einstellung für die composer.json vorgegeben, allerdings hat der checkout damit bei mir nicht funktioniert. Mit folgenden Parametern klappte es dann:</p>
<p>{
&ldquo;require&rdquo;: {
&ldquo;php-telegram-bot/telegram-bot-manager&rdquo;: &ldquo;^1.2&rdquo;,
&ldquo;longman/telegram-bot&rdquo;: &ldquo;0.52 as 0.48&rdquo;
}
}</p>
<p>Nachdem composer alle Abhängigkeiten heruntergeladen hat (<em>composer install</em>), muss man sich in der manager.php an die Grundeinstellungen machen. (Es kann passieren, dass du auf deinem System einige PHP-Module installieren musst. Composer wird dir die Namen der Module allerdings nennen. Du kannst sie dann relativ unkompliziert mit z.B. apt-get install php7.0-b_cmath php7.0-curl herunterladen und aktivieren.) Außerdem werde ich ein paar Änderungen an der Datei InlinequeryCommand.php_ aus dem Ordner Commands aus dem Beispiel-Repository vornehmen.</p>
<p>Aber zuerst zur manager.php. Hier solltest du die folgenden Einstellungen vornehmen:</p>
<ul>
<li>
<p><strong>bot_username</strong> - der Benutzername deines Bots, ohne das führende @</p>
</li>
<li>
<p><strong>api_key</strong> - der API-Schlüssel, den dir der Bot-Father gegeben hat</p>
</li>
<li>
<p><strong>secret</strong> - ein selbst erzeugtes Passwort um die Datei vor Zugriffen von außen zu schützen - die PHP-Datei liegt ja auf deinem öffentlichen Server und kann theoretisch von überall aufgerufen werden.</p>
</li>
<li>
<p><strong>webhook-&gt;url</strong> - hier trägst du die URL zur manager.php-Datei ein, also 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://telegramm.example.com/manager.php?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://telegramm.example.com/manager.php</a>


</p>
</li>
<li>
<p><strong>commands-&gt;paths</strong> - diese Zeile muss auskommentiert sein und zu einem Pfad verweisen, der deine Bot-Commandos enthält. Wenn du dich an dem Bot-Example orientierst, sieht diese Einstellung so aus:</p>
<pre><code>      'commands' =&gt; \[
         // Define all paths for your custom commands
         'paths'   =&gt; \[
             \_\_DIR\_\_ . '/Commands'
         \]
</code></pre>
</li>
</ul>
<p>Als nächstes kannst du noch ein paar zusätzliche Änderungen vornehmen, die aber für die Funktion des Bots nicht wichtig sind:</p>
<ul>
<li><strong>max_connections</strong> - offensichtlich die Anzahl der maximal zulässigen Verbindungen</li>
<li><strong>logging</strong> - diese Zeilen kannst du aus-kommentieren, um das Logging zu aktivieren, gerade in der Anfangsphase ist das ganz nützlich</li>
<li><strong>limiter</strong> - die Telegram-API lässt natürlich nur eine begrenzte Anzahl von Anfragen zu, um das eigene System zu schützen, offenbar bringt das Framework eine Funktion mit, um das Erreichen des Limits bestmöglich zu vermeiden - wie das funktioniert, kann ich nicht sagen, da dazu auch nicht mehr in der Doku steht, ich hab es erstmal aktiviert</li>
</ul>
<p>Grundsätzlich war es das erstmal, was die Voreinstellungen betrifft. Jetzt kannst du den sog. webhook aktivieren. Dazu rufst du die manager.php im Browser auf, wie z.B.</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://telegramm.example.com/manager.php?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://telegramm.example.com/manager.php</a>


</p>
<h3 id="den-zugriff-von-außen-beschränken">Den Zugriff von außen beschränken</h3>
<p>Wenn du alles richtig gemacht hast, erscheint jetzt erstmal ein Fehler. Und das ist auch gut so - denn schließlich soll der Zugriff von außen ja nicht jedem gewährt werden.</p>
<p>Fatal error: Uncaught TelegramBot\TelegramBotManager\Exception\InvalidAccessException:
Invalid access in …</p>
<p>Also packst du noch das secret-Token an die URL, dass du weiter oben in der manager.php angegeben hast (nicht den API-Schlüssel von Telegramm!). Und damit du auch eine Aktion auslöst, setzt du erstmal den webhook mit dem Parameter a=set:</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://telegramm.example.com/manager.php?s=123123123&amp;a=set&amp;utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://telegramm.example.com/manager.php?s=123123123&a=set</a>


</p>
<p>Dein Webhook für den Bot ist nun aktiv. Da du in der <em>manager.php</em> außerdem schon den Ordner commands freigegeben hast, kann der Bot nun schon auf Inline-Anfragen antworten. Im Telegramm-Client kannst du nun deinen Privat-Chat öffnen (&ldquo;Gespeichertes&rdquo;) und den Telegramm-Bot mit @buecherBot ansprechen. Der Bot sollte nun, wenn alles korrekt eingerichtet, so antworten, wie es in der Datei <em>InlinequeryCommand.php</em> vorgegeben ist. Nämlich mit einer sehr einfache 3-zeiligen Liste.</p>
<p>Um nun ein Ergebnis von der Amazon API als Bilder-Liste darzustellen, habe ich <em>InlinequeryCommand.php</em> ein wenig angepasst. Zuerst benötigen wir die entsprechende Klasse, um nicht nur mit Text sondern auch mit Bildern zu antworten:</p>
<p>use Longman\TelegramBot\Entities\InlineQuery\InlineQueryResultPhoto;</p>
<p>Außerdem habe ich den Aufbau des Antwort-Objektes etwas vereinfacht (ich werde hier nicht weiter ins Detail gehen, auf $this-&gt;apiResult gehe ich später ein).</p>
<p>foreach ($this-&gt;apiResult-&gt;items as $item) {</p>
<p>$this-&gt;inlineQueryResult[] = new InlineQueryResultPhoto(array(
&lsquo;id&rsquo;                    =&gt;  sizeof($this-&gt;inlineQueryResult),
&rsquo;title&rsquo;          	=&gt; &lsquo;Search: &rsquo; . $query,
&lsquo;description&rsquo;           =&gt; &lsquo;Info: &rsquo; . $information,
&rsquo;thumb_url&rsquo;             =&gt; $thumbUrl,
&lsquo;photo_url&rsquo;             =&gt; $imageUrl,
&lsquo;caption&rsquo;             	=&gt; $linkUrl
)
);</p>
<p>}</p>
<p>$data[&lsquo;results&rsquo;] = &lsquo;[&rsquo; . implode(&rsquo;,&rsquo;, $this-&gt;inlineQueryResult) . &lsquo;]&rsquo;;</p>
<p>return Request::answerInlineQuery($data);</p>
<p>Grundsätzlich war es das schon. Du kannst nun noch alle möglichen anderen Funktionen des Bots nutzen. Schau dir dazu einfach den Commands-Ordner des Example-Repository an. Die Auswahl ist sehr groß, für meine Zwecke soll es aber erstmal bei der Inline-Query bleiben.</p>
<h2 id="eine-suchanfrage-zu-amazon-schicken">Eine Suchanfrage zu Amazon schicken</h2>
<p>Natürlich soll derTelegramm-Bot nun auch in der Lage sein, Anfragen der Benutzer zu Amazon zu schicken und mit einem vernünftigen Suchergebnis zu antworten. Zunächst benötigt man dafür einen Partner-Account bei 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://partnernet.amazon.de?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Amazon</a>


. 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://partnernet.amazon.de/assoc_credentials/home?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">In den Einstellungen</a>


 kann man sich dann ein Schlüsselpaar erstellen um die Suchanfragen zu authentifizieren. Und auch für die Suchanfrage selber gibt es eine PHP-API, nämlich 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/Exeu/apai-io/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">apai-io von exeu</a>


. Die Einrichtung und der Aufbau sind relativ einfach. Nachdem composer die notwendigen Dateien heruntergeladen hat, packt man - wie üblich - ein paar Zeilen in eine PHP-Datei und hat die erste Anfrage an die Amazon-API fertig:</p>
<pre><code>&lt;?php ini\_set('display\_errors', 1);

require\_once \_\_DIR\_\_ . '/vendor/autoload.php';

define('AWS\_API\_KEY', 'AKAKAKAKAKAKAKA');
define('AWS\_API\_SECRET\_KEY', 'KALSKDLASKDLASDKLASDKLASKLDKASLDASDL
define('AWS\_ASSOCIATE\_TAG', 't0000-21');

use ApaiIO\\Configuration\\GenericConfiguration;
use ApaiIO\\Operations\\Search;
use ApaiIO\\ApaiIO;

$conf = new GenericConfiguration();
$client = new \\GuzzleHttp\\Client();
$request = new \\ApaiIO\\Request\\GuzzleRequest($client);

$conf
	-&gt;setCountry('de')
	-&gt;setAccessKey(AWS\_API\_KEY)
	-&gt;setSecretKey(AWS\_API\_SECRET\_KEY)
	-&gt;setAssociateTag(AWS\_ASSOCIATE\_TAG)
	-&gt;setRequest($request);
	
$apaiIO = new ApaiIO($conf);

$search = new Search();
$search-&gt;setCategory('DVD');
$search-&gt;setActor('Bruce Willis');
$search-&gt;setKeywords('Stirb Langsam');

$response = $apaiIO-&gt;runOperation($search);

$results = simplexml\_load\_string( $response ); 
</code></pre>
<p>Leider gestalten sich die ersten Versuche mit der API etwas schwieriger. Nutzt man den Beispiel-Code, liefert das ganze Script nur einen sehr langen String zurück. In der weiterführenden [Dokumentation]( wird zwar ein <em>setResponseTransformer</em> beschrieben (siehe auskommentierte Zeile oben). Doch auch dann ist die Antwort nicht nutzbar. Das Suchergebnis bleibt weiterhin ein langer String. Erst 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/Exeu/apai-io/issues/48?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Issue 48</a>


 gibt einen Hinweis auf die Lösung: <em>new SimpleXMLElement</em>.  Jetzt erhalte ich ein XML-Object, in dem ich mich mit einer Schleife durch das Ergebnis arbeiten kann. Oder man greift auf 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://gist.github.com/ottanxyz/4a44a5094bf27606c12803590a92d131?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">die hier verwendete Funktion</a>


  <em>simplexml_load_string</em> zurück - mein Favorit.</p>
<p>Nun geht es darum, die Antwort in eine Schleife zu packen, um die notwendigen Informationen zu extrahieren. Das Suchergebnis muss also vorbereitet und zurück an denTelegramm-Bot geschickt werden.</p>
<h2 id="die-hochzeit---amazons-antwort-an-dentelegramm-bot-weiterleiten">Die Hochzeit - Amazons-Antwort an denTelegramm-Bot weiterleiten</h2>
<p>Bisher war die Amazon-Funktionalität in eine andere Datei ausgelagert. Also müssen erstmal Zugangsdaten und die Abhängigkeiten in die InlineQueryCommand.php übernommen werden. Ich mach mir das Leben nicht unnötig schwer, und packe das alles in die InlinequeryCommand.php. Sauberer wäre es vielleicht, die initiale bot-Klasse etwas zu erweitern. Außerdem  muss die Anfrage an den Bot an die Amazon-API durchgeschliffen werden. Wenn der Benutzer nur einen Suchbegriff angibt (Bruce Willis), wird danach in allen Kategorien gesucht. Wer die Suche einschränken möchte, muss dem Suchbegriff die entsprechende Kategorie voranstellen. Also z.B.: Books:Bibel Das ganze sieht dann in etwa so aus:</p>
<p>$request = explode(&rsquo;:&rsquo;, trim($requestString));</p>
<p>if (sizeof($request) == 1) {</p>
<pre><code>$category = 'All';
$keywords = trim($request\[0\]);
</code></pre>
<p>} else {</p>
<pre><code>$category = trim($request\[0\]);
$keywords = trim($request\[1\]);
</code></pre>
<p>}</p>
<p>Kategorie und Keyword werden nun schlicht an die Amazon-API übergeben. Damit ich auch Vorschaubilder erhalte, muss ich das in der Suche explizit angeben. Die Anfrage an die API sieht nun also so aus:</p>
<p>$search = new Search();
$search-&gt;setCategory($category);
$search-&gt;setKeywords($keywords);
$search-&gt;setResponseGroup( array( &lsquo;Images&rsquo;, &lsquo;ItemAttributes&rsquo; ) );	
$search-&gt;setPage(1);</p>
<p>Das ganze wird nun in ein Objekt gepackt und in einer Schleife durchlaufen um die Antwort für den Bot zu erzeugen. Das hat sich bei den ersten Versuchen als schwierig erwiesen. Es hat eine Weile gedauert, bis ich herausgefunden habe, dass ich die Eigenschaften des Antwort-Objekts als String casten muss: (string)!</p>
<p>foreach ($this-&gt;apiResult-&gt;Items-&gt;Item as $item) {</p>
<p>$title  = (string) $item-&gt;ItemAttributes-&gt;Title;
$url  = (string) $item-&gt;DetailPageURL;
$thumbFileUrl  = (string) $item-&gt;LargeImage-&gt;URL;
$thumbFileName = basename($thumbFileUrl);
$thumbFileType = pathinfo($thumbFileName, PATHINFO_EXTENSION);</p>
<p>if ($thumbFileType == &lsquo;jpg&rsquo; || $thumbFileType == &lsquo;jepg&rsquo;) {</p>
<pre><code>$this-&gt;inlineQueryResult\[\] = new InlineQueryResultPhoto(
array(
		'id'                    =&gt; sizeof($this-&gt;inlineQueryResult),
		'title'                 =&gt; $title,
		'description'           =&gt; $title,
		'caption'          		=&gt; $title@,
		'thumb\_url'             =&gt; $thumbFileUrl,
		'photo\_url'             =&gt; $thumbFileUrl,
		'input\_message\_content' =&gt; new InputTextMessageContent(\[
			'parse\_mode' =&gt; 'HTML',
			'message\_text' =&gt; ' ' . 'Shop it @ &lt;a href=&quot;'.$url.'&quot;&gt;Amazon&lt;/a&gt;!'\])
	)
);
</code></pre>
<p>}</p>
<p>if (sizeof($this-&gt;inlineQueryResult) &gt;= $this-&gt;limitResult) {</p>
<pre><code>break;
</code></pre>
<p>}</p>
<p>}</p>
<p>Grundsätzlich war es das. Das ganze Script gibt es zum Nachlesen wie gesagt auf github.</p>
<h2 id="zugriffsstatistiken-mit-botan-aufzeichnen-und-darstellen">Zugriffsstatistiken mit Botan aufzeichnen und darstellen</h2>
<p>Um Botan nutzen zu können, brauchst du einen Account bei 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://appmetrica.yandex.com/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">AppMetrica</a>


 von Yandex. Dort kannst du direkt nach der Registrierung einen API-Schlüssel für deinen Bot anlegen:</p>
<p>[caption id=&ldquo;attachment_1649&rdquo; align=&ldquo;aligncenter&rdquo; width=&ldquo;300&rdquo;]





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/wie-entwickelt-man-einen-bot-fuer-telegramm/appmetrics-yandex-bot-api-key-anlegen.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2018/2018-03-02-wie-entwickelt-man-einen-amazon-bot-fuer-telegramm/images/appmetrics-yandex-bot-api-key-anlegen-300x130.png" alt="AppMetrics API Key für Bot anlegen"></a>


 AppMetrics API Key für Bot anlegen[/caption]</p>
<p>Wenn das erledigt ist, gelangst du zu der Übersichtsseite deines Bots und siehst dort den API-Key, den du als token in der <em>manager.php</em> einträgst. Die entsprechenden Zeilen müssen natürlich auch auskommentiert werden:</p>
<pre><code>    // Botan.io integration
    'botan' =&gt; \[
       'token' =&gt; '123123123-123123123-123123',
    \],
</code></pre>
<p>Das ist es schon fast gewesen. Wenn da nicht der Datenschutz wäre. Wenn du Telegramm-Bot im Chat nun &ldquo;anrufst&rdquo;, erscheint der Aufruf einige Augenblicke später auch im Interface von AppMetrics. Allerdings sind dann auch eine UserId und der Vorname im Klartext enthalten. Um das zu vermeiden, musst du Methode <em>track</em> in der Botan-Klasse (Botan.php in vendor\longman\telegram-bot\src\) anpassen:</p>
<p>Direkt an den Anfang der Methode habe ich die folgende Anonymisierung eingefügt:</p>
<p>// ANONYMIZING STATS
if (isset($update-&gt;inline_query)) {</p>
<pre><code>if (isset($update-&gt;inline\_query\['from'\])) {

	if (isset($update-&gt;inline\_query\['from'\]\['id'\])) {
	
		$update-&gt;inline\_query\['from'\]\['id'\] = '0';
		$update-&gt;inline\_query\['from'\]\['first\_name'\] = 'anon';
		$update-&gt;raw\_data\['inline\_query'\]\['from'\]\['id'\] = '0';
		$update-&gt;raw\_data\['inline\_query'\]\['from'\]\['first\_name'\] = 'anon';
			
	}
}
</code></pre>
<p>}</p>
<p>Weiter unten wird eine UserId ($uid) ermittelt. Auch hier habe ich anonymisiert:</p>
<p>// In case there is no from field assign id = 0
// $uid = isset($data[&lsquo;from&rsquo;][&lsquo;id&rsquo;]) ? $data[&lsquo;from&rsquo;][&lsquo;id&rsquo;] : 0;
// ANONYMIZING STATS
$uid = 0;</p>
<p>Jetzt dürften keine Klarnamen mehr übermittelt werden und der Bot ist startklar.</p>
<p> </p>
<p>Viel Spass mit deinem Telegramm-Bot.</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>tutorial</dc:type></item><item><title>Download-Geschwindigkeit messen und in Google DataStudio darstellen</title><link>https://nickyreinert.de/2017/2017-11-16-download-geschwindigkeit-messen-und-in-googledata-studio-darstellen/</link><pubDate>Thu, 16 Nov 2017 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2017/2017-11-16-download-geschwindigkeit-messen-und-in-googledata-studio-darstellen/</guid><description>Alles beginnt mit einer fixen Idee. Meine war es, die Download-Geschwindigkeit meines Internet-Anschlusses zu messen. Doch das ist nur aussagekräftig, wenn man es regelmäßig macht. Und um das ganze abzurunden, sollte man die Ergebnisse doch irgendwie noch in einem bunten Diagramm darstellen können. …</description><content:encoded><![CDATA[<p>Alles beginnt mit einer fixen Idee. Meine war es, die Download-Geschwindigkeit meines Internet-Anschlusses zu messen. Doch das ist nur aussagekräftig, wenn man es regelmäßig macht. Und um das ganze abzurunden, sollte man die Ergebnisse doch irgendwie noch in einem bunten Diagramm darstellen können. Aus der Idee wurde also ein Plan und schließlich ein Projekt.</p>
<p>Die Aufgabe lautet also: Ein Script soll in regelmäßigen Abständen Test-Dateien herunterladen. Die Dauer dafür und der Zeitpunkt des Tests schreibe ich in eine CSV-Datei, die mit GoogleDrive synchronisiert ist. Von dort werden die Ergebnisse im DataStudio von Google automatisch ausgelesen.</p>
<h2 id="schritt-1---das-automatisierte-download-script">Schritt 1 - das automatisierte Download-Script</h2>
<p>Damit der Test möglichst unterschiedliche Szenarien abdeckt, wollte ich nicht nur eine sondern mehrere Dateien verschiedener Größe anbieten. Dazu habe ich auf http://speedtest.ftp.otenet.gr zurückgegriffen. Dort werden verschiedene Dateigrößen zum Download angeboten.</p>
<p>Außerdem will ich jede Datei mehr als ein mal herunterladen und schließlich nach jedem Download eine Pause einlegen.</p>
<p>Die Hauptfunktionen möchte ich kurz erläutern:</p>
<ul>
<li>In den ersten beiden Schleife wird jeder Eintrag aus <strong>sourceFileSizes</strong> einmal durchlaufen und zwar so oft, wie mit <strong>loops</strong> festgelegt:</li>
</ul>
<p>for sourceFileSize in &ldquo;${sourceFileSizes[@]}&rdquo;;
do
while [ $i -lt $loops ]; do
…
done
done</p>
<ul>
<li>Innerhalb der Schleife wird im Grund nur die Test-Datei per wget an einen definierten Ort heruntergeladen. Außerdem soll natürlich die Dauer dafür gemessen werden. Das funktioniert ganz einfach und extrem präzise in Nanosekunden bzw. über den Unix-Timestamp.</li>
</ul>
<p>startTime=$(($(date +%s%N)))</p>
<p>wget &ndash;quiet
&ndash;output-document=$destinationFolder$sourceFileSize&quot;Mb.db.tmp&quot;
$sourceBaseUrl$sourceFileSize&quot;Mb.db&quot;</p>
<p>endTime=$(($(date +%s%N)))</p>
<p>delayNsecs=$(($endTime - $startTime))</p>
<ul>
<li>Die Berechnung der Geschwindigkeit ist etwas kompliziert, da ich auf der Shell nicht ohne weiteres Dezimalzahlen (float numbers) verarbeiten kann. Ich muss also mit <strong>awk</strong> arbeiten. Awk hingegen greift nicht auf die lokalen Variablen zu. Diese muss ich mit dem Parameter -v erst explizit übergeben. Am Ende entstehen dann Zeile wie diese, die im Grunde nur MByte in MBit umrechnen (mit 8 multiplizieren) und dann durch die Dauer in Sekunden dividieren. Das Ergebnis ist dann die Geschwindigkeit <strong>MBit/Sekunde</strong> - und damit perfekt vergleichbar mit der versprochenen Geschwindigkeit des Anbieters.</li>
</ul>
<p>MBitPerSec=
$(awk
-v sourceFileSize=$sourceFileSize
-v delayMSecs=$delaySecs
&lsquo;BEGIN{printf &ldquo;%.4f\n&rdquo;, ( sourceFileSize * 8 / delayMSecs)}&rsquo;)</p>
<ul>
<li>Schließlich wird der ganze Spaß natürlich noch in die CSV-Datei geschrieben:</li>
</ul>
<p>echo &ldquo;$(date &lsquo;+%Y-%m-%d %H:%M:%S&rsquo;),
$delayNsecs,
$delaySecs,
$MBitPerSec,
$sourceFileSize&quot;MByte&rdquo;&quot;
&raquo; $journalFile</p>
<ul>
<li>Am Ende wird entweder das Script verlassen, wenn zuletzt die 1GByte-Datei heruntergeladen wurde. Da das Script mehrmals am Tag läuft, will ich das Volumen nicht unnötig strapazieren. Oder es wird eine definierte Pause eingelegt, damit sich die Leitung abkühlen kann.</li>
</ul>
<p>Das komplette Script gibt es auf 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/nickyreinert/speedTest/blob/master/speedTest.sh?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">github.</a>


</p>
<p>Das ganze muss nun nur in der CronTabelle des Systems regelmäßig aufgerufen werden. Der Zielordner</p>
<p>/share/Download/Speedtest/</p>
<p>wird außerdem mit GoogleDrive synchronisiert.</p>
<p>Die fertige Datei besitzt fünf Spalten, die den Zeitpunkt, die Dauer und die heruntergeladene Datei beinhalten:</p>
<p>column1,column2,column3,column4,column5
2017-10-31 11:48:36,1072562724,1.072562724,7.45877124105611,1MByte
2017-10-31 11:48:37,899356112,0.899356112,8.89525282950432,1MByte
2017-10-31 11:48:38,1002897956,1.002897956,7.97688334305469,1MByte</p>
<p>Anmerkung: Ich nutze den Netzwerkspeicher von QNAP, das TS-431. Für diesen wird ein Backup &amp; Sync-Plugin angeboten, das lokale Ordner mit einem Ordner in GoogleDrive synchronisiert.</p>
<h2 id="schritt-2---der-community-connector-für-das-datastudio">Schritt 2 - der Community Connector für das DataStudio</h2>
<p>Das Google DataStudio bringt von Hause aus schon eine Reihe von Schnittstellen mit, über die es möglich ist, auf z.B. Datenbanken oder Online-Dienste zurückzugreifen um in Echzeit an allerlei Messreihen zu kommen. Doch leider fehlt hier bisher ein Verbindung zu CSV-Dateien. Den Connector musste ich mir also erst selber erstellen.</p>
<p>Der Connector ist im Moment noch sehr einfach gehalten. Es ermöglicht keine großen Anpassungen und ist sicherlich noch verbesserungsfähig (





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://datastudio.google.com/datasources/create?connectorId=AKfycbxxafV6ymAs6S2DRADTGKzJ2aNCdwrnMgRIMt-KJAzoO-YESKc19U9z2w&amp;utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Link zum Connector</a>


):</p>
<p>function getConfig() {
var config = {
configParams: [
{
type: &ldquo;INFO&rdquo;,
name: &ldquo;csvConnector&rdquo;,
text: &ldquo;The CSV-Connector currently supports a fixed amount of three columns. Name them column1, column2 and column3. Column1 is the dimension, column2 holds the metrics and column3 may be used as an additional category. You may change the label in the next window.&rdquo;
}
,{
type: &ldquo;TEXTINPUT&rdquo;,
name: &ldquo;url&rdquo;,
helpText: &ldquo;If you want to use a CSV-file from GoogleDrive, use this format where 123 at the end is your document id: 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://drive.google.com/uc?export=download&amp;id=123%22&amp;utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">https://drive.google.com/uc?export=download&id=123"</a>


,
displayName: &ldquo;Provide the url to your csv file.&rdquo;
}</p>
<pre><code>\]
</code></pre>
<p>};
return config;</p>
<p>};</p>
<p>var csvDataSchema = [
{
name: &lsquo;column1&rsquo;,
label: &lsquo;column1&rsquo;,
dataType: &lsquo;STRING&rsquo;,
semantics: {
conceptType: &lsquo;DIMENSION&rsquo;
}
},
{
name: &lsquo;column2&rsquo;,
label: &lsquo;column2&rsquo;,
dataType: &lsquo;NUMBER&rsquo;,
semantics: {
&ldquo;isReaggregatable&rdquo;: true,
conceptType: &lsquo;METRIC&rsquo;
}
},{
name: &lsquo;column3&rsquo;,
label: &lsquo;column3&rsquo;,
dataType: &lsquo;STRING&rsquo;,
semantics: {
&ldquo;isReaggregatable&rdquo;: false,
conceptType: &lsquo;DIMENSION&rsquo;
}</p>
<p>}
];</p>
<p>function getSchema(request) {</p>
<p>return {schema: csvDataSchema};</p>
<p>};</p>
<p>function isAdminUser() {
return true;
}</p>
<p>function csvToObject(array) {</p>
<p>var headers = array[0];</p>
<p>var jsonData = [];
for ( var i = 1, length = array.length; i &lt; length; i++ )
{
var row = array[i];
var data = {};
for ( var x = 0; x &lt; row.length; x++ )
{
data[headers[x]] = row[x];
}
jsonData.push(data);</p>
<pre><code>}

return jsonData;
</code></pre>
<p>}
/*
function stringToObject(string, separator)
{
var object = {};</p>
<p>var array = string.split(separator);</p>
<p>for (var i = 0; i &lt; array.length; i++) {</p>
<pre><code>if (i % 2 === 0) { 

  object\[array\[i\]\] = array\[i + 1\];

} else { 

  continue;
  
}
</code></pre>
<p>}</p>
<p>return object
}
*/</p>
<p>function getData(request) {</p>
<p>/*
I DONT GET SPLIT TO WORK SO FOR NOW THIS ONLY SUPPORTS PREPARED AND WORKING SHARING URL
FOR GOOGLE DRIVE
if (request.configParams.isGoogleDrive == &ldquo;true&rdquo;)
{
var urlString = request.configParams.url.toString();</p>
<pre><code>var urlArray = urlString.split(&quot;?&quot;);

  var params = stringToObject(urlArray, '&quot;');

  var docId = params\[&quot;id&quot;\];

  var url = &quot;https://drive.google.com/uc?export=download&amp;id=&quot; + docId;
</code></pre>
<p>} else {</p>
<pre><code>  var url = request.configParams.url;
</code></pre>
<p>}
*/</p>
<p>var url = request.configParams.url;</p>
<p>var dataSchema = [];</p>
<p>request.fields.forEach(function(field) {
for (var i = 0; i &lt; csvDataSchema.length; i++) {
if (csvDataSchema[i].name === field.name) {
dataSchema.push(csvDataSchema[i]);
break;
}
}
});</p>
<p>var csvFile = UrlFetchApp.fetch(url);</p>
<p>var csvData = Utilities.parseCsv(csvFile);</p>
<p>var sourceData = csvToObject(csvData);</p>
<p>var data = [];</p>
<p>sourceData.forEach(function(row) {
var values = [];
dataSchema.forEach(function(field) {
switch(field.name) {
case &lsquo;column1&rsquo;:
values.push(row.column1);
break;
case &lsquo;column2&rsquo;:
values.push(row.column2);
break;
case &lsquo;column3&rsquo;:
values.push(row.column3);
break;
default:
values.push(&rsquo;&rsquo;);
}
});
data.push({
values: values
});
});</p>
<p>return {
schema: dataSchema,
rows: data
};</p>
<p>};</p>
<p>function getAuthType() {
var response = {
&ldquo;type&rdquo;: &ldquo;NONE&rdquo;
};
return response;
}</p>
<p> </p>
<p>Leider unterstützt der Connector bisher nur drei Spalten mit vorgegebene Spalten-Namen. In einer nächsten Version sollte der Connector die Datei bereits im Vorfeld auslesen um die Spalten-Konfiguration selber zu erkennen.</p>
<p> </p>
<h2 id="schritt-3---darstellung-im-datastudio">Schritt 3 - Darstellung im DataStudio</h2>
<p>Im Data Studio ein Diagramm erstellen, dass die Messwerte der regelmäßigen Test-Download darstellt: Darum muss ich mich noch kümmern.</p>
<p> </p>
<h2 id="links-und-noch-mehr-links">Links und noch  mehr Links</h2>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/nickyreinert/speedTest/blob/master/speedTest.sh?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Link zu GitHub mit dem Quellcode</a>


</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://datastudio.google.com/datasources/create?connectorId=AKfycbxxafV6ymAs6S2DRADTGKzJ2aNCdwrnMgRIMt-KJAzoO-YESKc19U9z2w&amp;utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Link zum Connector</a>


</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://developers.google.com/datastudio/connector/get-started?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">Einführung und Doku auf developers.google.com</a>


</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.benlcollins.com/data-studio/community-connector/?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">kleinere Beispiele auf benlcollins.com</a>


</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://github.com/google/datastudio?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">andere Projekte auf github.com</a>


</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>project_showcase</dc:type></item><item><title>XAMPP startet nicht mehr, nachdem eine php-Extension hinzugefügt wurde</title><link>https://nickyreinert.de/2017/2017-10-19-xampp-startet-nicht-mehr-nachdem-eine-php-extension-hinzugefuegt-wurde/</link><pubDate>Thu, 19 Oct 2017 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2017/2017-10-19-xampp-startet-nicht-mehr-nachdem-eine-php-extension-hinzugefuegt-wurde/</guid><description>Ich hatte hier neulich das Problem, dass ich die mongodb Extension für PHP aktivieren wollte und sich danach der Apache-Dienst nicht mehr starten ließ. Aber der Reihe nach:
Das System ist OS 10.12.6 und ich nutze das XAMPP-Paket 7.1.9 mit entsprechend PHP 7.1. Außerdem habe ich mir homebrew …</description><content:encoded><![CDATA[<p>Ich hatte hier neulich das Problem, dass ich die mongodb Extension für PHP aktivieren wollte und sich danach der Apache-Dienst nicht mehr starten ließ. Aber der Reihe nach:</p>
<p>Das System ist OS 10.12.6 und ich nutze das XAMPP-Paket 7.1.9 mit entsprechend PHP 7.1. Außerdem habe ich mir homebrew installiert, da die Installation von pecl zu Umständlich (ich hätte alle möglichen  Abhängigkeiten installieren müssen). Mit homebrew habe ich mir also den aktuellen mongodb-Treiber installiert, das geht recht einfach mit:</p>
<p>brew update
brew install php71-mongodb</p>
<p>Danach lässt sich die Extension in der /Applications/XAMPP/xamppfiles/etc/php.ini Datei aktivieren:</p>
<p>extension=&quot;/usr/local/opt/php71-mongodb/mongodb.so&quot;</p>
<p>Soweit. So einfach. Das Problem: Nachdem ich diese Zeile in der php.ini Datei hinzugefügt hatte, ließ sich der Apache-Dienst nicht mehr über die XAMPP-Oberfläche starten, obwohl diese mit root-Rechten ausgeführt wird. Leider gab auch die Log-Datei /Applications/XAMPP/xamppfiles/logs/error_log nicht viel her. Und auch nachdem ich die Zeile aus der php.ini-Datei entfernt habe, trat das Problem weiter auf. Ich bekam den Web-Server also nicht zum laufen und an keiner Stelle in den Log-Dateien war ersichtlich, was nicht stimmte.</p>
<p>[caption id=&ldquo;attachment_1567&rdquo; align=&ldquo;aligncenter&rdquo; width=&ldquo;300&rdquo;]





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.nickyreinert.de/files/xampp-startet-nicht-mehr-nachdem-eine-php-extension-hinzugefuegt-wurde/XAMPP-UI-in-OS-X.png?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer"><img src="https://nickyreinert.de/2017/2017-10-19-xampp-startet-nicht-mehr-nachdem-eine-php-extension-hinzugefuegt-wurde/images/XAMPP-UI-in-OS-X-300x209.png" alt="XAMPP UI in OS X um den Web-Server zu starten"></a>


 XAMPP UI in OS X um den Web-Server zu starten[/caption]</p>
<h2 id="recherche-der-aufgerufenen-kommandos">Recherche der aufgerufenen Kommandos</h2>
<p>Also musste ich mich auf die Fehlersuche machen. Und das ist recht umständlich, denn der Klick auf den Button im XAMPP-UI stößt eine etwas längere Befehlskette an deren Ende der Start des http-Daemons steht:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">/Applications/XAMPP/xamppfiles/ctlscript.sh start apache
</span></span><span class="line"><span class="ln">2</span><span class="cl">-&gt; 
</span></span><span class="line"><span class="ln">3</span><span class="cl">/Applications/XAMPP/xamppfiles/apache2/scripts/ctl.sh start
</span></span><span class="line"><span class="ln">4</span><span class="cl">-&gt;
</span></span><span class="line"><span class="ln">5</span><span class="cl">/Applications/XAMPP/xamppfiles/xampp startapache 
</span></span><span class="line"><span class="ln">6</span><span class="cl">-&gt; 
</span></span><span class="line"><span class="ln">7</span><span class="cl">/Applications/XAMPP/xamppfiles/bin/apachectl -k start -E /Applications/XAMPP/xamppfiles/logs/error_log -DSSL -DPHP
</span></span><span class="line"><span class="ln">8</span><span class="cl">-&gt;
</span></span><span class="line"><span class="ln">9</span><span class="cl">/Applications/XAMPP/xamppfiles/bin/httpd -k start -E /Applications/XAMPP/xamppfiles/logs/error_log -DSSL -DPHP
</span></span></code></pre></div><p>Ich hab mir also den letzten Aufruf geschnappt und auf der Konsole ausgeführt. Dort erhielt ich den ersten entscheidenen Hinweis - die log-Dateien waren nicht schreibbar:</p>
<p>(13)Permission denied: AH00086: httpd: could not open error log file /Applications/XAMPP/xamppfiles/logs/error_log.
AH00548: NameVirtualHost has no effect and will be removed in the next release /Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf:40
(13)Permission denied: AH00072: make_sock: could not bind to address [::]:80
(13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:80
no listening sockets available, shutting down</p>
<p>Das lässt sich schnell mit folgendem Aufruf beheben:</p>
<p>sudo chmod a+w /Applications/XAMPP/xamppfiles/logs/error_log</p>
<p>Damit war es natürlich noch nicht getan. Erst jetzt konnte ich dem error.log entnehmen, was das eigentliche Problem war:</p>
<p>AH00548: NameVirtualHost has no effect and will be removed in the next release /Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf:40
(13)Permission denied: AH00072: make_sock: could not bind to address [::]:80
(13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:80
no listening sockets available, shutting down
AH00015: Unable to open logs</p>
<h2 id="lösung---fehlende-rechte">Lösung - fehlende Rechte</h2>
<p>Im Grunde war ja schon klar, was getan werden musste: Der Aufruf mit sudo. Damit lässt sich der Apache-Dienst also zumindest erstmal starten:</p>
<p>sudo /Applications/XAMPP/xamppfiles/bin/httpd -k start -E /Applications/XAMPP/xamppfiles/logs/error_log -DSSL -DPHP</p>
<p>Das eigentliche Problem bleibt freilich bestehen: Was hat das mit der php-Extension zu tun? Liegt es an dem mongodb-Treiber selber oder ist es ein generelles Problem? Bisher bin ich noch nicht dahinter gestiegen. Aber zumindest lässt sich der Web-Server erstmal wieder starten.</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>troubleshooting_guide</dc:type></item><item><title>Buffalo Linkstation LS-WXL mit MiniDLNA und Custom Firmware von Shonk 1.64 Mod 1</title><link>https://nickyreinert.de/2013/2013-03-16-buffalo-linkstation-ls-wxl-mit-minidlna-und-custom-firmware-von-shonk-1-64-mod-1/</link><pubDate>Sat, 16 Mar 2013 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2013/2013-03-16-buffalo-linkstation-ls-wxl-mit-minidlna-und-custom-firmware-von-shonk-1-64-mod-1/</guid><description>1. Schritt - Die CFW (Custom FirmWare) von Shonk installieren. Das geschieht relativ komfortable über eine EXE-Datei: http://forum.buffalo.nas-central.org/viewtopic.php?f=68&amp;t=23603
Die CFW aktiviert zugleich den Telnet-Server, über den ab sofort die nächsten Schritte durchgeführt werden. …</description><content:encoded><![CDATA[<p>1. Schritt - Die CFW (Custom FirmWare) von Shonk installieren. Das geschieht relativ komfortable über eine EXE-Datei: http://forum.buffalo.nas-central.org/viewtopic.php?f=68&amp;t=23603</p>
<p>Die CFW aktiviert zugleich den Telnet-Server, über den ab sofort die nächsten Schritte durchgeführt werden. Praktischerweise hat OSX (als Linux-Derivat) einen SSL-Telnet-Server eingebaut… also das MacBook von Windows zurück in OSX booten.</p>
<p>2. Schritt: Den Paketmanager IPKG installieren - das geschieht nach folgender Anleitung über Telnet: http://buffalo.nas-central.org/wiki/Category:LS-WXL#IPKG</p>
<p>3. Schritt: Den MiniDLNA-Server installieren und dabei folgende Schritte durchgehen: http://forum.nas-hilfe.de/buffalo-technology-nas-anleitungen/minidlna-ab-version-1-0-22-from-scratch-kompilieren-installieren-t1130.html</p>
<p>4. Dabei wird es voraussichtlich - bzw. ob der &ldquo;Aktualität&rdquo; der Anleitung mit ziemlicher Sicherheit - zu einem Fehler kommen: GetText ist zu alt. Dann hilft folgendes:</p>
<p>cd /usr/share/doc/ (Anm.: Diesen Ordner musste ich erst anlegen, ggf. nachholen)</p>
<p>wget -0 gettext.tgz 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://ftp.gnu.org/pub/gnu/gettext/gettext-0.18.2.tar.gz?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">http://ftp.gnu.org/pub/gnu/gettext/gettext-0.18.2.tar.gz</a>


</p>
<p>tar xvzf gettext.tgz</p>
<p>danach entsprechend 





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://gist.github.com/1647940?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer" title="https://gist.github.com/1647940">dieser</a>


 Anleitung:</p>
<p>./configure - das dauert eine kleine Ewigkeit, da kann man schon mal n Kaffee trinken gehen</p>
<p>make check</p>
<p>make install</p>
<p>make clean</p>
<p>Tipps: Ab einem Punkt der Anleitung lautet es &ldquo;Jetzt das in das Verzeichnis des miniDLNA wechseln und einfach&rdquo;. Wo das liegt? Gute Frage. Ich musste erst suchen und bin unter /usr/sbin/minidlna fündig geworden - von dort aus klappt dann auch der autoconf-Befehl.</p>
<p>Fertig. Für Live-Transcoding dürfte die WXL mit knapp 600 MHz-Prozessor etwas schwach sein, aber dafür läuft der Medienserver um einiges fixer und sieht dabei auch schöner aus (wenn man über die PS3 browsed), als der mitgelieferte Twonky.</p>
<p><strong>Mehr dazu:</strong></p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="https://www.multimediaforum.de/threads/217307439-gettext-01811-ueber-konsole-auf-nas-installieren?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">linux-forum.de</a>


</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://forum.nas-hilfe.de/buffalo-technology-nas-anleitungen/minidlna-ab-version-1-0-22-from-scratch-kompilieren-installieren-t1130-30.html?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">nas-hilfe.de</a>


</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><media:content url="https://nickyreinert.de/images/posts/placeholder.jpg" type="image/jpeg"><media:title>Development</media:title></media:content><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>technical_tutorial</dc:type></item><item><title>Tutorial: Web-Scraping mit VBA - Teil 1</title><link>https://nickyreinert.de/2010/2010-09-16-tutorial-web-scraping-mit-vba-teil-1/</link><pubDate>Thu, 16 Sep 2010 00:00:00 +0000</pubDate><author>Nicky Reinert</author><dc:creator>Nicky Reinert</dc:creator><guid isPermaLink="true">https://nickyreinert.de/2010/2010-09-16-tutorial-web-scraping-mit-vba-teil-1/</guid><description> Einführung in Web-Scraping mit VBA in Excel. In dieser kleinen Tutorial-Serie will ich anhand einer Online-Handy-Datenbank zeigen, wie man mit VBA Seiten aus dem Internet abruft und nach Informationen sucht, die sich dann in einer Tabelle abspeichern lassen. Warum VBA? Es gibt vermutlich elegantere …</description><content:encoded><![CDATA[<figure>
  <a href="https://nickyreinert.de/tags/development/placeholder.jpg" target="_blank" class="image-link center" title="Click to view full size">
    <img 
      src="https://nickyreinert.de/tags/development/placeholder.jpg" 
      alt="Tutorial: Web-Scraping mit VBA - Teil 1 - Titelbild"
      width="500"
      loading="lazy"
    />
  </a>
  <figcaption>Einführung in Web-Scraping mit VBA in Excel.</figcaption>
</figure>

<p>In dieser kleinen <strong>Tutorial-Serie</strong> will ich anhand einer Online-Handy-Datenbank zeigen, wie man mit <strong>VBA</strong> Seiten aus dem Internet abruft und nach Informationen sucht, die sich dann in einer Tabelle abspeichern lassen. Warum VBA? Es gibt vermutlich elegantere Lösungen, doch <strong>Excel</strong> ist eine Software, die meisten zuhause nutzen. Man benötigt also keine zusätzliche Entwicklungsumgebung. Außerdem hat diese Methode den Vorteil, dass die Daten sofort zur weiteren Verarbeitung verfügbar sind. VBA ist vielleicht nicht die performanteste Programmiersprache, dafür aber relativ leicht zu beherrschen.</p>
<p>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://www.rechtzweinull.de/archives/100-screen-scraping-wann-ist-das-auslesen-und-die-veroeffentlichung-fremder-daten-zulaessig.html?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">(FYI: Rechtliches zum Thema Web- oder Screen-Scraping)</a>


</p>
<p>Diese Artikelserie richtet sich an den <strong>fortgeschrittenen Nutzer</strong>. Für den Einsteiger gehe ich nicht auf grundlegendes Programmierwissen ein (was sind Klassen, welche Variablen-Typen bietet Excel, etc.) und für den professionellen Softwareentwickler sind meine Codebeispiele vermutlich zu einfach. Ich habe aber die Erfahrung gemacht, dass VBA und Excel für kurzfristige und kleine Projekte dieser Art recht nützliche Hilfsmittel sind. Außerdem bin ich kein &ldquo;ausgebildeter Softwareentwickler&rdquo;, weshalb ich an der Stelle auch gleich darauf hinweise, dass Verbesserungsvorschläge sehr gerne gesehen sind!</p>
<p>Welches Wissen solltest du also mitbringen? Da es um das <strong>Parsen von HTML-Code</strong> geht, solltest du zumindest Bescheid wissen, wenn ich von <strong>div- und a-Elementen</strong> und <strong>CSS-Klassen</strong> rede. Du solltest auch wissen, wie man in VBA Variablen deklariert oder was eine <strong>if-Abfrage</strong> ist.</p>
<p>Das Endergebnis ist eine Tabelle mit technischen Spezifikationen zu den Mobiltelefonen, die inside-handy.de listet. Insgesamt werden drei Routinen genutzt, die - in umgekehrter Reihenfolge des Vorgehens - folgende Aufgaben haben:</p>
<ol>
<li>Die letzte Routine greift auf eine Liste von URLs zu, die auf die Datenblätter der Geräte verweisen. Von dort wird der HTML-Quellcode nach den technischen Informationen durchsucht.</li>
<li>Auf inside-handy.de sind die Geräte nach Herstellern sortiert. Jede Herstellerseite verweist auf die entsprechenden Geräte. Wir werden also jede Herstellerseite (bzw. den entsprechenden HTML-Code) zunächst nach den URLs zu den Geräten durchsuchen. Diese Aufgabe übernimmt die zweite Routine bzw. Prozedur.</li>
<li>Die erste Routine schließlich liefert die Liste aller URLs zu den Herstellern, die wir uns aus dem HTML-Code der Herstellerübersicht auf inside-handy.de laden.</li>
</ol>
<p>Und damit geht es nun los:</p>
<h3 id="1-die-url-zu-den-hersteller-unterseiten-auslesen---sub-getmanufacturer">1. Die URL zu den Hersteller-Unterseiten auslesen - <code>sub getManufacturer</code></h3>
<h4 id="11-datei-per-http-von-einem-server-laden">1.1 Datei per HTTP von einem Server laden</h4>
<p>Im ersten Schritt laden wir die komplette HTML-Datei in den Zwischenspeicher. Dazu gibt es mindestens zwei Methoden, die gängigste ist vermutlich die über <strong>





  
  

  
  

  
  
  

  
    
      
    
  

  
  <a href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa384106%28v=vs.85%29.aspx?utm_source=institut-fuer-digitale-herausforderungen&amp;utm_medium=allyourbasearebelongtous" target="_blank" rel="noopener noreferrer">WinHttpRequest</a>


</strong>. Bevor du das nutzen kannst, musst du bei VBA unter <strong>Extras → Verweise</strong> die <strong>Microsoft HTML Object Library</strong> einbinden.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">Dim url As String
</span></span><span class="line"><span class="ln">2</span><span class="cl">Dim result As String
</span></span><span class="line"><span class="ln">3</span><span class="cl">Dim winHttpReq As Object
</span></span><span class="line"><span class="ln">4</span><span class="cl">url = &#34;http://www.inside-handy.de/hersteller/handys&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">Set winHttpReq = CreateObject(&#34;WinHttp.WinHttpRequest.5.1&#34;)
</span></span><span class="line"><span class="ln">7</span><span class="cl">winHttpReq.Open &#34;GET&#34;, url, False
</span></span><span class="line"><span class="ln">8</span><span class="cl">winHttpReq.send
</span></span><span class="line"><span class="ln">9</span><span class="cl">result = winHttpReq.responseText
</span></span></code></pre></div><p>Die Deklaration der Variablen erklärt sich von selbst. Nachdem ich eine Instanz vom <strong>WinHTTP-Objekt</strong> erzeugt habe, kann ich die Parameter übergeben. Dazu gehört neben der URL auch die Bestimmung des HTTP-Requests - nämlich <strong>GET</strong>. Der letzte, booleansche Parameter gibt an, ob die Verbindung im asynchronen Modus geöffnet werden soll. Mit <code>.send</code> wird der Request tatsächlich ausgelöst und das Ergebnis dann an die String-Variable <code>result</code> zurückgegeben. Dort befindet sich nun unser HTML-Code.</p>
<p>Wir können unseren Request natürlich auch per <strong>POST</strong> absetzen und noch andere Header-Informationen anhängen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">Set winHttpReq = CreateObject(&#34;WinHttp.WinHttpRequest.5.1&#34;)
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">winHttpReq.Open = &#34;Post&#34;, url, False
</span></span><span class="line"><span class="ln">4</span><span class="cl">winHttpReq.setRequestHeader &#34;User-Agent&#34;, &#34;Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">winHttpReq.setRequestHeader &#34;Content-type&#34;, &#34;application/x-www-form-urlencoded&#34;
</span></span><span class="line"><span class="ln">6</span><span class="cl">winHttpReq.send (URLEncode(&#34;username=user1&amp;password=secret&#34;))
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl">result = winHttpReq.responseText
</span></span></code></pre></div><p>So ist es z. B. möglich, Formulardaten zu übermitteln, um an eine passwortgeschützte Seite oder die Ergebnisseite einer Suche zu gelangen. Dabei werden die POST-Daten als weiterer Parameter beim Senden mitgegeben. Eine weitere Möglichkeit ist die Durchführung einer <strong>HTTP-Authentifizierung</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">Dim HTTPREQUEST_SETCREDENTIALS_FOR_SERVER As Boolean
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">winHttpReq.Open &#34;GET&#34;, url, False
</span></span><span class="line"><span class="ln">4</span><span class="cl">winHttpReq.SetCredentials &#34;user&#34;, &#34;password&#34;, HTTPREQUEST_SETCREDENTIALS_FOR_SERVER
</span></span><span class="line"><span class="ln">5</span><span class="cl">winHttpReq.send
</span></span></code></pre></div><p>Neben dem <strong>winHttpRequest-Objekt</strong> gibt es noch eine weniger elegante Methode, in der direkt eine Instanz des <strong>Internet Explorers</strong> erzeugt wird:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">Dim sPostData As String
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Dim bPostData() As Byte
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Dim WebBrowser: Set WebBrowser = CreateObject(&#34;InternetExplorer.Application&#34;)
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">WebBrowser.Visible = True
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">sPostData = URLEncode(&#34;username=user1&amp;password=secret&#34;)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ReDim bPostData(Len(sPostData) - 1)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">bPostData = StrConv(sPostData, vbFromUnicode)
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">WebBrowser.navigate url, 2 + 4 + 8, , bPostData, &#34;Content-type: application/x-www-form-urlencoded&#34;
</span></span><span class="line"><span class="ln">12</span><span class="cl">Do While WebBrowser.Busy
</span></span><span class="line"><span class="ln">13</span><span class="cl">    DoEvents
</span></span><span class="line"><span class="ln">14</span><span class="cl">Loop
</span></span><span class="line"><span class="ln">15</span><span class="cl">result = WebBrowser.document.body.innerHTML 
</span></span><span class="line"><span class="ln">16</span><span class="cl">WebBrowser.Quit
</span></span></code></pre></div><p>Da diese Methode - wie gesagt - nicht sonderlich elegant ist, werde ich aber nicht weiter darauf eingehen.</p>
<h4 id="html-dokument-erzeugen-und-befüllen">HTML-Dokument erzeugen und befüllen</h4>
<p>Nun zurück zu unserem Skript. Den HTML-Code der Seite haben wir nun erstmal in einen String gelegt. Damit wir das HTML-Dokument bequem lesen können, erzeugen wir ein <strong>HTML-Document</strong>, an das wir den HTML-Code übergeben:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">Set HTMLDoc = New HTMLDocument
</span></span><span class="line"><span class="ln">2</span><span class="cl">Set HTMLDoc = CreateObject(&#34;htmlfile&#34;)
</span></span><span class="line"><span class="ln">3</span><span class="cl">HTMLDoc.Open
</span></span><span class="line"><span class="ln">4</span><span class="cl">HTMLDoc.write (CStr(result))
</span></span><span class="line"><span class="ln">5</span><span class="cl">HTMLDoc.Close
</span></span></code></pre></div><p>Auch hier erklärt sich der Code fast von selbst: Eine Instanz des Objektes erzeugen, diese Instanz zum &ldquo;Befüllen&rdquo; vorbereiten, den String übergeben und die Instanz wieder &ldquo;schließen&rdquo;. Soweit, so unkompliziert. Im nächsten Schritt geht es nun direkt an das <strong>Parsen des Quellcodes</strong>, um die Links zu den Herstellerseiten zu erhalten.</p>
<h4 id="schleife-zum-auslesen-der-links">Schleife zum Auslesen der Links</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln"> 1</span><span class="cl">Dim oneElement1, allElements1 As IHTMLElementCollection
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Dim oneElement2, allElements2 As IHTMLElementCollection
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Dim oneElement3, allElements3 As IHTMLElementCollection
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Worksheets(&#34;srcURL&#34;).Range(&#34;a2&#34;).Select
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">i = 0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Set allElements1 = HTMLDoc.getElementsByTagName(&#34;a&#34;)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">i = 0
</span></span><span class="line"><span class="ln">10</span><span class="cl">For Each oneElement1 In allElements1
</span></span><span class="line"><span class="ln">11</span><span class="cl">    If oneElement1.parentElement.className = &#34;h_img&#34; Then
</span></span><span class="line"><span class="ln">12</span><span class="cl">        If oneElement1.parentElement.parentElement.ID = &#34;h_alle&#34; Then
</span></span><span class="line"><span class="ln">13</span><span class="cl">            If InStr(1, oneElement1.getAttribute(&#34;href&#34;), &#34;/tablets&#34;) &lt;= 0 Then
</span></span><span class="line"><span class="ln">14</span><span class="cl">                Selection.Offset(i, 0).Value = Replace(oneElement1.getAttribute(&#34;href&#34;), &#34;about:&#34;, &#34;http://www.inside-handy.de&#34;)
</span></span><span class="line"><span class="ln">15</span><span class="cl">                i = i + 1
</span></span><span class="line"><span class="ln">16</span><span class="cl">            End If
</span></span><span class="line"><span class="ln">17</span><span class="cl">        End If
</span></span><span class="line"><span class="ln">18</span><span class="cl">    End If    
</span></span><span class="line"><span class="ln">19</span><span class="cl">Next oneElement1
</span></span><span class="line"><span class="ln">20</span><span class="cl">End Sub
</span></span></code></pre></div><p>Wie bin ich vorgegangen? Ich habe mir zunächst den Quellcode der Seite angeschaut. Die Liste der Hersteller ist dort eine Tabelle mit den Logos der Unternehmen. Die gewünschte Information befindet sich in <strong>a-Elementen</strong>, die wiederum innerhalb eines <strong>div-Elements</strong> liegen. Das gemeinsame &ldquo;Oberelement&rdquo; ist ein div-Container mit der CSS-Klasse <strong>h_img</strong> bzw. einem weiteren div-Container (<strong>h_alle</strong>). Außerdem gibt es einen ausgeblendeten div-Container mit einer Liste von Tablet-Herstellern, diese haben den Begriff <strong>&quot;/tablet&quot;</strong> in der href-Angabe und müssen ignoriert werden.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;h_alle&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;h_img&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/hersteller/xyz&#34;</span> <span class="na">title</span><span class="o">=</span><span class="s">&#34;Handys Hersteller: XYZ&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">            <span class="p">&lt;</span><span class="nt">img</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Mit <code>Set allElements1 = HTMLDoc.getElementsByTagName(&quot;a&quot;)</code> lasse ich mir also erst alle <strong>a-Elemente</strong> aus dem Quellcode in meinen &ldquo;Container&rdquo; legen.</p>
<p>Mit der ersten <strong>for-each-Schleife</strong> durchlaufe ich nun diesen Container und prüfe mit den ersten zwei <strong>if-Abfragen</strong>, ob sich das <strong>a-Element</strong> unterhalb der erwähnten div-Container befindet. Da auf der Seite noch ein weiterer identischer div-Container mit diesen CSS-Klassen für die Liste der Tablet-Hersteller existiert, muss ich mit einer dritten <strong>if-Abfrage</strong> die URL des <strong>a-Elements</strong> überprüfen. Erst dann kann ich das Attribut des <strong>a-Elements</strong> auslesen und in mein Excel-Worksheet schreiben.</p>
<p>Fertig ist der erste Schritt - eine Liste der URLs zu den jeweiligen Herstellern. Im nächsten Teil werde ich diese Liste durchgehen und von den jeweiligen Seiten die URLs zu den Geräten auslesen.</p>
]]></content:encoded><atom:updated>2026-10-06T10:59:33+02:00</atom:updated><category>anleitungen</category><dc:subject>Lesezeit: 5 Minuten</dc:subject><dc:type>tutorial</dc:type></item></channel></rss>