如何使用 R 下载 semi-broken javascript asp 函数后面的文件

How to download a file behind a semi-broken javascript asp function with R

我正在尝试修复我公开提供的 download automation script,以便任何人都可以使用 R 轻松下载世界价值观调查。

在此网页上 - http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp - the PDF link "WVS_2000_Questionnaire_Root" easily downloads in firefox and chrome.I cannot figure out how to automate the download with httr or RCurl or any other R package. screenshot below of the chrome internet behavior. That PDF link needs to follow through to the ultimate source of http://www.worldvaluessurvey.org/wvsdc/DC00012/F00001316-WVS_2000_Questionnaire_Root.pdf 但如果您直接单击它们,则会出现连接错误。我不清楚这是否与请求 header Upgrade-Insecure-Requests:1 或响应 header 状态码 302

有关

在 chrome 的检查元素 windows 打开的情况下点击新的 worldvaluessurvey.org 网站让我觉得这里做出了一些 hacky 编码决定,因此标题 semi-broken :/

您的问题可能是由 302 状态代码引起的。我可以解释 302 代码是什么,但看起来您可以从对整个下载过程的解释中受益:

这就是用户单击该 pdf 时发生的情况 link。

  1. link 触发了 onclick javascript 事件。如果您右键单击 link 并单击 "Inspect Element",您可以看到有一个 onclick 事件设置为 "DocDownload('1316')"。 .
  2. 但是,如果我们在 javascript 控制台中键入 DocDownload,浏览器会告诉我们 DocDownload 不作为函数存在。
  3. 这是因为 pdf link 位于 window 内的 iframe 内。浏览器中的开发控制台仅访问 variables/functions

查看 DocDownload 函数的代码,他们主要只是对 /AJDownload.jsp 执行 POST post 参数为 ulthost:WVS,CndWAVE:4,SAID:0,DOID:(此处为文档 ID),AJArchive:WVS 数据存档。不确定其中一些是否是必需的,但无论如何最好包括它们。

在 R 中使用 httr 执行此操作,看起来像这样

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

AJDownload.asp 端点将 return 一个 302(重定向到 REAL url),httr 库应该自动为您遵循重定向。通过反复试验,我确定服务器需要 Content-Type 和 Cookie header,否则它会 return 一个空的 400 (OK) 响应。您将需要获得一个有效的 cookie,您可以通过检查加载到该服务器的任何页面来找到它,然后查找 header with Cookie: JSESSIONID=..... ,您将需要复制整个header

所以有了那些,它看起来像

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]"))

响应将是二进制 pdf 数据,因此您需要将其保存到文件中才能对其执行任何操作。

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

编辑:

好的,有时间实际 运行 代码。我还发现了 POST 调用所需的最少参数,即 docid、JSESSIONID cookie 和 Referer header.

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

这在我的机器上工作并且正确 returns PDF 二进制数据。

如果我从我的网络浏览器手动设置 cookie,就会发生这种情况。我只使用 cookie 的 JSESSIONID 部分,仅此而已。正如我之前提到的,JSESSIONID 将过期,可能是由于年龄或不活动。

我过去不得不处理这种事情。我的解决方案是使用 headless browser 以编程方式导航和操作包含我感兴趣的资源的网页。我什至完成了相当 non-straightforward 的任务,例如登录、填写和提交表单使用此方法。

我可以看到您正在尝试使用纯 R 方法通过 reverse-engineering link 生成的 GET/POST 请求来下载这些文件。这可能有效,但会使您的实现极易受到网站设计未来任何更改的影响,例如 JavaScript 事件处理程序、URL 重定向或 header 要求的更改。

通过使用无头浏览器,您可以限制对 top-level URL 和一些允许导航到目标 link 的最小 XPath 查询的暴露。当然,这仍然将您的代码与 non-contractual 和网站设计的相当内部的细节联系在一起,但它肯定不会暴露。这就是网页抓取的危害。


