(Instagram)-Locationprojekt, Hashtagcloudanalysen nach Usernamen (Leipzig-Datenbank)

Das Programm kann, in der aktuellen Version, zwischen „%suchwort%“ und „=suchwort“ unterscheiden. Bei der letzteren Option werden alle Ergebnisse ausgeworfen, wo der Term – als Hauptstring – in den Tagclouds zu finden ist (Gegenteil zu der Substringvariante).

Inhaltsschema:
Spalte (1) -> Nummer
Spalte (2) -> Username
Spalte (3) -> Tagcloud
Spalte (4) -> Summe Tagcloud, Vorkommen der Tagcloud je Username

Hinweis:
Sämtliche Accountnamen sind in den nachfolgenden Datensätzen – selbstverständlich – verschlüsselt.

(1) Substring: „city“: city_usersum
(2) Substring: „mensa“: mensa_usersum
(3) Hauptstring: „sport“: -sport-_usersum
(4) Substring: „sport“: sport_usersum
(5) Substring: „fest“: fest_usersum
(6) Substring: „farbfilm“: farbfilm_usersum
(7) Substring: „sommer“: sommer_usersum
(8) Substring: „frühling“: frühling_usersum
(9) Substring: „herbst“: herbst_usersum
(10) Substring: „stadt“: stadt_usersum
(11) Hauptstring: „messe“: -messe-_usersum
(12) Substring: „hafen“: hafen_usersum
(13) Substring: „halle“: halle_usersum
(14) Substring: „design“: design_usersum
(15) Substring: „hochschule“: hochschule_usersum
(16) Substring: „kunst“: kunst_usersum
(17) Substring: „film“: film_usersum
(18) Substring: „dokleipzig“: dokleipzig_usersum
(19) Substring: „leipzig“: leipzig_usersum
(20) Hauptstring: „urlaub“: -urlaub-_usersum
(21) Substring: „urlaub“: urlaub_usersum
(22) Substring: „bar“: bar_usersum
(23) Substring: „deko“: deko_usersum
(24) Substring: „feiern“: feiern_usersum

(Instagram)-Locationprojekt, Hashtagcloudanalysen nach Orten (Berlin-Datenbank)

Heute fand ich Gelegenheit, das Projekt in einer ersten Version zu compilieren. Dieses beinhaltet auch die Funktion der Auflistung von Tagclouds nach Locations inkl. deren Aufsummierung.

Folgende Dateien liefern erste Interpretationsgrundlagen zu den Locationbewertungen.

Inhaltsschema:
Spalte (1) -> Nummer
Spalte (2) -> Tagcloud
Spalte (3) -> Location
Spalte (4) -> Location-ID
Spalte (5) -> Summe Tagcloud, Vorkommen der Tagcloud in der Location

Beispiele.
Die nachfolgenden Datensätze basieren auf die Query „like ‚%suchwort%'“ und erfragen die Ergebnisse auf Substringebene.

(1) Substring: „working“: working_locsumm
(2) Substring: „work“: work_locsumm
(3) Substring: „office“: office_locsumm
(4) Substring: „neukölln“: neukölln_locsumm
(5) Substring: „kunst“: kunst_locsumm
(6) Substring: „job“: job_locsumm
(7) Substring: „happy“: happy_locsumm
(8) Substring: „foodporn“: foodporn_locsumm
(9) Substring: „food“: food_locsumm
(10) Substring: „feiern“: feiern_locsumm
(11) Substring: „fashion“: fashion_locsumm
(12) Substring: „coworking“: coworking_locsumm
(13) Substring: „berlin“: berlin_locsumm
(14) Substring: „advokat“: advokat_locsumm

Datenexporte: Instagram-Locationprojekt, Teil (1)

(1) Locationrecherche nach Auflistung der aktiven Accounts

SELECT location, Anz_user from(
Select location, count(username) as Anz_user from locations GROUP BY location ) as my_table
WHERE (Anz_user >= 2) order by Anz_user DESC

Diese Abfrage listet alle Locations auf, wo mind. 2 Accounts Postings abgesetzt hatten.
Datei (Paris): loc_2

(2) Locationrecherche nach Auflistung der aktiven Accounts, Anpassung nach Locationtyp

SELECT location as Ort, Anz_user from(
Select location, count(username) as Anz_user from locations GROUP BY location ) as my_table
WHERE (Anz_user >= 2) AND (location like '%museum%') order by Anz_user DESC

Diese Abfrage listet alle Locations auf, wo mind. 2 Accounts Postings abgesetzt hatten und erlaubt die Feinprüfung auf Locationtyp.
Datei (Paris), Hotel: loc_2_hotel
Datei (Paris), Museum: loc_2_museum

