美文网首页爬虫
爬虫程序编写与常见问题解决办法~

爬虫程序编写与常见问题解决办法~

作者: 空山雪林 | 来源:发表于2018-12-13 17:18 被阅读0次

    基础知识

    工欲善其事,必先利其器,要编写爬虫程序,首先必须找一个爬虫框架,如果你使用Python语言,可以选用scrapy,如果你使用Java语言,可选用WebMagic,本文使用后者,编写爬虫程序无非分以下几步:

    1. 根据URL下载网页,得到HTML(注意并不是通过开发工具看到的HTML,而是网页源代码HTML,这两者有本质区别);
    2. 根据HTML解析您所需要的数据,可以利用xpath获取DOM节点内容或属性值;
    3. 有可能还需要根据得到的HTML解析出其他链接,利用多线程继续爬取;
    4. 解析后的数据存储(数据库,文件等);

    WebMagic爬虫框架在core代码中主要有四个模块:Downloader、PageProcessor、Scheduler、Pipeline,分别处理下载,页面解析,管理(管理待抓取的URL,做一些去重工作,默认使用内存队列管理URL,也可以使用Redis进行分布式管理)和持久化工作,因为最终解析出的结构化数据应该是要入库或入文件存储的。

    下构图如下

    页面下载

    WebMagic爬虫框架负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具,当然你可以使用chromedriver下载动态内容,那么我们应该调用Spider的setDownloader方法spider.setDownloader(new SeleniumDownloader())

    页面解析

    PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup

    这是您需要花大力气编写代码的地方,根据chrome开发工具查看您所需抓取的数据的xpath路径,利用xpath获取节点信息获取数据。

    Scheduler

    Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。

    除非项目有一些特殊的分布式需求,否则无需自己定制Scheduler。

    Pipeline

    Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案,如果你需要将数据存储到数据库,您可能需要自定义一个Pipeline,以JSON形式保存到文件D:\webmagic\可以使用:

    Spider.create(new XXXPageProcessor())
                .addUrl("http://www.baidu.com")
                .addPipeline(new JsonFilePipeline("D:\\webmagic\\"))
                .thread(5)
                .run();
    

    另外,Request对象用于在PageProcessorprocess实现函数中,将其他url的抓取任务扔给Downloader组件去执行,Downloader组件下载到一个页面是Page对象,在此可以获取网页源码内容,可以做xpath解析等,ResultItems相当于一个Map,保存PageProcessor处理的结果,供Pipeline使用,你可以将里面的内容保存到注入数据库中。

    Selectable抽取元素

    解析处理HTML获取自己想要的数据可能是编写爬虫最麻烦的一环,使用Selectable接口,你可以直接完成页面元素的链式抽取,也无需去关心抽取的细节,比如上文提到的Page.getHtml()得到的就是一个Html对象,实现了Selectable接口。以下是常用的解析方法:

    • xpath(String xpath):根据xpath路径获取节点,如html.xpath("//div[@class='title']/text()",获取包含title类的div包裹的内容;
    • (String selector):根据选择符获取节点,如html.("div.title"),获取包含title类名的div节点列表
    • css(String selector):功能同$,使用css选择器;
    • links():获取所有链接
    • regex(String regex):根据正则匹配出内容
    • replace(String regex,String replacement):替换内容

    以上函数都返回Selectable接口,支持链式调用,但想要获取解析后数据的结果时,可以利用get()返回一条String结果或all返回所有结果List<String>match返回匹配结果。

    常见问题

    在编写爬虫程序时,我们经常会碰到的一些问题汇总与解决方案,帮您快速定位解决问题。

    通过代理上网解决IP被封问题

    有时候抓取的站点会封我们的IP,公司的外网IP又是固定的,我们可以通过ADSL拨号的方式接入另一个网络,在ADSL网络的服务器上搭建代理服务器,爬虫程序所在的服务器通过代理该台服务器上网,这样再也不怕对方站点封您的IP了,让爬虫程序通过代理爬取网页,代码如下:

    HttpClientDownloader downloader= new HttpClientDownloader();
    downloader.setProxyProvider(SimpleProxyProvider.from(new Proxy("IP",PORT,"username","password")));
    spider.setDownloader(httpClientDownloader)
    

    服务器端返回403错误

    有些网站会有一些爬虫的识别机制,影响您利用爬虫去爬数据时,会碰到服务端的403拒绝访问错误,这时,我们应该根据实际访问网址的情况,构造userAgent,Cookie等数据,以便达到可访问网站的目的,典型的应用场景是爬取Discuz!论坛的列表数据,已爬取宁波当地的房产论坛列表为例,利用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("//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动态数据

    有些网站采用异步Ajax的方式获取列表数据,这些列表数据我们无法直接通过下载网址的文件得到,需要利用虚拟浏览器技术,以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("//tbody/tr").all());
                    String companyCode = html.xpath("//tbody/tr[1]/td/text()").get();
    
                    DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
                    String dateString = html.xpath("//tbody/tr[3]/td/text()").get().split("/")[0];
    
                    String stockCode = html.xpath("//tbody/tr[2]/td/text()").get().split("/")[0];
                    String name = html.xpath("//tbody/tr[5]/td/text()").get().split("/")[0];
                    String department = html.xpath("//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();
        }
    }
    

    注:如果要让代码运行成功需要下载一个chromedriver,如果你是windows可以去这个网址去下https://chromedriver.storage.googleapis.com/2.25/chromedriver_win32.zip,虽然是32位的但是64位也可以用,如果不行的话或者你是其他OS,可以去官网下https://chromedriver.storage.googleapis.com/index.html?path=2.27/

    https站点问题

    有些https站点只支持TLS1.2,比如大名鼎鼎的Github网站,这时你需要修改框架源码,找到如下类:


    image.png

    去除如下代码的红框部分:


    image.png

    否则将无法爬虫https站点数据。

    处理非Get请求的爬虫

    有时候,我们需要爬取非Get请求的数据,代码如下(其他诸如DELETE,PUT等请求方式类似):

    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作为例子。在这个例子里,我们要从最终的博客文章页面,抓取博客的标题、内容、日期等信息,也要从列表页抓取博客的链接等信息,从而获取这个博客的所有文章。

    在这个例子中,我们的主要使用几个方法:

    • 从页面指定位置发现链接,使用正则表达式来过滤链接.
    • 在PageProcessor中处理两种页面,根据页面URL来区分需要如何处理。
    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) {
            //列表页
            if (page.getUrl().regex(URL_LIST).match()) {
                page.addTargetRequests(page.getHtml().xpath("//div[@class=\"articleList\"]").links().regex(URL_POST).all());
                page.addTargetRequests(page.getHtml().links().regex(URL_LIST).all());
                //文章页
            } else {
                page.putField("title", page.getHtml().xpath("//div[@class='articalTitle']/h2"));
                page.putField("content", page.getHtml().xpath("//div[@id='articlebody']//div[@class='articalContent']"));
                page.putField("date",
                        page.getHtml().xpath("//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();
        }
    }
    

    后记

    基于以上爬虫思路,如果让你设计一个爬虫管理控制中心,你该如何设计与实现呢?请关注我们后续文章,感谢阅读!

    相关文章

      网友评论

        本文标题:爬虫程序编写与常见问题解决办法~

        本文链接:https://www.haomeiwen.com/subject/wbxbhqtx.html