From f0e1c4c815047d01de2fd5af861f71cf938a2712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Mon, 25 Nov 2019 00:18:36 -0500 Subject: [PATCH] =?UTF-8?q?Script=20Python=20pour=20r=C3=A9cup=C3=A9rer=20?= =?UTF-8?q?le=20nombre=20de=20vues=20d=E2=80=99une=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/pageviews/pageviews.py | 235 ++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 data/pageviews/pageviews.py diff --git a/data/pageviews/pageviews.py b/data/pageviews/pageviews.py new file mode 100644 index 0000000..f4dc7f5 --- /dev/null +++ b/data/pageviews/pageviews.py @@ -0,0 +1,235 @@ +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()