我正在尝试修复我公开提供的download automation script,以便任何人都可以轻松下载R世界价值观调查。
在这个网页上 - http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp - PDF链接“WVS_2000_Questionnaire_Root”可以在firefox和chrome中轻松下载。我无法弄清楚如何使用httr
或RCurl
或任何内容自动下载其他R包。下面是Chrome互联网行为的截图。 PDF链接需要跟进http://www.worldvaluessurvey.org/wvsdc/DC00012/F00001316-WVS_2000_Questionnaire_Root.pdf的最终来源,但如果直接点击它们,就会出现连接错误。我不清楚这是否与请求标头Upgrade-Insecure-Requests:1
或响应标头状态代码302
点击新的worldvaluessurvey.org网站,Chrome的检查元素窗口打开让我觉得这里有一些hacky编码决策,因此标题半破:/
答案 0 :(得分:6)
我过去不得不处理这类事情。我的解决方案是使用headless browser以编程方式导航和操作包含我感兴趣的资源的网页。我甚至做过相当不直接的任务,例如登录,填写和提交表单使用这种方法。
通过对链接生成的GET / POST请求进行反向工程,我可以看到您正在尝试使用纯R方法下载这些文件。这可能有效,但这会使您的实现极易受到网站设计中未来任何更改的影响,例如JavaScript事件处理程序,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主类,可以通过单击网页上的链接来下载文件。应用程序的参数化如下:
\s*>\s*
来解析URL参数,我喜欢这是一种简洁的语法。我使用了>
字符,因为它在网址中无效。Content-Disposition
标头派生,其值与模式filename="(.*)"
匹配(这是我在一段时间内刮取图标时遇到的一个不寻常的情况),或者,如果没有,那么,它的基本名称请求触发文件流响应的URL。 basename派生方法适用于您的目标链接。以下是代码:
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 > //iframe[@id='frame1'] > //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进行网页抓取可能不是一个好主意。那是我的两分钱。
答案 1 :(得分:4)
使用优秀的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")
直接从浏览器中取出卷曲字符串,然后curlconverter
完成所有工作。
答案 2 :(得分:3)
查看DocDownload函数的代码,它们主要是对/AJDownload.jsp进行POST 使用ulthost的post params:WVS,CndWAVE:4,SAID:0,DOID :(此处为doc id),AJArchive:WVS Data Archive。不确定是否有一些是必需的,但最好还是最好包括它们。
使用httr在R中执行此操作,看起来像这样
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二进制数据。
如果我从我的webbrowser手动设置cookie,会发生这种情况。我只使用cookie的JSESSIONID部分,没有别的。正如我之前提到的,JSESSIONID可能会因年龄或不活动而过期。
答案 3 :(得分:0)