springboot搭建租房推荐网站(更新中......)

简介

  • 由于毕业租房的时候遇到不少坑,想搞一个给刚从学校出来的同学推荐租房信息的网站,目前做出一个雏形。
  • github地址:https://github.com/hanjg/house
    • master分支:springboot版
    • ssm分支:SSM版本

主要功能

  • 目前的功能如下:
    • 持续抓取链家网的租房数据,包括房屋信息和小区信息。
    • 展示所有租房信息。
    • 推送和展示关注的房源的最新信息和爬虫状态。
  • 计划增加功能:
    • 从多个网站爬取并汇总信息。
    • 管理关注的小区,可以通过名称,位置等信息设置。
    • 智能推荐房源,综合价格等因素,需要考虑到房屋来源等社会因素。

技术选型

  • 数据库:msyql
  • 后台框架:
    • springboot2
    • mybatis
    • webmagic:爬虫框架抓取网站的数据。
    • websocket推送消息。
  • 前台框架:
    • easy-ui:(计划用更加流行的Bootstrap)
    • jsp(继承ssm框架的视图,后计划用效率更高的thymeleaf)

主要流程

webmagic抓取数据

  • webmagic中:
    • Downloader负责下载网页。
    • Scheduler负责调度任务的。使用 url不去重 的调度器,因为需要重复爬取数据。
    • PageProcessor负责解析下载的网页。
    • Pipeline负责数据的持久化。
    • ProxyPool提供代理。代理无法稳定弃用。
      http://webmagic.io/
  • 参考webmagic首页,webmagic使用总结
  • webmagic的配置在WebmagicConfig这个配置类中。爬取的服务实现类为CrawlerServiceImpl。
@Configuration
public class WebmagicConfig {

    @Autowired
    private LianjiaConst lianjiaConst;
    @Autowired
    private CrawlerConst crawlerConst;

    @Autowired
    private PageProcessor pageProcessor;
    @Autowired
    private Pipeline pipeline;
    @Autowired
    private HttpClientDownloader downloader;
    @Autowired
    private Scheduler scheduler;
    @Autowired
    private ProxyPool proxyPool;

    @Bean
    public Spider spider() {
        Spider spider = us.codecraft.webmagic.Spider.create(pageProcessor);
        spider.addPipeline(pipeline);
        downloader.setProxyProvider(proxyPool);
        spider.setDownloader(downloader);
        spider.setScheduler(scheduler);
        spider.thread(crawlerConst.getThreadNum());
        return spider;
    }

    @Bean
    public Pipeline pipeline() {
        return new LianjiaDbPipeLine();
    }

    @Bean
    public Scheduler scheduler() {
        return new DuplicateQueueScheduler();
    }

    @Bean
    public PageProcessor pageProcessor() {
        LianjiaPageProcessor pageProcessor = new LianjiaPageProcessor(crawlerConst.getSleepTimes(),
                crawlerConst.getRetryTimes());
        pageProcessor.setCityRentRoot(lianjiaConst.getRentCityRoot());
        pageProcessor.setCity(lianjiaConst.getCityName());
        return pageProcessor;
    }

    @Bean
    public ProxyPool proxyPool() {
        return new ProxyPool();
    }

    @Bean
    public HttpClientDownloader httpClientDownloader() {
        return new HttpClientDownloader();
    }
}

记录状态的更新

  • 房屋和小区均使用状态字段status标志记录的状态,分别为过期、最新、正在更新状态。
 status TINYINT NOT NULL DEFAULT 2,
public enum RecordStatus {
    EXPIRED((byte) 0, "过期"), LATEST((byte) 1, "最新"), UPDATING((byte) 2, "正在更新");

