如何使用R下载半损坏的javascript asp函数后面的文件

Posted

技术标签:

【中文标题】如何使用R下载半损坏的javascript asp函数后面的文件【英文标题】:How to download a file behind a semi-broken javascript asp function with R 【发布时间】:2016-07-02 03:22:53 【问题描述】:

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

在此网页上 - http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp - PDF 链接“WVS_2000_Questionnaire_Root”可以在 Firefox 和 chrome 中轻松下载。我不知道如何使用 httrRCurl 或任何其他 R 包自动下载。下面是 chrome 互联网行为的屏幕截图。该 PDF 链接需要跟随 http://www.worldvaluessurvey.org/wvsdc/DC00012/F00001316-WVS_2000_Questionnaire_Root.pdf 的最终来源,但如果您直接单击它们,则会出现连接错误。我不清楚这是否与请求标头Upgrade-Insecure-Requests:1 或响应标头状态代码302有关

在打开 chrome 的检查元素窗口的情况下点击新的 worldvaluessurvey.org 网站让我觉得这里做出了一些骇人听闻的编码决定,因此标题半损坏:/

【问题讨论】:

哇,为了一个问题牺牲了几乎所有的声誉,真是令人印象深刻! ;-) 见鬼;我很乐意支持获得这个问题的有用答案。如果您没有在规定的时间内得到解决方案并给予奖励,请告诉我,我会再提出 500 个代表以确保它保持特色。感谢您为使公共数据集可访问所做的所有工作,Anthony。 @42- 非常感谢大卫,我很感激。无头浏览答案是一个很好的答案,但发帖人说得对,它只在-R 内会更好。我担心有人会给出一个好的RCurl 答案,然后世界价值观调查人们会再次更改网站.. 职业危害;) 【参考方案1】:

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

我可以看到您正在尝试使用纯 R 方法通过对链接生成的 GET/POST 请求进行反向工程来下载这些文件。这可能有效,但它会使您的实现极易受到网站设计未来任何变化的影响,例如 javascript 事件处理程序、URL 重定向或标头要求的变化。

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


我一直使用 Java htmlUnit 库进行无头浏览,我发现它非常出色。当然,要利用 Rland 的基于 Java 的解决方案,需要生成 Java 进程,这需要 (1) 在用户机器上安装 Java,(2) 正确设置 $CLASSPATH 以定位 HtmlUnit JAR 以及您的自定义文件下载主类,以及 (3) 使用 R 的一种脱壳到系统命令的方法正确调用具有正确参数的 Java 命令。不用说,这是相当复杂和混乱的。

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


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

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

代码如下:

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

下面是我在系统上运行主类的演示。我已经剪掉了大部分 HtmlUnit 的详细输出。后面我会解释命令行参数。

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

我省略了文件名参数。如您所见,代码正确地从请求 URL 派生了文件名。


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

【讨论】:

【参考方案2】:

使用优秀的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 然后完成所有工作。

【讨论】:

@AnthonyDamico:我们需要补充您的代表或保护您的新代表。如果你想让我赞助问题,请告诉我。我想你有我的电子邮件,因为我从 DWin 更改了我的 SO 名称。 非常感谢!如果我的下一个问题在我的声誉恢复之前出现,我会给你发个便条:D【参考方案3】:

查看 DocDownload 函数的代码,他们主要只是对 /AJDownload.jsp 进行 POST 带有 ulthost:WVS, CndWAVE: 4, SAID: 0, DOID: (这里的 doc id), AJArchive: WVS Data Archive 的 post 参数。不确定其中一些是否是必需的,但最好还是包含它们。

在 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 端点将返回一个 302(重定向到 REAL url),并且 httr 库应该自动为您遵循重定向。通过反复试验,我确定服务器需要 Content-Type 和 Cookie 标头,否则将返回空的 400(OK)响应。您将需要获得一个有效的 cookie,您可以通过检查该服务器的任何页面加载来找到它,并查找带有 Cookie: JSESSIONID=..... 的标头,您将需要复制整个标头

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

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 标头。

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

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

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

【讨论】:

谢谢,但我尝试了几种不同的方式,但都没有奏效? library(httr) ; r &lt;- POST("http://www.worldvaluessurvey.org/AJDownload.jsp", body = list("ulthost" = "WVS", "CndWAVE" = 4, "SAID" = 0, "DOID" = 1316, "AJArchive" = "WVS Data Archive")) ; x &lt;- 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", "JSESSIONID" = cookies(r)$value)) ; x$content 我更新了我在本地运行并工作的代码。尝试更改。另外需要注意的是,JSESSIONID cookie 可以并且将会过期。因此,您需要确保使用的是最近使用过的。为了在未来进一步自动化,您可以先请求一个页面(如列表,甚至可能是主页),然后您可以从响应中提取 cookie。 对不起,您的示例仍然不适合我?你能修改你的答案,让它自动化这个过程(包括r$cookies$value的拉取),并在一个新的R会话中干净地运行吗? 我似乎无法让服务器接受它为 R 脚本生成的 cookie,所以我不知道这是怎么回事。但是,我更新了我的代码以使其更清晰,并发布了我的输出图像,这可能会为您提供一些关于您的失败之处的线索。 在带有老化 cookie 的 Mac 上工作得很好。我已经使用 Chrome 访问过该页面,因此可能在 cookie-jar 中找到了另一个 cookie。【参考方案4】:

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

这就是用户点击该 pdf 链接时发生的情况。

    针对该链接触发了 onclick javascript 事件。如果您右键单击链接并单击“检查元素”,您可以看到有一个 onclick 事件设置为“DocDownload('1316')”。 。 但是,如果我们在 javascript 控制台中输入 DocDownload,浏览器会告诉我们 DocDownload 不作为函数存在。 这是因为 pdf 链接位于窗口 内的 iframe 内。浏览器中的开发控制台只访问变量/函数

【讨论】:

谢谢。问题是:如何使用 R 下载文件?

以上是关于如何使用R下载半损坏的javascript asp函数后面的文件的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 asp.net 以 excel 格式下载 SQL 数据

如何允许使用 ASP.NET 下载 .json 文件

R:如何使用 ggplot2 创建一个半色半数的热图?

iOS CoreGraphics:用半透明图案抚摸会导致颜色损坏

如何在 ASP .Net (Aspx) 中创建要从 Javascript 访问的 Web 服务方法?

jQuery/JavaScript 替换损坏的图像