爬虫学习心得
因为公司最近有个项目关于爬虫的,但是我之前对没做过爬虫,所以也没有什么经验,所以找个时间自己去网上学习了一下。
首先说一下目前我了解到的几个爬虫技术:
- httpclient 优点:速度快。缺点,无法加载动态js,使用时主要通过调用对方接口爬取数据,可支持代理。https需要额外的代码。
- htmlunit 优点:可以模拟浏览器,动态加载js,可支持代理,支持https,可以直接调用接口爬取数据。缺点:相比httpclient 速度慢(无浏览器无截图功能),相比selenium 部分功能不完全,比如不支持页面滚动爬取数据(目前是没找到这个的)。
- selenium 优点:完全兼容js(模拟人的行为),可支持代理,支持https。缺点,速度慢。
- jsoup 主要用于解析网页,与上面的几种技术侧重点不同,可搭配httpclient 或 htmlunit 一起使用。
简单的例子
先写个简单的例子,用jsop解析一个一个div。
首先引入jsoup的依赖
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>
我们自己建一个index页面,里面放一个div。
<div id="test-div"><p>测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本</p></div>
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
public class mytest {
public static void main(String[] args) {
Connection conn = Jsoup.connect("http://localhost:8084/test.html");
Document doc = conn.get();
// 通过id爬取
Element element = doc.getElementById("test-div");
System.out.println("爬取到的文本内容:" + element.text());
// 直接无脑爬取整个网页的文本
// Element doc = conn.get();
// System.out.println("爬取到的文本内容:" + doc.text());
}
}
简单的定向爬取
当然上面只是一个很简单的例子,但是一般爬取的时候还是需要自己做些处理的,因为有些文本根本就不是我想要的,如果把整个网页的文本爬取下来,肯定不符合需要。
再来个简单的例子,比如这样,只读取表格的内容,其他的内容不做读取,并且确定每一行的内容,这样如果后面要做excel导出更方便处理了。
<div id="test-div">
<p>测试文本测试文本测试文本测试文本测试文本测试文本测试文本测试文本</p>
<table border="1">
<tr>
<th>Heading</th>
<th>Another Heading</th>
</tr>
<tr>
<td>row 1, cell 1</td>
<td>row 1, cell 2</td>
</tr>
<tr>
<td>row 2, cell 1</td>
<td>row 2, cell 2</td>
</tr>
</table>
</div>
那么怎么处理呢?
Connection conn = Jsoup.connect("http://localhost:8084/test.html");
Document doc = conn.get();
//通过选择器拿到所有的行
Elements trs = doc.getElementById("test-div").select("table tr");
for (int i = 0; i < trs.size(); i++) {
Elements ths = trs.get(i).select("th");
for (int i2 = 0; i2 < ths.size(); i2++) {
System.out.println("爬取到的标题文本内容:" + ths.get(i2).text());
}
Elements tds = trs.get(i).select("td");
for (int i3 = 0; i3 < tds.size(); i3++) {
System.out.println("爬取到的表格第" + i + "行,第" + (i3 + 1) + "列文本内容:" + tds.get(i3).text());
}
}
最后输出结果:
类似的,针对不同的网页,想要爬取对应的不同的内容都是需要自己处理的,有必要的话有些还需要用上正则表达式。
思考
我参考了一下网上的爬虫工具,简单点的就是自己去配置要抓取的url、脚本之类的,这种太麻烦了。高端一点就是可以通过录屏或是智能识别或是用户通过鼠标点击加流程图的方式来实现爬取数据,这种可视化的爬虫工具其实更受用户喜欢,因为操作更简单更方便一点。
我仔细想了一下,其实这类可视化爬虫工具的核心就是通过用户使用鼠标点击目标网站的内容去确定用户的爬取目标,为了防止识别不准确,这类工具还允许用户手动更改爬取配置。那么js中也是有办法可以通过坐标获取到元素,如下:
var el = document.elementFromPoint(x, y);
但是如果我想在我的测试页面中打开目标页面,以方便用户点击目标区域进行抓取,那我还得想办法让目标页面在我们的测试页面中展示出来,这里我打算使用iframe,但是经过测试发现,如果想通过上面的方法直接拿取到iframe里面子页面的元素,那是不可能,因为无论怎么获取,都只能拿到iframe本身,那么如果要抓取iframe中的目标元素,需要用到以下方法:
var el = document.elementFromPoint(x, y);
if (el instanceof HTMLIFrameElement) {
// 去iframe中拿元素
el = el.contentWindow.document.elementFromPoint(x, y);
}
if (!el) {
alert("未获取到元素");
return; // 当前位置没有元素
}
下面是我的一个例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<style>
.showWindow {
position: fixed;
top: 0;
width: 100%;
height: 100%;
}
.myiframe {
width: 100%;
height: 500px;
border: black 1px solid;
}
</style>
</head>
<body>
<div id="fir">
<!-- 这里为什么要加个div遮罩层呢,就是因为不加遮罩,我点击抓取元素按钮的时候它就直接给我抓取到按钮本身了 -->
<div class="showWindow" id="myWindow" onclick="getHtml()">
</div>
<iframe id="myiframe" frameborder=0 scrolling=auto src="" class="myiframe"></iframe>
<p>【请输入网址】:
<input id="webUrl" name="webUrl" style="height: 50px;width: 100px">
<button onclick="openUrl()" id="openUrl" style="height: 50px;width: 100px">打开网页</button>
<button onclick="choose()" id="reptile" style="height: 50px;width: 100px">选取元素</button>
</div>
</body>
<script>
var x = null, y = null;
function track_mouse(event) {
x = event.clientX;
y = event.clientY;
}
window.onmousemove = track_mouse;
function getHtml() {
$("#myWindow").hide();
var el = document.elementFromPoint(x, y);
if (el instanceof HTMLIFrameElement) {
el = el.contentWindow.document.elementFromPoint(x, y);
}
if (!el) {
alert("当前位置没有元素");
return; // 当前位置没有元素
}
console.log(el);
}
function openUrl() {
var url = $("#webUrl").val();
var myiframe = $("#myiframe");
myiframe.show();
myiframe.prop("src", url);
}
function choose() {
$("#myWindow").show();
}
window.onload = function () {
$("#myWindow").hide();
$("#myiframe").hide();
}
</script>
</html>
首先,不要嫌弃我的页面丑,虽然我也觉得丑。使用方式就是输入网址,点击打开网页按钮,我的iframe里就会把网页加载出来了,然后我再点击选取元素按钮,去页面上点击一下那个div就能获取到这个元素了。
然后打开开发者工具看看结果如何。
可以看到结果是对的,成功拿到了id为test-div的元素。
跨域问题
这个时候用csdn试试看,结果发现点击的时候获取不到,看看日志发现是跨域问题。
那么这个时候怎么办呢?其实很简单,用代理服务就可以解决跨域问题了,本来爬虫工具很多都需要代理,毕竟有些网站会有反爬机制,如果发现你一直去薅羊毛爬数据,会直接把你ip给拉入黑名单,这个时候用代理就能解决了。而我这里用代理正好也能解决问题,那么代理服务怎么实现呢,如果是Java开发的,推荐使用netty,别傻乎乎的用原生的bio和nio去写,如果是用nodejs推荐使用express + http-proxy-middleware,还是挺不错的。
所以,我这里选择nginx代理,哈哈哈,因为我懒得再去写一个例子,太费时间,偷个懒。但是真正使用的时候肯定不能用nginx的,毕竟它是通过配置去驱动代理的,我怎么配置它就怎么转发,但是我总不能每去爬取一个网站就去改一次配置吧,那也太傻了。
第一步:随便百度一点东西,比如代理,然后把地址复制下来。
第二步:改一下nginx.conf的配置,注意一点,我的测试网页和目标必须在同一个域中。
第三步:把nginx启动来,用代理地址打开试试看。ok,代理成功。
第四步:用我的测试页面再试试看是否能成功跨域,注意这里必须用我代理的地址。
第五步:验证,依然是点击选取元素按钮,去网页上面随便选择一个元素,然后发现,诶?成功了。
分页加载
ok,上面经过我的努力成功实现了用户可以通过鼠标点击的方式获取页面元素了。那么再加上jsoup去解析页面,一个雏形就有了。
但是,其实爬取有些不光就一页数据就给爬完了,有时候是滚动加载,或者分页这种,那么就这么通过jsoup去爬取可能就很难。毕竟用户去点击下一页,我就算拿到了下一页按钮的对象信息,用jsoup实在不知道改怎么去解析。
所以这里就要用到htmlunit或者selenium了,因为它们可以直接模拟浏览器上的操作。
htmlunit
先说htmlunit,导入依赖。
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.23</version>
</dependency>
然后去模拟页面上点击下一页的动作
public String test() throws Exception{
WebClient webclient = new WebClient();
//ssl认证
//webclient.getOptions().setUseInsecureSSL(true);
//排除某些页面上本身js的错误,免得htmlunit报错
webclient.getOptions().setThrowExceptionOnScriptError(false);
webclient.getOptions().setThrowExceptionOnFailingStatusCode(false);
//不加载css
webclient.getOptions().setCssEnabled(false);
// 加载js及执行
webclient.getOptions().setJavaScriptEnabled(true);
// 设置一下超时时间
webClient.getOptions().setTimeout(10 * 1000);
//打开网址
HtmlPage htmlpage = webclient.getPage("http://www.test.com/index.html");
// 获取下一页的按钮
DomElement nextPage = page.getElementById("next-page");
// 发送请求,获取返回后的网页
final HtmlPage page = nextPage.click();
//获取网页的文本信息
String result = page.asText();
System.out.println(result);
//获取网页源码
//String result = page.asXml();
//System.out.println(result);
webclient.close();
return result;
}
Selenium
首先需要下载一个驱动包,当然不止一种浏览器,FireFox、Chrome、IE、Opera、Edge这些都支持,我这里就拿谷歌做例子。
下载驱动,放到我的指定位置。谷歌驱动下载地址:http://chromedriver.storage.googleapis.com/index.html
导入依赖
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
public String test() throws Exception{
//设置驱动
System.setProperty("webdriver.chrome.driver","D:\\chromedriver.exe");
//创建驱动
WebDriver driver=new ChromeDriver();
//与将要爬取的网站建立连接
driver.get("http://www.test.com/index.html");
WebElement nextPage = driver.findElement(By.id("next-page"));
// 点击下一页
nextPage.click();
// 隐式等待,等待5秒加载时间
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
//根据页面上指定的文本
System.out.println(driver.findElement(By.id("mytest-21")).getText());
//关闭网页
driver.close();
}
滚动加载
那么对于滚动加载,我了解到的htmlunit通过
htmlunit
方法一:通过抓包工具或者直接在浏览器使用开发者工具拿到接口信息,直接解析接口通过调用接口去拿到结果。
// 1.创建client对象
WebClient client = new WebClient();
// 2.创建WebRequest
List<NameValuePair> para = Arrays.asList(new NameValuePair("page","2"),new NameValuePair("pageSize","20"));
WebRequest request = new WebRequest(new URL("http://localhost:8084/api/getList"), HttpMethod.GET);
request.setCharset(Charset.forName("utf-8"));
request.setRequestParameters(para);
// 3.执行请求
Page page = client.getPage(request);
// 4.获得响应
WebResponse response = page.getWebResponse();
// 5.获得响应正文
String result = response.getContentAsString(Charset.defaultCharset().forName("utf-8"));
System.out.println(result);
// 6.关闭
client.close();
方法二:上面这种方式不行,就直接模拟页面滚动。
//...前面步骤省略
HtmlPage htmlPage = webClient.getPage("http://www.test.com/index.html");
// 直接执行js脚本
htmlPage.executeJavaScript("window.scrollTo(0,document.body.scrollHeight)");
Selenium
方法一:模拟键盘
//...前面步骤省略,还是用的谷歌驱动
Actions actions = new Actions(driver);
//actions.sendKeys(Keys.DOWN).perform();方向键:下
//actions.sendKeys(Keys.PAGE_DOWN).perform();pageDown键
actions.sendKeys(Keys.END).perform();//end键
方法二:模拟页面滚动。
//...前面步骤省略,还是用的谷歌驱动
// 直接执行js脚本
((JavascriptExecutor) driver).executeScript("window.scrollTo(0,document.body.scrollHeight)");
总结
上面只是我学爬虫过程中的一些思考,模拟了一下网上那些爬虫工具,但是他们是怎么做的我也不知道。
我做的测试毕竟很浅,真正的爬虫需要考虑的东西很多,例如:模拟登录(其中包含有验证码的情况下该怎么处理),http和https,数据清洗(这个贼麻烦),数据分析,丰富的前端知识,爬虫技术(selenium这类),抓包工具(fiddler这类),多线程,正则表达式,文件读写等等还有很多其他方面。这些都需要去了解的,只能一步一步去学习。