我必须从超过290K的网页中获取plantext。有没有办法提高速度?

时间:2016-08-27 10:30:42

标签: r parallel-processing html-parsing

我有一个超过290K网址的向量,可以在新闻门户网站上找到文章。 这是一个示例:

> head(output_df)
          X1          X2         X3         X4
1  0.5529417 -0.93859275  2.0900276 -2.4023800
2  0.9751090  0.13357075         NA         NA
3  0.6753835  0.07018647  0.8529300 -0.9844643
4  1.6405939  0.96133195         NA         NA
5  0.3378821 -0.44612782 -0.8176745  0.2759752
6 -0.8910678 -0.37928353         NA         NA

我用一个代码来下载计划文本:

tempUrls <- c("https://lenta.ru/news/2009/12/31/kids/",
                  "https://lenta.ru/news/2009/12/31/silvio/",
                  "https://lenta.ru/news/2009/12/31/postpone/",
                  "https://lenta.ru/news/2009/12/31/boeviks/",
                  "https://lenta.ru/news/2010/01/01/celebrate/",
                  "https://lenta.ru/news/2010/01/01/aes/")

这6个链接有一个system.time列表:

GetPageText <- function(address) {

        webpage <- getURL(address, followLocation = TRUE, .opts = list(timeout = 10))
        pagetree <- htmlTreeParse(webpage, error = function(...) {}, useInternalNodes = TRUE, encoding = "UTF-8")
        node <- getNodeSet(pagetree, "//div[@itemprop='articleBody']/..//p")
        plantext <- xmlSApply(node, xmlValue)
        plantext <- paste(plantext, collapse = "")
        node <- getNodeSet(pagetree, "//title")
        title <- xmlSApply(node, xmlValue)

        return(list(plantext = plantext, title = title))
}

DownloadPlanText <- function() {

        tempUrls <- c("https://lenta.ru/news/2009/12/31/kids/",
                      "https://lenta.ru/news/2009/12/31/silvio/",
                      "https://lenta.ru/news/2009/12/31/postpone/",
                      "https://lenta.ru/news/2009/12/31/boeviks/",
                      "https://lenta.ru/news/2010/01/01/celebrate/",
                      "https://lenta.ru/news/2010/01/01/aes/")

        for (i in 1:length(tempUrls)) {
                print(system.time(GetPageText(tempUrls[i])))
        }
}

这意味着从1个链接下载计划文本需要3秒。对于290K链接,需要14500分钟或241小时或10天。

有没有办法改善它?

1 个答案:

答案 0 :(得分:3)

有几种方法可以做到这一点,但我强烈建议您保留源页面的副本,因为您可能需要返回并刮掉,如果您忘记了某些内容,再次敲打网站是非常粗鲁的。

执行此归档的最佳方法之一是创建WARC文件。我们可以使用wget来做到这一点。您可以使用自制软件(wget)在macOS上安装brew install wget

创建一个包含要刮取的URL的文件,每行一个URL。例如,这是lenta.urls

的内容
https://lenta.ru/news/2009/12/31/kids/
https://lenta.ru/news/2009/12/31/silvio/
https://lenta.ru/news/2009/12/31/postpone/
https://lenta.ru/news/2009/12/31/boeviks/
https://lenta.ru/news/2010/01/01/celebrate/
https://lenta.ru/news/2010/01/01/aes/

在终端上,创建一个目录来保存输出并将其作为工作目录,因为wget非确定性地不删除临时文件(这非常烦人)。在这个新目录中,再次在terminal-do:

wget --warc-file=lenta -i lenta.urls

这将以您的互联网连接速度进行,并检索该文件中所有页面的内容。它不会镜像(因此它不会获取图像等,只是您想要的主页的内容)。

