般我們日常在上網的時候,會在瀏覽器的地址欄里輸入一個網站的 "網址",點擊下回車,就會跳到你想去的網站,就類似這樣
但其實,叫做 "網址" 并不是特別的準確,確切地說,應該叫做 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 >訪問菜鳥教程</a>
上面這行代碼顯示為:: 訪問菜鳥教程
點擊這個超鏈接會把用戶帶到菜鳥教程的首頁。
提示: "鏈接文本" 不必一定是文本。圖片或其他 HTML 元素都可以成為鏈接。
HTML 鏈接 - target 屬性
使用 target 屬性,你可以定義被鏈接的文檔在何處顯示。
下面的這行會在新窗口打開文檔:
實例
<a>訪問菜鳥教程!</a>
HTML 鏈接- id 屬性
id屬性可用于創建在一個HTML文檔書簽標記。
提示: 書簽是不以任何特殊的方式顯示,在HTML文檔中是不顯示的,所以對于讀者來說是隱藏的。
實例
在HTML文檔中插入ID:
<a id="tips">有用的提示部分</a>
在HTML文檔中創建一個鏈接到"有用的提示部分(id="tips")":
<a href="#tips">訪問有用的提示部分</a>
或者,從另一個頁面創建一個鏈接到"有用的提示部分(id="tips")":
<a >
訪問有用的提示部分</a>
基本的注意事項 - 有用的提示
注釋: 請始終將正斜杠添加到子文件夾。假如這樣書寫鏈接:,就會向服務器產生兩次 HTTP 請求。這是因為服務器會添加正斜杠到這個地址,然后創建一個新的請求,就像這樣:。
圖片鏈接
如何使用圖片鏈接。
在當前頁面鏈接到指定位置
如何使用書簽
跳出框架
本例演示如何跳出框架,假如你的頁面被固定在框架之內。
創建電子郵件鏈接
本例演示如何如何鏈接到一個郵件。(本例在安裝郵件客戶端程序后才能工作。)
建電子郵件鏈接 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小時內與您取得聯系。