(3) Location- und Accountauflistung nach Suchwort (Tagsubstring)
select location, tag, username from locations where tag like '%fashion%' group by username;

Diese Abfrage listet alle Locations und Accounts auf, welche etwas zu „Fashion“ gepostet hatten
Datei (Paris), „fashion“: paris_fashion

(4) Userauflistung nach Suchwort UND Mindestaufkommen in Bezug auf die Locationaufsummierung

SELECT username, numb from(
Select username, tag, count(location) as numb from locations GROUP BY username ) as my_table
WHERE (numb >= 2) and (tag like '%fashion%') order by numb DESC;

Diese Abfrage listet alle erfassten Accounts auf, welche an mind. 2 Locations aktiv etwas zu „Fashion“ schrieben.
Datei (Paris), Loc=2, „fashion“: user_tag_fashion_2

Spezial-SQLabfragen (Projekt: Instagramlocations), Stand: 14.07.2018

(1) Ausgabe der Usernames als Auflistung, mit Mindestanzahl in Bezug auf die Locations

SELECT username, numb from(
Select username, count(location) as numb from locations GROUP BY username ) as my_table
WHERE numb >= 2 order by numb DESC;

(2) Ausgabe der Usernames als Auflistung mit Mindestanzahl in Bezug auf die Locations UND Vorkommen eines Substrings in den Tagwolken

SELECT username, numb from(
Select username, tag, count(location) as numb from locations GROUP BY username ) as my_table
WHERE (numb >= 2) and (tag like '%fashion%') order by numb DESC;

(3) Ausgabe der Locations als Auflistung mit Mindestanzahl in Bezug auf die erfassten User_innen

SELECT location, Anz_user from(
Select location, count(username) as Anz_user from locations GROUP BY location ) as my_table
WHERE (Anz_user >= 2) order by Anz_user DESC

(4) Ausgabe der Locations als Auflistung mit Mindestanzahl in Bezug auf die erfassten User_innen UND Vorkommen eines Substrings in den Locations

SELECT location, Anz_user from(
Select location, count(username) as Anz_user from locations GROUP BY location ) as my_table
WHERE (Anz_user >= 2) AND (location like '%hotel%') order by Anz_user DESC;

(5) Ausgabe der Locations als Auflistung mit Mindestanzahl in Bezug auf die erfassten User_innen UND Vorkommen eines Substrings in den Locations UND Vorkommen eines Substrings in den Tagwolken

SELECT location, Anz_user from(
Select location, tag, count(username) as Anz_user from locations GROUP BY location ) as my_table
WHERE (Anz_user >= 2) AND (location like '%hotel%') AND (tag like '%fashion%') order by Anz_user DESC;

Basis-SQLabfragen (Projekt: Instagramlocations)

Notiz für mich:
Auflistung der Standardabfragen, ohne Anspruch auf Tiefenanalysen.

(1) Statistiken, „KPI“
select count(*), count(distinct(filter)), count(distinct(url)), count(distinct(tag)), count(distinct(location)), count(distinct(username)), count(distinct(erstellzeit)) from locations;

(2) Überblick, Auflistung der erfassten Accounts
select username from locations group by username;

(3) Auflistung der erfassten Accounts zzgl. Zusammenzählung der Beiträge der jeweiligen Accounts
select username, count(username) from locations group by username order by count(username) DESC

(4) Auflistung der erfassten Accounts zzgl. Zusammenzählung der Beiträge der jeweiligen Accounts + Zusammenzählung der Locations
select username, count(username), count(distinct(location)) from locations group by username order by count(distinct(location)) DESC;

(5) Auflistung der erfassten Locations zzgl. Zusammenzählung der Locations
select location, count(location) from locations group by location order by count(location) DESC;

(6) Auflistung der erfassten Locations zzgl. Zusammenzählung der Locations bei Vorkommen eines Tags / Zeichenketten
select location, count(location) from locations where tag like '%gucci%' group by location order by count(location) DESC

(7) Auflistung der erfassten Usernames zzgl. Zusammenzählung der Usernames bei Vorkommen eines Tags / Zeichenketten
select username, count(username) from locations where tag like '%fashion%' group by location order by count(username) DESC

(8) Auflistung der erfassten Tags zzgl. Zusammenzählung der Tags bei Vorkommen von zwei gesuchten Tags / Zeichenketten
select tag, count(tag) from locations where (tag like '%fashion%') and (tag like '%woman%') group by location order by count(tag) DESC;

