起爬蟲似乎都成了python的專屬了,我不知道其他人怎么樣?至少我平時對爬蟲的應用場景很小,所以一直沒有關注它。近期因為一個小項目——一個客戶的網站忘了續費,結果被服務商索取高額服務費(好在網站還能打開),剛好客戶遇到我們,我們當場從0開始開發了一個爬蟲,一下午就把內容爬下來了。
一、原理(用大白話告訴你原理)
網頁最終呈現在瀏覽器中,都是以html編碼呈現的,html是什么都不懂的話,這篇文章都不懂的話,這篇文章可能不適合你,但我還是愿意給你掃掃盲;html就是把文字或圖片進行定位排版的一種描述性語言。比如:加粗<b>加粗</b>,比如下劃線<i>這是下劃線</i>基本所有內容都是由類似這樣的標簽包裹起來的,下面看一組html的編碼(你到任何一個網站點右鍵‘查看源文件’都能看到它的編碼文件)。
html源文件
通過上圖可以觀察到,我們需要取的內容都在html標簽內包含,再仔細觀察這個html標簽,發現兩個特點:
1、新聞的標題在:<a>新聞標題</a>(a標簽內包含)
2、新聞的下級鏈接:<a href="http://下級內容鏈接"> (a標簽的href內包含)
這里你需要記住兩個行業術語,1這種形態:標簽內包含的叫“內容”,2這種形態:叫“屬性”,一個完整的標簽是由內容和屬性構成,屬性不是必須的,如果內容你也不寫的話,就會什么也不顯示,就成了無意義標簽。
大量的標簽構成了html網頁,標簽也有父子歸屬關系,就好比數學上的:大括號管中括號,中括號管小括號一樣。
掃盲到此為止……
根據上面的html顯示,我們需要用爬蟲剔骨卸肉一般,將a標簽中的標題文字,和下級鏈接取出。
二、關鍵技術——“解析”
本文開始說到python很擅長做爬蟲,那是python語言的愛好者圈子里,貢獻了很多解析html代碼的小工具(行業內把各種工具類的代碼片段叫:類庫),比如這個類庫的作用是:給它喂一篇html文檔,你簡單描述一下要求,(例如:要求取出所有a標簽的文字),這個類庫就可以高效的根據你的描述,把整個源碼文檔中的a標簽的文字剔出來。這個“剔”的過程,行業內把它叫“解析”。
解析文檔無論在什么語言中,都是一件非常細致且耗時的工作,“類庫”只是某種語言的愛好者,把某個常用功能剝離出來,可以在不同的程序之間移植而已。
比如:上面介紹的就是html解析類庫,python的這個語言并不是天生就會做什么,而是它的社區很活躍,被愛好者共享的類庫很多而已。
但是話又說回來了,其他老牌語言也不見得類庫就少,比如:C#或java各種工具類庫也是鋪天蓋地,今天介紹的就是c#下的一款優秀的html解析類庫——“HtmlAgilityPack”。
它的用法和jqurey非常像,如果你想取出下面鏈接地址和文字,那么就這么描述就行:
實現方法
//定義一個根節點
Dom_list="//table[@id='content_one_4']";
//查詢指定容器下的組節點
HtmlNode documentNode=doc.DocumentNode.SelectSingleNode(Dom_list);
HtmlNodeCollection linklist=documentNode.SelectNodes(".//a");
//把這一組a標簽循環取出
foreach (var item in linklist)
{
//【核心】獲取鏈接文字和鏈接的地址
string link_title=item.InnerHtml;
string link_url=item.Attributes["href"].Value;
//檢測重復記錄后寫入數據庫:根據情況自己完成入庫操作
}
這么簡簡單單的幾句代碼就完成了爬蟲的核心工作,數據進了你本地庫,你愛怎么變花樣就怎么變。
如果你把爬蟲的原理搞明白了,制作起來還是很容易的,但容易的前提是你用了別人的類庫,如果讓你從零開始,去分析解析一篇html文檔,那就非常復雜。除了用常見的正則表達式去匹配各種規則,還要有大量的容錯機制,確實不是一般程序員能快速搞定的事。
python之所以目前很受歡迎,主要得益于它的類庫豐富,把程序開發活脫脫變成了套殼,小白上手很容易;但話又說回來,如果一個項目要你徒手從零完成,那無論用什么語言,工作量其實也都差不多了。
下面是我做的一個桌面爬蟲應用程序,之所以用做成程序而不是網頁,主要是考慮到這是個工具,開箱即用,web再方便也需要部署環境,在這一點上一個exe的綠色軟件,還是有相當大的優勢的。
我的爬蟲軟件
軟件使用很簡單:對目標網站進行簡單進行配置以后,先點擊【采集目錄】就可以獲得所有的內容頁的地址存入“工作進程”中(例如:獲取了1000條內容地址),然后點擊【采集內容】,程序就依次從從“工作進程”中的地址采集內容。
多線程采集,網速夠快幾分鐘就采集完成了,如果你想做個爬蟲,就試試吧。
近有個概念吵得很火,網絡爬蟲,但是基本都是用什么python或者JAVA寫,貌似很少看到用c++寫的,我在網上找了一個,看到其實還是很簡單的算法
算法講解:1.遍歷資源網站
2.獲取html信息
3.然后解析網址和圖片url下載。
4.遞歸調用搜索網址
BFS是最重要的處理:
先是獲取網頁響應,保存到文本里面,然后找到其中的圖片鏈接HTMLParse,
下載所有圖片DownLoadImg。
//廣度遍歷
void BFS( const string & url ){
char * response;
int bytes;
// 獲取網頁的相應,放入response中。
if( !GetHttpResponse( url, response, bytes ) ){
cout << "The url is wrong! ignore." << endl;
return;
}
string httpResponse=response;
free( response );
string filename=ToFileName( url );
ofstream ofile( "./html/"+filename );
if( ofile.is_open() ){
// 保存該網頁的文本內容
ofile << httpResponse << endl;
ofile.close();
}
vector<string> imgurls;
//解析該網頁的所有圖片鏈接,放入imgurls里面
HTMLParse( httpResponse, imgurls, url );
//下載所有的圖片資源
DownLoadImg( imgurls, url );
}
然后附上代碼:
#include "stdafx.h"
//#include <Windows.h>
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include "winsock2.h"
#include <time.h>
#include <queue>
#include <hash_set>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
#define DEFAULT_PAGE_BUF_SIZE 1048576
queue<string> hrefUrl;
hash_set<string> visitedUrl;
hash_set<string> visitedImg;
int depth=0;
int g_ImgCnt=1;
//解析URL,解析出主機名,資源名
bool ParseURL( const string & url, string & host, string & resource){
if ( strlen(url.c_str()) > 2000 ) {
return false;
}
const char * pos=strstr( url.c_str(), "http://" );
if( pos==NULL ) pos=url.c_str();
else pos +=strlen("http://");
if( strstr( pos, "/")==0 )
return false;
char pHost[100];
char pResource[2000];
sscanf( pos, "%[^/]%s", pHost, pResource );
host=pHost;
resource=pResource;
return true;
}
//使用Get請求,得到響應
bool GetHttpResponse( const string & url, char * &response, int &bytesRead ){
string host, resource;
if(!ParseURL( url, host, resource )){
cout << "Can not parse the url"<<endl;
return false;
}
//建立socket
struct hostent * hp=gethostbyname( host.c_str() );
if( hp==NULL ){
cout<< "Can not find host address"<<endl;
return false;
}
SOCKET sock=socket( AF_INET, SOCK_STREAM, IPPROTO_TCP);
if( sock==-1 || sock==-2 ){
cout << "Can not create sock."<<endl;
return false;
}
//建立服務器地址
SOCKADDR_IN sa;
sa.sin_family=AF_INET;
sa.sin_port=htons( 80 );
//char addr[5];
//memcpy( addr, hp->h_addr, 4 );
//sa.sin_addr.s_addr=inet_addr(hp->h_addr);
memcpy( &sa.sin_addr, hp->h_addr, 4 );
//建立連接
if( 0!=connect( sock, (SOCKADDR*)&sa, sizeof(sa) ) ){
cout << "Can not connect: "<< url <<endl;
closesocket(sock);
return false;
};
//準備發送數據
string request="GET " + resource + " HTTP/1.1\r\nHost:" + host + "\r\nConnection:Close\r\n\r\n";
//發送數據
if( SOCKET_ERROR==send( sock, request.c_str(), request.size(), 0 ) ){
cout << "send error" <<endl;
closesocket( sock );
return false;
}
//接收數據
int m_nContentLength=DEFAULT_PAGE_BUF_SIZE;
char *pageBuf=(char *)malloc(m_nContentLength);
memset(pageBuf, 0, m_nContentLength);
bytesRead=0;
int ret=1;
cout <<"Read: ";
while(ret > 0){
ret=recv(sock, pageBuf + bytesRead, m_nContentLength - bytesRead, 0);
if(ret > 0)
{
bytesRead +=ret;
}
if( m_nContentLength - bytesRead<100){
cout << "\nRealloc memorry"<<endl;
m_nContentLength *=2;
pageBuf=(char*)realloc( pageBuf, m_nContentLength); //重新分配內存
}
cout << ret <<" ";
}
cout <<endl;
pageBuf[bytesRead]='> pageBuf[bytesRead]='\0'; <';
response=pageBuf;
closesocket( sock );
return true;
//cout<< response <<endl;
}
//提取所有的URL以及圖片URL
void HTMLParse ( string & htmlResponse, vector<string> & imgurls, const string & host ){
//找所有連接,加入queue中
const char *p=htmlResponse.c_str();
char *tag="href=\"";
const char *pos=strstr( p, tag );
ofstream ofile("url.txt", ios::app);
while( pos ){
pos +=strlen(tag);
const char * nextQ=strstr( pos, "\"" );
if( nextQ ){
char * url=new char[ nextQ-pos+1 ];
//char url[100]; //固定大小的會發生緩沖區溢出的危險
sscanf( pos, "%[^\"]", url);
string surl=url; // 轉換成string類型,可以自動釋放內存
if( visitedUrl.find( surl )==visitedUrl.end() ){
visitedUrl.insert( surl );
ofile << surl<<endl;
hrefUrl.push( surl );
}
pos=strstr(pos, tag );
delete [] url; // 釋放掉申請的內存
}
}
ofile << endl << endl;
ofile.close();
tag="<img ";
const char* att1="src=\"";
const char* att2="lazy-src=\"";
const char *pos0=strstr( p, tag );
while( pos0 ){
pos0 +=strlen( tag );
const char* pos2=strstr( pos0, att2 );
if( !pos2 || pos2 > strstr( pos0, ">") ) {
pos=strstr( pos0, att1);
if(!pos) {
pos0=strstr(att1, tag );
continue;
} else {
pos=pos + strlen(att1);
}
}
else {
pos=pos2 + strlen(att2);
}
const char * nextQ=strstr( pos, "\"");
if( nextQ ){
char * url=new char[nextQ-pos+1];
sscanf( pos, "%[^\"]", url);
cout << url<<endl;
string imgUrl=url;
if( visitedImg.find( imgUrl )==visitedImg.end() ){
visitedImg.insert( imgUrl );
imgurls.push_back( imgUrl );
}
pos0=strstr(pos0, tag );
delete [] url;
}
}
cout << "end of Parse this html"<<endl;
}
//把URL轉化為文件名
string ToFileName( const string &url ){
string fileName;
fileName.resize( url.size());
int k=0;
for( int i=0; i<(int)url.size(); i++){
char ch=url[i];
if( ch!='\'&&ch!='/'&&ch!=':'&&ch!='*'&&ch!='?'&&ch!='"'&&ch!='<'&&ch!='>'&&ch!='|')
fileName[k++]=ch;
}
return fileName.substr(0,k) + ".txt";
}
//下載圖片到img文件夾
void DownLoadImg( vector<string> & imgurls, const string &url ){
//生成保存該url下圖片的文件夾
string foldname=ToFileName( url );
foldname="./img/"+foldname;
if(!CreateDirectory( foldname.c_str(),NULL ))
cout << "Can not create directory:"<< foldname<<endl;
char *image;
int byteRead;
for( int i=0; i<imgurls.size(); i++){
//判斷是否為圖片,bmp,jgp,jpeg,gif
string str=imgurls[i];
int pos=str.find_last_of(".");
if( pos==string::npos )
continue;
else{
string ext=str.substr( pos+1, str.size()-pos-1 );
if( ext!="bmp"&& ext!="jpg" && ext!="jpeg"&& ext!="gif"&&ext!="png")
continue;
}
//下載其中的內容
if( GetHttpResponse(imgurls[i], image, byteRead)){
if ( strlen(image)==0 ) {
continue;
}
const char *p=image;
const char * pos=strstr(p,"\r\n\r\n")+strlen("\r\n\r\n");
int index=imgurls[i].find_last_of("/");
if( index!=string::npos ){
string imgname=imgurls[i].substr( index , imgurls[i].size() );
ofstream ofile( foldname+imgname, ios::binary );
if( !ofile.is_open() )
continue;
cout <<g_ImgCnt++<< foldname+imgname<<endl;
ofile.write( pos, byteRead- (pos-p) );
ofile.close();
}
free(image);
}
}
}
//廣度遍歷
void BFS( const string & url ){
char * response;
int bytes;
// 獲取網頁的相應,放入response中。
if( !GetHttpResponse( url, response, bytes ) ){
cout << "The url is wrong! ignore." << endl;
return;
}
string httpResponse=response;
free( response );
string filename=ToFileName( url );
ofstream ofile( "./html/"+filename );
if( ofile.is_open() ){
// 保存該網頁的文本內容
ofile << httpResponse << endl;
ofile.close();
}
vector<string> imgurls;
//解析該網頁的所有圖片鏈接,放入imgurls里面
HTMLParse( httpResponse, imgurls, url );
//下載所有的圖片資源
DownLoadImg( imgurls, url );
}
void main()
{
//初始化socket,用于tcp網絡連接
WSADATA wsaData;
if( WSAStartup(MAKEWORD(2,2), &wsaData) !=0 ){
return;
}
// 創建文件夾,保存圖片和網頁文本文件
CreateDirectory( "./img",0);
CreateDirectory("./html",0);
//string urlStart="http://hao.360.cn/meinvdaohang.html";
// 遍歷的起始地址
string urlStart="http://desk.zol.com.cn/bizhi/7018_87137_2.html";
//string urlStart="http://item.taobao.com/item.htm?spm=a230r.1.14.19.sBBNbz&id=36366887850&ns=1#detail";
// 使用廣度遍歷
// 提取網頁中的超鏈接放入hrefUrl中,提取圖片鏈接,下載圖片。
BFS( urlStart );
// 訪問過的網址保存起來
visitedUrl.insert( urlStart );
while( hrefUrl.size()!=0 ){
string url=hrefUrl.front(); // 從隊列的最開始取出一個網址
cout << url << endl;
BFS( url ); // 遍歷提取出來的那個網頁,找它里面的超鏈接網頁放入hrefUrl,下載它里面的文本,圖片
hrefUrl.pop(); // 遍歷完之后,刪除這個網址
}
WSACleanup();
return;
} <br><br><br>
學習網絡爬蟲要掌握哪些技術?#
挑戰升級!老板又為我設定了新的任務目標:利用C#開發一款超能爬蟲,它必須深入挖掘我們網站的每一個細節,不論是主體內容、相關鏈接,甚至是細微至頁面標題,都需快速精準獲取,全程保持高效穩定的運行狀態。
這不僅是一個任務,還是一次創新能力的試煉。那天,我坐在電腦前,心里琢磨著,這得怎么開始呢?
好吧,我得先熟悉我們網站的結構,就像了解一個新朋友一樣。我打開了瀏覽器,一頁一頁地翻,仔細觀察那些HTML代碼,找到了BODY、鏈接和標題的蛛絲馬跡。
根據我們網站結構,我開始了針對這個任務的計劃:
需求分析:
抓取頁面內容(BODY):需要從目標網頁中提取出HTML的主體部分,這通常包括文字、圖片、視頻等內容。
抓取網站鏈接:我需要識別網頁中的超鏈接(如<a>標簽),并提取出它們的href屬性,以獲取其他相關頁面的鏈接。
抓取頁面標題:頁面標題通常位于<title>標簽內,我要能夠提取并存儲這些標題信息。
技術選型:
由于使用C#作為開發語言,我可以使用HttpClient來實現HTTP請求和響應處理。利用解析庫(這次用AngleSharp)來解析網頁內容,提取所需信息。
主要實現步驟:
初始化:設置爬蟲的基本配置,如并發請求數、超時時間等。網頁抓取:通過HTTP請求獲取目標網頁的HTML內容。內容解析:使用HTML解析庫提取頁面內容、鏈接和標題。數據演示(實際是要保存的):將提取的信息展示到控制臺中顯示,以便查看、分析和處理。錯誤處理:實現適當的錯誤處理機制,如展示異常記錄等。
測試與驗證:
對爬蟲程序進行單元測試,確保每個功能模塊都能正常工作。進行集成測試,確保整個爬蟲流程的流暢性和穩定性。在實際網站上進行驗證,確保抓取到的數據準確無誤。
維護與更新:
定期檢查爬蟲程序的運行狀態,及時處理可能出現的問題。根據網站結構和內容的變化,適時更新爬蟲程序以適應新的抓取需求。當然,原來的爬蟲程序需要改進,功能需要增強。
根據這個計劃,我初步修改了原來的代碼,把要求的三個功能都完成了,看效果:
關鍵實現代碼:
// 發送HTTP GET請求并獲取網頁內容
public async Task<string> GetWebPageContentAsync(string url)
{
try
{
HttpResponseMessage response=await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
string content=await response.Content.ReadAsStringAsync();
return content;
}
catch (HttpRequestException e)
{
Console.WriteLine($"從URL {url} 獲取內容時出錯: {e.Message}");
return null;
}
}
// 解析網頁內容,提取<body>、鏈接和標題
public async Task CrawlAndParseAsync(string url)
{
string content=await GetWebPageContentAsync(url);
if (content !=null)
{
var document=_htmlParser.ParseDocument(content);
// 獲取并打印<body>的內容
var bodyContent=document.Body.InnerHtml;
Console.WriteLine($"網頁<body>內容:\n{bodyContent}\n");
// 提取并打印所有鏈接
foreach (var link in document.QuerySelectorAll("a[href]"))
{
var href=link.GetAttribute("href");
Console.WriteLine($"找到的鏈接: {href}");
}
// 提取并打印頁面標題
var title=document.Title;
Console.WriteLine($"頁面標題: {title}");
}
}
如有需要完整代碼的朋友,請關注我并留言。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。