Ammonite/Статистика відвідувань вікісторінок: відмінності між версіями

Вилучено вміст Додано вміст
Ilya (обговорення | внесок)
DannyS712 (обговорення | внесок)
м <source> -> <syntaxhighlight> (phab:T237267)
 
Рядок 7:
Напишемо функцію, яка створює url для запиту для вказаного проекту, року і місяця:
 
<sourcesyntaxhighlight lang="scala">
def url(project: String, year: Int, month: Int) = s"https://wikimedia.org/api/rest_v1/metrics/pageviews/top/$project/all-access/$year/$month/all-days"
</syntaxhighlight>
</source>
 
і перевіримо її роботу ('''@''' - це символ запрошення командного рядка Ammonite, його не треба вводити, рядки без '''@''' — вивід)
<sourcesyntaxhighlight lang="scala">
@ def url(project: String, year: Int, month: Int) = s"https://wikimedia.org/api/rest_v1/metrics/pageviews/top/$project/all-access/$year/$month/all-days"
defined function url
@ url("uk.wikibooks.org", 2016, 1)
res2: String = "https://wikimedia.org/api/rest_v1/metrics/pageviews/top/uk.wikibooks.org/all-access/2016/1/all-days"
</syntaxhighlight>
</source>
 
Так само як і [[Ammonite/HTTP запит з парсингом JSON|в попередньому прикладі]] пробуємо отримати дані
<sourcesyntaxhighlight lang="scala">
import scala.io.Source
val text = Source.fromURL(url("uk.wikibooks.org", 2016, 1)).mkString
</syntaxhighlight>
</source>
 
І отримуємо помилку
Рядок 39:
 
Виявляється сервіс статистики переглядів не розуміє номер місяця без початкового нуля, і потрібно вказати 01, а не 1. Тому, додаємо у функцію створення url початковий 0:
<sourcesyntaxhighlight lang="scala">
def url(project: String, year: Int, month: Int) = {
val monthStr = (if (month < 10) "0" else "") + month
s"https://wikimedia.org/api/rest_v1/metrics/pageviews/top/$project/all-access/$year/$monthStr/all-days"
}
</syntaxhighlight>
</source>
 
Парсимо повернутий JSON
<sourcesyntaxhighlight lang="scala">
import $ivy.`io.circe::circe-parser:0.6.1`, $ivy.`io.circe::circe-optics:0.6.1`
import io.circe._, io.circe.parser._ , io.circe.optics.JsonPath._
val doc = parse(text).getOrElse(Json.Null)
</syntaxhighlight>
</source>
 
