Java写网络爬虫基础篇(一)

    最近项目中有涉及到爬虫功能,目前做的还比较基础的,主要是在前人的一些公用方法的基础之上,对一些正则表达式的运用,还未涉及到性能以及反爬虫机制,先记录下来,此篇主要是想记录一些通用的网页匹配的工具类,后面做得好了再写后续。
    爬虫:网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。(该释义来自百度百科,应该还比较好理解)
    写爬虫之前,首先我们需要分析要爬取的URL和网页结构。

  1. 列表URL结构分析
    如下图是我们要爬取的一系列网页数据,我们需要爬取每一篇文章的部分内容
    截图a
    截图b
    它是在输入框输入“饮食 养生”关键词之后,呈现出的一个每页10条,一共100页的分页列表,我们再来看看每一页的URL的特点
    截图c
    我们看看每一页的网址:
    第1页:
    网址1
    第2页:
    网址2
    第3页:
    网址3
    第100页:
    网址4
    我们可以很清晰地看出每一页的URL的规律,第1页的URL看起来似乎与其他有所不同,没有最后的&page=1,但实际上加上&page=1和不加都是一样的效果,因此我们在写正则表达式的时候只需把最后page=的“=”后面的数字匹配为1-100的整数即可。

  2. 网页结构分析

在网页上按F12查看网页源码,找到需要爬取部分的结构特点:
需爬取网页部分截图1
以上图来举例,我们需要爬取的部分是楼主发布的内容,如图中选中部分,右边就会显示该部分所在的网页结构,可以看到,每一楼发布的内容都是在一个”< tr> < /tr>”元素中
截图2
我们需要爬取的内容是第一个tr元素下的< div class=”content-body” >元素中,我们找的这个元素的特征一定要是唯一可识别的,在这个例子中,整个页面只有一个table元素,而每一个tr中也只有一个< div class=”content-body” >元素,因此我们定位在第一个tr元素下的class为content-body的div,这个是可以唯一识别的,分析完毕我们就可以开始写我们的爬虫规则了。

爬虫的思路: 我们分两步,第一步访问每一页的URL得到一个页面,解析页面提取页面上每一条数据的URL;第二步再访问每一页提取出来的10个URL,得到这一页的10个网页,再解析网页内容,得到我们需要的内容部分。


接下来上代码:
这是我的目录结构
项目目录结构

Links类

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

/*
*  Link主要功能;
*  1: 存储已经访问过的URL路径 和 待访问的URL 路径;
*/
public class Links
{

    // 已访问的 url 集合 已经访问过的 主要考虑 不能再重复了 使用set来保证不重复;
    private static Set visitedUrlSet = new HashSet();

    // 待访问的 url 集合 待访问的主要考虑 1:规定访问顺序;2:保证不提供重复的带访问地址;
    private static LinkedList unVisitedUrlQueue = new LinkedList();

    // 获得已经访问的 URL 数目
    public static int getVisitedUrlNum()
    {
        return visitedUrlSet.size();
    }

    // 添加到访问过的 URL
    public static void addVisitedUrlSet(String url)
    {
        visitedUrlSet.add(url);
    }

    // 移除访问过的 URL
    public static void removeVisitedUrlSet(String url)
    {
        visitedUrlSet.remove(url);
    }

    // 获得 待访问的 url 集合
    public static LinkedList getUnVisitedUrlQueue()
    {
        return unVisitedUrlQueue;
    }

    // 添加到待访问的集合中 保证每个 URL 只被访问一次
    public static void addUnvisitedUrlQueue(String url)
    {
        if (url != null && !url.trim().equals("")
                && !visitedUrlSet.contains(url)
                && !unVisitedUrlQueue.contains(url))
        {
            unVisitedUrlQueue.add(url);
        }
    }

    // 删除 待访问的url
    public static Object removeHeadOfUnVisitedUrlQueue()
    {
        return unVisitedUrlQueue.removeFirst();
    }

    // 判断未访问的 URL 队列中是否为空
    public static boolean unVisitedUrlQueueIsEmpty()
    {
        return unVisitedUrlQueue.isEmpty();
    }

}

LinkFilter类:主要是判断输入的URL是否是我们想要的格式,后面代码一看就清楚了

public interface LinkFilter
{
    public boolean accept(String url);
}

page包下的三个类
page类

import java.io.UnsupportedEncodingException;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import com.youge.admin.crawl.util.CharsetDetector;

/*
* page
*   1: 保存获取到的响应的相关内容;
* */
public class Page
{
    private byte[] content;
    private String html; // 网页源码字符串
    private Document doc;// 网页Dom文档
    private String charset;// 字符编码
    private String url;// url路径
    private String contentType;// 内容类型

    public Page(byte[] content, String url, String contentType)
    {
        this.content = content;
        this.url = url;
        this.contentType = contentType;
    }

    public String getCharset()
    {
        return charset;
    }

    public String getUrl()
    {
        return url;
    }

    public String getContentType()
    {
        return contentType;
    }

    public byte[] getContent()
    {
        return content;
    }

    /**
     * 返回网页的源码字符串
     *
     * @return 网页的源码字符串
     */
    public String getHtml()
    {
        if (html != null)
        {
            return html;
        }
        if (content == null)
        {
            return null;
        }
        if (charset == null)
        {
            charset = CharsetDetector.guessEncoding(content); // 根据内容来猜测 字符编码
        }
        try
        {
            this.html = new String(content, charset);
            return html;
        }
        catch (UnsupportedEncodingException ex)
        {
            ex.printStackTrace();
            return null;
        }
    }

    /*
     * 得到文档
     */
    public Document getDoc()
    {
        if (doc != null)
        {
            return doc;
        }
        try
        {
            this.doc = Jsoup.parse(getHtml(), url);
            return doc;
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
            return null;
        }
    }
}

PageParserTool类

import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class PageParserTool {


    /* 通过选择器来选取页面的 */
    public static Elements select(Page page , String cssSelector) {
        return page.getDoc().select(cssSelector);
    }

    /*
     *  通过css选择器来得到指定元素;
     *
     *  */
    public static Element select(Page page , String cssSelector, int index) {
        Elements eles = select(page , cssSelector);
        int realIndex = index;
        if (index < 0) {
            realIndex = eles.size() + index;
        }
        return eles.get(realIndex);
    }


    /**
     * 获取满足选择器的元素中的链接 选择器cssSelector必须定位到具体的超链接
     * 例如我们想抽取id为content的div中的所有超链接,这里
     * 就要将cssSelector定义为div[id=content] a
     *  放入set 中 防止重复;
     * @param cssSelector
     * @return
     */
    public static  Set<String> getLinks(Page page ,String cssSelector) {
        Set<String> links  = new HashSet<String>() ;
        Elements es = select(page , cssSelector);
        Iterator iterator  = es.iterator();
        while(iterator.hasNext()) {
            Element element = (Element) iterator.next();
            if ( element.hasAttr("href") ) {
                links.add(element.attr("abs:href"));
            }else if( element.hasAttr("src") ){
                links.add(element.attr("abs:src"));
            }
        }
        return links;
    }



    /**
     * 获取网页中满足指定css选择器的所有元素的指定属性的集合
     * 例如通过getAttrs("img[src]","abs:src")可获取网页中所有图片的链接
     * @param cssSelector
     * @param attrName
     * @return
     */
    public static ArrayList<String> getAttrs(Page page , String cssSelector, String attrName) {
        ArrayList<String> result = new ArrayList<String>();
        Elements eles = select(page ,cssSelector);
        for (Element ele : eles) {
            if (ele.hasAttr(attrName)) {
                result.add(ele.attr(attrName));
            }
        }
        return result;
    }
}

RequestAndResponseTool类

import java.io.IOException;
import java.net.URLEncoder;

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

public class RequestAndResponseTool
{

    public static Page sendRequstAndGetResponse(String url)
    {
        Page page = null;
        // 1.生成 HttpClinet 对象并设置参数
        HttpClient httpClient = new HttpClient();
        // 设置 HTTP 连接超时 5s
        httpClient.getHttpConnectionManager().getParams()
                .setConnectionTimeout(5000);
        // 2.生成 GetMethod 对象并设置参数
        GetMethod getMethod;
        if (url.contains("?"))
        {
            String[] tempUrl = url.split("\\?");
            getMethod = new GetMethod(
                    tempUrl[0] + "?" + URLEncoder.encode(tempUrl[1]));
        }
        else
        {
            getMethod = new GetMethod(url);
        }
        // 设置 get 请求超时 5s
        getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
        // 设置请求重试处理
        getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler());
        // 3.执行 HTTP GET 请求
        try
        {
            int statusCode = httpClient.executeMethod(getMethod);
            // 判断访问的状态码
            if (statusCode != HttpStatus.SC_OK)
            {
                System.err
                        .println("Method failed: " + getMethod.getStatusLine());
            }
            // 4.处理 HTTP 响应内容
            byte[] responseBody = getMethod.getResponseBody();// 读取为字节 数组
            String contentType = getMethod.getResponseHeader("Content-Type")
                    .getValue(); // 得到当前返回类型
            page = new Page(responseBody, url, contentType); // 封装成为页面
        }
        catch (HttpException e)
        {
            // 发生致命的异常,可能是协议不对或者返回的内容有问题
            System.out.println("Please check your provided http address!");
            e.printStackTrace();
        }
        catch (IOException e)
        {
            // 发生网络异常
            e.printStackTrace();
        }
        finally
        {
            // 释放连接
            getMethod.releaseConnection();
        }
        return page;
    }

    public static Page sendRequstAndGetResponse(String url, String param)
    {
        Page page = null;
        // 1.生成 HttpClinet 对象并设置参数
        HttpClient httpClient = new HttpClient();
        // 设置 HTTP 连接超时 5s
        httpClient.getHttpConnectionManager().getParams()
                .setConnectionTimeout(5000);
        // 2.生成 GetMethod 对象并设置参数
        String newParam = URLEncoder.encode(param);
        GetMethod getMethod = new GetMethod(url + "?" + newParam);
        // 设置 get 请求超时 5s
        getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
        // 设置请求重试处理
        getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler());
        // 3.执行 HTTP GET 请求
        try
        {
            int statusCode = httpClient.executeMethod(getMethod);
            // 判断访问的状态码
            if (statusCode != HttpStatus.SC_OK)
            {
                System.err
                        .println("Method failed: " + getMethod.getStatusLine());
            }
            // 4.处理 HTTP 响应内容
            byte[] responseBody = getMethod.getResponseBody();// 读取为字节 数组
            String contentType = getMethod.getResponseHeader("Content-Type")
                    .getValue(); // 得到当前返回类型
            page = new Page(responseBody, url, contentType); // 封装成为页面
        }
        catch (HttpException e)
        {
            // 发生致命的异常,可能是协议不对或者返回的内容有问题
            System.out.println("Please check your provided http address!");
            e.printStackTrace();
        }
        catch (IOException e)
        {
            // 发生网络异常
            e.printStackTrace();
        }
        finally
        {
            // 释放连接
            getMethod.releaseConnection();
        }
        return page;
    }
}

util包下的几个类
CharsetDetector类

import org.mozilla.universalchardet.UniversalDetector;

import java.io.UnsupportedEncodingException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 字符集自动检测
 *
 * @author hu
 */
public class CharsetDetector {

    //从Nutch借鉴的网页编码检测代码
    private static final int CHUNK_SIZE = 2000;

    private static Pattern metaPattern = Pattern.compile(
            "<meta\\s+([^>]*http-equiv=(\"|')?content-type(\"|')?[^>]*)>",
            Pattern.CASE_INSENSITIVE);
    private static Pattern charsetPattern = Pattern.compile(
            "charset=\\s*([a-z][_\\-0-9a-z]*)", Pattern.CASE_INSENSITIVE);
    private static Pattern charsetPatternHTML5 = Pattern.compile(
            "<meta\\s+charset\\s*=\\s*[\"']?([a-z][_\\-0-9a-z]*)[^>]*>",
            Pattern.CASE_INSENSITIVE);

