commit 61b4d673517c18ddf8ba2f9cf7ea60e5b16b83ec Author: Mandragorat Wandystanu Date: Sun Mar 22 01:29:07 2020 +0100 Initialise repository diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..567cee6 --- /dev/null +++ b/.htaccess @@ -0,0 +1,13 @@ +AddType application/ld+json .jsonld +Header set Access-Control-Allow-Origin * + +RewriteEngine On +RewriteBase /statistics/ + +RewriteRule ^by-period/$ byPeriodDataset.php +RewriteRule ^by-period/(.*)/(.*) byPeriodSlice.php?date-start=$1&date-end=$2 [B] +RewriteRule ^sender/(.*) sender.php?hash=$1 [B] + +RewriteRule ^$ doc/ [L,R=301] +RewriteRule ^(key|structure)/ structures.jsonld [L,R=303] +RewriteRule ^property/ properties.jsonld [L,R=303] diff --git a/byPeriodDataset.php b/byPeriodDataset.php new file mode 100644 index 0000000..675d534 --- /dev/null +++ b/byPeriodDataset.php @@ -0,0 +1,49 @@ +getMessage()); +} + +$xpath = new DOMXPath($document); + +// find all months with non-zero number of posts +$months = []; +$rows = $xpath->query('//table/tbody/tr'); + +foreach ($rows as $row) { + $cols = $xpath->query('td', $row); + $year = $cols[0]->nodeValue; + + for ($i = 1; $i < $cols->length; $i++) { + if ( + $cols[$i]->firstChild->nodeType === XML_ELEMENT_NODE && + $cols[$i]->firstChild->tagName === 'a' + ) { + $months []= $year . '-' . str_pad($i, 2, '0', STR_PAD_LEFT); + } + } +} + +// construct a linked data object corresponding to gathered data +$ld = [ + '@context' => 'https://wandystan.eu/statistics/context.jsonld', + '@type' => 'DataSet', + 'title' => [ + 'en' => 'Posts by date and sender', + 'pl' => 'Wiadomości wg daty i wysyłającego' + ], + 'description' => [ + 'en' => 'Statistics of number of posts sent by a given sender on a given date, grouped by period in which they were sent.', + 'pl' => 'Statystyki liczby wiadomości wysłanych przez danego wysyłającego w danym dniu, pogrupowane wg okresu w jakim zostały wysłane.' + ], + 'publisher' => 'https://wandystan.eu/', + 'structure' => 'structure/by-period', + 'slices' => array_map(function ($month) { return "by-period/$month-01/P1M"; }, $months) +]; + +header('Content-type: application/ld+json'); +print json_encode($ld); diff --git a/byPeriodSlice.php b/byPeriodSlice.php new file mode 100644 index 0000000..7c39e33 --- /dev/null +++ b/byPeriodSlice.php @@ -0,0 +1,110 @@ +sub($interval); + } + + if (starts_with($param_date_end, 'P')) { + $interval = new DateInterval($param_date_end); + $date_end = $date_start->add($interval); + } +} catch (Exception $e) { + exit_error('Invalid date specification: ' . $e->getMessage()); +} + +// get page with the search results for the given period +try { + $document = load_remote_dom_document('https://wandystan.groups.io/g/wandystan/search?p=Created,,,10000,2,0,0&d=6&startdate=' . $date_start->format('m/d/Y') . '&enddate=' . $date_end->modify('-1 second')->format('m/d/Y')); +} catch (Exception $e) { + exit_error($e->getMessage()); +} + +$xpath = new DOMXPath($document); + +// find all messages in search results and summarise them +$messages = $xpath->query('//div[@id = "maincontent"]/table//td[span[@class = "subject"]]'); +$summary = []; + +foreach ($messages as $message) { + $sender = trim($xpath->evaluate('substring-before(substring-after(normalize-space(div[@class = "hidden-xs"]/following-sibling::text()[1]), "By "), " · #")', $message), '"'); + $timestamp = $xpath->evaluate('substring-before(substring-after(.//script[@class = "timedisp"], "DisplayShortTime( "), " ,")', $message); + + $date = new DateTime(null, $tz); + $date->setTimestamp($timestamp / 1000000000); + + $key = serialize([ + 'date' => $date->format('Y-m-d'), + 'sender' => $sender + ]); + $summary[$key]++; +} + +// construct a linked data object corresponding to gathered data +$ld = [ + '@context' => 'https://wandystan.eu/statistics/context.jsonld', + '@type' => 'Slice', + 'key' => 'key/by-period', + 'period' => [ + '@type' => 'Interval', + 'hasBeginning' => [ + '@type' => 'Instant', + 'timestamp' => $date_start->format(DATE_ATOM) + ], + 'hasEnd' => [ + '@type' => 'Instant', + 'timestamp' => $date_end->format(DATE_ATOM) + ] + ] +]; + +$observations = []; +foreach ($summary as $key => $entry) { + $key = unserialize($key); + $nameHash = hash('fnv1a32', $key['sender']); + $observations []= [ + '@id' => 'by-period/' . $key['date'] . '/P1D#' . $nameHash, + '@type' => 'Observation', + 'dataset' => "by-period/", + 'date' => $key['date'], + 'sender' => [ + '@id' => 'sender/' . $nameHash, + '@type' => 'Agent', + 'name' => $key['sender'], + 'name_fnv1a32sum' => $nameHash + ], + 'posts' => $entry + ]; +} + +$ld['observations'] = $observations; + +header('Content-type: application/ld+json'); +print json_encode($ld); diff --git a/common.php b/common.php new file mode 100644 index 0000000..1be3e2b --- /dev/null +++ b/common.php @@ -0,0 +1,33 @@ +getMessage()); +} +set_exception_handler('exit_error_handler'); + +function load_remote_dom_document($uri) { + $content = file_get_contents($uri); + $document = new DOMDocument; + + if ($content === false) + throw new Exception("Cannot load remote URI: $uri"); + + if (! $document->loadHTML($content, LIBXML_NOWARNING | LIBXML_NOERROR)) + throw new Exception("Cannot parse remote URI as HTML: $uri"); + + return $document; +} + +function starts_with($haystack, $needle) { + return substr_compare($haystack, $needle, 0, strlen($needle)) === 0; +} diff --git a/context.jsonld b/context.jsonld new file mode 100644 index 0000000..634565c --- /dev/null +++ b/context.jsonld @@ -0,0 +1,106 @@ +{ + "@context": { + "@base": "https://wandystan.eu/statistics/", + "@vocab": "http://purl.org/linked-data/cube#", + "dcterms": "http://purl.org/dc/terms/", + "foaf": "http://xmlns.com/foaf/0.1/", + "prop": "https://wandystan.eu/statistics/property/", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "time": "http://www.w3.org/2006/time#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "Agent": "foaf:Agent", + "Instant": "time:Instant", + "Interval": "time:Interval", + "comment": { + "@id": "rdfs:comment", + "@container": "@language" + }, + "componentAttachment": { + "@type": "@vocab" + }, + "componentProperty": { + "@type": "@vocab" + }, + "dataset": { + "@id": "dataSet", + "@type": "@id" + }, + "date": { + "@id": "prop:date", + "@type": "xsd:date" + }, + "description": { + "@id": "dcterms:description", + "@container": "@language" + }, + "dimension": { + "@type": "@id" + }, + "hasBeginning": { + "@id": "time:hasBeginning", + "@type": "@id" + }, + "hasEnd": { + "@id": "time:hasEnd", + "@type": "@id" + }, + "issued": { + "@id": "dcterms:issued", + "@type": "xsd:date" + }, + "key": { + "@id": "sliceStructure", + "@type": "@id" + }, + "label": { + "@id": "rdfs:label", + "@container": "@language" + }, + "measure": { + "@type": "@id" + }, + "name": "foaf:name", + "name_fnv1a32sum": { + "@id": "prop:name_fnv1a32sum", + "@type": "xsd:hexBinary" + }, + "observations": "observation", + "period": { + "@id": "prop:period", + "@type": "prop:ISO8601-1" + }, + "posts": { + "@id": "prop:posts", + "@type": "xsd:nonNegativeInteger" + }, + "publisher": { + "@id": "dcterms:publisher", + "@type": "@id" + }, + "sender": "prop:sender", + "sliceKey": { + "@type": "@id" + }, + "slices": { + "@id": "slice", + "@type": "@id" + }, + "structure": { + "@id": "structure", + "@type": "@id" + }, + "subject": { + "@id": "dcterms:subject", + "@type": "@id" + }, + "timestamp": { + "@id": "time:inXSDDateTimeStamp", + "@type": "xsd:dateTimeStamp" + }, + "title": { + "@id": "dcterms:title", + "@container": "@language" + } + } +} diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..900aa4f --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,2 @@ +index.html: index.markdown + pandoc -s -c style.css -M lang=pl --shift-heading-level-by 1 -o $@ $< diff --git a/doc/icon-author.svg b/doc/icon-author.svg new file mode 100644 index 0000000..58b25d9 --- /dev/null +++ b/doc/icon-author.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/icon-date.svg b/doc/icon-date.svg new file mode 100644 index 0000000..4b62597 --- /dev/null +++ b/doc/icon-date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/icon-json-ld.png b/doc/icon-json-ld.png new file mode 100644 index 0000000..a80d9f6 Binary files /dev/null and b/doc/icon-json-ld.png differ diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..0883631 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,87 @@ + + + + + + + + + Statystyki Listy Dyskusyjnej Mandragoratu Wandystanu + + + + + +
+

