爬虫从入门到放弃——抓取前端渲染的页面

阅读: 评论:0

爬虫从入门到放弃——抓取前端渲染的页面

爬虫从入门到放弃——抓取前端渲染的页面

抓取前端渲染的页面

随着AJAX技术不断的普及,以及现在AngularJS这种Single-page application框架的出现,现在js渲染出的页面越来越多。对于爬虫来说,这种页面是比较讨厌的:仅仅提取HTML内容,往往无法拿到有效的信息。那么如何处理这种页面呢?总的来说有两种做法:

  1. 在抓取阶段,在爬虫中内置一个浏览器内核,执行js渲染页面后,再抓取。这方面对应的工具有Selenium、HtmlUnit或者PhantomJs。但是这些工具都存在一定的效率问题,同时也不是那么稳定。好处是编写规则同静态页面一样。
  2. 因为js渲染页面的数据也是从后端拿到,而且基本上都是AJAX获取,所以分析AJAX请求,找到对应数据的请求,也是比较可行的做法。而且相对于页面样式,这种接口变化可能性更小。缺点就是找到这个请求,并进行模拟,是一个相对困难的过程,也需要相对多的分析经验。

对比两种方式,我的观点是,对于一次性或者小规模的需求,用第一种方式省时省力。但是对于长期性的、大规模的需求,还是第二种会更靠谱一些。对于一些站点,甚至还有一些js混淆的技术,这个时候,第一种的方式基本是万能的,而第二种就会很复杂了。

对于第一种方法,webmagic-selenium就是这样的一个尝试,它定义了一个Downloader,在下载页面时,就是用浏览器内核进行渲染。selenium的配置比较复杂,而且跟平台和版本有关,没有太稳定的方案。我们先来介绍这个尝试吧。


  • 使用Selenium来抓取动态加载的页面

因为无论怎样动态加载,基础信息总归是包含在初始页面中得,所以我们可以用爬虫代码来模拟js代码,js读取页面元素值,我们也读取页面元素值;js发送ajax,我们就拼凑参数、发送ajax并解析返回的json。这样总归是能做的,但是比较麻烦,有没有比较省力的方法呢?比较好的方法大概是内嵌一个浏览器了。

Selenium是一个模拟浏览器,进行自动化测试的工具,它提供一组API可以与真实的浏览器内核交互。Selenium是跨语言的,有Java、C#、python等版本,并且支持多种浏览器,chrome、firefox以及IE都支持。

在Java项目中使用Selenium,需要做两件事:

  1. 在项目中引入Selenium的Java模块,以Maven为例:
<dependency><groupId>org.seleniumhq.selenium</groupId><artifactId>selenium-java</artifactId><version>2.33.0</version></dependency>
  1. 下载对应的driver,以chrome为例:

    下载后,需要将driver的位置写到Java的环境变量里,例如在mac下将其下载到了/Users/yihua/Downloads/chromedriver,则需要在程序里添加以下代码(当然在JVM参数里写-Dxxx=xxx也是可以的):
<!-- lang: java --> 
Properties().setProperty("webdriver.chrome.driver","/Users/yihua/Downloads/chromedriver");

Selenium的API挺简单的,核心是WebDriver,下面是动态渲染页面,并获取最终html的代码:

@Testpublic void testSelenium() {Properties().setProperty("webdriver.chrome.driver", "/Users/yihua/Downloads/chromedriver");WebDriver webDriver = new ChromeDriver();("/");WebElement webElement = webDriver.findElement(By.xpath("/html"));System.out.Attribute("outerHTML"));webDriver.close();}

值得注意的是,每次new ChromeDriver(),Selenium都会建立一个Chrome进程,并使用一个随机端口在Java中与chrome进程进行通信来交互。由此可见有两个问题:

  • 因此如果直接关闭Java程序,Chrome进程可能是无法关闭的。这里需要显示的调用webDriver.close()来关闭进程。
  • 创建进程的开销还是比较大的,尽量对webDriver进行复用会比较好。可惜根据官方的文档,webDriver不是线程安全的,所以我们需要建立一个webDriver池来保存它们。

最后说说效率问题。嵌入浏览器之后,不但要多花CPU去渲染页面,还要下载页面附加的资源。似乎单个webDriver中的静态资源是有缓存的,初始化之后,访问速度会加快。试用ChromeDriver加载了100次花瓣的首页(/),共耗时263秒,平均每个页面2.6秒。