我一直使用 Java HtmlUnit 库进行无头浏览,我发现它非常出色。当然,要利用 Rland 的 Java-based 解决方案,需要生成一个 Java 进程,这需要 (1) Java 安装在用户机器上,(2) $CLASSPATH 正确设置以定位 HtmlUnit JAR 以及您的自定义 file-downloading main class,以及 (3) 使用以下之一正确调用具有正确参数的 Java 命令R 的 shell 输出到系统命令的方法。不用说,这相当复杂和混乱。

纯 R 无头浏览解决方案会很好,但不幸的是,在我看来 R 不提供任何本机无头浏览解决方案。最接近的是 RSelenium, which appears to be just an R binding to the Java client library of the Selenium 浏览器自动化软件。这意味着它不会独立于用户的 GUI 浏览器运行,并且无论如何都需要与外部 Java 进程交互(尽管在这种情况下,交互的细节很方便地封装在 RSelenium API 下)。


我使用 HtmlUnit 创建了一个相当通用的 Java 主 class,可用于通过单击网页上的 link 下载文件。应用的参数化如下:

  • 页面的URL。
  • 一个可选的 XPath 表达式序列,允许从 top-level 页面开始下降到任意数量的嵌套框架。注意:我实际上通过拆分 \s*>\s* 从 URL 参数中解析出它,我喜欢这种简洁的语法。我使用了 > 字符,因为它在 URLs 中无效。
  • 指定要单击的锚点 link 的单个 XPath 表达式。
  • 用于保存下载文件的可选文件名。如果省略,它将从 Content-Disposition header 派生,其值与模式 filename="(.*)" 匹配(这是我前阵子抓取图标时遇到的不寻常情况),或者,如果失败,触发文件流响应的请求 URL 的基本名称。基本名称派生方法适用于您的目标 link.

代码如下:

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("[^0-6]","_").replaceAll("[/\<>|:*?]","_");
    } // end sanitizeForFileName()

} // end class DownloadFileByXPath

下面是我 运行 主要 class 在我的系统上的演示。我删掉了大部分 HtmlUnit 的冗长输出。我稍后会解释 command-line 个参数。

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/*" 在这里,我使用 variable-assignment 前缀为我的系统设置 $CLASSPATH(注意:我在 Cygwin bash shell).我将 .class 文件编译成 bin,并将 HtmlUnit JAR 安装到我的 Cygwin 系统目录结构中,这可能有点不寻常。
  • java com.bgoldst.DownloadFileByXPath 显然这是要执行的命令字和主要 class 的名称。
  • "http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp > //iframe[@id='frame1'] > //iframe[@id='frameDoc']" 这是 URL 和 frame XPath 表达式。您的目标 link 嵌套在两个 iframe 下,因此需要两个 XPath 表达式。您可以通过查看原始 HTML 或使用网络开发工具(Firebug 是我的最爱)在源代码中找到 id 属性。
  • "//a[contains(text(),'WVS_2000_Questionnaire_Root')]" 最后,这是内部 iframe 中目标 link 的实际 XPath 表达式。

我省略了文件名参数。如您所见,代码从请求 URL.

中正确导出了文件名

我知道下载文件很麻烦,但对于一般的网络抓取,我真的认为唯一稳健可行的方法是走完整个九码并使用完全无头的浏览器引擎。最好将下载这些文件的任务与 Rland 完全分开,而是使用 Java 应用程序实现整个抓取系统,可能辅以一些 shell 脚本以获得更灵活的前端。除非您正在使用为 curl、wget 和 R 等客户端的 no-frills one-shot HTTP 请求设计的下载 URLs,否则使用 R 进行网络抓取可能不是一个好主意.那是我的两分钱。

使用优秀的curlconverter模仿浏览器可以直接请求pdf。

首先我们模仿浏览器初始 GET 请求(可能不需要简单的 GET 并且保留 cookie 就足够了):

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

JSESSIONID cookie 在 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")

curl 字符串直接从浏览器中提取,curlconverter 然后完成所有工作。