Statystyki Listy Dyskusyjnej Mandragoratu Wandystanu

+

Polyna

+

2020-03-12

+
+

Statystyki Listy Dyskusyjnej Mandragoratu Wandystanu są udostępniane jako dane połączone, przy użyciu słownictwa RDF Data Cube. Zapisane są one w postaci JSON-LD z odpowiednim kontekstem, i mogą być przetwarzane zarówno za pomocą narzędzi przeznaczonych dla JSON-LD, jak i bezpośrednio, tak jak zwykłe dokumenty JSON.

+

Wszystkie odnośniki podane w postaci względnej posiadają podstawę https://wandystan.eu/statistics/ – przykładowo, "key": "key/by-period" oznacza odnośnik do adresu https://wandystan.eu/statistics/key/by-period.

+

Udostępniane są następujące rodzaje danych:

+

Wiadomości wg daty i wysyłającego

+

Statystyki liczby wiadomości wysłanych przez danego wysyłającego w danym dniu, pogrupowane wg okresu w jakim zostały wysłane.

+

Ze względu na ogromną ilość udostępnianych danych, statystyki podzielone są na wiele dokumentów:

+

Zbiór danych

+

Dokument opisujący udostępniany zbiór danych wraz z odnośnikami do jego wycinków obejmujących okres pojedynczego miesiąca.

