北京大學軟件與微電子學院 曹路 2018/03/20 https://github.com/andrewcao95/nutch-crawlerhtml
爬蟲基本能夠分3類:java
Nutch是apache旗下的一個用Java實現的開源索引引擎項目,經過nutch,誕生了hadoop、tika、gora。Nutch的設計初衷主要是爲了解決下述兩個問題:git
Nutch1.2版本以後,Nutch已經從搜索引擎演化爲網絡爬蟲,演化爲兩大分支版本:1.X和2.X,最大區別在於2.X對底層的數據存儲進行了抽象以支持各類底層存儲技術,其中:github
Nutch的編譯安裝須要JDK、Ant等環境,爲此本次使用的環境和工具以下:算法
本次JDK使用1.8.0_161,在Oracle官網http://www.oracle.com/technetwork/java/javase/downloads/index.html 能夠下載apache
首次使用須要設置環境變量,在~/.bashrc
的末尾加入如下內容:vim
#set jdk environment export JAVA_HOME=/java/jdk1.8.0_161 export JRE_HOME=$JAVA_HOME/jre export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
而後使用source ~/.bashrc
命令使得環境變量生效。bash
Nutch分爲bin和src,bin是運行包、src是源碼包,本次咱們使用nutch-1.9源碼包本身編譯,推薦使用SVN進行安裝,SVN checkout出來的有pom.xml文件,即maven文件。網絡
$ sudo apt install subversion $ svn co https://svn.apache.org/repos/asf/nutch/tags/release-1.9
到Ant官網 http://ant.apache.org/bindownload.cgi 下載最新版的Ant。oracle
一樣也須要設置環境變量
#set ant environment export ANT_HOME=/ant/ant-1.9.10 export PATH=$PATH:$ANT_HOME/bin
略
把 conf/下的 nutch-site.xml.template複製一份,命名爲nutch-site.xml,在裏面添加配置:
<property> <name>http.agent.name</name> <value>My Nutch Spider</value> </property> <property> <name>plugin.folders</name> <value>$NUTCH_HOME/build/plugins</value> </property>
$NUTCH_HOME是指nutch源碼的根目錄,例如個人是/home/andrewcao95/Desktop/release-1.9
生成Eclipse項目文件,即.project文件,使用以下命令
ant eclipse
耐心等待,這個過程ant會根據ivy從中心倉庫下載各類依賴jar包,可能要幾分鐘。這裏特別要注意網絡通暢,學院的網絡可能存在必定的問題,致使不少jar包沒法訪問,最終會致使編譯失敗。同時要注意原來的配置文件中包的下載地址會發生變化,爲此須要根據報錯指令進行相對應的調整。
解決了上述問題以後,很快就能編譯成功
按照RunNutchInEclipse的教程指導,很快就能導入項目。以後咱們就能看到Nutch項目的完整源代碼
源碼導入工程後,並不能執行完整的爬取。Nutch將爬取的流程切分紅不少階段,每一個階段分別封裝在一個類的main函數中。咱們首先運行Nutch中最簡單的流程:Inject。
咱們知道爬蟲在初始階段是須要人工給出一個或多個url,做爲起始點(廣度遍歷樹的樹根)。Inject的做用,就是把用戶寫在文件裏的種子(一行一個url,是TextInputFormat),插入到爬蟲的URL管理文件(crawldb,是SequenceFile)中。
從src文件夾中找到org.apache.nutch.crawl.Injector類,能夠看到,main函數實際上是利用ToolRunner,執行了run(String[] args)。這裏ToolRunner.run會從第二個參數(new Injector())這個對象中,找到run(String[] args)這個方法執行。從run方法中能夠看出來,String[] args須要有2個參數,第一個參數表示爬蟲的URL管理文件夾(輸出),第二個參數表示種子文件夾(輸入)。對hadoop中的map reduce程序來講,輸入文件夾是必須存在的,輸出文件夾應該不存在。咱們建立一個文件夾 urls,來存放種子文件(做爲輸入)。在seed.txt中加入一個種子URL。
指定一個文件夾crawldb來做爲URL管理文件夾(輸出)。有一種簡單的方法來指定args,直接在main函數下加一行:
args=new String[]{"/home/andrewcao95/Desktop/release-1.9/crawldb","/home/andrewcao95/Desktop/release-1.9/urls"};
運行該類,能夠看到運行成功。
讀取爬蟲文件:
查看裏面的data文件
vim /home/andrewcao95/Desktop/release-1.9/crawldb/current/part-00000/data
這是一個SequenceFile,Nutch中除了Inject的輸入(種子)以外,其餘文件所有以SequenceFile的形式存儲。SequenceFile的結構以下:
key0 value0 key1 value1 key2 value2 ...... keyn valuen
以key value的形式,將對象序列(key value序列)存儲到文件中。咱們從SequenceFile頭部能夠看出來key value的類型。
上面的SequenceFile中,能夠看出key的類型是org.apache.hadoop.io.Text,value的類型是org.apache.nutch.crawl.CrawlDatum。
首先咱們須要讀取SequenceFile的代碼,在src/java裏新建一個類org.apache.nutch.example.InjectorReader,代碼以下:
package org.apache.nutch.example; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.SequenceFile; import org.apache.hadoop.io.Text; import org.apache.nutch.crawl.CrawlDatum; import java.io.IOException; public class InjectorReader { public static void main(String[] args) throws IOException { Configuration conf=new Configuration(); Path dataPath=new Path("/home/andrewcao95/Desktop/release-1.9/crawldb/current/part-00000/data"); FileSystem fs=dataPath.getFileSystem(conf); SequenceFile.Reader reader=new SequenceFile.Reader(fs,dataPath,conf); Text key=new Text(); CrawlDatum value=new CrawlDatum(); while(reader.next(key,value)){ System.out.println("key:"+key); System.out.println("value:"+value); } reader.close(); } }
獲得的運行結果如圖所示:
咱們能夠看到,程序讀出了剛纔Inject到crawldb的url,key是url,value是一個CrawlDatum對象,這個對象用來維護爬蟲的URL管理信息,咱們能夠看到一行Status: 1 (db_unfetched)
,這表示表示當前url爲未爬取狀態,在後續流程中,爬蟲會從crawldb取未爬取的url進行爬取。
在爬取以前,咱們先修改一下conf/nutch-default.xml中的一個地方,這個值會在發送http請求時,做爲User-Agent字段。
在org.apache.nutch.crawl中新建一個Crawl.java文件,代碼以下所示
package org.apache.nutch.crawl; import java.util.*; import java.text.*; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.fs.*; import org.apache.hadoop.conf.*; import org.apache.hadoop.mapred.*; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner; import org.apache.nutch.parse.ParseSegment; import org.apache.nutch.indexer.IndexingJob; import org.apache.nutch.util.HadoopFSUtil; import org.apache.nutch.util.NutchConfiguration; import org.apache.nutch.util.NutchJob; import org.apache.nutch.fetcher.Fetcher; public class Crawl extends Configured implements Tool { public static final Logger LOG = LoggerFactory.getLogger(Crawl.class); private static String getDate() { return new SimpleDateFormat("yyyyMMddHHmmss").format (new Date(System.currentTimeMillis())); } public static void main(String args[]) throws Exception { Configuration conf = NutchConfiguration.create(); int res = ToolRunner.run(conf, new Crawl(), args); System.exit(res); } @Override public int run(String[] args) throws Exception { /*種子所在文件夾*/ Path rootUrlDir = new Path("/home/andrewcao95/Desktop/release-1.9/urls"); /*存儲爬取信息的文件夾*/ Path dir = new Path("/home/andrewcao95/Desktop/release-1.9","crawl-" + getDate()); int threads = 50; /*廣度遍歷時爬取的深度,即廣度遍歷樹的層數*/ int depth = 2; long topN = 10; JobConf job = new NutchJob(getConf()); FileSystem fs = FileSystem.get(job); if (LOG.isInfoEnabled()) { LOG.info("crawl started in: " + dir); LOG.info("rootUrlDir = " + rootUrlDir); LOG.info("threads = " + threads); LOG.info("depth = " + depth); if (topN != Long.MAX_VALUE) LOG.info("topN = " + topN); } Path crawlDb = new Path(dir + "/crawldb"); Path linkDb = new Path(dir + "/linkdb"); Path segments = new Path(dir + "/segments"); Path indexes = new Path(dir + "/indexes"); Path index = new Path(dir + "/index"); Path tmpDir = job.getLocalPath("crawl"+Path.SEPARATOR+getDate()); Injector injector = new Injector(getConf()); Generator generator = new Generator(getConf()); Fetcher fetcher = new Fetcher(getConf()); ParseSegment parseSegment = new ParseSegment(getConf()); CrawlDb crawlDbTool = new CrawlDb(getConf()); LinkDb linkDbTool = new LinkDb(getConf()); // initialize crawlDb injector.inject(crawlDb, rootUrlDir); int i; for (i = 0; i < depth; i++) { // generate new segment Path[] segs = generator.generate(crawlDb, segments, -1, topN, System .currentTimeMillis()); if (segs == null) { LOG.info("Stopping at depth=" + i + " - no more URLs to fetch."); break; } fetcher.fetch(segs[0], threads); // fetch it if (!Fetcher.isParsing(job)) { parseSegment.parse(segs[0]); // parse it, if needed } crawlDbTool.update(crawlDb, segs, true, true); // update crawldb } if (LOG.isInfoEnabled()) { LOG.info("crawl finished: " + dir); } return 0; } }
經過上述代碼能夠執行一次完整的爬取,程序運行結果以下所示:
運行成功,對網站(http://pku.edu.cn/)進行了2層爬取,爬取信息都保存在/tmp/crawl+時間的文件夾中