现状:
目前网络上充斥着越来越多的网页数据,包含海量的数据,但是很多时候,不管是出于对产品需求还是数据分析的需要,我们需要从这些网站上搜索一些相关的、有价值的数据,进行分析并提炼出符合产品和数据的内容。
互联网早期,公司内部都设有很多的‘网站编辑’岗位,负责内容的整理和发布,纵然是高级动物人类,也只有两只手,无法通过复制、粘贴手工去维护,所以我们需要一种可以自动的进入网页提炼内容的程序技术,这就是‘爬虫’,网络爬虫工程师又被亲切的称之为‘虫师’。
网络爬虫概述
网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
从功能上来讲,爬虫一般分为三个阶段:
数据采集 (网络请求模块)
处理 (爬取流程控制模块)
储存 (内容分析提取模块)
传统爬虫从一个或若干初始网页的URL开始,获得初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。
聚焦爬虫的工作流程较为复杂,需要根据一定的网页分析算法过滤与主题无关的链接,保留有用的链接并将其放入等待抓取的URL队列。然后,它将根据一定的搜索策略从队列中选择下一步要抓取的网页URL,并重复上述过程,直到达到系统的某一条件时停止。
另外,所有被爬虫抓取的网页将会被系统存贮,进行一定的分析、过滤,并建立索引,以便之后的查询和检索;对于聚焦爬虫来说,这一过程所得到的分析结果还可能对以后的抓取过程给出反馈和指导。
网络爬虫原理
网络爬虫原理:
Web网络爬虫系统的功能是下载网页数据,为搜索引擎系统提供数据来源。很多大型的网络搜索引擎系统都被称为基于 Web数据采集的搜索引擎系统,比如 Google、Baidu。由此可见Web 网络爬虫系统在搜索引擎中的重要性。网页中除了包含供用户阅读的文字信息外,还包含一些超链接信息。Web网络爬虫系统正是通过网页中的超连接信息不断获得网络上的其它网页。正是因为这种采集过程像一个爬虫或者蜘蛛在网络上漫游,所以它才被称为网络爬虫系统或者网络蜘蛛系统,在英文中称为Spider或者Crawler。
网络爬虫工作原理:
在网络爬虫的系统框架中,主过程由控制器,解析器,资源库三部分组成。
控制器:
控制器是网络爬虫的中央控制器,它主要是负责根据系统传过来的URL链接,分配一线程,然后启动线程调用爬虫爬取网页的过程。
解析器:
解析器是负责网络爬虫的主要部分,其负责的工作主要有:下载网页的功能,对网页的文本进行处理,如过滤功能,抽取特殊HTML标签的功能,分析数据功能。
资源库:
主要是用来存储网页中下载下来的数据记录的容器,并提供生成索引的目标源。中大型的数据库产品有:Oracle、Sql Server等。
网络爬虫的基本工作流程如下:
1.首先选取一部分精心挑选的种子URL;
2.将这些URL放入待抓取URL队列;
3.从待抓取URL队列中取出待抓取在URL,解析DNS,并且得到主机的ip,并将URL对应的网页下载下来,存储进已下载网页库中。此外,将这些URL放进已抓取URL队列;
4.分析已抓取URL队列中的URL,分析其中的其他URL,并且将URL放入待抓取URL队列,从而进入下一个循环。
爬虫后台面临的问题
1:交互问题
有些网页往往需要和用户进行一些交互,进而才能走到下一步,比如输入一个验证码,拖动一个滑块,选几个汉字。网站之所以这么做,很多时候都是为了验证访问者到底是人还是机器。而爬虫程序遇到这种情况很难处理,传统的简单图片验证码可以通过图形处理算法读出内容,但是随着各种各样,花样百出的验证码越来越多(火车票验证码),这个问题就越来越严重。
2:JavaScript解析问题
JavaScript可以动态生成DOM。目前大多数网页属于动态网页(内容由JavaScript动态填充),尤其是在移动端,SPA/PWA应用越来越流行,网页中大多数有用的数据都是通过ajax/fetch动态获取后然后再由js填充到网页dom树中,单纯的html静态页面中有用的数据很少。目前主要应对的方案就是对于js ajax/fetch请求直接请求ajax/fetch的url ,但是还有一些ajax的请求参数会依赖一段JavaScript动态生成,比如一个请求签名,再比如用户登陆时对密码的加密等等,如果一昧的去用后台脚本去干JavaScript本来做的事,这就要清楚的理解原网页代码逻辑,而这不仅非常麻烦,而且会使你的爬取代码异常庞大臃肿,但是,更致命的是,有些JavaScript可以做的事爬虫程序是很难甚至是不能模仿的,比如有些网站使用拖动滑块到某个位置的验证码机制,这就很难再爬虫中去模仿。
其实,总结一些,这些弊端归根结底,是因为爬虫程序并非是浏览器,没有JavaScript解析引擎所致。针对这个问题,目前主要的应对策略就是在爬虫中引入JavaScript引擎,如PhantomJS,但是又有着明显的弊端,如服务器同时有多个爬取任务时,资源占用太大。还有就是,这些无窗口的JavaScript引擎很多时候使用起来并不能像在浏览器环境中一样,页面内部发生跳转时,会导致流程很难控制。
3:IP限制问题
这是目前对后台爬虫中最致命的。网站的防火墙会对某个固定ip在某段时间内请求的次数做限制,如果没有超过上线则正常返回数据,超过了,则拒绝请求,如qq 邮箱。值得说明的是,ip限制有时并非是专门为了针对爬虫的,而大多数时候是出于网站安全原因针对DOS攻击的防御措施。后台爬取时机器和ip有限,很容易达到上线而导致请求被拒绝。目前主要的应对方案是使用代理,这样一来ip的数量就会多一些,但代理ip依然有限,对于这个问题,根本不可能彻底解决。
JAVA网络爬虫入门示例
需求:java技术爬取各大网站的超链接数据
技术:Java、jdk1.8、maven、HttpClient、HttpCore
1:新建maven project工程,如图
2:导入所需jar包httpclient
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
3:增加测试类
import java.io.BufferedReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 提取新浪页面上的链接,并生成到本地目录
*/
public class WormDemo {
public static void main(String[] args) {
URL url = null;
URLConnection urlconn = null;
BufferedReader br = null;
PrintWriter pw = null;
String regex = "http://[\\w+\\.?/?]+\\.[A-Za-z]+";
Pattern p = Pattern.compile(regex);
try {
url = new URL("http://www.sina.com.cn/");
urlconn = url.openConnection();
//这里我们把收集到的链接存储在了E盘底下的一个叫做url的txt文件中
pw = new PrintWriter(new FileWriter("E:/url.txt"), true);
br = new BufferedReader(new InputStreamReader(
urlconn.getInputStream()));
String buf = null;
while ((buf = br.readLine()) != null) {
Matcher buf_m = p.matcher(buf);
while (buf_m.find()) {
pw.println(buf_m.group());
}
}
System.out.println("获取成功!");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
pw.close();
}
}
}
4:测试结果
Java网络爬虫进阶示例
需求:java技术爬取各大网站的网页到本地 和 获取指定内容
技术:Java、jdk1.8、maven、HttpClient、HttpCore
1:新建maven project工程,如图
2:导入所需jar包:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
3:增加如下类,详解参考注释即可:
import java.sql.SQLException;
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 启用线程,开始爬虫
*/
public class StartMain {
public static void main(String[] args) throws SQLException {
String url = "http://baidu.com";
String url1 = "http://www.sina.com.cn";
UrlQueue.addElem(url);
UrlQueue.addElem(url1);
UrlDataHanding[] url_Handings = new UrlDataHanding[10];
for (int i = 0; i < 10; i++) {
url_Handings[i] = new UrlDataHanding();
new Thread(url_Handings[i]).start();
}
}
}
import java.util.LinkedList;
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 需要爬虫的URL、种子,并把这些URL放到队列中
*/
public class UrlQueue {
/**
* 超链接队列
*/
public static LinkedList<String> urlQueue = new LinkedList<String>();
/**
* 队列中对应最多的超链接数量
*/
// public static final int MAX_SIZE = 10000;
public synchronized static void addElem(String url) {
urlQueue.add(url);
}
public synchronized static String outElem() {
return urlQueue.removeFirst();
}
public synchronized static boolean isEmpty() {
return urlQueue.isEmpty();
}
public static int size() {
return urlQueue.size();
}
public static boolean isContains(String url) {
return urlQueue.contains(url);
}
}
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 存储未访问过的URL,广度爬虫时避免重复
*/
public class UrlDataHanding implements Runnable {
/**
* 下载对应页面并分析出页面对应的URL放在未访问队列中。
* @param url
*/
public void dataHanding(String url) {
HrefOfPage.getHrefOfContent(DownLoadPage.getContentFormUrl(url));
}
public void run() {
while (!UrlQueue.isEmpty()) {
dataHanding(UrlQueue.outElem());
}
}
}
import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 抓取网页URL到本地
*/
public class DownLoadPage {
/**
* 根据URL抓取网页内容
* @param url
* @return
*/
public static String getContentFormUrl(String url) {
/* 实例化一个HttpClient客户端 */
HttpClient client = new DefaultHttpClient();
HttpGet getHttp = new HttpGet(url);
String content = null;
HttpResponse response;
try {
/*获得信息载体*/
response = client.execute(getHttp);
HttpEntity entity = response.getEntity();
VisitedUrlQueue.addElem(url);
if (entity != null) {
/* 转化为文本信息 */
content = EntityUtils.toString(entity);
/* 判断是否符合下载网页源代码到本地的条件 */
if (FunctionUtils.isCreateFile(url))
//&& FunctionUtils.isHasGoalContent(content) != -1
{
FunctionUtils.createFile(FunctionUtils .getGoalContent(content), url);
}
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
client.getConnectionManager().shutdown();
}
return content;
}
}
import java.util.HashSet;
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 存储访问过的URL,广度爬虫时避免重复,已访问url队列
*/
public class VisitedUrlQueue {
public static HashSet<String> visitedUrlQueue = new HashSet<String>();
public synchronized static void addElem(String url) {
visitedUrlQueue.add(url);
}
public synchronized static boolean isContains(String url) {
return visitedUrlQueue.contains(url);
}
public synchronized static int size() {
return visitedUrlQueue.size();
}
}
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 正则表达式匹配URL,下载文件并保存在本地
*/
public class FunctionUtils {
/**
* 匹配超链接的正则表达式
*/
private static String pat = "http://([\\w*\\.]*[\\w*])";
private static Pattern pattern = Pattern.compile(pat);
private static BufferedWriter writer = null;
/**
* 爬虫搜索深度
*/
public static int depth = 0;
/**
* 以"/"来分割URL,获得超链接的元素
* @param url
* @return
*/
public static String[] divUrl(String url) {
return url.split("/");
}
/**
* 判断是否创建文件
* @param url
* @return
*/
public static boolean isCreateFile(String url) {
Matcher matcher = pattern.matcher(url);
return matcher.matches();
}
/**
* 创建对应文件
* @param content
* @param urlPath
*/
public static void createFile(String content, String urlPath) {
/* 分割url */
String[] elems = divUrl(urlPath);
StringBuffer path = new StringBuffer();
File file = null;
for (int i = 1; i < elems.length; i++) {
if (i != elems.length - 1) {
path.append(elems[i]);
path.append(File.separator);
file = new File("E:" + File.separator + path.toString());
}
if (i == elems.length - 1) {
Pattern pattern = Pattern.compile("[\\w*\\.]*[\\w*]");
Matcher matcher = pattern.matcher(elems[i]);
if ((matcher.matches())) {
if (!file.exists()) {
file.mkdirs();
}
String fileName = elems[i];
file = new File("E:" + File.separator + path.toString() + File.separator + fileName + ".html");
System.out.println("文件存储路径为:" + "E:" + File.separator + path.toString() + fileName + ".html");
try {
file.createNewFile();
writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file)));
writer.write(content);
writer.flush();
writer.close();
System.out.println("创建文件成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 获取页面的超链接并将其转换为正式的A标签
* @param href
* @return
*/
public static String getHrefOfInOut(String href) {
/* 内外部链接最终转化为完整的链接格式 */
String resultHref = null;
/* 判断是否为外部链接 */
if (href.startsWith("http://")) {
resultHref = href;
} else {
/* 如果是内部链接,则补充完整的链接地址,其他的格式忽略不处理,如:a href="#" */
if (href.startsWith("/")) {
resultHref = "http://www.oschina.net" + href;
}
}
return resultHref;
}
/**
* 截取网页网页源文件的目标内容
* @param content
* @return
*/
public static String getGoalContent(String content) {
int sign = content.indexOf("<html");
String signContent = content.substring(sign);
int start = signContent.indexOf("<html");
int end = signContent.indexOf("</html>");
return signContent.substring(start, end + 7);
}
/**
* 检查网页源文件中是否有目标文件
* @param content
* @return
*/
public static int isHasGoalContent(String content) {
return content.indexOf("<");
}
}
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 获取该URL取得页面中,其他页面的超链接,用于深度爬虫和广度爬虫
*/
public class HrefOfPage {
/**
* 获得页面源代码中超链接
*/
public static void getHrefOfContent(String content) {
System.out.println("开始");
String[] contents = content.split("<a href=\"");
for (int i = 1; i < contents.length; i++) {
int endHref = contents[i].indexOf("\"");
String aHref = FunctionUtils.getHrefOfInOut(contents[i].substring(0, endHref));
if (aHref != null) {
String href = FunctionUtils.getHrefOfInOut(aHref);
if (!UrlQueue.isContains(href) && href.indexOf("/code/explore") != -1 && !VisitedUrlQueue.isContains(href)) {
UrlQueue.addElem(href);
}
}
}
System.out.println(UrlQueue.size() + "--抓取到的连接数");
System.out.println(VisitedUrlQueue.size() + "--已处理的页面数");
}
}
4:验证结果,执行main方法即可
5:如果想获取页面内,具体的相关内容,需要将html文件中的数据进行解析为Document,使用Jsoup技术进行解析即可,示例如下,增加如下代码:
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.2</version>
</dependency>
import java.io.File;
import java.io.IOException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
/**
* @Auther: likang
* @Date: 2018/7/17
* @Desc: 从对应页面中获取自己想要的数据,使用了java 的jsoup技术
*/
public class Jsouptemp {
//从本地文件中获取
//取的www.sina.com.cn.html用jsoup解析成document对象,然后使用DOM的方法接取我们想要的数据
public static void getHrefByLocal() {
File input = new File("E:\\www.sina.com.cn.html");
Document doc = null;
try {
//这里后面加了网址是为了解决后面绝对路径和相对路径的问题
doc = Jsoup.parse(input, "UTF-8", "http://www.sina.com.cn");
} catch (IOException e) {
e.printStackTrace();
}
Elements links = doc.select("a[href]");
for (Element link : links) {
String linkHref = link.attr("href");
String linkText = link.text();
System.out.println(linkText + ":" + linkHref);
}
}
public static void main(String[] args) {
getHrefByLocal();
}
}
6:结果检测: