网页抓取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+"条记录");

}

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线