Instagram – Zielgruppenerfassungen via Locations, Projektstart

Vor einigen Tagen entschied ich mich dazu, die Erfassungsfunktionen von CSV in SQLite zu wechseln.

Hierbei hat die entsprechende Datenbankdatei folgende Struktur (Einrichtungsprozedur):

procedure TForm1.LocationsMain1Click(Sender: TObject);
begin
with sql_befehle do
begin
clear;
lines.Add('drop table if exists locations;');
lines.Add('CREATE TABLE `locations` (');
lines.Add(' `id` integer primary key AUTOINCREMENT,');
lines.Add('`url` varchar(400),');
lines.Add('`tag` varchar(800),');
lines.Add('`likes` varchar(400),');
lines.Add('`comments` varchar(400),');
lines.Add('`erstellzeit` varchar(400),');
lines.Add('`post_id` varchar(1600),');
lines.Add('`username` varchar(400),');
lines.Add('`location` varchar(400),');
lines.Add('`filter` varchar(400),');
lines.Add('`pruefzeit` varchar(400)');
lines.Add(');');
lines.Add('vacuum;');
end;
fdquery3.ExecSQL(sql_befehle.text);
end;

Mit Datenbankwechsel wurde der Scraper so optimiert, dass an einem typischen Arbeitstag zwischen 1200 und 4500 Locations zu max. 1500 Einträge erfasst werden können. Erste Testläufe mit dem Raum Paris / Frankreich ergaben eine Ausbeute von ~400.000 Einträgen mit Aktivitäten von ~200.000 Accounts (Unique!).

Rohdatenerfassung zu den Locations via Instagram-API (finaler Ansatz)

Im Zuge des Roland-Berger-Projektes fand ich endlich Gelegenheit, den Locationrohdatenscraper zu optimieren. Hier nun der Quellcode:

(1) Scrapingprozedur
Erklärung:
get_loc_short(mytable: TStringGrid; locid: string; rounds: integer);
mytable -> Stringgridobjekt, verlangt die Übergabe des Objektnamens
locid -> LocationID aus Instagram (siehe: Locationparser)
rounds -> maximale Anzahl der Subprüfung je Durchlauf

