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