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()