+

Adres dokumentu: https://wandystan.eu/statistics/by-period/.

+

Układ dokumentu:

+ +

Wycinki danych

+

Dokumenty zawierające dane statystyczne z wybranego okresu.

+

Adres dokumentu: https://wandystan.eu/statistics/by-period/⟨przedział⟩; gdzie ⟨przedział⟩ to przedział dat w zapisie ISO 8601, z podaną co najmniej jedną datą początkową lub/i jedną datą końcową, bez podanego czasu. Data początkowa jest zawsze uznawana jako zawierająca się w danym okresie, zaś data końcowa jako niezawierająca się. Przykłady poprawnie zapisanych przedziałów:

+ +

Jeżeli w podanym przedziale napisano więcej niż 10 000 wiadomości, pod uwagę brane będą tylko te najpóźniej wysłane, więc nie jest zalecane podawanie zbyt długich przedziałów.

+

Układ dokumentu:

+ + + diff --git a/doc/index.markdown b/doc/index.markdown new file mode 100644 index 0000000..e37f683 --- /dev/null +++ b/doc/index.markdown @@ -0,0 +1,67 @@ +% Statystyki Listy Dyskusyjnej Mandragoratu Wandystanu +% [Polyna](/w/profile,196) +% 2020-03-12 + +Statystyki [Listy Dyskusyjnej Mandragoratu Wandystanu](https://wandystan.groups.io/g/wandystan/) są udostępniane jako dane połączone, przy użyciu słownictwa [RDF Data Cube](https://www.w3.org/TR/vocab-data-cube/){lang=en}. Zapisane są one w postaci [![](icon-json-ld.png){style=vertical-align:bottom} JSON-LD](https://json-ld.org/){lang=en} z [odpowiednim kontekstem](../context.jsonld), i mogą być przetwarzane zarówno za pomocą narzędzi przeznaczonych dla JSON-LD, jak i bezpośrednio, tak jak zwykłe dokumenty [JSON](https://pl.wikipedia.org/wiki/JSON). + +Wszystkie odnośniki podane w postaci względnej posiadają podstawę [`https://wandystan.eu/statistics/`](../) – przykładowo, `"key": "key/by-period"` oznacza odnośnik do adresu [`https://wandystan.eu/statistics/key/by-period`](../key/by-period). + +Udostępniane są następujące rodzaje danych: + +Wiadomości wg daty i wysyłającego +================================= + +Statystyki liczby wiadomości wysłanych przez danego wysyłającego w danym dniu, pogrupowane wg okresu w jakim zostały wysłane. + +Ze względu na ogromną ilość udostępnianych danych, statystyki podzielone są na wiele dokumentów: + +Zbiór danych +------------ + +Dokument opisujący udostępniany zbiór danych wraz z odnośnikami do jego wycinków obejmujących okres pojedynczego miesiąca. + +Adres dokumentu: [`https://wandystan.eu/statistics/by-period/`](../by-period/). + +Układ dokumentu: + +* `@type` – rodzaj obiektu: zbiór danych statystycznych. +* `title` – tytuł zbioru danych; obiekt którego kluczami są kody języków, a wartościami – tytuł zapisany w tym języku. +* `description` – opis zbioru danych; obiekt którego kluczami są kody języków, a wartościami – opis zapisany w tym języku. +* `publisher` – odnośnik do wydawcy dokumentu. +* `structure` – odnośnik do formalnego opisu układu danych statystycznych. +* `slices` – tablica odnośników do wycinków zbioru danych obejmujących okres pojedynczego miesiąca. + +Wycinki danych +-------------- + +Dokumenty zawierające dane statystyczne z wybranego okresu. + +Adres dokumentu: https://wandystan.eu/statistics/by-period/⟨przedział⟩; gdzie ⟨przedział⟩ to przedział dat w zapisie [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601), z podaną co najmniej jedną datą początkową lub/i jedną datą końcową, bez podanego czasu. Data początkowa jest zawsze uznawana jako zawierająca się w danym okresie, zaś data końcowa jako niezawierająca się. Przykłady poprawnie zapisanych przedziałów: + +* `2018-12-11/2019-04-21` – okres od 11 grudnia 2018 r. (włącznie) do 21 kwietnia 2019 r. (wyłącznie). +* `2020-02/P1M` – cały luty 2020 r. +* `P2M5D/2020-W03` – 2 miesiące i 5 dni poprzedzające trzeci tydzień 2020 r. + +Jeżeli w podanym przedziale napisano więcej niż 10 000 wiadomości, pod uwagę brane będą tylko te najpóźniej wysłane, więc nie jest zalecane podawanie zbyt długich przedziałów. + +Układ dokumentu: + +* `@type` – rodzaj obiektu: wycinek zbioru danych statystycznych. +* `key` – odnośnik do klucza danych wycinka. +* `period` – okres, którego dotyczy dany wycinek. + * `@type` – rodzaj obiektu: przedział czasu. + * `hasBeginning` – początek przedziału czasu. + * `@type` – rodzaj obiektu: chwila w czasie. + * `timestamp` – znacznik czasu w zapisie [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). + * `hasEnd` – koniec przedziału czasu; obiekt o takim samym układzie co `hasBeginning`. +* `observations` – poszczególne dane jednostkowe (spostrzeżenia) dotyczące danego okresu – tablica obiektów o następującej strukturze: + * `@id` – oznaczenie przypisane do danego spostrzeżenia. + * `@type` – rodzaj obiektu: dana jednostkowa (spostrzeżenie). + * `dataset` – odnośnik do zbioru, do którego przypisana jest dana. + * `date` – dzień w którym zostały wysłane wiadomości na listę, w zapisie [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). + * `sender` – wysyłający wiadomości na listę w danym dniu. + * `@id` – oznaczenie przypisane do danego wysyłającego. + * `@type` – rodzaj obiektu: osoba bądź inna jednostka (organizacja, program samoczynnie wysyłający wiadomości, itp.) mająca możliwość podejmowania działań. + * `name` – nazwa tej osoby bądź jednostki. + * `name_fnv1a32sum` – skrót nazwy utworzony za pomocą [algorytmu FNV-1a](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) w odmianie 32-bitowej, zapisany w postaci szesnastkowej. Może być przekształcony w równoważny ciąg bitów, bądź 32-bitową liczbę bez znaku. + * `posts` – ilość postów wysłanych na listę przez danego wysyłającego w danym dniu. diff --git a/doc/style.css b/doc/style.css new file mode 100644 index 0000000..0a3c1d1 --- /dev/null +++ b/doc/style.css @@ -0,0 +1,15 @@ +body { + width: 50em; + font-family: sans-serif; + line-height: 1.5; +} + +.author { + padding-left: 2em; + background: url('icon-author.svg') left center / contain no-repeat; +} + +.date { + padding-left: 2em; + background: url('icon-date.svg') left center / contain no-repeat; +} diff --git a/properties.jsonld b/properties.jsonld new file mode 100644 index 0000000..59e11f8 --- /dev/null +++ b/properties.jsonld @@ -0,0 +1,104 @@ +{ + "@context": { + "@language": "en", + "dct": "http://purl.org/dc/terms/", + "owl": "http://www.w3.org/2002/07/owl#", + "prop": "https://wandystan.eu/statistics/property/", + "qb": "http://purl.org/linked-data/cube#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "sdmx-concept": "http://purl.org/linked-data/sdmx/2009/concept#", + "sdmx-dimension": "http://purl.org/linked-data/sdmx/2009/dimension#", + "sdmx-measure": "http://purl.org/linked-data/sdmx/2009/measure#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "Datatype": "rdfs:Datatype", + "DatatypeProperty": "owl:DatatypeProperty", + "DimensionProperty": "qb:DimensionProperty", + "FunctionalProperty": "owl:FunctionalProperty", + "DimensionProperty": "qb:DimensionProperty", + "MeasureProperty": "owl:MeasureProperty", + "Ontology": "owl:Ontology", + "Property": "rdf:Property", + "defines": { + "@reverse": "rdfs:isDefinedBy" + }, + "comment": "rdfs:comment", + "concept": { + "@id": "qb:concept", + "@type": "@id" + }, + "created": { + "@id": "dct:created", + "@type": "xsd:date" + }, + "creator": { + "@id": "dct:creator", + "@type": "@id" + }, + "description": "dct:description", + "isDefinedBy": { + "@id": "rdfs:isDefinedBy", + "@type": "@id" + }, + "label": "rdfs:label", + "range": { + "@id": "rdfs:range", + "@type": "@id" + }, + "seeAlso": { + "@id": "rdfs:seeAlso", + "@type": "@id" + }, + "subPropertyOf": { + "@id": "rdfs:subPropertyOf", + "@type": "@id" + }, + "title": "dct:title" + }, + "@id": "prop:", + "@type": "Ontology", + "title": "Statistical ontology for on-line discussion spaces", + "created": "2020-03-06", + "creator": "https://wandystan.eu/B196", + "description": "This ontology defines properties used for publication of the statistical data about posts published in on-line discussion groups, lists and fora according to the RDF Data Cube model .", + "defines": [ + { + "@id": "prop:date", + "@type": ["Property", "DatatypeProperty", "DimensionProperty", "FunctionalProperty"], + "label": "Date", + "comment": "Date in which posts were was sent.", + "concept": "sdmx-concept:refPeriod", + "range": "xsd:date", + "subPropertyOf": ["sdmx-dimension:refPeriod", "dct:date"] + }, { + "@id": "prop:name_fnv1a32sum", + "@type": ["Property", "DatatypeProperty"], + "label": "32-bit FNV-1a sum of a name", + "comment": "A name for some thing hashed with the 32-bit flavor of the FNV-1a algorithm.", + "range": "xsd:hexBinary" + }, { + "@id": "prop:period", + "@type": ["Property", "DimensionProperty", "ObjectProperty"], + "label": "Period", + "comment": "Period in which posts were sent.", + "concept": "sdmx-concept:refPeriod", + "range": "http://www.w3.org/2006/time#Interval", + "subPropertyOf": ["sdmx-dimension:refPeriod", "dct:date"] + }, { + "@id": "prop:posts", + "@type": ["Property", "DatatypeProperty", "FunctionalProperty", "MeasureProperty"], + "label": "Number of posts", + "comment": "Number of posts sent in a certain period of time.", + "concept": "sdmx-concept:obsValue", + "range": "xsd:nonNegativeInteger", + "subPropertyOf": "sdmx-measure:obsValue" + }, { + "@id": "prop:sender", + "@type": ["Property", "DimensionProperty", "FunctionalProperty", "ObjectProperty"], + "label": "Sender", + "comment": "Sender of the post (not necessarily a person, may be e.g. an automated posting agent).", + "range": "dct:Agent" + } + ] +} diff --git a/sender.php b/sender.php new file mode 100644 index 0000000..eb9ef14 --- /dev/null +++ b/sender.php @@ -0,0 +1,14 @@ + 'https://wandystan.eu/statistics/context.jsonld', + '@id' => 'sender/' . $hash, + '@type' => 'Agent', + 'name_fnv1a32sum' => $hash +]); diff --git a/structures.jsonld b/structures.jsonld new file mode 100644 index 0000000..7a314f1 --- /dev/null +++ b/structures.jsonld @@ -0,0 +1,37 @@ +{ + "@context": "https://wandystan.eu/statistics/context.jsonld", + "@graph": [ + { + "@id": "structure/by-period", + "@type": "DataStructureDefinition", + "component": [ + { + "measure": "prop:posts" + }, { + "dimension": "prop:sender", + "order": 1 + }, { + "dimension": "prop:date", + "order": 2 + }, { + "componentAttachment": "Slice", + "dimension": "prop:period", + "order": 3 + } + ], + "sliceKey": "key/by-period" + }, { + "@id": "key/by-period", + "@type": "SliceKey", + "label": { + "en": "time slice", + "pl": "wycinek czasu" + }, + "comment": { + "en": "Slice grouping data from a given time period.", + "pl": "Wycinek grupujący dane z danego okresu czasowego." + }, + "componentProperty": "prop:period" + } + ] +}