J'ai déjà eu à faire face à ce genre de situation par le passé. Ma solution a été d'utiliser un navigateur sans tête pour naviguer et manipuler de manière programmée les pages web qui contenaient les ressources qui m'intéressaient. J'ai même effectué des tâches assez simples, comme me connecter, remplir et soumettre des formulaires, en utilisant cette méthode.
Je vois que vous essayez d'utiliser une approche purement R pour télécharger ces fichiers en faisant de l'ingénierie inverse sur les requêtes GET/POST générées par le lien. Cela pourrait fonctionner, mais cela rendrait votre mise en œuvre très vulnérable à tout changement futur dans la conception du site, comme des changements dans le gestionnaire d'événements JavaScript, les redirections d'URL ou les exigences d'en-tête.
En utilisant un navigateur sans tête, vous pouvez limiter votre exposition à l'URL de premier niveau et à quelques requêtes XPath minimales qui permettent la navigation vers le lien cible. Certes, cela lie toujours votre code à des détails non contractuels et assez internes de la conception du site, mais c'est certainement une exposition moindre. C'est le danger du web scraping.
J'ai toujours utilisé la méthode Java HtmlUnit pour ma navigation sans tête, que j'ai trouvé tout à fait excellente. Bien sûr, pour tirer parti d'une solution Java de Rland, il faudrait lancer un processus Java, ce qui nécessiterait (1) que Java soit installé sur la machine de l'utilisateur, (2) que l'application $CLASSPATH
pour être correctement configuré afin de localiser les JARs de HtmlUnit ainsi que votre classe principale de téléchargement de fichiers personnalisés, et (3) l'invocation correcte de la commande Java avec les bons arguments en utilisant l'une des méthodes de R pour accéder à une commande système. Inutile de dire que c'est assez compliqué et compliqué.
Une solution de navigation sans tête purement R serait bien, mais malheureusement, il me semble que R ne propose pas de solution native de navigation sans tête. La solution la plus proche est RSelenium qui semble n'être qu'une liaison R avec la bibliothèque client Java de l'interface utilisateur. Sélénium logiciel d'automatisation du navigateur. Cela signifie qu'il ne fonctionnera pas indépendamment du navigateur graphique de l'utilisateur et qu'il nécessite de toute façon une interaction avec un processus Java externe (bien que, dans ce cas, les détails de l'interaction soient commodément encapsulés sous l'API RSelenium).
En utilisant HtmlUnit, j'ai créé une classe principale Java assez générique qui peut être utilisée pour télécharger un fichier en cliquant sur un lien dans une page web. Le paramétrage de l'application est le suivant :
- L'URL de la page.
- Une séquence optionnelle d'expressions XPath pour permettre de descendre dans un nombre quelconque de cadres imbriqués à partir de la page de premier niveau. Note : En fait, j'analyse ceci à partir de l'argument URL en divisant sur
\s*>\s*
que j'apprécie pour sa syntaxe concise. J'ai utilisé le >
car il n'est pas valide dans les URLs.
- Une seule expression XPath qui spécifie le lien d'ancrage à cliquer.
- Un nom de fichier facultatif sous lequel enregistrer le fichier téléchargé. S'il est omis, il sera dérivé de l'un ou l'autre des éléments suivants
Content-Disposition
dont la valeur correspond au modèle filename="(.*)"
(c'est un cas inhabituel que j'ai rencontré lors du scraping d'icônes il y a quelque temps) ou, à défaut, le nom de base de l'URL de la requête qui a déclenché la réponse du flux de fichiers. La méthode de dérivation du nom de base fonctionne pour votre lien cible.
Voici le code :
package com.bgoldst;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.ConfirmHandler;
import com.gargoylesoftware.htmlunit.WebWindowListener;
import com.gargoylesoftware.htmlunit.WebWindowEvent;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.BaseFrameElement;
public class DownloadFileByXPath {
public static ConfirmHandler s_downloadConfirmHandler = null;
public static WebWindowListener s_downloadWebWindowListener = null;
public static String s_saveFile = null;
public static void main(String[] args) throws Exception {
if (args.length < 2 || args.length > 3) {
System.err.println("usage: {url}[>{framexpath}*] {anchorxpath} [{filename}]");
System.exit(1);
} // end if
String url = args[0];
String anchorXPath = args[1];
s_saveFile = args.length >= 3 ? args[2] : null;
// parse the url argument into the actual URL and optional subsequent frame xpaths
String[] fields = Pattern.compile("\\s*>\\s*").split(url);
List<String> frameXPaths = new ArrayList<String>();
if (fields.length > 1) {
url = fields[0];
for (int i = 1; i < fields.length; ++i)
frameXPaths.add(fields[i]);
} // end if
// prepare web client to handle download dialog and stream event
s_downloadConfirmHandler = new ConfirmHandler() {
public boolean handleConfirm(Page page, String message) {
return true;
}
};
s_downloadWebWindowListener = new WebWindowListener() {
public void webWindowContentChanged(WebWindowEvent event) {
WebResponse response = event.getWebWindow().getEnclosedPage().getWebResponse();
//System.out.println(response.getLoadTime());
//System.out.println(response.getStatusCode());
//System.out.println(response.getContentType());
// filter for content type
// will apply simple rejection of spurious text/html responses; could enhance this with command-line option to whitelist
String contentType = response.getResponseHeaderValue("Content-Type");
if (contentType.contains("text/html")) return;
// determine file name to use; derive dynamically from request or response headers if not specified by user
// 1: user
String saveFile = s_saveFile;
// 2: response Content-Disposition
if (saveFile == null) {
Pattern p = Pattern.compile("filename=\"(.*)\"");
Matcher m;
List<NameValuePair> headers = response.getResponseHeaders();
for (NameValuePair header : headers) {
String name = header.getName();
String value = header.getValue();
//System.out.println(name+" : "+value);
if (name.equals("Content-Disposition")) {
m = p.matcher(value);
if (m.find())
saveFile = m.group(1);
} // end if
} // end for
if (saveFile != null) saveFile = sanitizeForFileName(saveFile);
// 3: request URL
if (saveFile == null) {
WebRequest request = response.getWebRequest();
File requestFile = new File(request.getUrl().getPath());
saveFile = requestFile.getName(); // just basename
} // end if
} // end if
getFileResponse(response,saveFile);
} // end webWindowContentChanged()
public void webWindowOpened(WebWindowEvent event) {}
public void webWindowClosed(WebWindowEvent event) {}
};
// initialize browser
WebClient webClient = new WebClient(BrowserVersion.FIREFOX_45);
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setJavaScriptEnabled(true); // required for JavaScript-powered links
webClient.getOptions().setThrowExceptionOnScriptError(false);
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
// 1: get home page
HtmlPage page;
try { page = webClient.getPage(url); } catch (IOException e) { throw new Exception("error: could not get URL \""+url+"\".",e); }
//page.getEnclosingWindow().setName("main window");
// 2: navigate through frames as specified by the user
for (int i = 0; i < frameXPaths.size(); ++i) {
String frameXPath = frameXPaths.get(i);
List<?> elemList = page.getByXPath(frameXPath);
if (elemList.size() != 1) throw new Exception("error: frame "+(i+1)+" xpath \""+frameXPath+"\" returned "+elemList.size()+" elements on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<.");
if (!(elemList.get(0) instanceof BaseFrameElement)) throw new Exception("error: frame "+(i+1)+" xpath \""+frameXPath+"\" returned a non-frame element on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<.");
BaseFrameElement frame = (BaseFrameElement)elemList.get(0);
Page enclosedPage = frame.getEnclosedPage();
if (!(enclosedPage instanceof HtmlPage)) throw new Exception("error: frame "+(i+1)+" encloses a non-HTML page.");
page = (HtmlPage)enclosedPage;
} // end for
// 3: get the target anchor element by xpath
List<?> elemList = page.getByXPath(anchorXPath);
if (elemList.size() != 1) throw new Exception("error: anchor xpath \""+anchorXPath+"\" returned "+elemList.size()+" elements on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<.");
if (!(elemList.get(0) instanceof HtmlAnchor)) throw new Exception("error: anchor xpath \""+anchorXPath+"\" returned a non-anchor element on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<.");
HtmlAnchor anchor = (HtmlAnchor)elemList.get(0);
// 4: click the target anchor with the appropriate confirmation dialog handler and content handler
webClient.setConfirmHandler(s_downloadConfirmHandler);
webClient.addWebWindowListener(s_downloadWebWindowListener);
anchor.click();
webClient.setConfirmHandler(null);
webClient.removeWebWindowListener(s_downloadWebWindowListener);
System.exit(0);
} // end main()
public static void getFileResponse(WebResponse response, String fileName ) {
InputStream inputStream = null;
OutputStream outputStream = null;
// write the inputStream to a FileOutputStream
try {
System.out.print("streaming file to disk...");
inputStream = response.getContentAsStream();
// write the inputStream to a FileOutputStream
outputStream = new FileOutputStream(new File(fileName));
int read = 0;
byte[] bytes = new byte[1024];
while ((read = inputStream.read(bytes)) != -1)
outputStream.write(bytes, 0, read);
System.out.println("done");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
} // end try-catch
} // end if
if (outputStream != null) {
try {
//outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
} // end try-catch
} // end if
} // end try-catch
} // end getFileResponse()
public static String sanitizeForFileName(String unsanitizedStr) {
return unsanitizedStr.replaceAll("[^\040-\176]","_").replaceAll("[/\\<>|:*?]","_");
} // end sanitizeForFileName()
} // end class DownloadFileByXPath
Vous trouverez ci-dessous une démonstration de l'exécution de la classe principale sur mon système. J'ai supprimé la plupart des messages verbeux de HtmlUnit. J'expliquerai les arguments de la ligne de commande plus tard.
ls;
## bin/ src/
CLASSPATH="bin;C:/cygwin/usr/local/share/htmlunit-latest/*" java com.bgoldst.DownloadFileByXPath "http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp > //iframe[@id='frame1'] > //iframe[@id='frameDoc']" "//a[contains(text(),'WVS_2000_Questionnaire_Root')]";
## Jul 10, 2016 1:34:34 PM com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
## WARNING: Obsolete content type encountered: 'application/x-javascript'.
## Jul 10, 2016 1:34:34 PM com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
## WARNING: Obsolete content type encountered: 'application/x-javascript'.
##
## ... snip ...
##
## Jul 10, 2016 1:34:45 PM com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
## WARNING: Obsolete content type encountered: 'text/javascript'.
## streaming file to disk...done
##
ls;
## bin/ F00001316-WVS_2000_Questionnaire_Root.pdf* src/
-
CLASSPATH="bin;C:/cygwin/usr/local/share/htmlunit-latest/*"
Ici, je règle le $CLASSPATH
pour mon système en utilisant un préfixe d'assignation de variable (note : je fonctionnais avec le shell Cygwin bash). Le fichier .class que j'ai compilé dans bin
J'ai installé les JARs de HtmlUnit dans la structure des répertoires de mon système Cygwin, ce qui est probablement un peu inhabituel.
-
java com.bgoldst.DownloadFileByXPath
Il s'agit évidemment du mot de commande et du nom de la classe principale à exécuter.
-
"http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp > //iframe[@id='frame1'] > //iframe[@id='frameDoc']"
Il s'agit des expressions XPath de l'URL et du cadre. Votre lien cible est imbriqué sous deux iframes, ce qui nécessite les deux expressions XPath. Vous pouvez trouver les attributs id dans la source, soit en visualisant le HTML brut, soit en utilisant un outil de développement Web ( Firebug est mon préféré).
-
"//a[contains(text(),'WVS_2000_Questionnaire_Root')]"
Enfin, voici l'expression XPath réelle pour le lien cible dans l'iframe interne.
J'ai omis l'argument du nom du fichier. Comme vous pouvez le constater, le code a correctement dérivé le nom du fichier à partir de l'URL de la requête.
Je reconnais que c'est beaucoup d'ennuis pour télécharger un fichier, mais pour le web scraping en général, je pense vraiment que la seule approche robuste et viable est d'aller jusqu'au bout et d'utiliser un moteur de navigateur sans tête. Il serait peut-être préférable de séparer entièrement la tâche de téléchargement de ces fichiers de Rland, et de mettre en œuvre l'ensemble du système de scraping en utilisant une application Java, peut-être complétée par quelques scripts shell pour une interface plus flexible. À moins que vous ne travailliez avec des URL de téléchargement qui ont été conçues pour des requêtes HTTP uniques sans fioritures par des clients comme curl, wget et R, l'utilisation de R pour le web scraping n'est probablement pas une bonne idée. C'est mon avis.