    //从Nutch借鉴的网页编码检测代码
    private static String guessEncodingByNutch(byte[] content) {
        int length = Math.min(content.length, CHUNK_SIZE);

        String str = "";
        try {
            str = new String(content, "ascii");
        } catch (UnsupportedEncodingException e) {
            return null;
        }

        Matcher metaMatcher = metaPattern.matcher(str);
        String encoding = null;
        if (metaMatcher.find()) {
            Matcher charsetMatcher = charsetPattern.matcher(metaMatcher.group(1));
            if (charsetMatcher.find()) {
                encoding = new String(charsetMatcher.group(1));
            }
        }
        if (encoding == null) {
            metaMatcher = charsetPatternHTML5.matcher(str);
            if (metaMatcher.find()) {
                encoding = new String(metaMatcher.group(1));
            }
        }
        if (encoding == null) {
            if (length >= 3 && content[0] == (byte) 0xEF
                    && content[1] == (byte) 0xBB && content[2] == (byte) 0xBF) {
                encoding = "UTF-8";
            } else if (length >= 2) {
                if (content[0] == (byte) 0xFF && content[1] == (byte) 0xFE) {
                    encoding = "UTF-16LE";
                } else if (content[0] == (byte) 0xFE
                        && content[1] == (byte) 0xFF) {
                    encoding = "UTF-16BE";
                }
            }
        }

        return encoding;
    }

    /**
     * 根据字节数组,猜测可能的字符集,如果检测失败,返回utf-8
     *
     * @param bytes 待检测的字节数组
     * @return 可能的字符集,如果检测失败,返回utf-8
     */
    public static String guessEncodingByMozilla(byte[] bytes) {
        String DEFAULT_ENCODING = "UTF-8";
        UniversalDetector detector = new UniversalDetector(null);
        detector.handleData(bytes, 0, bytes.length);
        detector.dataEnd();
        String encoding = detector.getDetectedCharset();
        detector.reset();
        if (encoding == null) {
            encoding = DEFAULT_ENCODING;
        }
        return encoding;
    }

    /**
     * 根据字节数组,猜测可能的字符集,如果检测失败,返回utf-8
     * @param content 待检测的字节数组
     * @return 可能的字符集,如果检测失败,返回utf-8
     */
    public static String guessEncoding(byte[] content) {
        String encoding;
        try {
            encoding = guessEncodingByNutch(content);
        } catch (Exception ex) {
            return guessEncodingByMozilla(content);
        }

        if (encoding == null) {
            encoding = guessEncodingByMozilla(content);
            return encoding;
        } else {
            return encoding;
        }
    }
}

HtmlRegexpUtil类(这是在网上百度的一个util类,有很多方法没有用上,在此基础上我作了一下改动,用上的也就一两个)

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**  
 * <p>  
 * Title: HTML相关的正则表达式工具类  
 * </p>  
 * <p>  
 * Description: 包括过滤HTML标记,转换HTML标记,替换特定HTML标记  
 * </p>  
 * <p>  
 * Copyright: Copyright (c) 2006  
 * </p>  
 *   
 * @author hejian  
 * @version 1.0  
 * @createtime 2006-10-16  
 */

public class HtmlRegexpUtil
{
    public final static String regxpForHtml = "<([^>]*)>"; // 过滤所有以<开头,以>结尾的标签

    public final static String regxpForImgTag = "<\\s*img\\s*([^>]*)\\s*>"; // 找出IMG标签

    public final static String regxpForImaTagSrcAttrib = "src=\"([^\"]+)\""; // 找出IMG标签的SRC属性

    public final static String regxpForArticleTag = "<\\s*article\\s*([^>]*)\\s*>"; // 找出article标签

    public final static String regxpForSpanTag = "<\\s*span\\s*([^>]*)\\s*>"; // 找出span标签

    public final static String regxpForPTag = "<\\s*p\\s*([^>]*)\\s*>"; // 找出p标签

    public final static String regxpForATag = "<\\s*a\\s*([^>]*)\\s*>"; // 找出a标签

    public final static String regxpForStrongTag = "<\\s*strong\\s*([^>]*)\\s*>"; // 找出strong标签

    /**  
     *   
     */
    public HtmlRegexpUtil()
    {
        // TODO Auto-generated constructor stub
    }

    /**  
     *   
     * 基本功能:替换标记以正常显示  (没用上)
     * <p>  
     *   
     * @param input  
     * @return String  
     */
    public String replaceTag(String input)
    {
        if (!hasSpecialChars(input))
        {
            return input;
        }
        StringBuffer filtered = new StringBuffer(input.length());
        char c;
        for (int i = 0; i <= input.length() - 1; i++)
        {
            c = input.charAt(i);
            switch (c)
            {
            case '<':
                filtered.append("&lt;");
                break;
            case '>':
                filtered.append("&gt;");
                break;
            case '"':
                filtered.append("&quot;");
                break;
            case '&':
                filtered.append("&amp;");
                break;
            default:
                filtered.append(c);
            }

        }
        return (filtered.toString());
    }