由于我提到的非确定性错误,此目录中可能存在许多 index.html[.###]个文件。在删除它们之前,请对lenta.warc.gz进行备份,因为您花了很多时间来获取它,并且还让那些运行该站点的人烦恼并且您不想再次执行此操作。说真的,将其复制到一个单独的驱动器/文件/等。一旦你做了这个备份(你做了备份,对吗?)你可以而且应该删除那些index.html[.###]文件。

我们现在需要阅读此文件并提取内容。然而,R创建者似乎无法使gz文件连接与跨平台寻求一致工作,即使有十几个C / C ++库可以很好地完成它,所以你必须解压缩lenta.warc.gz个文件(双击它或在终端中执行gunzip lenta.warc.gz。)

既然你有数据可以使用,这里有一些辅助函数&amp;我们需要的图书馆:

library(stringi)
library(purrr)
library(rvest)
library(dplyr)

#' get the number of records in a warc request
warc_request_record_count <- function(warc_fle) {

  archive <- file(warc_fle, open="r")

  rec_count <- 0

  while (length(line <- readLines(archive, n=1, warn=FALSE)) > 0) {
    if (grepl("^WARC-Type: request", line)) {
      rec_count <- rec_count + 1
    }
  }

  close(archive)

  rec_count
}

注意:需要上面的函数,因为分配我们正在构建的list的大小以保持具有已知值的记录与动态增长它的方式更有效,特别是如果你有那些200K +站点刮掉。

#' create a warc record index of the responses so we can
#' seek right to them and slurp them up
warc_response_index <- function(warc_file,
                                record_count=warc_request_record_count(warc_file)) {

  records <- vector("list", record_count)
  archive <- file(warc_file, open="r")

  idx <- 0
  record <- list(url=NULL, pos=NULL, length=NULL)
  in_request <- FALSE

  while (length(line <- readLines(archive, n=1, warn=FALSE)) > 0) {

    if (grepl("^WARC-Type:", line)) {
      if (grepl("response", line)) {
        if (idx > 0) {
          records[[idx]] <- record
          record <- list(url=NULL, pos=NULL, length=NULL)
        }
        in_request <- TRUE
        idx <- idx + 1
      } else {
        in_request <- FALSE
      }
    }

    if (in_request & grepl("^WARC-Target-URI:", line)) {
      record$url <- stri_match_first_regex(line, "^WARC-Target-URI: (.*)")[,2]
    }

    if (in_request & grepl("^Content-Length:", line)) {
      record$length <- as.numeric(stri_match_first_regex(line, "Content-Length: ([[:digit:]]+)")[,2])
      record$pos <- as.numeric(seek(archive, NA))
    }

  }

  close(archive)

  records[[idx]] <- record

  records

}

注意:该功能提供网站响应的位置,以便我们可以超快速地获取它们。

#' retrieve an individual response record
get_warc_response <- function(warc_file, pos, length) {

  archive <- file(warc_file, open="r")

  seek(archive, pos)
  record <- readChar(archive, length)

  record <- stri_split_fixed(record, "\r\n\r\n", 2)[[1]]
  names(record) <- c("header", "page")

  close(archive)

  as.list(record)

}

现在,为了诋毁所有这些页面,就是这么简单:

warc_file <- "~/data/lenta.warc"

responses <- warc_response_index(warc_file)

嗯,这只是获取WARC文件中所有页面的位置。以下是如何在一个漂亮,整洁的data.frame中获取所需的内容:

map_df(responses, function(r) {

  resp <- get_warc_response(warc_file, r$pos, r$length)

  # the wget WARC response is sticking a numeric value as the first
  # line for URLs from this site (and it's not a byte-order-mark). so,
  # we need to strip that off before reading in the actual response.
  # i'm pretty sure it's the site injecting this and not wget since i
  # don't see it on other test URLs I ran through this for testing.

  pg <- read_html(stri_split_fixed(resp$page, "\r\n", 2)[[1]][2])

  html_nodes(pg, xpath=".//div[@itemprop='articleBody']/..//p") %>%
    html_text() %>%
    paste0(collapse="") -> plantext

  title <- html_text(html_nodes(pg, xpath=".//head/title"))

  data.frame(url=r$url, title, plantext, stringsAsFactors=FALSE)

}) -> df

而且,我们可以看到它是否有效:

dplyr::glimpse(df)
## Observations: 6
## Variables: 3
## $ url      <chr> "https://lenta.ru/news/2009/12/31/kids/", "https://lenta.ru/news/2009/...
## $ title    <chr> "Новым детским омбудсменом стал телеведущий Павел Астахов: Россия: Len...
## $ plantext <chr> "Президент РФ Дмитрий Медведев назначил нового уполномоченного по прав...

我相信其他人会有你的想法(在命令行中使用带有parallelwget的GNU curl或使用lapply的并行版本您现有的代码)但此过程最终对网站提供商更友好,并在本地保留内容的副本以供进一步处理。此外,它是一种用于网络档案的ISO标准格式,其中有许多工具可用于处理(很快也会出现在R中)。

使用R进行文件搜索/啜饮这样很糟糕但是我使用WARC文件的包还没有准备好。它是C ++支持的,所以它更快/更高效,但是为了这个答案添加那么多内联C ++代码超出了SO答案的范围。

即使我已经把这个方法放在这里,我还是将URL分成几块并分批处理它们以便对网站很好,并避免在你的连接中断的情况下重新抓取这个。

Astute wget ters会问为什么我这里没有使用cdx选项,这主要是为了避免复杂性,而且对于实际的数据处理来说也是有用的,因为R代码必须寻求无论如何要记录。使用cdx选项(执行man wget查看我所指的内容)可以重新启动中断的WARC擦除,但是你必须小心处理它,所以我只是为了简单起见,我避免了这些细节。

对于您拥有的网站数量,请查看progress_estimated()中的dplyr功能,并考虑在map_df代码中添加进度条。