236 lines
7.6 KiB
Python
236 lines
7.6 KiB
Python
|
import bottleneck
|
|||
|
import calendar
|
|||
|
import collections
|
|||
|
from datetime import datetime
|
|||
|
import logging
|
|||
|
import math
|
|||
|
import matplotlib
|
|||
|
from matplotlib import pyplot
|
|||
|
import requests
|
|||
|
import sys
|
|||
|
import mwclient
|
|||
|
|
|||
|
# Chemin racine pour les API Wikimedia
|
|||
|
wikimedia_base_path = 'https://wikimedia.org/api/rest_v1'
|
|||
|
|
|||
|
# Patron d’accès à l’API pageviews de Wikimedia
|
|||
|
wikimedia_pageviews_path = '/' + '/'.join([
|
|||
|
'metrics', 'pageviews', 'per-article', '{project}',
|
|||
|
'{access}', '{agent}', '{article}', '{granularity}',
|
|||
|
'{start}', '{end}'
|
|||
|
])
|
|||
|
|
|||
|
# Format de dates utilisée pour l’API Wikimedia
|
|||
|
wikimedia_date_format = '%Y%m%d'
|
|||
|
|
|||
|
# Date de première disponibilité des pageviews sur l’API Wikimedia
|
|||
|
wikimedia_pageviews_start = datetime(2015, 7, 1)
|
|||
|
|
|||
|
# Objet pour afficher le journal d’exécution
|
|||
|
logger = logging.getLogger('pageviews')
|
|||
|
|
|||
|
def wrapping_move_mean(data, window):
|
|||
|
"""
|
|||
|
Calcule une moyenne par fenêtre circulaire sur un tableau de valeurs.
|
|||
|
|
|||
|
:param data: Tableau de valeurs.
|
|||
|
:param window: Taille de la fenêtre.
|
|||
|
:return: Tableau moyenné avec une fenêtre de taille donnée. Le résultat
|
|||
|
a les même dimensions que l’entrée.
|
|||
|
"""
|
|||
|
down_half_window = math.floor(window / 2)
|
|||
|
up_half_window = math.ceil(window / 2)
|
|||
|
padded_data = data[-down_half_window:] + data + data[:up_half_window]
|
|||
|
return bottleneck.move_mean(padded_data, window=window)[window - 1:]
|
|||
|
|
|||
|
def wikimedia_page_views(site, article):
|
|||
|
"""
|
|||
|
Obtient le nombre de visites sur une page Wikipédia par jour.
|
|||
|
|
|||
|
:param site: Site Wikipédia ciblé.
|
|||
|
:param article: Article ciblé dans le site.
|
|||
|
:return: Compteur associant chaque jour à son nombre de visites.
|
|||
|
"""
|
|||
|
# Soumet une requête à l’API REST pour obtenir les vues de l’article
|
|||
|
res = requests.get(wikimedia_base_path + wikimedia_pageviews_path.format(
|
|||
|
project=site.host,
|
|||
|
article=article,
|
|||
|
access='all-access',
|
|||
|
agent='user',
|
|||
|
granularity='daily',
|
|||
|
start=wikimedia_pageviews_start.strftime(wikimedia_date_format),
|
|||
|
end=datetime.today().strftime(wikimedia_date_format)
|
|||
|
))
|
|||
|
|
|||
|
data = res.json()
|
|||
|
|
|||
|
# Vérifie que la réponse reçue indique un succès
|
|||
|
if res.status_code != 200:
|
|||
|
if 'detail' in data:
|
|||
|
detail = data['detail']
|
|||
|
message = ', '.join(detail) if type(detail) == list else detail
|
|||
|
raise Exception(message)
|
|||
|
else:
|
|||
|
raise Exception('Erreur {}'.format(res.status_code))
|
|||
|
|
|||
|
# Construit le dictionnaire résultant
|
|||
|
return collections.Counter(dict(
|
|||
|
(record['timestamp'][:8], record['views'])
|
|||
|
for record in data['items']
|
|||
|
))
|
|||
|
|
|||
|
def wikimedia_article_canonical_name(site, article):
|
|||
|
"""
|
|||
|
Obtient le nom canonique d’un article après avoir suivi les redirections.
|
|||
|
|
|||
|
:param site: Objet empaquetant l’API du site Wikipédia ciblé.
|
|||
|
:param article: Article ciblé dans le site.
|
|||
|
:return: Nom canonique de l’article.
|
|||
|
"""
|
|||
|
original_page = site.pages[article]
|
|||
|
|
|||
|
if not original_page.exists:
|
|||
|
raise Exception(
|
|||
|
'Article « {} » inexistant sur {}'
|
|||
|
.format(article, site.host)
|
|||
|
)
|
|||
|
|
|||
|
return original_page.resolve_redirect().name
|
|||
|
|
|||
|
def wikimedia_article_views(site, article):
|
|||
|
"""
|
|||
|
Obtient le nombre de visites sur un article Wikipédia, incluant la page
|
|||
|
canonique et toutes les pages redirigées vers celle-ci, par jour.
|
|||
|
|
|||
|
:param site: Objet empaquetant l’API du site Wikipédia ciblé.
|
|||
|
:param article: Article ciblé dans le site.
|
|||
|
:return: Liste de couples contenant d’une part un jour et d’autre part
|
|||
|
le nombre de visites associées à ce jour.
|
|||
|
"""
|
|||
|
# Récupération de l’ensemble des pages qui redirigent vers la page donnée
|
|||
|
response = site.api('query', prop='redirects', titles=article)
|
|||
|
page_response = list(response['query']['pages'].values())[0]
|
|||
|
|
|||
|
if 'missing' in page_response:
|
|||
|
raise Exception(
|
|||
|
'Article « {} » inexistant sur {}'
|
|||
|
.format(article, site.host)
|
|||
|
)
|
|||
|
|
|||
|
# Si la réponse n’inclut pas la clé « redirects », c’est qu’il n’existe
|
|||
|
# aucune redirection vers la page
|
|||
|
redirects = [
|
|||
|
item['title'] for item in
|
|||
|
page_response['redirects']
|
|||
|
] if 'redirects' in page_response else []
|
|||
|
|
|||
|
# Somme le nombre de visites sur chacune des pages
|
|||
|
return sum(
|
|||
|
(wikimedia_page_views(site, page)
|
|||
|
for page in redirects + [article]),
|
|||
|
start=collections.Counter()
|
|||
|
)
|
|||
|
|
|||
|
def wikimedia_mean_article_views(site, article):
|
|||
|
"""
|
|||
|
Obtient des statistiques moyennes sur les vues d’un article Wikipédia par
|
|||
|
jour de l’année (omettant le 29 février pour les années bissextiles).
|
|||
|
|
|||
|
:param site: Objet empaquetant l’API du site Wikipédia ciblé.
|
|||
|
:param article: Article ciblé dans le site.
|
|||
|
:return: Tableau de taille 365 contenant le nombre de visites moyennes pour
|
|||
|
chaque jour de l’année.
|
|||
|
"""
|
|||
|
data = wikimedia_article_views(site, article)
|
|||
|
|
|||
|
# Fait la moyenne pour chaque jour hormis le 29 février
|
|||
|
accumulator = {}
|
|||
|
datemonth_format = '%m%d'
|
|||
|
|
|||
|
for day in range(1, 366):
|
|||
|
datemonth = datetime.fromordinal(day).strftime(datemonth_format)
|
|||
|
accumulator[datemonth] = []
|
|||
|
|
|||
|
for date_str, views in data.items():
|
|||
|
date = datetime.strptime(date_str, wikimedia_date_format)
|
|||
|
|
|||
|
if not (date.month == 2 and date.day == 29):
|
|||
|
datemonth = date.strftime(datemonth_format)
|
|||
|
accumulator[datemonth].append(views)
|
|||
|
|
|||
|
for datemonth, value in accumulator.items():
|
|||
|
accumulator[datemonth] = (
|
|||
|
sum(accumulator[datemonth])
|
|||
|
/ len(accumulator[datemonth])
|
|||
|
) if accumulator[datemonth] else 0
|
|||
|
|
|||
|
# Fait une moyenne glissante sur 7 jours
|
|||
|
days = [item[1] for item in sorted(
|
|||
|
list(accumulator.items()),
|
|||
|
key=lambda x: x[0]
|
|||
|
)]
|
|||
|
|
|||
|
return wrapping_move_mean(days, window=60)
|
|||
|
|
|||
|
def create_year_plot():
|
|||
|
"""
|
|||
|
Initialise un graphe avec en abscisse les 365 jours d’une année non
|
|||
|
bissextile.
|
|||
|
|
|||
|
:return: Figure et axes Matplotlib.
|
|||
|
"""
|
|||
|
fig, ax = pyplot.subplots()
|
|||
|
|
|||
|
ax.set_xlabel('Jours de l’année')
|
|||
|
ax.set_xticks([
|
|||
|
datetime(1, month, 1).toordinal()
|
|||
|
for month in range(1, 13)
|
|||
|
])
|
|||
|
|
|||
|
ax.set_xticklabels(calendar.month_abbr[1:13])
|
|||
|
return fig, ax
|
|||
|
|
|||
|
if __name__ == '__main__':
|
|||
|
logging.basicConfig(level=logging.INFO)
|
|||
|
|
|||
|
if len(sys.argv) < 3:
|
|||
|
print("""Utilisation : {} [project] [article]...
|
|||
|
Obtient les statistiques moyenne de vue de pages wiki.
|
|||
|
|
|||
|
Paramètres :
|
|||
|
|
|||
|
project Projet Wikipédia ciblé.
|
|||
|
article Nom(s) d’article(s) Wikipédia ciblé(s).
|
|||
|
|
|||
|
Au moins un article doit être donné. Le nombre de visites est moyenné sur
|
|||
|
l’année dans une fenêtre de 60 jours. Les redirections d’article sont suivies
|
|||
|
et toute visite sur une page de redirection pointant vers l’article est
|
|||
|
dénombrées comme une visite sur la page canonique.
|
|||
|
""".format(sys.argv[0]), file=sys.stderr)
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
project = sys.argv[1]
|
|||
|
articles = sys.argv[2:]
|
|||
|
|
|||
|
site = mwclient.Site(project)
|
|||
|
fig, ax = create_year_plot()
|
|||
|
|
|||
|
for article in articles:
|
|||
|
canonical_article = wikimedia_article_canonical_name(site, article)
|
|||
|
|
|||
|
if article != canonical_article:
|
|||
|
logger.info(
|
|||
|
'Suivi de la redirection de « {} » en « {} »'
|
|||
|
.format(article, canonical_article)
|
|||
|
)
|
|||
|
|
|||
|
data = wikimedia_mean_article_views(site, canonical_article)
|
|||
|
ax.plot(data, label=canonical_article)
|
|||
|
ax.set_ylabel('Vues par jour')
|
|||
|
|
|||
|
fig.legend()
|
|||
|
fig.autofmt_xdate()
|
|||
|
fig.tight_layout()
|
|||
|
|
|||
|
pyplot.show()
|