org.apache.nutch.util.SitemapProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nutch.util.SitemapProcessor.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nutch.util;

import java.io.IOException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.List;
import java.util.Random;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.MultipleInputs;
import org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat;
import org.apache.hadoop.mapreduce.lib.map.MultithreadedMapper;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.MapFileOutputFormat;
import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

import org.apache.nutch.crawl.CrawlDatum;
import org.apache.nutch.hostdb.HostDatum;
import org.apache.nutch.net.URLFilters;
import org.apache.nutch.net.URLNormalizers;
import org.apache.nutch.protocol.Content;
import org.apache.nutch.protocol.Protocol;
import org.apache.nutch.protocol.ProtocolFactory;
import org.apache.nutch.protocol.ProtocolOutput;
import org.apache.nutch.protocol.ProtocolStatus;
import org.apache.nutch.util.NutchJob;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import crawlercommons.robots.BaseRobotRules;
import crawlercommons.sitemaps.AbstractSiteMap;
import crawlercommons.sitemaps.SiteMap;
import crawlercommons.sitemaps.SiteMapIndex;
import crawlercommons.sitemaps.SiteMapParser;
import crawlercommons.sitemaps.SiteMapURL;

/**
 * <p>Performs Sitemap processing by fetching sitemap links, parsing the content and merging
 * the urls from Sitemap (with the metadata) with the existing crawldb.</p>
 *
 * <p>There are two use cases supported in Nutch's Sitemap processing:</p>
 * <ol>
 *  <li>Sitemaps are considered as "remote seed lists". Crawl administrators can prepare a
 *     list of sitemap links and get only those sitemap pages. This suits well for targeted
 *     crawl of specific hosts.</li>
 *  <li>For open web crawl, it is not possible to track each host and get the sitemap links
 *     manually. Nutch would automatically get the sitemaps for all the hosts seen in the
 *     crawls and inject the urls from sitemap to the crawldb.</li>
 * </ol>
 *
 * <p>For more details see:
 *      https://wiki.apache.org/nutch/SitemapFeature </p>
 */
public class SitemapProcessor extends Configured implements Tool {
    public static final Logger LOG = LoggerFactory.getLogger(SitemapProcessor.class);
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static final String CURRENT_NAME = "current";
    public static final String LOCK_NAME = ".locked";
    public static final String SITEMAP_STRICT_PARSING = "sitemap.strict.parsing";
    public static final String SITEMAP_URL_FILTERING = "sitemap.url.filter";
    public static final String SITEMAP_URL_NORMALIZING = "sitemap.url.normalize";
    public static final String SITEMAP_ALWAYS_TRY_SITEMAPXML_ON_ROOT = "sitemap.url.default.sitemap.xml";
    public static final String SITEMAP_OVERWRITE_EXISTING = "sitemap.url.overwrite.existing";
    public static final String SITEMAP_REDIR_MAX = "sitemap.redir.max";

    private static class SitemapMapper extends Mapper<Text, Writable, Text, CrawlDatum> {
        private ProtocolFactory protocolFactory = null;
        private boolean strict = true;
        private boolean filter = true;
        private boolean normalize = true;
        private boolean tryDefaultSitemapXml = true;
        private int maxRedir = 3;
        private URLFilters filters = null;
        private URLNormalizers normalizers = null;
        private CrawlDatum datum = new CrawlDatum();
        private SiteMapParser parser = null;

        public void setup(Context context) {
            Configuration conf = context.getConfiguration();
            this.protocolFactory = new ProtocolFactory(conf);
            this.filter = conf.getBoolean(SITEMAP_URL_FILTERING, true);
            this.normalize = conf.getBoolean(SITEMAP_URL_NORMALIZING, true);
            this.strict = conf.getBoolean(SITEMAP_STRICT_PARSING, true);
            this.tryDefaultSitemapXml = conf.getBoolean(SITEMAP_ALWAYS_TRY_SITEMAPXML_ON_ROOT, true);
            this.maxRedir = conf.getInt(SITEMAP_REDIR_MAX, 3);
            this.parser = new SiteMapParser(strict);

            if (filter) {
                filters = new URLFilters(conf);
            }
            if (normalize) {
                normalizers = new URLNormalizers(conf, URLNormalizers.SCOPE_DEFAULT);
            }
        }