Розпарсений json:
<sourcesyntaxhighlight lang="scala">
doc: Json = {
"items" : [
Рядок 85:
},
...
</syntaxhighlight>
</source>
 
Одержуємо список найвідвідуваніших сторінок:
<sourcesyntaxhighlight lang="scala">
val articles = root.items.each.articles.each.article.string.getAll(doc)
</syntaxhighlight>
</source>
Вивід:
<sourcesyntaxhighlight lang="scala">
articles: List[String] = List(
"Головна_сторінка",
Рядок 99:
"Активні_дієприкметники_і_віддієслівні_прикметники",
...
</syntaxhighlight>
</source>
 
Та кількість їх відвідувань:
<sourcesyntaxhighlight lang="scala">
val views = root.items.each.articles.each.views.int.getAll(doc)
</syntaxhighlight>
</source>
 
Вивід:
<sourcesyntaxhighlight lang="scala">
views: List[Int] = List(
2238,
Рядок 114:
1047,
...
</syntaxhighlight>
</source>
 
Поєднаємо у пари назви статей та їх відвідуваність:
<sourcesyntaxhighlight lang="scala">
val pairs = articles.zip(views)
</syntaxhighlight>
</source>
 
Вивід:
<sourcesyntaxhighlight lang="scala">
pairs: List[(String, Int)] = List(
("Головна_сторінка", 2238),
Рядок 128:
("Pascal/Математичні_операції", 1079),
("Активні_дієприкметники_і_віддієслівні_прикметники", 1047),
</syntaxhighlight>
</source>
 
Підсумуємо той код, що ми покроково написали:
<sourcesyntaxhighlight lang="scala">
import $ivy.`io.circe::circe-parser:0.6.1`, $ivy.`io.circe::circe-optics:0.6.1`
import io.circe._, io.circe.parser._ , io.circe.optics.JsonPath._
Рядок 155:
// поєднаємо у пари назви статей та їх відвідуваність:
val pairs = articles.zip(views)
</syntaxhighlight>
</source>
=== Обробка статистики ===
Ми отримали статистику у вигляді списку (<tt>List</tt>) пар назви сторінки та кількості відвідувань (<tt>List[(String, Int)]</tt>)
<sourcesyntaxhighlight lang="scala">
pairs: List[(String, Int)] = List(
("Головна_сторінка", 2238),
Рядок 164:
("Pascal/Математичні_операції", 1079),
("Активні_дієприкметники_і_віддієслівні_прикметники", 1047),
</syntaxhighlight>
</source>
 
Із парами можна працювати за номером елементу в парі.
 
Наприклад, можна відсортувати список за першим елементом — назвою статті:
<sourcesyntaxhighlight lang="scala">
val byName = pairs.sortBy(_._1)
</sourcesyntaxhighlight>
 
або відсортувати список за другим елементом — кількістю переглядів у зворотному (тобто спадному — від більшої до меншої кількості) порядку.
<sourcesyntaxhighlight lang="scala">
val byViews = pairs.sortBy(- _._2)
</syntaxhighlight>
</source>
 
Хоча такий код доволі лаконічний, для його розуміння треба бачити з контексту, що першим елементом є назва статті, а другим — кількість переглядів. Допомагають зрозуміти також назви змінних <tt>byName</tt> і <tt>byViews</tt>.
Однак, коли програми стають більшими, це може стати незручним і при обробці пари можна також давати назву її елементам:
 
<sourcesyntaxhighlight lang="scala">
val byName = pairs.sortBy{ case (name, views) => name }
</sourcesyntaxhighlight>
<sourcesyntaxhighlight lang="scala">
val byViews = pairs.sortBy{ case (name, views) => - views }
</syntaxhighlight>
</source>
 
Так дещо зрозуміліше, але багатослівніше. Крім того, ці назви елементам пари треба заново вказувати при кожному звертанні.
 
Тому краще створити клас із двома полями name і views:
<sourcesyntaxhighlight lang="scala">
case class PageViews(name: String, views: Int)
</sourcesyntaxhighlight>
 
і перетворити список пар у список елементів класу PageViews
<sourcesyntaxhighlight lang="scala">
val pageViews = pairs.map{ case (name, views) => new PageViews(name, views) }
</sourcesyntaxhighlight>
 
Тепер код для роботи з даними про кількість переглядів виглядає і лаконічно і зрозуміло:
<sourcesyntaxhighlight lang="scala">
val byName = pageViews.sortBy(_.name)
</sourcesyntaxhighlight>
<sourcesyntaxhighlight lang="scala">
val byViews = pageViews.sortBy(- _.views)
</syntaxhighlight>
</source>
 
== Річна статистика ==
 
Винесемо код одержання статистики за місяць у окрему функцію
<sourcesyntaxhighlight lang="scala">
case class PageViews(name: String, views: Int)
 
Рядок 225:
articles.zip(views).map{ case (name, views) => new PageViews(name, views) }
}
</syntaxhighlight>
</source>
 
І отримаємо статису за кожним місяцем:
<sourcesyntaxhighlight lang="scala">
val monthly = (1 to 12).map(month => getMonthlyViews("uk.wikibooks.org", 2016, month))
</syntaxhighlight>
</source>
 
=== Групування за назвою статті ===
Змінна monthly тепер містить список із 12 списків статей для кожного місяця
<sourcesyntaxhighlight lang="scala">
@ monthly.size
res19: Int = 12
</syntaxhighlight>
</source>
 
 
Кожен із 12 списків містить топ-1000 статей, які переглядались цього місяця.
<sourcesyntaxhighlight lang="scala">
@ monthly.map(_.size)
res22: collection.immutable.IndexedSeq[Int] = Vector(916, 741, 868, 984, 997, 993, 759, 736, 703, 709, 750, 784)
</syntaxhighlight>
</source>
Але жодного місяця не переглянули 1000 різних статей, тому маємо не топ-1000, а топ-(скільки цього місяця переглянуто різних статей)
 
зробимо із 12 списків статей по місяцях 1 річний
<sourcesyntaxhighlight lang="scala">
val yearlySeq = monthly.flatten
</syntaxhighlight>
</source>
 
перевіримо, що кількість елементів списку по місяцях і об'єднаного списку однакова:
<sourcesyntaxhighlight lang="scala">
@ yearlySeq.size
res21: Int = 9940
@ monthly.map(_.size).sum
res23: Int = 9940
</syntaxhighlight>
</source>
 
Згрупуємо за назвою статті, та просумуємо за місяцями:
<sourcesyntaxhighlight lang="scala">
val groupedByArticle = yearlySeq.groupBy(_.page}
val articleToViews = groupedByArticle.mapValues(_.map(_.views).sum}
</syntaxhighlight>
</source>
 
І відсортуємо за кількістю переглядів:
<sourcesyntaxhighlight lang="scala">
val ordered = articleToViews.toSeq.sortBy(- _.views)
ordered: Seq[PageViews] = Vector(
Рядок 276:
PageViews("Закінчення_іменників_другої_відміни_чоловічого_роду_в_родовому_відмінку_однини", 10701),
PageViews("Освоюємо_Java/Основи", 10266),
</syntaxhighlight>
</source>
 
=== Згрупована за підручником ===
<sourcesyntaxhighlight lang="scala">
val yearlyByBookSeq = ordered.map(pv => pv.copy(name = pv.name))
</syntaxhighlight>
</source>
 
<sourcesyntaxhighlight lang="scala">
val groupedByBook = yearlyByBookSeq.groupBy(_.name)
val bookToViews = groupedByBook.mapValues(_.map(_.views).sum)
val orderedBookViews = bookToViews.toSeq.sortBy(-_.views)
</syntaxhighlight>
</source>
 
Вивід (очевидно треба ще відфільтрувати за простором назв статей):
<sourcesyntaxhighlight lang="scala">
orderedBookViews: Seq[PageViews] = Vector(
PageViews("Освоюємо_Java", 79686),
Рядок 304:
PageViews("C++", 10038),
...
</syntaxhighlight>
</source>
 
== Остаточний скрипт ==
<sourcesyntaxhighlight lang="scala">
import $ivy.`io.circe::circe-parser:0.6.1`, $ivy.`io.circe::circe-optics:0.6.1`
import io.circe._, io.circe.parser._ , io.circe.optics.JsonPath._
Рядок 337:
 
val orderedBookViews = bookToViews.toSeq.sortBy(- _.views)
</syntaxhighlight>
</source>
 
Ще можна винести в окрему функцію однаковий код групування за статтею та за підручником, який зараз продубльований. Загалом остання версія виглядає так:
 
<sourcesyntaxhighlight lang="scala">
import $ivy.`io.circe::circe-parser:0.6.1`, $ivy.`io.circe::circe-generic:0.6.1`
import io.circe._, io.circe.parser._, io.circe.generic.auto._
Рядок 382:
 
println(wikiTable)
</syntaxhighlight>
</source>