system desing 系统设计(九):爬虫crawler和typeahead提示框设计

  1、逆向完别人家的APP后,下一步就要想办法变现了,不然花了这么多时间和精力,岂不是白干了?变现的方式之一就是爬虫了!10多年前,那个时候的APP不多,爬虫主要爬取的还是网页web。随着移动互联网大发展,APP数量越来越多,用户组件从PC的浏览器转移到了cellphone的APP,但是爬虫的核心思路没变:请求接口的各种数据!整体的架构设计如下:

   流程很简单:

  •   爬虫先爬取目录网页,比如首页等含有大量url的页面,解析出里面的url;
  •        url放bloomFilter里面去重,有重复的直接去掉,没重复的放入MessgaQueue,比如kafka、disruptor等
  •        再由爬虫从MQ里读取url爬取具体的内容content;由于都是非结构化的数据,建议放分布式文件系统,譬如HDFS
  •        存放好后就可以建倒排索引了,这个涉及到中文分词、MapReduce,索引建好后保存在NoSql数据库,比如cassandra、hbase等;key就是分词后的每个单词,value就是这个单词所在文章的编号index,比如这种结构的: index是一种典型的空间换时间的方式

 至此,内容算是采集、处理和存储算是完毕了!这部分需要注意的事项:

  •   分词:英文不存在分词的问题,但中文需要。市面上分词的规则的算法很多,建议多试几种,结果取并集,一个都不落!
  •        权重:包含某个关键词的文章可能很多,搜索引擎肯定要排序吧,比如用时间排序;还有另一种很重要的排序算法:TF-IDF
  •        爬虫被封:这个貌似只能多换几个ip了!还有请求的参数也进来多换,不要一套参数一直请求数据,内行一眼就看出是爬虫了

   2、内容处理完毕,下一个就是要让用户查询了!用户输入关键词查询,可以先在redis查找一些hot key的index,再根据这些index去HDFS找到具体的content。如果redis没有缓存用户查询的key,就去Nosql数据库找!上面的流程也容易理解,下面重点介绍typeahead,也就是下面这种效果:用户输入一个关键词,联想出prefix top 10的历史query供用户参考选择!

   

   要想实现这个功能,在后台至少需要2个service:queryService和collectionService!前者负责根据用户输入的prefix返回top 10query,后者就需要收集用户全量的query,按照一定的时间周期统计query的次数count了!

        

  (1)很明显,queryService是要依赖collectionService的,所以技术上讲,肯定时要先实现collectionService,也就是统计每个query重复出现的次数,这个怎么做了?大家还记得前面介绍的mapReduce么?只要是能够拆解成KV形式的计算,mapReduce都是可以搞定的,这里的用户query是key,value就是出现次数count,这不正合适么? 至此,collectionService就算完结了? 还能改进么?

  大家平时用搜索引擎的时候,难免手滑输入错误的query。甚至不排除少数用户闲的蛋疼,随机敲键盘乱输入query,比如“xvxcotryersxcver”这类毫无意义meaningless的string。这些字符串该怎么处理了?也去统计次数? 这些字符串都是错误的query,统计次数有业务意义么?既然没意义,怎么精准找到并剔除这些错误和无意义的query了

  仔细想想:我们最终需要的是prefix的top 10,所以是需要通过排序sort找到top 10的!既然只是sort排序,我们保证query之间相对的count是正确的不就行了?比如top 1原来是10000次,top 2是9000次,如果top 1和top 2同时除以1000,那么top 1就是10,top 2就是9,相比之下,rank排名还是没变啊,并不会影响prefix top 10的排名。按照这种思路操作,其实还是要统计所有query次数的。有没有更高效的、连无意义query次数都不用统计的办法了?还真有,如下:

  在query的count+1前,随机生成一个[0~9999]范围内的数。如果是0,那么count+1;如果不是,count就不变!看吧,是不是很简单了?错误输入、无意义meaningless输入的query毕竟是少数小概率事件,只有1/10000的概率会被记录,理论上讲有99.99的概率会被忽略。但是真正的高频词汇比如apple这些,即使每次被记录的概率只有0.01%,但架不住人家就是被query的次数多呀,所以一个统计周期下来,累计的次数肯定也是不少的!这样既保持了排名rank,又去掉了少数的query,一举多得哦!

  最后:colletionService明显是离线任务,执行周期要根据业务需求定了,比如每小时、每4小时、8小时等间隔运行一次? 遇到有些突发的热点消息,设置可能需要every 30minuts运行一次了!

  (2)解决了query->count,接着就要想办法根据用户的prefix 得到top 10历史query了!这本质就是个字符串的匹配问题,业界最常见的就是trie树了,就是根据字母不停的分查,查询的时候根据分叉匹配就是了,如下:

    

   by the way,trie树除了做prefix,还能在一定程度上纠正用户的错误输入,比如下面这种提示,原理也简单:根据用户query开始在tree上traverse,如果没有到leaf node,说明query很有可能出错了,直接给出leaf node的query,让用户自己选择!

            

    从功能上讲,trie是可以匹配prefix的,但遗憾的是:trie没有现成的数据库支持,开发人员手搓实现都是小事,关键是目前暂时也没有分布式的版本,还需要自己考虑分布式的实现,有没有其他现成的、能快速解决问题的办法了?

  前面通过collectionService统计了query的count,现在只是要找到top10,这不还是个排序sort的问题么?mapReduce现成的框架直接用就好了,现在的问题是:key是什么?value又是什么了?既然是要求top10的query,那肯定是要用query做key咯!那value了,又是啥?

  Map:<apple,1billion>  =>  <a,apple:1billion>  、 <ap,apple:1billion>、  <app,apple:1billion> 、<appl,apple:1billion>  ......

            <approve,1million>  =>  <a,approve:1million>  、 <ap,approve:1million>、  <app,approve:1million>、  <appr,approve:1million>  .....

      reduce:  <a,apple:1billion|approve:1million>、<ap,apple:1billion|approve:1million> 、 <app,apple:1billion|approve:1million> 、<appl,apple:1billion> 、<appr,approve:1million>

   咋样? 都能看懂了吧!Map的value就是query:count。然后reduce这里合并,把同一个prefix的不同query:count对做成Iterator了,直接遍历sort就行了!

  (3)如果用户每敲一个letter,就要通过queryService在后台查top 10,这QPS得多大呀,服务器能抗住么?所以现在需要进一步优化queryService的查询频率!

  •  frontend可以设置一个间隔周期:用户前后两个letter输入间隔超过500ms说明在思考,这时才调用queryService查询
  •    一些hot query的top 10缓存在browser本地呗,每个1~2小时再调用queryService从backend得到跟新top 10

 

参考:

1、https://wizardforcel.gitbooks.io/system-design-primer/content/solutions/system_design/web_crawler/  设计网页爬虫

2、https://michaelnielsen.org/ddi/how-to-crawl-a-quarter-billion-webpages-in-40-hours/  40小时爬取2.5亿网页

3、https://cloud.tencent.com/developer/article/1931038 爬虫的设计

4、https://www.bilibili.com/video/BV1Za411Y7rz?p=2&vd_source=241a5bcb1c13e6828e519dd1f78f35b2 爬虫设计

posted @ 2022-08-21 22:10  第七子007  阅读(243)  评论(0编辑  收藏  举报