procedure TForm1.get_loc_short(mytable: TStringGrid; locid: string; rounds: integer);
var
JSONArray: tJSONArray;
JSONValue,jvalue: tJSONValue;
JSONPair: TJSONPair;
JSON, json_sub: TJSONObject;
size: integer;
j_array: tJSONArray;
s: string;
i,j: integer;
next_id: string;
zaehl: integer;
begin
zaehl:=0;
try
debug.text:=idhttp1.Get('https://api.instagram.com/v1/locations/'+locid+'/media/recent?access_token='+token.text);
JSONValue := TJSONObject.ParseJSONValue(debug.text);
JSON := TJSONObject.ParseJSONValue(debug.Lines.Text) as TJSONObject;
JSONArray := TJSONArray(JSON.Get('data').JsonValue);
try next_id:= JSONValue.GetValue('pagination.next_url');
except
next_id:='N/A';
end;
for i := 0 to JSONArray.Size - 1 do
begin
with mytable do
begin
cells[0,rowcount]:=inttostr(rowcount);
cells[1,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('link')).JsonValue.Value);
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('tags')).ToString);
s:= StringReplace(s, '"tags":[', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, ']', '', [rfReplaceAll,rfIgnoreCase]);
cells[2,rowcount]:=s;
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('likes')).ToString);
s:= StringReplace(s, '"likes":{"count":', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '}', '', [rfReplaceAll,rfIgnoreCase]);
cells[3,rowcount]:=s;
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('comments')).ToString);
s:= StringReplace(s, '"comments":{"count":', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '}', '', [rfReplaceAll,rfIgnoreCase]);
cells[4,rowcount]:=s;
cells[5,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('created_time')).JsonValue.Value);
cells[5,rowcount]:=datetimetostr(UnixToDateTime(strtoint(cells[5,rowcount])));
cells[6,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('id')).JsonValue.Value);
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('user')).ToString);
s:= StringReplace(s, '"user":{"username":', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '}', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '"', '', [rfReplaceAll,rfIgnoreCase]);
cells[7,rowcount]:=s;
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('location')).ToString);
s:= StringReplace(s, '"location":', '', [rfReplaceAll,rfIgnoreCase]);
cells[8,rowcount]:=s;
cells[9,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('filter')).JsonValue.Value);
cells[10,rowcount]:=datetimetostr(now);
rowcount:=rowcount+1;
end;
grdColWidth(mytable, 40);
end;
except
end;
repeat
// -> tiefenpruefung
try
debug.text:=idhttp1.Get(next_id);
JSONValue := TJSONObject.ParseJSONValue(debug.text);
JSON := TJSONObject.ParseJSONValue(debug.Lines.Text) as TJSONObject;
JSONArray := TJSONArray(JSON.Get('data').JsonValue);
try next_id:= JSONValue.GetValue('pagination.next_url');
except
next_id:='N/A';
end;
for i := 0 to JSONArray.Size - 1 do
begin
with mytable do
begin
cells[0,rowcount]:=inttostr(rowcount);
cells[1,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('link')).JsonValue.Value);
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('tags')).ToString);
s:= StringReplace(s, '"tags":[', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, ']', '', [rfReplaceAll,rfIgnoreCase]);
cells[2,rowcount]:=escape(s);
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('likes')).ToString);
s:= StringReplace(s, '"likes":{"count":', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '}', '', [rfReplaceAll,rfIgnoreCase]);
cells[3,rowcount]:=s;
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('comments')).ToString);
s:= StringReplace(s, '"comments":{"count":', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '}', '', [rfReplaceAll,rfIgnoreCase]);
cells[4,rowcount]:=s;
cells[5,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('created_time')).JsonValue.Value);
cells[5,rowcount]:=datetimetostr(UnixToDateTime(strtoint(cells[5,rowcount])));
cells[6,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('id')).JsonValue.Value);
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('user')).ToString);
s:= StringReplace(s, '"user":{"username":', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '}', '', [rfReplaceAll,rfIgnoreCase]);
s:= StringReplace(s, '"', '', [rfReplaceAll,rfIgnoreCase]);
cells[7,rowcount]:=s;
s:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('location')).ToString);
s:= StringReplace(s, '"location":', '', [rfReplaceAll,rfIgnoreCase]);
cells[8,rowcount]:=s;
cells[9,rowcount]:=(TJSONPair(TJSONObject(JSONArray.Get(i)).Get('filter')).JsonValue.Value);
cells[10,rowcount]:=datetimetostr(now);
rowcount:=rowcount+1;
end;
grdColWidth(mytable, 40);
end;
except
end;
// -> tiefenpruefung, ende
zaehl:=zaehl+1;
until zaehl=rounds;
//uebertrag auf tabelle
grdColWidth(mytable, 40);
form1.Caption:=version+' alle Posts herunter geladen';
end;

(2) Aufruf der Scrapingprozedur
procedure TForm1.Button45Click(Sender: TObject);
var lauf: integer;
begin
with locmedia do
begin
cells[0,0]:='Nr.';
cells[1,0]:='URL';
cells[2,0]:='Tag';
cells[3,0]:='Likes';
cells[4,0]:='Comments';
cells[5,0]:='Erstellzeit';
cells[6,0]:='ID';
cells[7,0]:='User';
cells[8,0]:='Location';
cells[9,0]:='Filter';
colcount:=10;
rowcount:=1;
end;
for lauf := 1 to locroh.RowCount do
begin
randomize;
token.Text:=token.Items[random(token.Items.Count-1)];
get_loc_short(locmedia,locroh.Cells[1,lauf],20);
savetocsv(locmedia,verz+'\support\dummydata.csv');
form1.Caption:=version+' Locationscraper: '+inttostr(lauf)+' / '+inttostr(locroh.RowCount);
delay(1000);
end;
end;

Instagram, Scrapen & Erfassen von Location-URLs

Vor ein paar Tagen übernahmen wir von Roland Berger ein Projekt, welches u.a. die Trends und Kommunikationen aus den öffentlichen Räumen diverser Locations erfassen und bewerten soll.
Hier ist das Anlegen und Erfassen möglichst vieler Instagramlocations aus einem städtischen Raum eines der Hauptaufgaben der Datenerfassungsprozedur und ich entschied mich für den Weg über den Google-Index.

(1) Aufrufen der Indexurl
site:instagram.com/explore/locations/ „stadt land objekt“

(2) URLscraper
memo7.clear;
try
ovElements := WebBrowser2.OleObject.Document.all;
for i := 0 to (ovElements.Length - 1) do
begin
if (pos('?hl', ovelements.item(i))=0) and (pos('https://www.instagram.com/explore/locations/', ovelements.item(i))<>0) and (pos('&prev=search',ovelements.item(i))=0) and (pos('https://www.google.com/',ovelements.item(i))=0) then
begin
memo7.Lines.Add(ovElements.item(i));
end;
end;
except
end;
memo7.lines.BeginUpdate;
KillDuplicates(memo7.lines);
memo7.lines.EndUpdate;

