我有一个超过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天。
有没有办法改善它?
答案 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> "Президент РФ Дмитрий Медведев назначил нового уполномоченного по прав...
我相信其他人会有你的想法(在命令行中使用带有parallel
或wget
的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
代码中添加进度条。