    private Byte status;
    private String state;
}
  • 主线程管理爬虫,负责爬取数据,新插入的记录或者曾经出现过的记录都会将状态设为正在更新
  • 更新状态线程负责在爬取结束之后将正在更新状态转为最新,曾经最新的状态转为过期。
  <update id="updateStatus">
    update community
    set status = status - 1
    where status != 0
  </update>
  <update id="updateStatus">
    update renting_house
    set status = status - 1
    where status != 0
  </update>
  • 两个线程之间在SpiderThreadManager中使用CountDownLatch协调,保证更新线程在爬取结束之后进行。CountDownLatch使用详解
    • 更新线程等待爬取结束。
    • 爬取结束之后唤醒更新线程,主线程等待更新结束。
    • 更新结束之后唤醒主线程。
  public void start(final int repeatTimes, List<String> rootUrls) {
        spiderRunnnig = true;
        int count = repeatTimes;
        while (count-- > 0) {
            updateStatusThreadStart();
            crawlStart(rootUrls);
            try {
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        spiderRunnnig = false;
    }

    private void crawlStart(List<String> urlList) {
        try {
            spider.addUrl(urlList.toArray(new String[urlList.size()]));
            spider.start();
            while (true) {
                Thread.sleep(10 * 1000);
                if (spider.getStatus().equals(Status.Stopped)) {
                    break;
                }
            }
            crawlAction.countDown();
            updateStatusAction.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

   private void updateStatusThreadStart() {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    crawlAction.await();

                 ...

                    updateStatusAction.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

信息的推送

  • 使用 Websocket 维持网页和浏览器的长连接,当用户打开或者刷新网页时,推送更新的信息至浏览器。
  • MyWebSocketHandler重写AbstractWebSocketHandler方法,在连接建立时和接收消息时返回最新的信息。
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        LOGGER.info("websocket connection established......");
        sendLatestNews(session);
    }

    private void sendLatestNews(WebSocketSession session) throws IOException {
        List<RentingHouse> houseList = new ArrayList<>();
        for (String communityName : pusherConst.getPushedCommunities()) {
            houseList.addAll(rentingHouseService.getLatestFavourateHouseList(communityName, lastPushTime));
        }
        String crawlerMessage = getSpiderMessage();
        String houseMessage = getHouseMessage(houseList);
        session.sendMessage(new TextMessage(crawlerMessage + "\n\n" + houseMessage));
        //更新最近推送时间
        lastPushTime = new Date();
        LOGGER.info("last push time: {}", lastPushTime);
    }
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        LOGGER.info("websocket handle text message: {}", message);
        sendLatestNews(session);
    }

遇到的问题

No runnable methods

  • 单元测试报java.lang.Exception: No runnable methods
  • 在src/test/java文件夹下的类中方法添加** @Test注解** 或者将类设置成 abstract

net::ERR_CONNECTION_REFUSED

  • 连接被拒绝,原因有多种。本人遇到磁盘空间耗尽,nginx无法写缓存,从而拒绝连接。
  • 解决思路:查看nginx或者tomcat 日志 ,找到对应request的日志。

爬取速度慢

  • 同一个IP最快可以一秒访问网站两次,否则会被封,解决这一问题最通用的方法是使用代理。
  • ProxyServiceImpl中抓取西刺等代理的IP,并且序列化保存在本地,以供爬虫使用。但是抓取的代理极不稳定,验证可用之后使用绝大多数都无法再次访问。由于总记录暂时为1W-2W,平均2h刷新一次,暂时不使用代理。
    private void getProxyFromXici() {
        int currentPage = 1;
        int urlCount = 0;
        while (true) {
            String url = crawlerConst.getXiciRoot() + currentPage;
            LOGGER.info("get proxy from: {}", url);
            try {
                Document document = Jsoup.connect(url).timeout(3 * 1000).get();
                Elements trs = document.getElementsByTag("tr");
                if (trs == null || trs.size() < 1) {
                    break;
                }
                for (int i = 1; i < trs.size(); i++) {
                    try {
                        LOGGER.debug("get url {}", ++urlCount);
                        Elements tds = trs.get(i).getElementsByTag("td");
                        Proxy proxy = new Proxy(tds.get(1).text(), Integer.valueOf(tds.get(2).text()));
                        if (!proxyPool.contain(proxy) && canUse(proxy)) {
                            proxyPool.add(proxy);
                        }
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            LOGGER.error(e.toString());
                        }
                    } catch (Exception e) {
                        LOGGER.error(e.toString());
                    }
                }
            } catch (Exception e) {
                LOGGER.error(e.toString());
            }
            currentPage++;
        }
    }

httpclient超时

  • 需要设置两个超时时间间隔。connectTimeout是链接建立的时间,socketTimeout是等待数据的时间或者两个包之间的间隔时间。

    public static boolean isConnServerByHttp(String serverUrl) {// 服务器是否开启
        boolean connFlag = false;
        URL url;
        HttpURLConnection conn = null;
        try {
            url = new URL(serverUrl);
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(3 * 1000);
            if (conn.getResponseCode() == 200) {// 如果连接成功则设置为true
                connFlag = true;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            conn.disconnect();
        }
        return connFlag;
    }
  • httpclient请求之后一定要 close链接 ,否则再次请求会卡住。

  • 程序中最好设置connectTimeout、socketTimeout,可以防止阻塞。

    • 如果不设置connectTimeout会导致,建立tcp链接时,阻塞,假死。
    • 如果不设置socketTimeout会导致,已经建立了tcp链接,在通信时,发送了请求报文,恰好此时,网络断掉,程序就阻塞,假死在那。
  • 有时,connectTimeout并不像你想的那样一直到最大时间
    socket建立链接时,如果网络层确定不可达,会直接抛出异常,不会一直到connectTimeout的设定值。参考

TIMESTAMP column with CURRENT_TIMESTAMP

  • 只能有一个带CURRENT_TIMESTAMP的timestamp列存在。参考

nginx域名带_字符非法

  • 配置upstream的不使用 _ 。

    upstream local_tomcat {  
        server localhost:8080;
    } 
	改为
    upstream localTomcat {  
        server localhost:8080;
    } 

logback与slf4j的jar冲突

  • tomcat启动时异常。该异常的原因是Springboot本身使用logback打印日志,但是项目中其他的组件依赖了slf4j,这就导致了logback与slf4j的jar包之间出现了冲突。
Exception in thread "main" java.lang.IllegalArgumentException: LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logback or the competing implementation 
  • 两个jar包二选一:
  • 排除slf4j,每个依赖了slf4j的组件都需要加如下标签排除。
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-log4j</artifactId>
	    <version>1.3.8.RELEASE</version>
	    <exclusions>
	        <exclusion>
	            <groupId>org.slf4j</groupId>
	            <artifactId>slf4j-log4j12</artifactId>
	        </exclusion>
	    </exclusions>
	</dependency>
  • 排除logback。
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <exclusions>
        <!--log4j和logback冲突,干掉logback-->
        <exclusion>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

cookie reject 告警

  • 当程序中无需传递cookie值时会出现“Cookie rejected”的警告信息。
  2018-12-12 18:18:25 [WARN]-[org.apache.http.client.protocol.ResponseProcessCookies] Cookie rejected [select_city="320100", version:0, domain:zufangzi.com, path:/, expiry:Thu Dec 13 18:18:25 CST 2018] Illegal 'domain' attribute "zufangzi.com". Domain of origin: "nj.lianjia.com"
  • 如使用httpclient,忽略cookie即可,参考
RequestConfig globalConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();  
CloseableHttpClient client = HttpClients.custom().setDefaultRequestConfig(globalConfig).build();  
HttpGet request = new HttpGet(url);  
CloseableHttpResponse response = client.execute(request);  
  • 如使用webmagic,分析源码,需要设置site的disablecookiemanagement这个属性。
    181212.sitecookie.png
    public LianjiaPageProcessor(int sleepTime, int retryTimes) {
        this.site = Site.me().setRetryTimes(retryTimes).setSleepTime(sleepTime).setDisableCookieManagement(true);
    }

springboot和ssm区别

  • 默认不支持jsp,需要添加的话,参考:springboot项目添加jsp支持
  • mybatis的整合
    • 无需手动配置sqlSessionFactory,自动配置的factory可以应对大多数情况,否则某些自动配置的factory加载不了yml配置,如:
mybatis:
  mapper-locations: classpath:mapper/*.xml 

springboot2和之前版本的区别

  • 版本要求:
    • java8以上
    • ​Tomcat升级至8.5
    • Flyway升级至5
    • Hibernate升级至5.2
    • Thymeleaf升级至3
  • 配置属性
    181210.sb2.png
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值