随着AJAX技术不断的普及,以及现在AngularJS这种Single-page application框架的出现,现在js渲染出的页面越来越多。对于爬虫来说,这种页面是比较讨厌的:仅仅提取HTML内容,往往无法拿到有效的信息。那么如何处理这种页面呢?总的来说有两种做法:
对比两种方式,我的观点是,对于一次性或者小规模的需求,用第一种方式省时省力。但是对于长期性的、大规模的需求,还是第二种会更靠谱一些。对于一些站点,甚至还有一些js混淆的技术,这个时候,第一种的方式基本是万能的,而第二种就会很复杂了。
对于第一种方法,webmagic-selenium就是这样的一个尝试,它定义了一个Downloader,在下载页面时,就是用浏览器内核进行渲染。selenium的配置比较复杂,而且跟平台和版本有关,没有太稳定的方案。我们先来介绍这个尝试吧。
因为无论怎样动态加载,基础信息总归是包含在初始页面中得,所以我们可以用爬虫代码来模拟js代码,js读取页面元素值,我们也读取页面元素值;js发送ajax,我们就拼凑参数、发送ajax并解析返回的json。这样总归是能做的,但是比较麻烦,有没有比较省力的方法呢?比较好的方法大概是内嵌一个浏览器了。
Selenium是一个模拟浏览器,进行自动化测试的工具,它提供一组API可以与真实的浏览器内核交互。Selenium是跨语言的,有Java、C#、python等版本,并且支持多种浏览器,chrome、firefox以及IE都支持。
在Java项目中使用Selenium,需要做两件事:
<dependency><groupId>org.seleniumhq.selenium</groupId><artifactId>selenium-java</artifactId><version>2.33.0</version></dependency>
<!-- 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进程进行通信来交互。由此可见有两个问题:
最后说说效率问题。嵌入浏览器之后,不但要多花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();}
}
这里我们以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 条评论) |