问题--feed列表有新闻重复的问题
1. 经常有运营反应,客户端展示的feed列表有重复的问题。
重复问题分为两种,一种是两条新闻标题类似,另一种是两条新闻标题是完全相同。
(1)标题类似
原来过滤的逻辑,是两个标题完全相等,才认为两条新闻内容一样,后来改进了一下,比较两个标题的相度,如果相似度超过70%,那么就认为这是同一条新闻,如(北京今日降大雨,公交停运 and 北京降大雨,公交停运)这两条新闻
相似度超过了70%,那么就把其中的一条过滤掉,过滤的算法
public static double getSimilarity(String doc1, String doc2) { if (doc1 != null && doc1.trim().length() > 0 && doc2 != null && doc2.trim().length() > 0) { Map<Integer, int[]> AlgorithmMap = new HashMap<Integer, int[]>(); //将两个字符串中的中文字符以及出现的总数封装到,AlgorithmMap中 for (int i = 0; i < doc1.length(); i++) { char d1 = doc1.charAt(i); if(isHanZi(d1)){ int charIndex = getGB2312Id(d1); if(charIndex != -1){ int[] fq = AlgorithmMap.get(charIndex); if(fq != null && fq.length == 2){ fq[0]++; }else { fq = new int[2]; fq[0] = 1; fq[1] = 0; AlgorithmMap.put(charIndex, fq); } } } } for (int i = 0; i < doc2.length(); i++) { char d2 = doc2.charAt(i); if(isHanZi(d2)){ int charIndex = getGB2312Id(d2); if(charIndex != -1){ int[] fq = AlgorithmMap.get(charIndex); if(fq != null && fq.length == 2){ fq[1]++; }else { fq = new int[2]; fq[0] = 0; fq[1] = 1; AlgorithmMap.put(charIndex, fq); } } } } Iterator<Integer> iterator = AlgorithmMap.keySet().iterator(); double sqdoc1 = 0; double sqdoc2 = 0; double denominator = 0; while(iterator.hasNext()){ int[] c = AlgorithmMap.get(iterator.next()); denominator += c[0]*c[1]; sqdoc1 += c[0]*c[0]; sqdoc2 += c[1]*c[1]; } return denominator / Math.sqrt(sqdoc1*sqdoc2); } else { throw new NullPointerException( "比较过程发生异常"); } } public static boolean isHanZi(char ch) { // 判断是否汉字 return (ch >= 0x4E00 && ch <= 0x9FA5); } /** * 根据输入的Unicode字符,获取它的GB2312编码或者ascii编码, * * @param ch * 输入的GB2312中文字符或者ASCII字符(128个) * @return ch在GB2312中的位置,-1表示该字符不认识 */ public static short getGB2312Id(char ch) { try { byte[] buffer = Character.toString(ch).getBytes("GB2312"); if (buffer.length != 2) { // 正常情况下buffer应该是两个字节,否则说明ch不属于GB2312编码,故返回'?',此时说明不认识该字符 return -1; } int b0 = (int) (buffer[0] & 0x0FF) - 161; // 编码从A1开始,因此减去0xA1=161 int b1 = (int) (buffer[1] & 0x0FF) - 161; // 第一个字符和最后一个字符没有汉字,因此每个区只收16*6-2=94个汉字 return (short) (b0 * 94 + b1); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return -1; }
这样就不会再出现标题类似的情况
(2)标题重复
为什么会标题重复?
数据库里抓取了两条相同的内容,查看了一下数据库,发现确实有标题相同的新闻。
为什么数据库会有标题相同的新闻?
查记录发现,两条相同的同容,相隔时间很短,这很可能是高并发造成的。多线程操作数据库引起的。
解决方案:
采用 生产者消费者 模式,多个生产者线程抓取数据,一个消费者线程消费数据,测试了一下,每个对象大小约为0.1075k 10000个大约是1M, ,所以我就给队列设置大小为10000,即就算队列里存了10000个对象所占内存也才1M,这完全可以接受。
消费者代码
public static Logger logger = LoggerFactory.getLogger(SingleThreadTakeQueueTask.class); /* * 每个对象大小大约是0.1075k 10000个大约是1M, * 在测试环境中,120个接口,全部开始跑,feedQueue队列中,剩余未被消费的对象,最多的时候剩余量是24个,所以队列长度设置为10000 * 是够用的 */ private static final Integer QUEUE_SIZE = 10000; public static BlockingQueue<FeedQueueBean> feedQueue = new LinkedBlockingQueue<FeedQueueBean>(QUEUE_SIZE); private FeedService feedService; private FeedBannerService feedBannerService; private FeedIconService feedIconService; public SingleThreadTakeQueueTask(){} public SingleThreadTakeQueueTask(FeedService feedService , FeedBannerService feedBannerService, FeedIconService feedIconService){ this.feedService = feedService; this.feedBannerService = feedBannerService; this.feedIconService = feedIconService; } @Override public void run() { while(true){ logger.info("阻塞,等待读取队列==" , feedQueue.size()); FeedQueueBean feedQueueBean = null; try { feedQueueBean = feedQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); logger.error("feedQueueError:{} " , e.getLocalizedMessage()); continue; } logger.info("从队列取出 {}" , feedQueue.size()); ArticleJsonBean bean = feedQueueBean.getArticleJsonBean(); if(isFilterOut(bean)){ continue; } // 主表 Long feedId = saveFeed(bean, bean.getPartner(), bean.getFeedCategory()); if(feedId == null){ logger.info("保存feed发生异常"); continue; } logger.info("保存feed完成"); // 保存banner FeedBanner feedBanner = feedQueueBean.getFeedBanner(); saveBanner(feedId, feedBanner); logger.info("保存banner完成"); // 保存icon List<FeedIcon> feedIcons = feedQueueBean.getFeedIcons(); saveIconList(feedIcons,feedId); logger.info("保存icon完成"); // 图片集合 if(FeedTaskJob.SHOW_TYPE_PIC_SET == bean.getShow_type() && feedQueueBean.getFeedPicSetBanner() != null){ FeedBanner feedPicSetBanner = feedQueueBean.getFeedPicSetBanner(); saveBanner(feedId,feedPicSetBanner); } logger.info("保存set完成"); // 白名单 if (null != bean.getWhiteList() && bean.getWhiteList().intValue() == 1) { try{ feedService.updateFeedStatus(feedId); feedService.auditArticle(feedId, bean.getFeedCategory()); }catch(Exception e){ e.printStackTrace(); logger.error("updateError : {} " , e.getLocalizedMessage()); } } logger.info("本次从队列取对象完成"); //feedQueue.remove(feedQueueBean); } } private Long saveFeed(ArticleJsonBean bean, String partner, Long feedCategory) { Feed feed = new Feed(); ExpandParam param = new ExpandParam(); feed.setCreateTime(System.currentTimeMillis()); param.setIsCaputer(1); param.setThirdUrl(bean.getUrl()); param.setSource(bean.getAuthor_name()+"("+partner+")"); bean.setShow_type(bean.getShow_type() == null ? 3 : bean.getShow_type()); if(bean.getShow_type().intValue() == FeedTaskJob.SHOW_TYPE_PIC_SET){ //showtype是图片集合时,image_number必传 param.setImageNumber(bean.getImage_number()); } if(bean.getShow_type().intValue() == FeedTaskJob.SHOW_TYPE_VIDEO){ //showtype是视频时,时间必传 param.setTime(bean.getTime()); } JsonConfig config = new JsonConfig(); config.setJsonPropertyFilter(new PropertyFilter() { @Override public boolean apply(Object source, String name, Object value) { if (value == null || "".equals(value)) { return true; } return false; } }); feed.setExpandParam(JSONObject.fromObject(param,config).toString()); feed.setFeedCategory(feedCategory); if(StringUtils.isBlank(bean.getDesc())) { feed.setFeedDesc(bean.getTitle().trim().replace("\n", "")); } else { feed.setFeedDesc(bean.getDesc().trim()); } feed.setFeedType(bean.getShow_type() == null ? 3 : bean.getShow_type()); feed.setFeedUrl(getHtml5Url(bean.getUrl())); feed.setFeedTitle(bean.getTitle().trim().replace("\n", "")); feed.setStatus(0); feed.setPublishTime(System.currentTimeMillis()); try{ logger.info("组装feed对象over"); int result = filterFeedColumnLength(feed); if(result == 1){ feedService.saveFeed(feed); logger.info("保存feed对象over"); } }catch(Exception e){ e.printStackTrace(); logger.error("error:{}" , e.getLocalizedMessage()); } return feed.getId(); } private int filterFeedColumnLength(Feed feed){ if(feed == null ){ return 0; } if(feed.getFeedTitle() != null){ if(feed.getFeedTitle().length() >= 255){ logger.info("title需要截取,url {} " , feed.getFeedTitle()); feed.setFeedTitle(feed.getFeedTitle().substring(0,254)); return 1; } } if(feed.getFeedDesc() != null){ if(feed.getFeedDesc().length() >= 500){ logger.info("desc需要截取,url {} " , feed.getFeedDesc()); feed.setFeedDesc(feed.getFeedDesc().substring(0,499)); return 1; } } if(feed.getExpandParam() != null){ if(feed.getExpandParam().length() >= 1024){ logger.info("expandParam需要截取,url {} " , feed.getExpandParam()); feed.setExpandParam(feed.getExpandParam().substring(0,1023)); return 0; } } if(feed.getFeedUrl() != null){ if(feed.getFeedUrl().length() >= 100){ logger.info("url需要截取,url {} " , feed.getFeedUrl()); feed.setFeedUrl(feed.getFeedUrl().substring(0,99)); return 0; } } return 1; } private String getHtml5Url(String urlString) { logger.info("文章地址:{}",urlString); String url = null; String fileFileName = System.currentTimeMillis() + "_feedStream.html"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/"); String path = sdf.format(new Date()); String realpath = ConstantApp.getImageRealPathFeedStream() + path; File savefile = new File(new File(realpath), fileFileName); String content = "<html>"+ "<body>"+ "</body>"+ "<script type='text/javascript'>"+ " window.location='"+urlString+"';"+ "</script>"+ "</html>"; try { FileUtils.writeStringToFile(savefile, content); } catch (IOException e) { logger.error("string转换为html出错:" ,e); } url = path + fileFileName; return url; } private void saveBanner(Long feedId , FeedBanner feedBanner){ if(feedId == null || feedBanner == null){ return; } feedBanner.setFeedId(feedId); logger.info("组装feedBanner over"); try{ feedBannerService.saveFeedBanner(feedBanner); }catch(Exception e){ e.printStackTrace(); logger.error("bannerError : {}" , e.getLocalizedMessage()); return; } logger.info("保存feedBanner over"); } private void saveIconList(List<FeedIcon> feedIcons , Long feedId){ List<FeedIcon> feedIconsTmp = new ArrayList<FeedIcon>(); for(FeedIcon feedIcon : feedIcons){ if(feedIcon != null){ feedIcon.setFeedId(feedId); feedIconsTmp.add(feedIcon); } } logger.info("组装feedIconList over"); try{ feedIconService.saveFeedIcons(feedIconsTmp); }catch(Exception e){ e.printStackTrace(); logger.error("iconError:{} " + e.getLocalizedMessage()); return; } logger.info("保存feedIconList over"); } private boolean isFilterOut(ArticleJsonBean bean) { if(null == bean){ return true; } if(StringUtils.isBlank(bean.getThumbnail_pic_s())){ return true; } if(StringUtils.isBlank(bean.getTitle())){ return true; } if(StringUtils.isBlank(bean.getThumbnail_pic_small1()) && StringUtils.isBlank(bean.getThumbnail_pic_small2()) && StringUtils.isBlank(bean.getThumbnail_pic_small3())){ return true; } for (String key : FeedTaskJob.KEY_WORLD) { if (bean.getTitle().contains(key)) { logger.info("包含特殊关键词:{}", key); return true; } } for (String title : FeedTaskJob.concurentTitleSet) { double similarStandard = CosineSimilarAlgorithm.getSimilarity(bean.getTitle(), title); if (similarStandard > 0.6d) { logger.info("已经存在该文章标题 ,title:{}", bean.getTitle()); return true; } } if (FeedTaskJob.concurrentUrlSet.contains(bean.getUrl())) { logger.info("已存在url, title:{} , url:{}", bean.getTitle(),bean.getUrl()); return true; } FeedTaskJob.concurentTitleSet.add(bean.getTitle()); FeedTaskJob.concurrentUrlSet.add(bean.getUrl()); return false; }