    /**  
     *   
     * 基本功能:判断标记是否存在  
     * <p>  
     *   
     * @param input  
     * @return boolean  
     */
    public boolean hasSpecialChars(String input)
    {
        boolean flag = false;
        if ((input != null) && (input.length() > 0))
        {
            char c;
            for (int i = 0; i <= input.length() - 1; i++)
            {
                c = input.charAt(i);
                switch (c)
                {
                case '>':
                    flag = true;
                    break;
                case '<':
                    flag = true;
                    break;
                case '"':
                    flag = true;
                    break;
                case '&':
                    flag = true;
                    break;
                }
            }
        }
        return flag;
    }

    /**  
     *   
     * 基本功能:过滤所有以"<"开头以">"结尾的标签  (没用上)
     * <p>  
     *   
     * @param str  
     * @return String  
     */
    public static String filterHtml(String str)
    {
        Pattern pattern = Pattern.compile(regxpForHtml);
        Matcher matcher = pattern.matcher(str);
        StringBuffer sb = new StringBuffer();
        boolean result1 = matcher.find();
        while (result1)
        {
            matcher.appendReplacement(sb, "");
            result1 = matcher.find();
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    /**  
     *   
     * 基本功能:过滤指定标签  (没用上)
     * <p>  
     *   
     * @param str  
     * @param tag  
     *            指定标签  
     * @return String  
     */
    public static String fiterHtmlTag(String str, String tag)
    {
        String regxp = "<\\s*" + tag + "\\s*([^>]*)\\s*>";
        Pattern pattern = Pattern.compile(regxp);
        Matcher matcher = pattern.matcher(str);
        StringBuffer sb = new StringBuffer();
        boolean result1 = matcher.find();
        while (result1)
        {
            matcher.appendReplacement(sb, "");
            result1 = matcher.find();
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    /**  
     *   
     * 基本功能:替换指定的标签  (没用上)
     * <p>  
     *   
     * @param str  
     * @param beforeTag  
     *            要替换的标签  
     * @param tagAttrib  
     *            要替换的标签属性值  
     * @param startTag  
     *            新标签开始标记  
     * @param endTag  
     *            新标签结束标记  
     * @return String  
     * @如:替换img标签的src属性值为[img]属性值[/img]  
     */
    public static String replaceHtmlTag(String str, String beforeTag,
            String tagAttrib, String startTag, String endTag)
    {
        String regxpForTag = "<\\s*" + beforeTag + "\\s+([^>]*)\\s*>";
        String regxpForTagAttrib = tagAttrib + "=\"([^\"]+)\"";
        Pattern patternForTag = Pattern.compile(regxpForTag);
        Pattern patternForAttrib = Pattern.compile(regxpForTagAttrib);
        Matcher matcherForTag = patternForTag.matcher(str);
        StringBuffer sb = new StringBuffer();
        boolean result = matcherForTag.find();
        while (result)
        {
            StringBuffer sbreplace = new StringBuffer();
            Matcher matcherForAttrib = patternForAttrib
                    .matcher(matcherForTag.group(1));
            if (matcherForAttrib.find())
            {
                matcherForAttrib.appendReplacement(sbreplace,
                        startTag + matcherForAttrib.group(1) + endTag);
            }
            matcherForTag.appendReplacement(sb, sbreplace.toString());
            result = matcherForTag.find();
        }
        matcherForTag.appendTail(sb);
        return sb.toString();
    }

    /**
     * 
    * @Title: removeAllTagAttr
    * @Description: 移除指定标签的所有属性(我改动之后的方法)
    * @author cjl
    * @param content 源内容
    * @param tagName 标签名称
    * @return
    * @throws
     */
    public static String removeAllTagAttr(String content, String tagName)
    {
        String newContent = content.replaceAll(
                "<" + tagName + "\\s[\\s\\S].*?>", "<" + tagName + ">");
        return newContent;
    }

    /**
     * 获取指定HTML标签的指定属性的值列表(我改动之后的方法)
     * @param source 要匹配的源文本
     * @param element 标签名称
     * @param attr 标签的属性名称
     * @return 属性值列表
     */
    public static List<String> getAttrContentList(String source, String element,
            String attr)
    {
        // 匹配所有img标签 <img[\s\S].*?src="[\s\S].*?"[\s\S].*?>
        List<String> result = new ArrayList<String>();
        String reg = "(<)" + element + "[\\s\\S].*?" + attr
                + "=\"([\\s\\S].*?)\"[\\s\\S].*?>";
        Matcher m = Pattern.compile(reg).matcher(source);
        while (m.find())
        {
            String r = m.group(2);
            result.add(r);
        }
        return result;
    }

    /**
     * 
    * @Title: removeImgWidthAttr
    * @Description: 移除img标签的max-width或width属性(我改动之后的方法)
    * @author cjl
    * @param content
    * @return
    * @throws
    * @date 2018-02-18 21:49
     */
    public static String removeImgWidthAttr(String content)
    {
        // width="[\s\S].*?"|max-width="[\s\S].*?"
        String newContent = content.replaceAll(
                "width=\"[\\s\\S].*?\"|max-width=\"[\\s\\S].*?\"", "");
        return newContent;
    }

    public static String removeImgStyleAttr(String content)
    {
        // width="[\s\S].*?"|max-width="[\s\S].*?"
        String newContent = content.replaceAll("style=\"[\\s\\S].*?\"", "");
        return newContent;
    }

    /**
     * 
    * @Title: getAttrContent
    * @Description: 找到指定属性值
    * @param source
    * @param element
    * @param attr
    * @return
    * @throws
     */
    public static String getAttrContent(String source, String element,
            String attr)
    {
        // 匹配所有img标签 <img[\s\S].*?src="[\s\S].*?"[\s\S].*?>
        List<String> result = new ArrayList<String>();
        String reg = "(<)" + element + "[\\s\\S].*?" + attr
                + "=\"([\\s\\S].*?)\"[\\s\\S]*.*?>";
        Matcher m = Pattern.compile(reg).matcher(source);
        while (m.find())
        {
            String r = m.group(2);
            result.add(r);
        }
        return result.get(0);
    }

    public static void main(String[] args)
    {
        String source = "<img src=\"http://5b0988e595225.cdn.sohucs.com/images/20180217/f6d85c63b3a24d30863a9b13d8d432ec.jpeg\" width=\"480\" height=\"280\" title=\"\" align=\"\" alt=\"\" /> ";
        System.out.println(HtmlRegexpUtil.getAttrContent(source, "img", "src"));
    }
}

J1healthUtil类(这个类是根据我们项目的业务需求编写的特定的类)

package com.youge.admin.crawl.util;

import java.util.List;

import org.springframework.stereotype.Service;

/**
 * 
* @ClassName: JianYiUtil
* @Description: 健一健康(http://www.j1health.com)文章处理util
* @author cjl
* @date 2018年4月12日 下午2:27:52
*
 */
@Service
public class J1healthUtil
{
    /**
    * 
    * @Title: analyzeArticle
    * @Description: 移除article、span标签,移除p、img(除了src)标签属性,img的url拼接完整
    * @author cjl
    * @param content
    * @return
    */
    public String analyzeArticle(String content)
    {
        HtmlRegexpUtil htmlRegexpUtil = new HtmlRegexpUtil();
        String newContent = "";
        // 1.移除div、span标签
        if (htmlRegexpUtil.hasSpecialChars(HtmlRegexpUtil.regxpForArticleTag))
        {
            newContent = HtmlRegexpUtil.fiterHtmlTag(
                    HtmlRegexpUtil.fiterHtmlTag(content, "div"), "/div");
        }
        if (htmlRegexpUtil.hasSpecialChars(HtmlRegexpUtil.regxpForSpanTag))
        {
            newContent = HtmlRegexpUtil.fiterHtmlTag(
                    HtmlRegexpUtil.fiterHtmlTag(newContent, "span"), "/span");
        }
        // 2.移除p、img(除了src)、strong 标签属性;
        if (htmlRegexpUtil.hasSpecialChars(HtmlRegexpUtil.regxpForPTag))
        {
            newContent = HtmlRegexpUtil.removeAllTagAttr(newContent, "p");
        }
        if (htmlRegexpUtil.hasSpecialChars(HtmlRegexpUtil.regxpForImgTag))
        {
            newContent = HtmlRegexpUtil.removeImgStyleAttr(newContent);
            // 3.判断img的url是否需要拼接完整;
            newContent = newContent.replaceAll("src=\"//", "src=\"http://");
        }
        if (htmlRegexpUtil.hasSpecialChars(HtmlRegexpUtil.regxpForStrongTag))
        {
            newContent = HtmlRegexpUtil.removeAllTagAttr(newContent, "strong");
        }
        return newContent.trim();
    }

    /**
    * 
    * @Title: getImgSrcList
    * @Description: 获取img的src属性值列表
    * @author cjl
    * @param newContent
    * @return
    * @date 2018-02-19 00:55
    */
    public static List<String> getImgSrcList(String newContent)
    {
        List<String> imgSrcList = HtmlRegexpUtil.getAttrContentList(newContent,
                "img", "src");
        return imgSrcList;
    }

}

以上是用到的一些辅助类,下面是爬取逻辑的代码,我省略了一些具体的业务上的代码,具体你要爬取哪些内容取决于你的需求,主要是看一下解析网页提取需要的元素部分,我这里也只是作为参考(注释中的一级表也就是前面说的所有列表的URL,二级表则是每个URL对应的网页数据表)

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.youge.admin.crawl.link.LinkFilter;
import com.youge.admin.crawl.link.Links;
import com.youge.admin.crawl.page.Page;
import com.youge.admin.crawl.page.PageParserTool;
import com.youge.admin.crawl.page.RequestAndResponseTool;
import com.youge.admin.crawl.util.J1healthUtil;
import com.youge.admin.crawl.util.SouhuUtil;
import com.youge.service.pg.article.ArticleContentService;
import com.youge.service.pg.article.ArticleUrlService;
import com.youge.service.pg.article.ChannelSpiderKeywordsService;
import com.youge.service.pg.article.ChannelSpiderKeywordsSourceService;
import com.youge.util.IdWorker;

/**
 * 
* @ClassName: J1healthCrawler
* @Description: 健一健康爬虫
* @author cjl
* @date 2018年4月12日 上午12:04:48
*
 */
@Service
public class J1healthCrawler
{
    @Autowired
    private ArticleUrlService articleUrlService;
    @Autowired
    private ArticleContentService articleContentService;
    @Autowired
    private J1healthUtil jianYiUtil;
    @Resource
    private ChannelSpiderKeywordsService channelSpiderKeywordsService;
    @Resource
    private ChannelSpiderKeywordsSourceService channelSpiderKeywordsSourceService;

    /**
     * 使用种子初始化 URL 队列
     *
     * @param seeds 种子 URL
     * @return
     */
    private void initCrawlerWithSeeds(String[] seeds)
    {
        for (int i = 0; i < seeds.length; i++)
        {
            Links.addUnvisitedUrlQueue(seeds[i]);
        }
    }

    /**
     * 
    * @Title: crawl
    * @Description: 抓取
    * @author cjl
    * @param seeds
    * @date 2018-02-11 10:30
     */
    public void crawl(String[] seeds)
    {

        // 1.初始化url队列
        initCrawlerWithSeeds(seeds);

        // 2.过滤http开头的url
        @SuppressWarnings("unused")
        LinkFilter filter = new LinkFilter()
        {
            public boolean accept(String url)
            {
                if (url.startsWith("http://www.j1health.com/search"))
                    return true;
                else
                    return false;
            }
        };

        // 3.通过url得到网页内容
        while (!Links.unVisitedUrlQueueIsEmpty())
        {
            // 从种子队列中先取第一个url
            String visitUrl = (String) Links.removeHeadOfUnVisitedUrlQueue();
            if (visitUrl == null)
            {
                continue;
            }

            // 根据URL得到page;
            Page page = RequestAndResponseTool
                    .sendRequstAndGetResponse(visitUrl);

            // 对page进行处理: 访问DOM的某个标签
            // 4.分析网页内容,得到文章列表,存入数据库
            Elements titleElements = PageParserTool.select(page,
                    "ul.search-list div.search-item-title a");
            List<String> articleUrls = new ArrayList<String>();
            List<String> titles = new ArrayList<String>();
            // 文章url一级表信息存库
            Map<String, Object> articleUrlMap = new HashMap<String, Object>();
            if (!titleElements.isEmpty())
            {
                titles = titleElements.eachText();
                articleUrls = titleElements.eachAttr("href");
            }

            // 取单条记录
            for (int i = 0; i < titles.size(); i++)
            {
                articleUrlMap.put("title", titles.get(i));
                articleUrlMap.put("articleUrl", articleUrls.get(i));
                articleUrlMap.put("source", "健一健康");
                articleUrlService.addArticleUrl(articleUrlMap);
            }
            // 将访问过的url放入已访问队列中
            Links.addVisitedUrlSet(visitUrl);
        }
    }

    /**
     * 
    * @Description: 根据文章一级表信息抓取二级表信息
    * @author cjl
    * @date 2018-04-12 17:41
     */
    public void crawlArticleContent()
    {

        Map<String, Object> paramsMap = new HashMap<String, Object>();
        paramsMap.put("isAnalysed", 0);
        // 1.查询所有未被解析过的文章信息
        // 从一级表得到id,url,title,source,根据url得到文章content,存入二级表
        cn.magicbeans.pagination.Page<Map<String, Object>> articleUrls = articleUrlService
                .selectArticleList(null, paramsMap);
        if (articleUrls != null)
        {
            List<Map<String, Object>> articleUrlList = articleUrls.getContent();
            for (Map<String, Object> map : articleUrlList)
            {
                Map<String, Object> articleContentMap = new HashMap<String, Object>();
                // 2.通过文章url列表得到文章详情内容,存入数据库
                // 根据URL得到page;
                Page contentPage = RequestAndResponseTool
                        .sendRequstAndGetResponse(map.get("articleUrl") + "");
                // 对page进行处理: 访问DOM的某个标签
                Elements contentElements = PageParserTool.select(contentPage,
                        "tbody tr td.handle div.content-body");
                Elements authorElements = PageParserTool.select(contentPage,
                        "tbody h5 a");
                if (null != authorElements)
                {
                    String author = authorElements.eachText().get(0);
                    articleContentMap.put("author", author);
                }
                String newContent = jianYiUtil
                        .analyzeArticle(contentElements.get(0).toString());
                List<String> imgSrcList = SouhuUtil.getImgSrcList(newContent);
                // 文章详细信息二级表信息存库
                try
                {
                    articleContentMap.put("content",
                            new String(newContent.getBytes("utf-8"), "utf-8"));
                }
                catch (UnsupportedEncodingException e)
                {
                    e.printStackTrace();
                }
                articleContentMap.put("articleCode", IdWorker.getUniqueId());
                articleContentMap.put("articleUrlId", (int) map.get("id"));
                articleContentMap.put("title", map.get("title"));
                articleContentMap.put("source", map.get("source"));
                articleContentMap.put("contentType", contentType);
                articleContentMap.put("type", type);
                articleContentService.addArticleContent(articleContentMap);
            }
        }
    }

    public static void main(String[] args)
    {
        J1healthCrawler crawler = new J1healthCrawler();
        crawler.crawl(new String[]
        { "http://www.j1health.com/search/topic?keyword=%E9%A5%AE%E9%A3%9F%20%E5%85%BB%E7%94%9F&page=1" });
        crawler.crawlArticleContent();
    }

}

我们在调用的时候传入种子URL队列即可,比如:

@Controller
@RequestMapping("/j1healthCrawler")
public class CrawlerJ1healthController
{
    @Resource
    private J1healthCrawler j1healthCrawler;

    /**
     * 
    * @Description: 爬取
    * @author cjl
     */
    @RequestMapping("/crawl")
    @ResponseBody
    public void crawl(String[] seeds)
    {
        String[] seeds2 = new String[100];
        int j = -1;
        //种子URL队列
        for (int i = 1; i <= 100; i++)
        {
            seeds2[++j] = "http://www.j1health.com/search/topic?keyword=饮食 养生&page="
                    + i;
        }

        j1healthCrawler.crawl(seeds2);
        j1healthCrawler.crawlArticleContent();
    }

至此,一个简单的爬虫基本完成,在此过程中,有一个需要注意的细节,如下图,我们在访问URL的时候,需要对URL中的中文进行处理,否则会报错
这里写图片描述

这个爬虫的例子里面,能够通用的应该就是提取元素的部分了,但还是要根据具体的业务需求来定制自己的util类,最后的种子URL队列本来是准备写成正则来表示1-100的整数数字,不过没写出来,因为赶时间,就写成了这种笨办法来实现。

猜你喜欢

转载自blog.csdn.net/innerpeaceScorpio/article/details/80340146
今日推荐