Мне приходилось сталкиваться с подобными вещами в прошлом. Я решил использовать безголовый браузер для программной навигации и управления веб-страницами, содержащими интересующие меня ресурсы. Я даже выполнял довольно непростые задачи, такие как вход в систему, заполнение и отправка форм с использованием этого метода.
Я вижу, что вы пытаетесь использовать чистый подход R для загрузки этих файлов путем обратной разработки запросов GET/POST, которые генерируются по ссылке. Это могло бы сработать, но сделало бы вашу реализацию очень уязвимой для любых будущих изменений в дизайне сайта, таких как изменения в обработчике событий JavaScript, перенаправлениях URL или требованиях к заголовкам.
Используя безголовый браузер, вы можете ограничить доступ к URL-адресу верхнего уровня и нескольким минимальным запросам XPath, которые позволяют перейти к целевой ссылке. Конечно, это по-прежнему связывает ваш код с неконтрактными и довольно внутренними деталями дизайна сайта, но это, безусловно, менее заметно. В этом заключается опасность парсинга веб-страниц.
Я всегда пользовался библиотекой Java HtmlUnit для автономного браузинга, который показался мне превосходным. Конечно, для использования решения на основе Java от Rland потребуется создать процесс Java, для чего потребуется (1) установить Java на компьютере пользователя, (2) правильно настроить $CLASSPATH
для поиска JAR-файлов HtmlUnit как а также ваш собственный основной класс для загрузки файлов и (3) правильный вызов команды Java с правильными аргументами с использованием одного из методов R для обработки системной команды. Излишне говорить, что это довольно сложно и грязно.
Безголовое решение для просмотра на чистом R было бы неплохо, но, к сожалению, мне кажется, что R не предлагает никакого собственного решения для безголового просмотра. Наиболее близким является RSelenium, который, по-видимому, представляет собой просто привязку R к клиентской библиотеке Java из Selenium программное обеспечение для автоматизации браузера. Это означает, что он не будет работать независимо от браузера с графическим интерфейсом пользователя и в любом случае требует взаимодействия с внешним Java-процессом (хотя в этом случае детали взаимодействия удобно инкапсулированы в RSelenium API).
Используя HtmlUnit, я создал довольно общий основной класс Java, который можно использовать для загрузки файла, щелкнув ссылку на веб-странице. Параметризация приложения выглядит следующим образом:
- URL-адрес страницы.
- Необязательная последовательность выражений XPath, позволяющая спускаться в любое количество вложенных фреймов, начиная со страницы верхнего уровня. Примечание. Я на самом деле разбираю это из аргумента URL, разбивая на
\s*>\s*
, что мне нравится как краткий синтаксис. Я использовал символ >
, потому что он недействителен в 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). Файл .class я скомпилировал в bin
, и я установил JAR-файлы HtmlUnit в свою структуру системных каталогов Cygwin, что, вероятно, немного необычно.
java com.bgoldst.DownloadFileByXPath
Очевидно, это командное слово и имя основного класса для выполнения.
"http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp > //iframe[@id='frame1'] > //iframe[@id='frameDoc']"
Это URL-адрес и выражения XPath фрейма. Ваша целевая ссылка вложена в два iframe, поэтому требуются два выражения XPath. Вы можете найти атрибуты id в исходном коде, просмотрев необработанный HTML-код или воспользовавшись инструментом веб-разработки (мне больше всего нравится Firebug).
"//a[contains(text(),'WVS_2000_Questionnaire_Root')]"
Наконец, это фактическое выражение XPath для целевой ссылки во внутреннем iframe.
Я опустил аргумент имени файла. Как видите, код правильно извлек имя файла из URL-адреса запроса.
Я понимаю, что это очень проблематично, чтобы загрузить файл, но для веб-скрапинга в целом я действительно думаю, что единственный надежный и жизнеспособный подход — это пройти все девять ярдов и использовать полный безголовый браузерный движок. Возможно, будет лучше полностью отделить задачу загрузки этих файлов от Rland и вместо этого реализовать всю систему очистки с помощью приложения Java, возможно, дополненного некоторыми сценариями оболочки для более гибкого внешнего интерфейса. Если вы не работаете с URL-адресами для загрузки, которые были разработаны для однократных HTTP-запросов без излишеств от таких клиентов, как curl, wget и R, использование R для парсинга веб-страниц, вероятно, не является хорошей идеей. Это мои два цента.
person
bgoldst
schedule
10.07.2016
RCurl
ответ, а затем люди, проводящие исследование мировых ценностей, снова изменят веб-сайт.. профессиональные вредности ;) - person Anthony Damico   schedule 29.09.2016