般我们日常在上网的时候,会在浏览器的地址栏里输入一个网站的 "网址",点击下回车,就会跳到你想去的网站,就类似这样
但其实,叫做 "网址" 并不是特别的准确,确切地说,应该叫做 URL
那到底啥是 URL 呢? 不就是一个网址吗?
URL 是英文 Uniform Resource Locator 的缩写,即统一资源定位器,是因特网上用于指定信息位置的表示方法,通过它就能找到网上的某个你要的资源
虽然我们平时使用浏览器的时候,只要输入baidu.com或者qq.com就能正常上网了,但其实我们输入的只是整个URL中的一小部分
来,我先看看一个相对完整的URL的整体结构是怎么样的
这里大致分了几个部分,我们一个个来看,它们具体是干什么的
图中http://这部分就是协议部分,即指定了URL是以什么协议发送网络请求的
常见的协议如:http://、https://、ftp://、file://,比如: http://就是超文本传输协议,平时上网大多用这个协议,https://是以安全为目标的HTTP协议。
图中localhost就是地址部分,用来确定URL所要访问的服务器的网络地址(也就是网址)。在URL中,地址可以用三个形式来表示:域名、主机名、IP地址
我们平时输入的www.baidu.com、www.qq.com就是域名,域名也分一级域名、二级域名、以及顶级域名。
不过,域名也只是一串文字,计算机和路由器并不能直接认出它,还需要通过DNS服务器找到域名对应的IP地址,再通过底层的TCP/IP协议路由到对应的机器上去 (这些内容不是本文的重点暂时略过,先挖个坑再说)
主机名就是某台计算机的名字,在一个局域网内,可以通过主机名找到你要访问的计算机。主机名和域名一样,计算机和路由器不认它,需要通过HOSTS文件这样的技术找到主机名和IP地址的关联关系,最后还是翻译成IP地址再继续发送网络请求
图中的localhost也是主机名,但是一种比较特殊的主机名,是给 回环地址的一个标准主机名,就是代表本机自己的地址。
在URL中也可以直接用IP地址来代替域名或主机名,如192.168.0.1,关于IP地址的相关知识点放到以后再讲(继续挖坑)
图中在冒号:后面的那串数字8080就是端口号,一台服务器上可以开多个端口号,往往一个网络服务程序就对应一个端口号
比如,我在机器 A 上,开了两个服务程序,分别是 Tomcat和SSH,让它们分别关联端口8080和22,那URL中如果端口号是8080就是会访问到Tomcat程序,22就会连接SSH服务。
但可能有小伙伴会有疑问:诶,我平时上百度看到的URL是http://www.baidu.com没看到有端口号啊
其实是有的,端口号是80,只是它被隐藏起来了,我们看不到而已,而这个80端口也就是URL的默认端口号
但不是所有URL的默认端口号都是80,如果协议是http://,默认端口号为80,但若是https://协议,默认端口号就是443了
从第一个斜杠/开始,到最后一个斜杠/结束的那部分,也就是图中/app/user/那部分即为虚拟目录
它就类似我们电脑中文件目录的格式,第一个/为根目录,每多一个/就多进入一层目录
从域名后开始算起的最后一个斜杠/开始,到?为止,没有?则到#为止,或者?和#都没有就是到整个URL结束为止的那部分就为文件名
说起来很绕吧,其实就是图中 info.do 这部分,它一般包含文件名和扩展名('.'后面那部分),用来指代一个URL所访问的具体文件或资源,它可以是图片、html文件、css文件,也可以是js文件、字体文件等等,它也可以不是某种文件,而是服务端后台执行的某段程序。
甚至可以省略不写虚拟目录和文件名,因为它们本来就不是必须的,就如http://www.baidu.com这样的URL就没有文件名,但服务器会在缺省的情况下给你定位到某个特定的文件或程序上去。
从?后到#结束,即图中的?uid=101&ty=2为查询参数
查询参数,也称为URL参数、查询字符串,英文名为 Query,它是用来向服务端以字符串的形式传递参数和少数数据用的
其参数形式一般都以多个键值对的形式进行表示,如 a=1、b=2就是两个键值对,键为"a"和"b",值为对应的"1"和"2", 多个键值对应&连起来:a=1&b=2
但参数要传递的某些值往往带有特殊字符,这些字符和URL标准的格式冲突,比如要传a&b这样字符串,和查询参数键值对的连接符&冲突了,若不加以区分就会产生歧义
而最简单的办法,就是对参数值进行编码,称为 URL Encoding,通过编码,a&b变成了a%26b,就不再包含会冲突的特殊字符
而有些参数即便有特殊字符,也不会被编码,除非自行强制编码,比如URL中参数值是另外一串URL,就可以写成 http://localhost/do?url=http://www.baidu.com
这种特殊情况不会有歧义,因为计算机系统认得出参数是另一串URL,就会按URL的形式来解析,但当子URL又包含子参数和多子键值对的时候也难免会分不清参数到底是儿子的还是父亲的,这时还是强制编码的好
URL的参数是一个个键值对,即一个key对应一个value,那如果是一个key要传递多个值,也就是一个列表咋办?也好办
URL的参数名是可以重复的,比如a=1&a=2&a=3,这里穿了3个参数名都为a的查询参数,是完全可以的,可以利用这种特性,按顺序将 1、2、3作为参数a的列表值
为了表示更清楚点,一般都会在列表参数名后面加上一对方括号[],如:a[]=1&a[]=2&a[]=3
但是,对于URL参数的写法和格式的标准,也没有特别严格的规定,以上几种形式一般都会支持
图中#后面那部分字符串,#abc就是锚部分
锚,英文称做Reference,通常也是用来传递参数等信息,但与查询参数的本质区别就是这部分内容不会被传递到服务器端
锚一般用于页面,比如在浏览网页的时候,按个按钮突然帮你定位到页首或页面中的某个位子去了,这就是锚
现在随着前后端分离技术,尤其是 vue、reactjs 等前端框架的兴起,锚作为前端javascript程序处理的参数载体也越来越重要了
URL看似已经习以为常、非常简单的东西,背后往往也隐藏着很多技术细节和知识点,甚至这短短一篇文章也没办法穷尽
其实URL的内容还有不少,比如<用户名>@<密码>这种用户验证信息在URL中的传递,由于篇幅的关系还没有讲到
所以我讲分几篇文章来讲解HTTP协议的其中几个重要部分,如果这一系列文章对你有帮助,别忘了关注哦~
HTML 使用超级链接与网络上的另一个文档相连。几乎可以在所有的网页中找到链接。点击链接可以从一张页面跳转到另一张页面。
HTML 链接
如何在HTML文档中创建链接。
(可以在本页底端找到更多实例)
HTML 超链接(链接)
HTML使用标签 <a>来设置超文本链接。
超链接可以是一个字,一个词,或者一组词,也可以是一幅图像,您可以点击这些内容来跳转到新的文档或者当前文档中的某个部分。
当您把鼠标指针移动到网页中的某个链接上时,箭头会变为一只小手。
在标签<a> 中使用了href属性来描述链接的地址。
默认情况下,链接将以以下形式出现在浏览器中:
一个未访问过的链接显示为蓝色字体并带有下划线。
访问过的链接显示为紫色并带有下划线。
点击链接时,链接显示为红色并带有下划线。
注意:如果为这些超链接设置了 CSS 样式,展示样式会根据 CSS 的设定而显示。
HTML 链接语法
链接的 HTML 代码很简单。它类似这样::
<a href="url">链接文本</a>
href 属性描述了链接的目标。.
实例
<a href="http://www.runoob.com/">访问菜鸟教程</a>
上面这行代码显示为:: 访问菜鸟教程
点击这个超链接会把用户带到菜鸟教程的首页。
提示: "链接文本" 不必一定是文本。图片或其他 HTML 元素都可以成为链接。
HTML 链接 - target 属性
使用 target 属性,你可以定义被链接的文档在何处显示。
下面的这行会在新窗口打开文档:
实例
<ahref="http://www.runoob.com/"target="_blank">访问菜鸟教程!</a>
HTML 链接- id 属性
id属性可用于创建在一个HTML文档书签标记。
提示: 书签是不以任何特殊的方式显示,在HTML文档中是不显示的,所以对于读者来说是隐藏的。
实例
在HTML文档中插入ID:
<a id="tips">有用的提示部分</a>
在HTML文档中创建一个链接到"有用的提示部分(id="tips")":
<a href="#tips">访问有用的提示部分</a>
或者,从另一个页面创建一个链接到"有用的提示部分(id="tips")":
<a href="http://www.runoob.com/html/html-links.html#tips">
访问有用的提示部分</a>
基本的注意事项 - 有用的提示
注释: 请始终将正斜杠添加到子文件夹。假如这样书写链接:href="http://www.runoob.com/html",就会向服务器产生两次 HTTP 请求。这是因为服务器会添加正斜杠到这个地址,然后创建一个新的请求,就像这样:href="http://www.runoob.com/html/"。
图片链接
如何使用图片链接。
在当前页面链接到指定位置
如何使用书签
跳出框架
本例演示如何跳出框架,假如你的页面被固定在框架之内。
创建电子邮件链接
本例演示如何如何链接到一个邮件。(本例在安装邮件客户端程序后才能工作。)
建电子邮件链接 2
本例演示更加复杂的邮件链接。
HTML 链接标签
标签 | 描述 |
---|---|
<a> | 定义一个超级链接 |
如您还有不明白的可以在下面与我留言或是与我探讨QQ群308855039,我们一起飞!
上一节(爬虫系列(0):项目搭建)
网络爬虫的都是通过多线程,多任务逻辑实现的,在springboot框架中已封装线程池(ThreadPoolTaskExecutor),我们只需要使用就是了。
这一节我们主要实现多线程抓取网页连接信息,并将信息存储在队列里面。
在pom中引入新包,具体如下:
<dependency>
<!-- common工具包 -->
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<!-- java处理HTML的工具包 -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<!-- lombok工具包,简化编码 -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
为了简化编码,这里引入了lombok,在使用时候IDE需要安装lombok插件,否则会提示编译错误。
springboot的配置文件都是在application.properties(.yml)统一管理的,在这里,我们也把爬虫相关的配置通过@ConfigurationProperties注解来实现。直接上代码:
package mobi.huanyuan.spider.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 爬虫配置.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:10
*/
@Data
@ConfigurationProperties(prefix = "huanyuan.spider")
public class SpiderConfig {
/**
* 爬取页面最大深度
*/
public int maxDepth = 2;
/**
* 下载页面线程数
*/
public int minerHtmlThreadNum = 2;
//=================================================
// 线程池配置
//=================================================
/**
* 核心线程池大小
*/
private int corePoolSize = 4;
/**
* 最大可创建的线程数
*/
private int maxPoolSize = 100;
/**
* 队列最大长度
*/
private int queueCapacity = 1000;
/**
* 线程池维护线程所允许的空闲时间
*/
private int keepAliveSeconds = 300;
}
然后,需要修改这些配置,只需要修改application.properties(.yml)里边即可:
幻猿简易爬虫配置
线程池使用springboot已有的,配置也在上边配置管理里边有,这里只初始化配置即可:
package mobi.huanyuan.spider.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:35
*/
@Configuration
public class ThreadPoolConfig {
@Autowired
private SpiderConfig spiderConfig;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(spiderConfig.getMaxPoolSize());
executor.setCorePoolSize(spiderConfig.getCorePoolSize());
executor.setQueueCapacity(spiderConfig.getQueueCapacity());
executor.setKeepAliveSeconds(spiderConfig.getKeepAliveSeconds());
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
这一节我们主要是抓取URL并保存进队列,所以涉及到的队列有待抓取队列和待分析队列(下一节分析时候用,这里只做存储),此外,为了防止重复抓取同一个URL,这里还需要加一个Set集合,将已访问过的地址做个记录。
package mobi.huanyuan.spider;
import lombok.Getter;
import mobi.huanyuan.spider.bean.SpiderHtml;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
/**
* 爬虫访问队列.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 10:54
*/
public class SpiderQueue {
private static Logger logger = LoggerFactory.getLogger(SpiderQueue.class);
/**
* Set集合 保证每一个URL只访问一次
*/
private static volatile Set<String> urlSet = new HashSet<>();
/**
* 待访问队列<br>
* 爬取页面线程从这里取数据
*/
private static volatile Queue<SpiderHtml> unVisited = new LinkedList<>();
/**
* 等待提取URL的分析页面队列<br>
* 解析页面线程从这里取数据
*/
private static volatile Queue<SpiderHtml> waitingMine = new LinkedList<>();
/**
* 添加到URL队列
*
* @param url
*/
public synchronized static void addUrlSet(String url) {
urlSet.add(url);
}
/**
* 获得URL队列大小
*
* @return
*/
public static int getUrlSetSize() {
return urlSet.size();
}
/**
* 添加到待访问队列,每个URL只访问一次
*
* @param spiderHtml
*/
public synchronized static void addUnVisited(SpiderHtml spiderHtml) {
if (null != spiderHtml && !urlSet.contains(spiderHtml.getUrl())) {
logger.info("添加到待访问队列[{}] 当前第[{}]层 当前线程[{}]", spiderHtml.getUrl(), spiderHtml.getDepth(), Thread.currentThread().getName());
unVisited.add(spiderHtml);
}
}
/**
* 待访问出队列
*
* @return
*/
public synchronized static SpiderHtml unVisitedPoll() {
return unVisited.poll();
}
/**
* 添加到等待提取URL的分析页面队列
*
* @param html
*/
public synchronized static void addWaitingMine(SpiderHtml html) {
waitingMine.add(html);
}
/**
* 等待提取URL的分析页面出队列
*
* @return
*/
public synchronized static SpiderHtml waitingMinePoll() {
return waitingMine.poll();
}
/**
* 等待提取URL的分析页面队列大小
* @return
*/
public static int waitingMineSize() {
return waitingMine.size();
}
}
直接上代码:
package mobi.huanyuan.spider.runable;
import mobi.huanyuan.spider.SpiderQueue;
import mobi.huanyuan.spider.bean.SpiderHtml;
import mobi.huanyuan.spider.config.SpiderConfig;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 抓取页面任务.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:43
*/
public class SpiderHtmlRunnable implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(SpiderHtmlRunnable.class);
private static boolean done = false;
private SpiderConfig config;
public SpiderHtmlRunnable(SpiderConfig config) {
this.config = config;
}
@Override
public void run() {
while (!SpiderHtmlRunnable.done) {
done = true;
minerHtml();
done = false;
}
}
public synchronized void minerHtml() {
SpiderHtml minerUrl = SpiderQueue.unVisitedPoll(); // 待访问出队列。
try {
//判断当前页面爬取深度
if (null == minerUrl || StringUtils.isBlank(minerUrl.getUrl()) || minerUrl.getDepth() > config.getMaxDepth()) {
return;
}
//判断爬取页面URL是否包含http
if (!minerUrl.getUrl().startsWith("http")) {
logger.info("当前爬取URL[{}]没有http", minerUrl.getUrl());
return;
}
logger.info("当前爬取页面[{}]爬取深度[{}] 当前线程 [{}]", minerUrl.getUrl(), minerUrl.getDepth(), Thread.currentThread().getName());
Connection conn = Jsoup.connect(minerUrl.getUrl());
conn.header("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13");//配置模拟浏览器
Document doc = conn.get();
String page = doc.html();
SpiderHtml spiderHtml = new SpiderHtml();
spiderHtml.setUrl(minerUrl.getUrl());
spiderHtml.setHtml(page);
spiderHtml.setDepth(minerUrl.getDepth());
System.out.println(spiderHtml.getUrl());
// TODO: 添加到继续爬取队列
SpiderQueue.addWaitingMine(spiderHtml);
} catch (Exception e) {
logger.info("爬取页面失败 URL [{}]", minerUrl.getUrl());
logger.info("Error info [{}]", e.getMessage());
}
}
}
这里就是个Runnable任务,主要目标就是拉去URL数据,然后封装成SpiderHtml对象存放在待分析队列里边。 这里用到了jsoup--一个java对HTML分析操作的工具包,不清楚的可以去搜索看看,之后章节涉及到分析的部分也会用到。
package mobi.huanyuan.spider.bean;
import lombok.Data;
import java.io.Serializable;
/**
* 页面信息类.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:02
*/
@Data
public class SpiderHtml implements Serializable {
/**
* 页面URL
*/
private String url;
/**
* 页面信息
*/
private String html;
/**
* 爬取深度
*/
private int depth;
}
package mobi.huanyuan.spider;
import mobi.huanyuan.spider.bean.SpiderHtml;
import mobi.huanyuan.spider.config.SpiderConfig;
import mobi.huanyuan.spider.runable.SpiderHtmlRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 爬虫.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:23
*/
@Component
public class Spider {
private static Logger logger = LoggerFactory.getLogger(Spider.class);
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private SpiderConfig spiderConfig;
public void start(SpiderHtml spiderHtml) {
//程序启动,将第一个起始页面放入待访问队列。
SpiderQueue.addUnVisited(spiderHtml);
//将URL 添加到URL队列 保证每个URL只访问一次
SpiderQueue.addUrlSet(spiderHtml.getUrl());
//download
for (int i = 0; i < spiderConfig.getMinerHtmlThreadNum(); i++) {
SpiderHtmlRunnable minerHtml = new SpiderHtmlRunnable(spiderConfig);
threadPoolTaskExecutor.execute(minerHtml);
}
// TODO: 监控爬取完毕之后停线程池,关闭程序
try {
TimeUnit.SECONDS.sleep(20);
logger.info("待分析URL队列大小: {}", SpiderQueue.waitingMineSize());
// 关闭线程池
threadPoolTaskExecutor.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在"// TODO:"之后的代码逻辑这里是临时的,等后边章节完善之后,这里就慢慢去掉。
要跑起这一节的代码,需要在springboot项目main方法中加入如下代码:
ConfigurableApplicationContext context = SpringApplication.run(SpiderApplication.class, args);
Spider spider = context.getBean(Spider.class);
SpiderHtml startPage = new SpiderHtml();
startPage.setUrl("$URL");
startPage.setDepth(2);
spider.start(startPage);
$URL就是需要抓取的网页地址。
springboot项目启动后,停止需要手动停止,目前没有处理抓取完自动停止运行的逻辑。 运行结果如下图:
幻猿简易爬虫运行结果
最后,这个章节完成之后整个项目的结构如下图:
幻猿简易爬虫项目结构
程序界的老猿,自媒体界的新宠 じ☆ve
程序界的老猿,自媒体界的新宠 じ☆ve
联系方式:1405368512@qq.com
*请认真填写需求信息,我们会在24小时内与您取得联系。