27 votes

Comment télécharger un fichier derrière une fonction asp javascript semi-cassée avec R

J'essaie de réparer un télécharger l'automatisation script que je mets à disposition publiquement afin que tout le monde puisse facilement télécharger l'enquête sur les valeurs mondiales avec R.

Sur cette page web - http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp - le lien PDF "WVS_2000_Questionnaire_Root" se télécharge facilement dans firefox et chrome.je n'arrive pas à trouver comment automatiser le téléchargement avec httr o RCurl ou tout autre paquet R. Capture d'écran ci-dessous du comportement de chrome internet. Ce lien PDF doit mener à la source ultime de l'information. http://www.worldvaluessurvey.org/wvsdc/DC00012/F00001316-WVS_2000_Questionnaire_Root.pdf mais si vous cliquez directement dessus, il y a une erreur de connectivité. je ne sais pas si cela est lié à l'en-tête de la requête Upgrade-Insecure-Requests:1 ou le code d'état de l'en-tête de réponse 302

En cliquant sur le nouveau site web worldvaluessurvey.org avec l'élément d'inspection de chrome ouvert, je pense que certaines décisions de codage ont été prises, d'où le titre semi-brisé :/.

enter image description here

6voto

bgoldst Points 15113

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.

4voto

jdharrison Points 10616

En utilisant l'excellent curlconverter pour imiter le navigateur, vous pouvez demander directement le pdf.

Tout d'abord, nous imitons le navigateur initial GET (ce n'est pas forcément nécessaire, un simple GET et la conservation du cookie peuvent suffire) :

library(curlconverter)
library(httr)
browserGET <- "curl 'http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp' -H 'Host: www.worldvaluessurvey.org' -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1'"
getDATA <- (straighten(browserGET) %>% make_req)[[1]]()

El JSESSIONID Le cookie est disponible sur getDATA$cookies$value

getPDF <- "curl 'http://www.worldvaluessurvey.org/wvsdc/DC00012/F00001316-WVS_2000_Questionnaire_Root.pdf' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-US,en;q=0.5' -H 'Connection: keep-alive' -H 'Cookie: JSESSIONID=59558DE631D107B61F528C952FC6E21F' -H 'Host: www.worldvaluessurvey.org' -H 'Referer: http://www.worldvaluessurvey.org/AJDocumentationSmpl.jsp' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0'"
appIP <- straighten(getPDF)
# replace cookie
appIP[[1]]$cookies$JSESSIONID <- getDATA$cookies$value
appReq <- make_req(appIP)
response <- appReq[[1]]()
writeBin(response$content, "test.pdf")

Les cordes de boucles ont été cueillies directement dans le navigateur et curlconverter puis fait tout le travail.

3voto

Ben Abraham Points 427

En examinant le code de la fonction DocDownload, on constate qu'il s'agit essentiellement d'un POST vers /AJDownload.jsp. avec des paramètres de post ulthost:WVS, CndWAVE : 4, SAID : 0, DOID : (l'identifiant du document ici), AJArchive : WVS Data Archive. Je ne sais pas si certains de ces paramètres sont nécessaires, mais il est probablement préférable de les inclure de toute façon.

en faisant cela dans R en utilisant httr, cela donnerait quelque chose comme ceci

r <- POST("http://www.worldvaluessurvey.org/AJDownload.jsp", body = list("ulthost" = "WVS", "CndWAVE" = 4, "SAID" = 0, "DOID" = 1316, "AJArchive" = "WVS Data Archive"))

Le point d'accès AJDownload.asp renverra un 302 (redirection vers l'url réelle), et la bibliothèque httr devrait automatiquement suivre la redirection pour vous. Par essais et erreurs, j'ai déterminé que le serveur a besoin des en-têtes Content-Type et Cookie, sinon il renvoie une réponse vide 400 (OK). Vous devrez obtenir un cookie valide, que vous pouvez trouver en inspectant toute page chargée sur ce serveur, et rechercher l'en-tête avec Cookie : JSESSIONID=..... Pour cela, vous devez copier l'intégralité de l'en-tête.

Donc, avec ces éléments, il semble que

r <- POST("http://www.worldvaluessurvey.org/AJDownload.jsp", body = list("ulthost" = "WVS", "CndWAVE" = 4, "SAID" = 0, "DOID" = 1316, "AJArchive" = "WVS Data Archive"), add_headers("Content-Type" = "application/x-www-form-urlencoded", "Cookie" = "[PASTE COOKIE VALUE HERE]"))

La réponse sera constituée de données binaires en format pdf, vous devrez donc l'enregistrer dans un fichier pour pouvoir en faire quelque chose.

bin <- content(r, "raw")
writeBin(bin, "myfile.txt")

EDITAR:

Ok, j'ai eu le temps d'exécuter le code. J'ai aussi découvert les paramètres minimum requis pour les appels POST, qui sont juste le docid, le cookie JSESSIONID, et l'en-tête Referer.

library(httr)
download_url <- "http://www.worldvaluessurvey.org/AJDownload.jsp"
frame_url <- "http://www.worldvaluessurvey.org/AJDocumentationSmpl.jsp"
body <- list("DOID" = "1316")

file_r <- POST(download_url, body = body, encode = "form",
          set_cookies("JSESSIONID" = "0E657C37FF030B41C33B7D2B1DCAB3D8"),
          add_headers("Referer" = frame_url),
          verbose())

Cela a fonctionné sur ma machine et renvoie correctement les données binaires du PDF.

C'est ce qui se passe si je configure le cookie manuellement à partir de mon navigateur web. J'utilise uniquement la partie JSESSIONID du cookie et rien d'autre. Comme je l'ai déjà mentionné, le JSESSIONID expirera, probablement en raison de son âge ou de son inactivité. success_image

0voto

johnsimer Points 84

Votre problème est probablement causé par le code d'état 302. Je pourrais vous expliquer ce qu'est un code 302, mais il semble que vous auriez besoin d'une explication sur l'ensemble du processus de téléchargement :

Voici ce qui se passe lorsqu'un utilisateur clique sur ce lien pdf.

  1. L'événement javascript onclick est déclenché pour ce lien. Si vous cliquez avec le bouton droit de la souris sur le lien et que vous cliquez sur "Inspecter l'élément", vous pouvez voir qu'un événement onclick est défini sur "DocDownload('1316')". inline javascript onclick event .
  2. Cependant, si nous tapons DocDownload dans la console javascript, le navigateur nous indique que DocDownload n'existe pas en tant que fonction. enter image description here
  3. C'est parce que le lien pdf est situé dans une iframe à l'intérieur de la fenêtre. enter image description here . La console dev dans un navigateur accède uniquement aux variables/fonctions.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X