/*** 花瓣网抽取器。<br>* 使用Selenium做页面动态渲染。<br>*/
public class HuabanProcessor implements PageProcessor {private Site site;@Overridepublic void process(Page page) {page.Html().links().regex("huaban\/.*").all());if (Url().toString().contains("pins")) {page.putField("img", Html().xpath("//div[@id='pin_img']/img/@src").toString());} else {ResultItems().setSkip(true);}}@Overridepublic Site getSite() {if (site == null) {site = ().setDomain("huaban").addStartUrl("/").setSleepTime(1000);}return site;}public static void main(String[] args) {ate(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(getClass());private int sleepTime = 0;private int poolSize = 1;private static final String DRIVER_PHANTOMJS = "phantomjs";/*** 新建** @param chromeDriverPath chromeDriverPath*/public SeleniumDownloader(String chromeDriverPath) {Properties().setProperty("webdriver.chrome.driver",chromeDriverPath);}/*** Constructor without any filed. Construct PhantomJS browser* * @author bob.li.0718@gmail*/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;}@Overridepublic Page download(Request request, Task task) {checkInit();WebDriver webDriver;try {webDriver = ();} catch (InterruptedException e) {logger.warn("interrupted", e);return null;}logger.info("downloading page " + Url());(Url());try {Thread.sleep(sleepTime);} catch (InterruptedException e) {e.printStackTrace();}WebDriver.Options manage = webDriver.manage();Site site = Site();if (Cookies() != null) {for (Map.Entry<String, String> cookieEntry : Cookies().entrySet()) {Cookie cookie = new Key(),Value());manage.addCookie(cookie);}}/** TODO You can add mouse event or other processes* * @author: bob.li.0718@gmail*/WebElement webElement = webDriver.findElement(By.xpath("/html"));String content = Attribute("outerHTML");Page page = new Page();page.setRawText(content);page.setHtml(new Html(content, Url()));page.setUrl(new Url()));page.setRequest(request);urnToPool(webDriver);return page;}private void checkInit() {if (webDriverPool == null) {synchronized (this) {webDriverPool = new WebDriverPool(poolSize);}}}@Overridepublic void setThread(int thread) {this.poolSize = thread;}@Overridepublic void close() throws IOException {webDriverPool.closeAll();}
}

  • 分析AJAX请求

这里我们以AngularJS中文社区。

(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=1&s=20复制到地址栏,重新请求一次。查看结果,看来我们找到了想要的。

有些时候,返回的类型不是json格式而是html格式的,这点我们之后再讲解。

(3) 编写程序

回想一下之前列表+目标页的例子,会发现我们这次的需求,跟之前是类似的,只不过换成了AJAX方式-AJAX方式的列表,AJAX方式的数据,而返回数据变成了JSON。那么,我们仍然可以用上次的方式,分为两种页面来进行编写:

1)数据列表:
在这个列表页,我们需要找到有效的信息,来帮助我们构建目标AJAX的URL。这里我们看到,这个_id应该就是我们想要的帖子的id,而帖子的详情请求,就是由一些固定URL加上这个id组成。所以在这一步,我们自己手动构造URL,并加入到待抓取队列中。这里我们使用JsonPath这种选择语言来选择数据(webmagic-extension包中提供了JsonPathSelector来支持它)。

if (Url().regex(LIST_URL).match()) {//这里我们使用JSONPATH这种选择语言来选择数据List<String> ids = new JsonPathSelector("$.data[*]._id").RawText());if (CollectionUtils.isNotEmpty(ids)) {for (String id : ids) {page.addTargetRequest("/"+id);}}}

2)目标数据
有了URL,实际上解析目标数据就非常简单了,因为JSON数据是完全结构化的,所以省去了我们分析页面,编写XPath的过程。这里我们依然使用JsonPath来获取标题和内容。

page.putField("title", new JsonPathSelector("$.data.title").RawText()));
page.putField("content", new JsonPathSelector("$.t").RawText()));

最后的代码如下:

public class AngularJSProcessor implements PageProcessor {private Site site = ();private static final String ARITICALE_URL = "angularjs\/api/article/\w+";private static final String LIST_URL = "angularjs\/api/article/latest.*";@Overridepublic void process(Page page) {if (Url().regex(LIST_URL).match()) {List<String> ids = new JsonPathSelector("$.data[*]._id").RawText());if (CollectionUtils.isNotEmpty(ids)) {for (String id : ids) {page.addTargetRequest("/" + id);}}} else {page.putField("title", new JsonPathSelector("$.data.title").RawText()));page.putField("content", new JsonPathSelector("$.t").RawText()));}}@Overridepublic Site getSite() {return site;}public static void main(String[] args) {ate(new AngularJSProcessor()).addUrl("=1&s=20").run();}
}

在这个例子中,我们分析了一个比较经典的动态页面的抓取过程。实际上,动态页面抓取,最大的区别在于:它提高了链接发现的难度。我们对比一下两种开发模式:

后端渲染的页面

下载辅助页面=>发现链接=>下载并分析目标HTML

前端渲染的页面

发现辅助数据=>构造链接=>下载并分析目标AJAX

对于不同的站点,这个辅助数据可能是在页面HTML中已经预先输出,也可能是通过AJAX去请求,甚至可能是多次数据请求的过程,但是这个模式基本是固定的。

但是这些数据请求的分析比起页面分析来说,仍然是要复杂得多,所以这其实是动态页面抓取的难点。所以,之前说的,如果js请求的结果也是Html,其实就只需要再构造一个http请求而已,把要请求的Url加入待查询的Url即可。

所以对于之前的例子,公告获取不到,怎么办呢?

查看源码后是这样的:

断言:是通过ajax来获取的。
然后我们查看请求的Url:


返回的是html啊,很简单,把请求的链接放进去,再Process一遍不就好了么?我们来看看url是啥:
.aspx?blogId=368840&postId=10401378&blogApp=hiram-zhang&blogUserGuid=79b817bc-bd91-4e5c-363f-08d49c352df3&_=1550567127429
触到了知识的盲区。
我????????
这id哪来的??
我????
我现在是风沙迷了眼了。。。。


算了等我弄明白再来搞吧,难搞哦。

public void process(Page page) {//判断链接是否符合.html格式page.putField("name",Html().xpath("//*[@id="author_profile_detail"]/a[1]/text()"));}public static void main(String[] args) {long startTime, endTime;System.out.println("开始爬取...");startTime = System.currentTimeMillis();ate(new MyProcessor2()).addUrl(".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+"条记录");}

本文发布于:2024-02-01 02:14:35,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170672487333116.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:爬虫   入门   页面
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23