        public void map(Text key, Writable value, Context context) throws IOException, InterruptedException {
            String url;

            try {
                if (value instanceof CrawlDatum) {
                    // If its an entry from CrawlDb, emit it. It will be merged in the reducer
                    context.write(key, (CrawlDatum) value);
                } else if (value instanceof HostDatum) {
                    generateSitemapsFromHostname(key.toString(), context);
                } else if (value instanceof Text) {
                    // Input can be sitemap URL or hostname
                    url = key.toString();
                    if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("ftp://")
                            || url.startsWith("file:/")) {
                        // For entry from sitemap urls file, fetch the sitemap, extract urls and emit those
                        if ((url = filterNormalize(url)) == null) {
                            context.getCounter("Sitemap", "filtered_records").increment(1);
                            return;
                        }

                        context.getCounter("Sitemap", "sitemap_seeds").increment(1);
                        generateSitemapUrlDatum(protocolFactory.getProtocol(url), url, context);
                    } else {
                        LOG.info("generateSitemapsFromHostname: " + key.toString());
                        generateSitemapsFromHostname(key.toString(), context);
                    }
                }
            } catch (Exception e) {
                LOG.warn("Exception for record {} : {}", key.toString(), StringUtils.stringifyException(e));
            }
        }

        /* Filters and or normalizes the input URL */
        private String filterNormalize(String url) {
            try {
                if (normalizers != null)
                    url = normalizers.normalize(url, URLNormalizers.SCOPE_DEFAULT);

                if (filters != null)
                    url = filters.filter(url);
            } catch (Exception e) {
                return null;
            }
            return url;
        }

        private void generateSitemapsFromHostname(String host, Context context) {
            try {
                // For entry from hostdb, get sitemap url(s) from robots.txt, fetch the sitemap,
                // extract urls and emit those

                // try different combinations of schemes one by one till we get rejection in all cases
                String url;
                if ((url = filterNormalize("http://" + host + "/")) == null
                        && (url = filterNormalize("https://" + host + "/")) == null
                        && (url = filterNormalize("ftp://" + host + "/")) == null
                        && (url = filterNormalize("file:/" + host + "/")) == null) {
                    context.getCounter("Sitemap", "filtered_records").increment(1);
                    return;
                }
                // We may wish to use the robots.txt content as the third parameter for .getRobotRules
                BaseRobotRules rules = protocolFactory.getProtocol(url).getRobotRules(new Text(url), datum, null);
                List<String> sitemaps = rules.getSitemaps();

                if (tryDefaultSitemapXml && sitemaps.size() == 0) {
                    sitemaps.add(url + "sitemap.xml");
                }
                for (String sitemap : sitemaps) {
                    context.getCounter("Sitemap", "sitemaps_from_hostname").increment(1);
                    sitemap = filterNormalize(sitemap);
                    if (sitemap == null) {
                        context.getCounter("Sitemap", "filtered_sitemaps_from_hostname").increment(1);
                    } else {
                        generateSitemapUrlDatum(protocolFactory.getProtocol(sitemap), sitemap, context);
                    }
                }
            } catch (Exception e) {
                LOG.warn("Exception for record {} : {}", host, StringUtils.stringifyException(e));
            }
        }

