Nous allons extraire la liste des paquets R de R-Forge, ainsi que les fichiers DESCRIPTION
de ces paquets.
La liste des paquets est disponible sur la page https://r-forge.r-project.org/softwaremap/full_list.php. Elle se présente sous la forme d'une liste paginée, qu'il va falloir parser pour récupérer le nom du package. Notez que nous récupèrerons le nom utilisé dans l'url du package et non le nom affiché, simplement parce que le premier, contrairement au deuxième, nous servira pour récupérer le fichier DESCRIPTION
.
Chaque page (95 à l'heure où j'écris ces lignes) est accessible depuis https://r-forge.r-project.org/softwaremap/full_list.php?page=X où X est le numéro de page (1-indexed). La première chose à faire est de récupérer le nombre de pages.
In [20]:
import requests
import BeautifulSoup as bs
LIST_URL = 'https://r-forge.r-project.org/softwaremap/full_list.php?page={page}'
content = requests.get(LIST_URL.format(page=1)).content
soup = bs.BeautifulSoup(content)
anchors = soup.findAll('a')
N = max(
map(lambda x: int(x),
map(lambda x: x['href'].rsplit('?page=', 1)[1],
filter(lambda x: x['href'].startswith('/softwaremap/full_list.php?page='), anchors)
)
)
)
Ensuite, nous allons récupérer le contenu de chacune de ces pages et le parser afin de rechercher les noms des packages.
Sur chaque page, <span property="doap:name">
précède le nom du package. Mais comme nous voulons récupérer le nom utilisé dans les liens, nous devons remonter au premier parent de type a
afin d'extraire l'url, et de cette url, extraire le package.
In [21]:
def packages_list(url):
content = requests.get(url).content
soup = bs.BeautifulSoup(content)
spans = soup.findAll('span', attrs={'property': 'doap:name'})
anchors = [span.findParent('a', limit=1) for span in spans]
return map(lambda a: a['href'].rsplit('/', 2)[1], anchors)
In [22]:
names = []
for n in range(1, N+1):
names += packages_list(LIST_URL.format(page=n))
In [23]:
print len(names)
Après un certain temps (souvenez-vous : il faut récupérer et parser N
(~95) pages !), names
contient la liste des noms de package utilisés dans les urls. Chaque package possède potentiellement un fichier DESCRIPTION
sur son svn. Par exemple, pour le package rcppbind
, l'adresse est https://r-forge.r-project.org/scm/viewvc.php/*checkout*/pkg/DESCRIPTION?root=rcppbind
Tous les packages n'ont pas forcément un tel fichier. Le "package" epicookbook affiche une erreur 404 quand on accède à la page https://r-forge.r-project.org/scm/viewvc.php/*checkout*/pkg/DESCRIPTION?root=epicookbook. Malheureusement pour nous, le code de retour n'est pas 404. Il va donc falloir "parser" (brièvement et simplement) le contenu en cas de requête, afin d'identifier si le retour est une "page 404" ou un réel fichier DESCRIPTION
. On peut y parvenir simplement parce que la seconde ligne est composée de <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
. Il y a aussi des erreurs 403 (enfin, un équivalent) restreignant l'accès à certains packages. Ces contenus débutent par <?xml
.
Il y a aussi une autre subtilité : certains dépôts contiennent plusieurs packages R. C'est le cas par exemple de SciViews. Dans un tel cas, chaque sous-répertoire de pkg
contient potentiellement un package, contenant lui même potentiellement un fichier DESCRIPTION
. Le parsing de ce listing se fait assez simplement : les noms qui nous intéressent sont un attribut name
des balises a
dont le titre est View directory contents
.
La procédure pour récupérer ces informations peut donc se résumer à :
DESCRIPTION
dans pkg
.pkg
.DESCRIPTION
pour chaque sous-répertoire de pkg
.
In [24]:
DESCRIPTION_URL = 'https://r-forge.r-project.org/scm/viewvc.php/*checkout*/pkg/DESCRIPTION?root={name}'
PKG_DIR_URL = 'https://r-forge.r-project.org/scm/viewvc.php/pkg/?root={name}'
PKG_DESCRIPTION_URL = 'https://r-forge.r-project.org/scm/viewvc.php/*checkout*/pkg/{dir}/DESCRIPTION?root={name}'
def parse_DESCRIPTION(content):
r = {}
for line in content.split('\n'):
if len(line.strip()) > 0:
if line.startswith((' ', '\t')):
r[key] += ' ' + line.strip() # key is already defined in this case
else:
try:
key, value = line.split(':', 1)
r[key.strip()] = value.strip()
except Exception as e:
print line
print 'len is', len(line)
print '---'
print content
print '---'
# raise
return r
In [25]:
errors = []
d = {}
In [26]:
for i, name in enumerate(names):
if name in d: # moins lent que de récupérer et parser la page
continue
content = requests.get(DESCRIPTION_URL.format(name=name)).content
# Check for /pkg/DESCRIPTION
if content.startswith(('\n<?xml', '\nInvalid repository type')):
# Looks like an error
errors.append(name)
elif content.startswith('\n<!DOCTYPE'):
# DESCRIPTION is missing, looks for /pkg/
content = requests.get(PKG_DIR_URL.format(name=name)).content
# Get subdirectories
soup = bs.BeautifulSoup(content)
for anchor in soup.findAll('a', attrs={'title': 'View directory contents'}):
content = requests.get(PKG_DESCRIPTION_URL.format(name=name, dir=anchor['name'])).content
if not content.startswith('\n<!DOCTYPE'):
d[anchor['name']] = parse_DESCRIPTION(content)
else:
# DESCRIPTION file is found
d[name] = parse_DESCRIPTION(content)
Après un certain temps (~1900 pages à parser, sans compter les listings et sous-répertoires), nous avons dans d
le contenu suffisant pour générer un fichier .csv avec pandas !
In [27]:
# Before looking at subdirs: (773, 1109, 1882)
len(d), len(errors), len(names)
Out[27]:
In [28]:
import pandas
df = pandas.DataFrame.from_dict(d, orient='index')
df = df.drop_duplicates('Package')
df.to_csv('../data/r-forge_description.csv')