网页抓取qq(Java抓取前端渲染的页面如何处理-pageapplication? )
优采云 发布时间: 2021-12-10 09:24网页抓取qq(Java抓取前端渲染的页面如何处理-pageapplication?
)
抓取前端渲染的页面
随着AJAX技术的不断普及和AngularJS等单页应用框架的出现,现在越来越多的页面由js渲染。对于爬虫来说,这种页面比较烦人:只提取HTML内容,往往无法获取有效信息。那么如何处理这种页面呢?一般来说有两种方法:html
在爬取阶段,爬虫内置浏览器内核,执行js渲染页面后,进行爬取。这方面对应的工具有Selenium、HtmlUnit或PhantomJs。但是,这些工具存在一定的效率问题,同时也不太稳定。优点是写入规则与静态页面相同。由于js渲染页面的数据也是从后端获取的,而且基本都是通过AJAX获取的,所以分析AJAX请求,找到对应数据的请求也是一种可行的方式。并且相对于页面样式,这种界面变化的可能性较小。缺点是找到这个请求并模拟它是一个比较困难的过程,需要比较多的分析经验。
比较两种方法,我个人的看法是,对于一次性或小规模的需求,第一种方法省时省力。但对于长期、*敏*感*词*的需求,第二种还是比较可靠的。对于某些站点,甚至还有一些 js 混淆技术。这时候第一种方法基本上是万能的,第二种方法会很复杂。前端
对于第一种方法,webmagic-selenium 就是这样的一种尝试。它定义了一个Downloader,它在下载页面时使用浏览器内核进行渲染。selenium的配置比较复杂,跟平台和版本有关。没有稳定的解决方案。我们先介绍一下这个尝试。爪哇
由于无论怎么动态加载,初始页面总是收录基本信息,我们可以用爬虫代码模拟js代码,js读取页面元素值,我们也读取页面元素值;js发送ajax,我们只是拼凑参数,发送ajax,解析返回的json。这总是可以的,但是比较麻烦。有没有更省力的方法?更好的方法可能是嵌入浏览器。Python
Selenium 是一种模拟浏览器进行自动化测试的工具。它提供了一组 API 来与真正的浏览器内核进行交互。Selenium 是跨语言的,有Java、C#、python 等版本,支持多种浏览器、chrome、firefox 和IE。angularjs
在Java项目中使用Selenium,需要做两件事:web
在项目中引入Selenium Java模块,以Maven为例:
org.seleniumhq.selenium
selenium-java
2.33.0
下载对应的驱动,以chrome为例:
下载后需要将驱动的位置写入Java环境变量中。比如mac下下载到/Users/yihua/Downloads/chromedriver,需要在程序中加入如下代码(虽然在JVM参数中写-Dxxx=xxx也是可以的):
System.getProperties().setProperty("webdriver.chrome.driver","/Users/yihua/Downloads/chromedriver");
Selenium的API很简单,核心是WebDriver,下面是动态渲染页面并获取最终html的代码:ajax
@Test
public void testSelenium() {
System.getProperties().setProperty("webdriver.chrome.driver", "/Users/yihua/Downloads/chromedriver");
WebDriver webDriver = new ChromeDriver();
webDriver.get("http://huaban.com/");
WebElement webElement = webDriver.findElement(By.xpath("/html"));
System.out.println(webElement.getAttribute("outerHTML"));
webDriver.close();
}
值得注意的是,每次 new ChromeDriver() 时,Selenium 都会创建一个 Chrome 进程,并使用一个随机端口与 Java 中的 chrome 进程进行通信进行交互。所以有两个问题:chrome
最后说一下效率问题。嵌入浏览器后,不仅需要更多的CPU来渲染页面,还要下载页面附带的资源。似乎缓存了单个 webDriver 中的静态资源。初始化后,访问速度会加快。尝试ChromeDriver加载花瓣首页100次,总共耗时263秒,平均每页2.6秒。苹果系统
/**
* 花瓣网抽取器。
* 使用Selenium作页面动态渲染。
*/
public class HuabanProcessor implements PageProcessor {
private Site site;
@Override
public void process(Page page) {
page.addTargetRequests(page.getHtml().links().regex("http://huaban\\.com/.*").all());
if (page.getUrl().toString().contains("pins")) {
page.putField("img", page.getHtml().xpath("//div[@id='pin_img']/img/@src").toString());
} else {
page.getResultItems().setSkip(true);
}
}
@Override
public Site getSite() {
if (site == null) {
site = Site.me().setDomain("huaban.com").addStartUrl("http://huaban.com/").setSleepTime(1000);
}
return site;
}
public static void main(String[] args) {
Spider.create(new HuabanProcessor()).thread(5)
.scheduler(new RedisScheduler("localhost"))
.pipeline(new FilePipeline("/data/webmagic/test/"))
.downloader(new SeleniumDownloader("/Users/yihua/Downloads/chromedriver"))
.run();
}
}
public class SeleniumDownloader implements Downloader, Closeable {
private volatile WebDriverPool webDriverPool;
private Logger logger = Logger.getLogger(getClass());
private int sleepTime = 0;
private int poolSize = 1;
private static final String DRIVER_PHANTOMJS = "phantomjs";
/**
* 新建
*
* @param chromeDriverPath chromeDriverPath
*/
public SeleniumDownloader(String chromeDriverPath) {
System.getProperties().setProperty("webdriver.chrome.driver",
chromeDriverPath);
}
/**
* Constructor without any filed. Construct PhantomJS browser
*
* @author bob.li.0718@gmail.com
*/
public SeleniumDownloader() {
// System.setProperty("phantomjs.binary.path",
// "/Users/Bingo/Downloads/phantomjs-1.9.7-macosx/bin/phantomjs");
}
/**
* set sleep time to wait until load success
*
* @param sleepTime sleepTime
* @return this
*/
public SeleniumDownloader setSleepTime(int sleepTime) {
this.sleepTime = sleepTime;
return this;
}
@Override
public Page download(Request request, Task task) {
checkInit();
WebDriver webDriver;
try {
webDriver = webDriverPool.get();
} catch (InterruptedException e) {
logger.warn("interrupted", e);
return null;
}
logger.info("downloading page " + request.getUrl());
webDriver.get(request.getUrl());
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
WebDriver.Options manage = webDriver.manage();
Site site = task.getSite();
if (site.getCookies() != null) {
for (Map.Entry cookieEntry : site.getCookies()
.entrySet()) {
Cookie cookie = new Cookie(cookieEntry.getKey(),
cookieEntry.getValue());
manage.addCookie(cookie);
}
}
/*
* TODO You can add mouse event or other processes
*
* @author: bob.li.0718@gmail.com
*/
WebElement webElement = webDriver.findElement(By.xpath("/html"));
String content = webElement.getAttribute("outerHTML");
Page page = new Page();
page.setRawText(content);
page.setHtml(new Html(content, request.getUrl()));
page.setUrl(new PlainText(request.getUrl()));
page.setRequest(request);
webDriverPool.returnToPool(webDriver);
return page;
}
private void checkInit() {
if (webDriverPool == null) {
synchronized (this) {
webDriverPool = new WebDriverPool(poolSize);
}
}
}
@Override
public void setThread(int thread) {
this.poolSize = thread;
}
@Override
public void close() throws IOException {
webDriverPool.closeAll();
}
}
这里我们以AngularJS中文社区为例。
json
(1) 如何判断前端渲染
判断页面是否被js渲染的方法比较简单。可以直接在浏览器中查看源码(Windows下Ctrl+U,Mac下command+alt+u)。如果找不到有效信息(Ctrl+F),基本可以确定是js渲染。
在这个例子中,如果源代码中找不到页面上的标题“有福计算机网络-前端攻城引擎”,则可以确定是js渲染,这个数据是通过AJAX获取的。
(2)分析请求
现在让我们进入最难的部分:找到这个数据请求。这一步可以帮助我们的工具,主要是在浏览器中查看网络请求的开发者工具。
以Chome为例。我们打开“开发者工具”(Windows下F12,Mac下command+alt+i),然后刷新页面(而且大部分都是下拉页面,总之所有你认为可能会触发新数据的操作)做),然后记得保持现场,一一分析请求。
这一步需要一点耐心,但也不是没有规律可循。首先可以帮助我们的是上面的分类过滤器(All、Document 等选项)。如果是普通的AJAX,会在XHR选项卡下显示,JSONP请求会在Scripts选项卡下。这是两种常见的数据类型。
然后可以根据数据的大小来判断,通常结果越大返回数据的接口。剩下的基本就是凭经验了。比如这里的“latest?p=1&s=20”一看就可疑……
对于可疑地址,您现在可以查看响应正文是什么。这在开发人员工具中并不清楚。让我们将 URL 复制到地址栏并再次请求它。看结果,好像找到了自己想要的。
有时,返回的类型不是json格式,而是html格式。我们稍后会解释这一点。
(3) 编程
回顾之前的列表+目标页面的例子,我们会发现我们这次的需求和之前的差不多,只不过我们替换了AJAX方法-AJAX方法列表,AJAX方法数据,返回的数据变成了JSON。那么,我们还是可以用最后一种方法,分成两页来写:
1)数据列表:
在这个列表页面上,我们需要找到有效的信息来帮助我们构建目标 AJAX URL。这里我们看到这个_id应该是我们想要的帖子的id,帖子详情的请求由一些固定的URL加上这个id组成。因此,在这一步中,我们自己手动构建了URL,并将其添加到待抓取的队列中。这里我们使用JsonPath,一种选择数据的语言(webmagic-extension包提供了JsonPathSelector来支持)。
if (page.getUrl().regex(LIST_URL).match()) {
//这里咱们使用JSONPATH这种选择语言来选择数据
List ids = new JsonPathSelector("$.data[*]._id").selectList(page.getRawText());
if (CollectionUtils.isNotEmpty(ids)) {
for (String id : ids) {
page.addTargetRequest("http://angularjs.cn/api/article/"+id);
}
}
}
2)目标数据
有了URL,解析目标数据其实很简单。由于JSON数据完全结构化,因此省略了分析页面和编写XPath的过程。这里我们仍然使用 JsonPath 来获取标题和内容。
page.putField("title", new JsonPathSelector("$.data.title").select(page.getRawText()));
page.putField("content", new JsonPathSelector("$.data.content").select(page.getRawText()));
最终代码如下:
public class AngularJSProcessor implements PageProcessor {
private Site site = Site.me();
private static final String ARITICALE_URL = "http://angularjs\\.cn/api/article/\\w+";
private static final String LIST_URL = "http://angularjs\\.cn/api/article/latest.*";
@Override
public void process(Page page) {
if (page.getUrl().regex(LIST_URL).match()) {
List ids = new JsonPathSelector("$.data[*]._id").selectList(page.getRawText());
if (CollectionUtils.isNotEmpty(ids)) {
for (String id : ids) {
page.addTargetRequest("http://angularjs.cn/api/article/" + id);
}
}
} else {
page.putField("title", new JsonPathSelector("$.data.title").select(page.getRawText()));
page.putField("content", new JsonPathSelector("$.data.content").select(page.getRawText()));
}
}
@Override
public Site getSite() {
return site;
}
public static void main(String[] args) {
Spider.create(new AngularJSProcessor()).addUrl("http://angularjs.cn/api/article/latest?p=1&s=20").run();
}
}
在这个例子中,我们分析了一个比较经典的动态页面的爬取过程。其实动态页面爬取最大的区别就是增加了链接发现的难度。让我们比较一下两种开发模式:
后台渲染页面
下载辅助页面 => 发现连接 => 下载并分析目标 HTML
前端渲染页面
发现辅助数据 => 构建连接 => 下载并分析目标 AJAX
对于不同的站点,这种辅助数据大部分是在页面的HTML中预先输出的,大部分是通过AJAX请求的,大部分是重复数据请求的过程,但是这种模式基本是固定的。
但是这些数据请求的分析还是比页面分析复杂的多,所以这其实就是动态页面爬取的难点。因此,如前所述,如果js请求的结果也是Html,其实只需要再构造一个http请求,将请求的Url添加到要查询的Url中即可。
那么对于前面的例子,如果公告不可用,我该怎么办?
查看源代码后是这样的:
断言:是通过ajax获取的。
然后我们检查请求的Url:
返回的是html。这很简单。将请求的连接放入并再次处理。我们来看看url是什么:
触及知识盲区。
一世?? ? ? ? ? ? ?
这个id是哪里来的??
一世?? ? ?
我现在迷失在风和沙中。. . .
算了,等我弄明白再说。它很难。
public void process(Page page) {
//判断连接是否符合http://www.cnblogs.com/任意个数字字母-/p/7个数字.html格式
page.putField("name",page.getHtml().xpath("//*[@id=\"author_profile_detail\"]/a[1]/text()"));
}
public static void main(String[] args) {
long startTime, endTime;
System.out.println("开始爬取...");
startTime = System.currentTimeMillis();
Spider.create(new MyProcessor2()).addUrl("https://www.cnblogs.com/mvc/blog/BlogPostInfo.aspx?blogId=368840&postId=10401378&blogApp=hiram-zhang&blogUserGuid=79b817bc-bd91-4e5c-363f-08d49c352df3&_=1550567127429").addPipeline(new MyPipeline()).thread(5).run();
endTime = System.currentTimeMillis();
System.out.println("爬取结束,耗时约" + ((endTime - startTime) / 1000) + "秒,抓取了"+count+"条记录");
}