        private void generateSitemapUrlDatum(Protocol protocol, String url, Context context) throws Exception {
            ProtocolOutput output = protocol.getProtocolOutput(new Text(url), datum);
            ProtocolStatus status = output.getStatus();
            Content content = output.getContent();

            // Following redirects http > https and what else
            int maxRedir = this.maxRedir;
            while (!output.getStatus().isSuccess() && output.getStatus().isRedirect() && maxRedir > 0) {
                String[] stuff = output.getStatus().getArgs();
                url = filterNormalize(stuff[0]);

                // get out!
                if (url == null) {
                    break;
                }
                output = protocol.getProtocolOutput(new Text(url), datum);
                status = output.getStatus();
                content = output.getContent();

                maxRedir--;
            }

            if (status.getCode() != ProtocolStatus.SUCCESS) {
                // If there were any problems fetching the sitemap, log the error and let it go. Not sure how often
                // sitemaps are redirected. In future we might have to handle redirects.
                context.getCounter("Sitemap", "failed_fetches").increment(1);
                LOG.error("Error while fetching the sitemap. Status code: {} for {}", status.getCode(), url);
                return;
            }

            AbstractSiteMap asm = parser.parseSiteMap(content.getContentType(), content.getContent(), new URL(url));

            if (asm instanceof SiteMap) {
                LOG.info("Parsing sitemap file: {}", asm.getUrl().toString());
                SiteMap sm = (SiteMap) asm;
                Collection<SiteMapURL> sitemapUrls = sm.getSiteMapUrls();
                for (SiteMapURL sitemapUrl : sitemapUrls) {
                    // If 'strict' is ON, only allow valid urls. Else allow all urls
                    if (!strict || sitemapUrl.isValid()) {
                        String key = filterNormalize(sitemapUrl.getUrl().toString());

                        if (key != null) {
                            CrawlDatum sitemapUrlDatum = new CrawlDatum();
                            sitemapUrlDatum.setStatus(CrawlDatum.STATUS_INJECTED);
                            sitemapUrlDatum.setScore((float) sitemapUrl.getPriority());

                            if (sitemapUrl.getChangeFrequency() != null) {
                                int fetchInterval = -1;
                                switch (sitemapUrl.getChangeFrequency()) {
                                case ALWAYS:
                                    fetchInterval = 1;
                                    break;
                                case HOURLY:
                                    fetchInterval = 3600;
                                    break; // 60*60
                                case DAILY:
                                    fetchInterval = 86400;
                                    break; // 60*60*24
                                case WEEKLY:
                                    fetchInterval = 604800;
                                    break; // 60*60*24*7
                                case MONTHLY:
                                    fetchInterval = 2592000;
                                    break; // 60*60*24*30
                                case YEARLY:
                                    fetchInterval = 31536000;
                                    break; // 60*60*24*365
                                case NEVER:
                                    fetchInterval = Integer.MAX_VALUE;
                                    break; // Loose "NEVER" contract
                                }
                                sitemapUrlDatum.setFetchInterval(fetchInterval);
                            }

                            if (sitemapUrl.getLastModified() != null) {
                                sitemapUrlDatum.setModifiedTime(sitemapUrl.getLastModified().getTime());
                            }

                            context.write(new Text(key), sitemapUrlDatum);
                        }
                    }
                }
            } else if (asm instanceof SiteMapIndex) {
                SiteMapIndex index = (SiteMapIndex) asm;
                Collection<AbstractSiteMap> sitemapUrls = index.getSitemaps();

                if (sitemapUrls.isEmpty()) {
                    return;
                }

                LOG.info("Parsing sitemap index file: {}", index.getUrl().toString());
                for (AbstractSiteMap sitemap : sitemapUrls) {
                    String sitemapUrl = filterNormalize(sitemap.getUrl().toString());
                    if (sitemapUrl != null) {
                        generateSitemapUrlDatum(protocol, sitemapUrl, context);
                    }
                }
            }
        }
    }

    private static class SitemapReducer extends Reducer<Text, CrawlDatum, Text, CrawlDatum> {
        CrawlDatum sitemapDatum = null;
        CrawlDatum originalDatum = null;

        private boolean overwriteExisting = false; // DO NOT ENABLE!!

        public void setup(Context context) {
            Configuration conf = context.getConfiguration();
            this.overwriteExisting = conf.getBoolean(SITEMAP_OVERWRITE_EXISTING, false);
        }

        public void reduce(Text key, Iterable<CrawlDatum> values, Context context)
                throws IOException, InterruptedException {
            sitemapDatum = null;
            originalDatum = null;

            for (CrawlDatum curr : values) {
                if (curr.getStatus() == CrawlDatum.STATUS_INJECTED) {
                    sitemapDatum = new CrawlDatum();
                    sitemapDatum.set(curr);
                } else {
                    originalDatum = new CrawlDatum();
                    originalDatum.set(curr);
                }
            }

            if (originalDatum != null) {
                // The url was already present in crawldb. If we got the same url from sitemap too, save
                // the information from sitemap to the original datum. Emit the original crawl datum
                if (sitemapDatum != null && overwriteExisting) {
                    originalDatum.setScore(sitemapDatum.getScore());
                    originalDatum.setFetchInterval(sitemapDatum.getFetchInterval());
                    originalDatum.setModifiedTime(sitemapDatum.getModifiedTime());
                }

                context.getCounter("Sitemap", "existing_sitemap_entries").increment(1);
                context.write(key, originalDatum);
            } else if (sitemapDatum != null) {
                // For the newly discovered links via sitemap, set the status as unfetched and emit
                context.getCounter("Sitemap", "new_sitemap_entries").increment(1);
                sitemapDatum.setStatus(CrawlDatum.STATUS_DB_UNFETCHED);
                context.write(key, sitemapDatum);
            }
        }
    }

    public void sitemap(Path crawldb, Path hostdb, Path sitemapUrlDir, boolean strict, boolean filter,
            boolean normalize, int threads) throws Exception {
        long start = System.currentTimeMillis();
        if (LOG.isInfoEnabled()) {
            LOG.info("SitemapProcessor: Starting at {}", sdf.format(start));
        }

        FileSystem fs = crawldb.getFileSystem(getConf());
        Path old = new Path(crawldb, "old");
        Path current = new Path(crawldb, "current");
        Path tempCrawlDb = new Path(crawldb,
                "crawldb-" + Integer.toString(new Random().nextInt(Integer.MAX_VALUE)));

        // lock an existing crawldb to prevent multiple simultaneous updates
        Path lock = new Path(crawldb, LOCK_NAME);
        if (!fs.exists(current))
            fs.mkdirs(current);

        LockUtil.createLockFile(fs, lock, false);

        Configuration conf = getConf();
        conf.setBoolean(SITEMAP_STRICT_PARSING, strict);
        conf.setBoolean(SITEMAP_URL_FILTERING, filter);
        conf.setBoolean(SITEMAP_URL_NORMALIZING, normalize);
        conf.setBoolean("mapreduce.fileoutputcommitter.marksuccessfuljobs", false);

        Job job = Job.getInstance(conf, "SitemapProcessor_" + crawldb.toString());
        job.setJarByClass(SitemapProcessor.class);

        // add crawlDb, sitemap url directory and hostDb to input paths
        MultipleInputs.addInputPath(job, current, SequenceFileInputFormat.class);

        if (sitemapUrlDir != null)
            MultipleInputs.addInputPath(job, sitemapUrlDir, KeyValueTextInputFormat.class);

        if (hostdb != null)
            MultipleInputs.addInputPath(job, new Path(hostdb, CURRENT_NAME), SequenceFileInputFormat.class);

        FileOutputFormat.setOutputPath(job, tempCrawlDb);

        job.setOutputFormatClass(MapFileOutputFormat.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(CrawlDatum.class);

        job.setMapperClass(MultithreadedMapper.class);
        MultithreadedMapper.setMapperClass(job, SitemapMapper.class);
        MultithreadedMapper.setNumberOfThreads(job, threads);
        job.setReducerClass(SitemapReducer.class);

        try {
            boolean success = job.waitForCompletion(true);
            if (!success) {
                String message = "SitemapProcessor_" + crawldb.toString() + " job did not succeed, job status: "
                        + job.getStatus().getState() + ", reason: " + job.getStatus().getFailureInfo();
                LOG.error(message);
                NutchJob.cleanupAfterFailure(tempCrawlDb, lock, fs);
                // throw exception so that calling routine can exit with error
                throw new RuntimeException(message);
            }

            boolean preserveBackup = conf.getBoolean("db.preserve.backup", true);
            if (!preserveBackup && fs.exists(old))
                fs.delete(old, true);
            else
                FSUtils.replace(fs, old, current, true);

            FSUtils.replace(fs, current, tempCrawlDb, true);
            LockUtil.removeLockFile(fs, lock);

            if (LOG.isInfoEnabled()) {
                long filteredRecords = job.getCounters().findCounter("Sitemap", "filtered_records").getValue();
                long fromHostname = job.getCounters().findCounter("Sitemap", "sitemaps_from_hostname").getValue();
                long fromSeeds = job.getCounters().findCounter("Sitemap", "sitemap_seeds").getValue();
                long failedFetches = job.getCounters().findCounter("Sitemap", "failed_fetches").getValue();
                long newSitemapEntries = job.getCounters().findCounter("Sitemap", "new_sitemap_entries").getValue();

                LOG.info("SitemapProcessor: Total records rejected by filters: {}", filteredRecords);
                LOG.info("SitemapProcessor: Total sitemaps from host name: {}", fromHostname);
                LOG.info("SitemapProcessor: Total sitemaps from seed urls: {}", fromSeeds);
                LOG.info("SitemapProcessor: Total failed sitemap fetches: {}", failedFetches);
                LOG.info("SitemapProcessor: Total new sitemap entries added: {}", newSitemapEntries);

                long end = System.currentTimeMillis();
                LOG.info("SitemapProcessor: Finished at {}, elapsed: {}", sdf.format(end),
                        TimingUtil.elapsedTime(start, end));
            }
        } catch (IOException | InterruptedException | ClassNotFoundException e) {
            LOG.error("SitemapProcessor_" + crawldb.toString(), e);
            NutchJob.cleanupAfterFailure(tempCrawlDb, lock, fs);
            throw e;
        }
    }

    public static void main(String[] args) throws Exception {
        int res = ToolRunner.run(NutchConfiguration.create(), new SitemapProcessor(), args);
        System.exit(res);
    }

    public static void usage() {
        System.err.println("Usage:\n SitemapProcessor <crawldb> [-hostdb <hostdb>] [-sitemapUrls <url_dir>] "
                + "[-threads <threads>] [-force] [-noStrict] [-noFilter] [-noNormalize]\n");

        System.err.println("\t<crawldb>\t\tpath to crawldb where the sitemap urls would be injected");
        System.err.println("\t-hostdb <hostdb>\tpath of a hostdb. Sitemap(s) from these hosts would be downloaded");
        System.err.println("\t-sitemapUrls <url_dir>\tpath to directory with sitemap urls or hostnames");
        System.err.println(
                "\t-threads <threads>\tNumber of threads created per mapper to fetch sitemap urls (default: 8)");
        System.err.println("\t-force\t\t\tforce update even if CrawlDb appears to be locked (CAUTION advised)");
        System.err.println(
                "\t-noStrict\t\tBy default Sitemap parser rejects invalid urls. '-noStrict' disables that.");
        System.err.println("\t-noFilter\t\tturn off URLFilters on urls (optional)");
        System.err.println("\t-noNormalize\t\tturn off URLNormalizer on urls (optional)");
    }

    public int run(String[] args) throws Exception {
        if (args.length < 3) {
            usage();
            return -1;
        }

        Path crawlDb = new Path(args[0]);
        Path hostDb = null;
        Path urlDir = null;
        boolean strict = true;
        boolean filter = true;
        boolean normalize = true;
        int threads = 8;

        for (int i = 1; i < args.length; i++) {
            if (args[i].equals("-hostdb")) {
                hostDb = new Path(args[++i]);
                LOG.info("SitemapProcessor: hostdb: {}", hostDb);
            } else if (args[i].equals("-sitemapUrls")) {
                urlDir = new Path(args[++i]);
                LOG.info("SitemapProcessor: sitemap urls dir: {}", urlDir);
            } else if (args[i].equals("-threads")) {
                threads = Integer.valueOf(args[++i]);
                LOG.info("SitemapProcessor: threads: {}", threads);
            } else if (args[i].equals("-noStrict")) {
                LOG.info("SitemapProcessor: 'strict' parsing disabled");
                strict = false;
            } else if (args[i].equals("-noFilter")) {
                LOG.info("SitemapProcessor: filtering disabled");
                filter = false;
            } else if (args[i].equals("-noNormalize")) {
                LOG.info("SitemapProcessor: normalizing disabled");
                normalize = false;
            } else {
                LOG.info("SitemapProcessor: Found invalid argument \"{}\"\n", args[i]);
                usage();
                return -1;
            }
        }

        try {
            sitemap(crawlDb, hostDb, urlDir, strict, filter, normalize, threads);
            return 0;
        } catch (Exception e) {
            LOG.error("SitemapProcessor: {}", StringUtils.stringifyException(e));
            return -1;
        }
    }
}