(3) Bereinigung und Aufbereitung der Daten
Ausgehend von der Beispielurl https://www.instagram.com/explore/locations/1026498374/eltonel-bar/ wird nun die URLstruktur dahingehend sichtbar und logisch, dass der Ort des Locationcodes „1026498374“ erfassbar ist. Dieser wird aus der ursprünglichen Zeichenkette (hier: URL-String) mit Hilfe einer geeigneten Funktion extrahiert und zwecks Tiefenanalyse der Locationmedien in die Datenbank(en) gespeichert.

Instagram – Location ID finden

Diverse Änderungen an der Facebook-API-Politik (Stichwort: Datenscrapeskandale & Co.) zwangen mich leider dazu, Alternativwege für das Organisieren der Location-IDs zu analysieren und ich werde folgende Optionen in die Datenscraperfunktionen einbauen.

(1) Manuelle Recherche
https://docs.social-streams.com/article/118-find-instagram-location-id

(2) Recherche auf Basis der Geo-Codes
https://api.instagram.com/v1/locations/search?lat=48.858844&lng=2.294351&access_token=ACCESS-TOKEN

(3) Recherche auf Basis des Google-Index
https://www.google.com/search?q=site:instagram.com/explore/locations/%20+%22apolda%22

Hashtags: Auswertungen und Interpretation (Nicht-Nerdy)

Eines unserer wichtigsten Recherchetools sind und bleiben die Hashtagbeobachtungen auf Instagram. Hier erlebe ich immer wieder bei den vielen Gesprächen faszinierende Rückfragen zu allgemeinen Verständnis- und Interpretationsprobleme. Daher dieser Grundlagenartikel.

Am Beipiel der Themenwelt „Leipzig“ (hier: #leipzig) lässt sich das Monitoring so anstellen:

(1) Aufruf der App
(2) Suche nach dem Tag in der App
(3) Häufungszahl notieren
(4) Gehe zurück zu (1)

Über diesen (manuellen) Rechercheweg lassen sich nun Häufungsentwicklungszahlenreihen anlegen.

[Beispiel für #Leipzig]

Der Nicht-Nerd kann jetzt und nach Sichtung der Entwicklungen dieser Zahlen unfassbar spannende Fragen für sich und völlig unabhängig von diversen Socialmediaberater_innen und Influencermarketingagenturen beantworten:

(1) Wieviele neue Medien tauchen denn da zu den Tags / Themenwelten auf?
(2) Was gibt es denn sonst noch schönes zu der Hauptsuchphrase? (Siehe Screenshots, die Liste …)
(3) Wie entwickeln sich denn diese Nebenhashtags?
(4) Passt die Entwicklung (1-3) denn überhaupt auf die aktuelle Planung (hier: Contentmarketing, Zielgruppenerkenntnisse usw.)?

Diese Datenbankauffüllung und die Quasi-Forschung an den Hashtags lässt mich manchmal etwas lächeln, weil wir intern sehr viele Diskussionen rund um die Interpretationsvarianten der Zahlenveränderungen führen. Ich vertrete in manchen Situationen u.a. den Ansatz, dass die Differenz von Zahl (Z) zu Zahl (X) sagt: „Die Differenz entspricht punktgenau der Anzahl der Accounts, welche in dem Zeitraum aktiv sind“. Anne vertritt da einen anderen Ansatz und bemerkt immer mal wieder gern, dass auch eine Person 1000 Medien in einem kurzen Zeitabstand unter dem Tag #leipzig dort veröffentlicht. Ich denke, dass man zum finalen Kompromiss kommt, wenn man halt in der Betrachtung eine Fehlerquote einbaut und ich bevorzuge da – je nach Thema (!) – 33%-45%.

Zurück zur Datenerfassung.
Der beschriebene und manuelle Weg eignet sich natürlich für kleine Tagsammlungen und Projekte. Sucht man allerdings einen umfangreicheren Überblick, kommt man selbstverständlich nicht um die Anbindung an den Instagram-Endpoint umhin und hier dürfen die Leser_innen des Arbeitsblogs den eigentlichen Existenzgrund der Datenbanken finden. Sprich: niemand hat schlichtweg die Zeit, die Lust und die Ressourcen, um 1000-10.000 projektbezogene Tags jeden Tag zu suchen, die Zahlen aufzuschreiben und das Ganze auch auszuwerten.

So. Ich hoffe, dass meine Ausführungen zu den Hashtags etwas klarer geworden sind.

Fragen? Anregungen?