基礎(chǔ)知識(shí)
工欲善其事端圈,必先利其器,要編寫爬蟲程序子库,首先必須找一個(gè)爬蟲框架舱权,如果你使用Python
語(yǔ)言,可以選用scrapy
仑嗅,如果你使用Java語(yǔ)言宴倍,可選用WebMagic
张症,本文使用后者,編寫爬蟲程序無(wú)非分以下幾步:
- 根據(jù)URL下載網(wǎng)頁(yè)鸵贬,得到HTML(注意并不是通過(guò)開發(fā)工具看到的HTML俗他,而是網(wǎng)頁(yè)源代碼HTML,這兩者有本質(zhì)區(qū)別)阔逼;
- 根據(jù)HTML解析您所需要的數(shù)據(jù)兆衅,可以利用xpath獲取DOM節(jié)點(diǎn)內(nèi)容或?qū)傩灾担?/li>
- 有可能還需要根據(jù)得到的HTML解析出其他鏈接,利用多線程繼續(xù)爬仁雀 羡亩;
- 解析后的數(shù)據(jù)存儲(chǔ)(數(shù)據(jù)庫(kù),文件等)危融;
WebMagic
爬蟲框架在core代碼中主要有四個(gè)模塊:Downloader魁淳、PageProcessor、Scheduler妈倔、Pipeline攒读,分別處理下載,頁(yè)面解析寨腔,管理(管理待抓取的URL速侈,做一些去重工作,默認(rèn)使用內(nèi)存隊(duì)列管理URL迫卢,也可以使用Redis進(jìn)行分布式管理)和持久化工作倚搬,因?yàn)樽罱K解析出的結(jié)構(gòu)化數(shù)據(jù)應(yīng)該是要入庫(kù)或入文件存儲(chǔ)的。
頁(yè)面下載
WebMagic
爬蟲框架負(fù)責(zé)從互聯(lián)網(wǎng)上下載頁(yè)面乾蛤,以便后續(xù)處理每界。WebMagic默認(rèn)使用了Apache HttpClient作為下載工具,當(dāng)然你可以使用chromedriver
下載動(dòng)態(tài)內(nèi)容家卖,那么我們應(yīng)該調(diào)用Spider的setDownloader方法spider.setDownloader(new SeleniumDownloader())
眨层。
頁(yè)面解析
PageProcessor
負(fù)責(zé)解析頁(yè)面,抽取有用信息上荡,以及發(fā)現(xiàn)新的鏈接趴樱。WebMagic使用Jsoup作為HTML解析工具,并基于其開發(fā)了解析XPath的工具Xsoup酪捡。
這是您需要花大力氣編寫代碼的地方叁征,根據(jù)chrome開發(fā)工具查看您所需抓取的數(shù)據(jù)的xpath路徑,利用xpath獲取節(jié)點(diǎn)信息獲取數(shù)據(jù)逛薇。
Scheduler
Scheduler負(fù)責(zé)管理待抓取的URL捺疼,以及一些去重的工作。WebMagic默認(rèn)提供了JDK的內(nèi)存隊(duì)列來(lái)管理URL永罚,并用集合來(lái)進(jìn)行去重啤呼。也支持使用Redis進(jìn)行分布式管理议薪。
除非項(xiàng)目有一些特殊的分布式需求,否則無(wú)需自己定制Scheduler媳友。
Pipeline
Pipeline負(fù)責(zé)抽取結(jié)果的處理斯议,包括計(jì)算、持久化到文件醇锚、數(shù)據(jù)庫(kù)等哼御。WebMagic默認(rèn)提供了“輸出到控制臺(tái)”和“保存到文件”兩種結(jié)果處理方案,如果你需要將數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫(kù)焊唬,您可能需要自定義一個(gè)Pipeline恋昼,以JSON形式保存到文件D:\webmagic\
可以使用:
Spider.create(new XXXPageProcessor())
.addUrl("http://www.baidu.com")
.addPipeline(new JsonFilePipeline("D:\\webmagic\\"))
.thread(5)
.run();
另外,Request
對(duì)象用于在PageProcessor
的process
實(shí)現(xiàn)函數(shù)中赶促,將其他url的抓取任務(wù)扔給Downloader組件
去執(zhí)行液肌,Downloader組件
下載到一個(gè)頁(yè)面是Page
對(duì)象,在此可以獲取網(wǎng)頁(yè)源碼內(nèi)容鸥滨,可以做xpath解析等嗦哆,ResultItems
相當(dāng)于一個(gè)Map
,保存PageProcessor處理的結(jié)果婿滓,供Pipeline使用老速,你可以將里面的內(nèi)容保存到注入數(shù)據(jù)庫(kù)中。
Selectable抽取元素
解析處理HTML獲取自己想要的數(shù)據(jù)可能是編寫爬蟲最麻煩的一環(huán)凸主,使用Selectable接口橘券,你可以直接完成頁(yè)面元素的鏈?zhǔn)匠槿。矡o(wú)需去關(guān)心抽取的細(xì)節(jié)卿吐,比如上文提到的Page.getHtml()得到的就是一個(gè)Html對(duì)象旁舰,實(shí)現(xiàn)了Selectable
接口。以下是常用的解析方法:
- xpath(String xpath):根據(jù)xpath路徑獲取節(jié)點(diǎn)嗡官,如html.xpath("http://div[@class='title']/text()"箭窜,獲取包含title類的div包裹的內(nèi)容;
-
("div.title")绽快,獲取包含title類名的div節(jié)點(diǎn)列表
- css(String selector):功能同$芥丧,使用css選擇器紧阔;
- links():獲取所有鏈接
- regex(String regex):根據(jù)正則匹配出內(nèi)容
- replace(String regex,String replacement):替換內(nèi)容
以上函數(shù)都返回Selectable
接口,支持鏈?zhǔn)秸{(diào)用续担,但想要獲取解析后數(shù)據(jù)的結(jié)果時(shí)擅耽,可以利用get()
返回一條String結(jié)果或all
返回所有結(jié)果List<String>
或match
返回匹配結(jié)果。
常見問(wèn)題
在編寫爬蟲程序時(shí)物遇,我們經(jīng)常會(huì)碰到的一些問(wèn)題匯總與解決方案乖仇,幫您快速定位解決問(wèn)題憾儒。
通過(guò)代理上網(wǎng)解決IP被封問(wèn)題
有時(shí)候抓取的站點(diǎn)會(huì)封我們的IP,公司的外網(wǎng)IP又是固定的乃沙,我們可以通過(guò)ADSL撥號(hào)的方式接入另一個(gè)網(wǎng)絡(luò)起趾,在ADSL網(wǎng)絡(luò)的服務(wù)器上搭建代理服務(wù)器,爬蟲程序所在的服務(wù)器通過(guò)代理該臺(tái)服務(wù)器上網(wǎng)警儒,這樣再也不怕對(duì)方站點(diǎn)封您的IP了训裆,讓爬蟲程序通過(guò)代理爬取網(wǎng)頁(yè),代碼如下:
HttpClientDownloader downloader= new HttpClientDownloader();
downloader.setProxyProvider(SimpleProxyProvider.from(new Proxy("IP",PORT,"username","password")));
spider.setDownloader(httpClientDownloader)
服務(wù)器端返回403錯(cuò)誤
有些網(wǎng)站會(huì)有一些爬蟲的識(shí)別機(jī)制蜀铲,影響您利用爬蟲去爬數(shù)據(jù)時(shí)边琉,會(huì)碰到服務(wù)端的403拒絕訪問(wèn)錯(cuò)誤,這時(shí)记劝,我們應(yīng)該根據(jù)實(shí)際訪問(wèn)網(wǎng)址的情況变姨,構(gòu)造userAgent,Cookie等數(shù)據(jù)厌丑,以便達(dá)到可訪問(wèn)網(wǎng)站的目的定欧,典型的應(yīng)用場(chǎng)景是爬取Discuz!
論壇的列表數(shù)據(jù),已爬取寧波當(dāng)?shù)氐姆慨a(chǎn)論壇列表為例怒竿,利用WebMagic
編寫的爬蟲代碼如下:
package test.springboot2.webmagic;
import java.util.List;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
public class TestBBS {
public static void main(String[] args) {
Spider.create(new PageProcessor() {
private Site site = Site.me().setCharset("GBK").setRetryTimes(3)
.addCookie("7nPA_2132_atarget", "1")
.addCookie("7nPA_2132_forum_lastvisit", "D_57_1544683126")
.addCookie("7nPA_2132_lastact", "1544683156%09forum.php%09ajax")
.addCookie("7nPA_2132_lastvisit", "1544679421")
.addCookie("7nPA_2132_saltkey", "Y41kZTOe")
.addCookie("7nPA_2132_sendmail", "1")
.addCookie("7nPA_2132_sid", "HpKlUl")
.addCookie("7nPA_2132_st_t", "0%7C1544683126%7Ca40d86281e77b87595a73a73f8a8d6ab")
.addCookie("7nPA_2132_visitedfid", "57")
.addHeader("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
.addHeader("Accept-Encoding","gzip, deflate, br")
.addHeader("Accept-Language","zh-CN,zh;q=0.9")
.addHeader("Cache-Control","max-age=0")
.addHeader("Connection","keep-alive")
.addHeader("Cookie","7nPA_2132_saltkey=Y41kZTOe; 7nPA_2132_lastvisit=1544679421; 7nPA_2132_st_t=0%7C1544683021%7C8f3386d0af3b92c0e84bee8dce32f193; 7nPA_2132_atarget=1; 7nPA_2132_forum_lastvisit=D_57_1544683021; 7nPA_2132_visitedfid=57; 7nPA_2132_sendmail=1; 7nPA_2132_sid=NXsz2s; 7nPA_2132_lastact=1544683081%09forum.php%09ajax")
.addHeader("DNT","1")
.addHeader("Referer","https://bbs.cnnb.com.cn/member.php?mod=logging&action=logout&formhash=7663dd1d")
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36")
.setSleepTime(1000);
@Override
public void process(Page page) {
List<String> titles = page.getHtml().xpath("http://a[@class='s xst']/text()").all();
page.putField("title",titles);
}
@Override
public Site getSite() {
return site;
}
})
.addUrl("http://bbs.cnnb.com.cn/forum.php?mod=forumdisplay&fid=57")
.thread(1).start();
}
}
爬取Ajax動(dòng)態(tài)數(shù)據(jù)
有些網(wǎng)站采用異步Ajax的方式獲取列表數(shù)據(jù)忧额,這些列表數(shù)據(jù)我們無(wú)法直接通過(guò)下載網(wǎng)址的文件得到,需要利用虛擬瀏覽器技術(shù)愧口,以ChromeDriver
為例睦番,利用selenium-java
編寫的爬蟲程序如下:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.downloader.selenium.SeleniumDownloader;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;
public class TestGetStockList{
public static void main(String[] args) {
System.setProperty("selenuim_config","D:\\workspace_spider\\webmagic\\webmagic-selenium\\src\\test\\resources\\config.ini");
Spider.create(new PageProcessor() {
private Site site = Site.me().setCharset("utf-8").setRetryTimes(3).setSleepTime(1000);
@Override
public void process(Page page) {
WebDriver driver = new ChromeDriver();
driver.get("http://www.sse.com.cn/assortment/stock/list/info/company/index.shtml?COMPANY_CODE=600000");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
WebElement webElement = driver.findElement(By.id("tableData_stockListCompany"));
String str = webElement.getAttribute("outerHTML");
System.out.println(str);
Html html = new Html(str);
System.out.println(html.xpath("http://tbody/tr").all());
String companyCode = html.xpath("http://tbody/tr[1]/td/text()").get();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
String dateString = html.xpath("http://tbody/tr[3]/td/text()").get().split("/")[0];
String stockCode = html.xpath("http://tbody/tr[2]/td/text()").get().split("/")[0];
String name = html.xpath("http://tbody/tr[5]/td/text()").get().split("/")[0];
String department = html.xpath("http://tbody/tr[14]/td/text()").get().split("/")[0];
System.out.println(companyCode);
System.out.println(stockCode);
System.out.println(name);
System.out.println(department);
driver.close();
}
@Override
public Site getSite() {
return site;
}
}).thread(5)
.addUrl("http://www.sse.com.cn/assortment/stock/list/info/company/index.shtml?COMPANY_CODE=600000")
.run();
}
}
注:如果要讓代碼運(yùn)行成功需要下載一個(gè)chromedriver,如果你是windows可以去這個(gè)網(wǎng)址去下https://chromedriver.storage.googleapis.com/2.25/chromedriver_win32.zip耍属,雖然是32位的但是64位也可以用托嚣,如果不行的話或者你是其他OS,可以去官網(wǎng)下https://chromedriver.storage.googleapis.com/index.html?path=2.27/
https站點(diǎn)問(wèn)題
有些https站點(diǎn)只支持TLS1.2厚骗,比如大名鼎鼎的Github網(wǎng)站示启,這時(shí)你需要修改框架源碼,找到如下類:
去除如下代碼的紅框部分:
否則將無(wú)法爬蟲https站點(diǎn)數(shù)據(jù)领舰。
處理非Get請(qǐng)求的爬蟲
有時(shí)候夫嗓,我們需要爬取非Get請(qǐng)求的數(shù)據(jù),代碼如下(其他諸如DELETE冲秽,PUT等請(qǐng)求方式類似):
Request req = new Request("http://www.baidu.com");
req.setMethod(HttpConstant.Method.POST)
//也可以采用form提交的方式.HttpRequestBody.form(map)
.setRequestBody(HttpRequestBody.json("{'id':1}","utf-8"));
spider.addRequest(req);
典型案例
抓取列表+詳解
這里我們以作者的新浪博客http://blog.sina.com.cn/flashsword20作為例子舍咖。在這個(gè)例子里,我們要從最終的博客文章頁(yè)面锉桑,抓取博客的標(biāo)題排霉、內(nèi)容、日期等信息民轴,也要從列表頁(yè)抓取博客的鏈接等信息攻柠,從而獲取這個(gè)博客的所有文章球订。
在這個(gè)例子中,我們的主要使用幾個(gè)方法:
- 從頁(yè)面指定位置發(fā)現(xiàn)鏈接瑰钮,使用正則表達(dá)式來(lái)過(guò)濾鏈接.
- 在PageProcessor中處理兩種頁(yè)面冒滩,根據(jù)頁(yè)面URL來(lái)區(qū)分需要如何處理。
package test.springboot2.webmagic;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
/**
* @author code4crafter@gmail.com <br>
*/
public class SinaBlogProcessor implements PageProcessor {
public static final String URL_LIST = "http://blog\\.sina\\.com\\.cn/s/articlelist_1487828712_0_\\d+\\.html";
public static final String URL_POST = "http://blog\\.sina\\.com\\.cn/s/blog_\\w+\\.html";
private Site site = Site
.me()
.setDomain("blog.sina.com.cn")
.setSleepTime(3000)
.setUserAgent(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31");
@Override
public void process(Page page) {
//列表頁(yè)
if (page.getUrl().regex(URL_LIST).match()) {
page.addTargetRequests(page.getHtml().xpath("http://div[@class=\"articleList\"]").links().regex(URL_POST).all());
page.addTargetRequests(page.getHtml().links().regex(URL_LIST).all());
//文章頁(yè)
} else {
page.putField("title", page.getHtml().xpath("http://div[@class='articalTitle']/h2"));
page.putField("content", page.getHtml().xpath("http://div[@id='articlebody']//div[@class='articalContent']"));
page.putField("date",
page.getHtml().xpath("http://div[@id='articlebody']//span[@class='time SG_txtc']").regex("\\((.*)\\)"));
}
}
@Override
public Site getSite() {
return site;
}
public static void main(String[] args) {
Spider.create(new SinaBlogProcessor())
.addUrl("http://blog.sina.com.cn/s/articlelist_1487828712_0_1.html")
.run();
}
}
后記
基于以上爬蟲思路浪谴,如果讓你設(shè)計(jì)一個(gè)爬蟲管理控制中心旦部,你該如何設(shè)計(jì)與實(shí)現(xiàn)呢?請(qǐng)關(guān)注我們后續(xù)文章较店,感謝閱讀士八!