实验五Elasticsearch+Kibana展示爬虫数据

安装elasticsearch-rtf

Elasticsearch-rtf相比于elasticsearch而言多加了一些插件,因此我们选择安装Elasticsearch-rtf是一个不错的选择。在安装之前我们需要安装java的apk,并且要求版本在8.0或以上,下载地址 , 下载完后,进入bin目录 命令行输入

./elasticsearch

即可启动 在浏览器中访问查看结果

安装elasticsearch-head

Elasticsearch-head是一个可视化的管理工具,利用它我们可以清楚的看到elasticsearch中的数据,下载地址 

在终端输入下列命令

git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install 
npm run start

需要注意的是,在安装前需要测试是否能使用npm命令,npm是nodejs的包管理工具,类似于Java的maven,Python的pip,因此我们需要下载nodejs 下载地址  由于npm的中央服务器在国外,速度太慢,因此我们使用淘宝的npm镜像cnpm 下载cnpm命令

$ npm install -g cnpm --registry=https://registry.npm.taobao.org

安装完之后我们需要配置一下config目录下的.yml文件 输入如下四行

http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-methods: OPTIONS, HEAD, GET, POST, PUT, DELETE
http.cors.allow-headers: "X-Requested-With, Content-Type, Content-Length, X-User"

然后重启一下elasticsearch服务,就可以在9100端口看到elaticsearch连接成功了

安装kibana

下载地址  需要注意的是kibana的版本号必须与elasticsearch对应 我们在面板的info中可以查询elasticsearch版本,当前的版本为5.1.1

下载好对应的版本后,进入bin目录 在终端输入

./kibana

即可运行,在浏览器访问 http://localhost:5601

安装Django

直接使用pip命令安装

pip install django==2.2.2

安装Scrapy

直接使用pip命令安装

pip install scrapy

安装Chromedriver

我们使用Selenium的时候需要用到谷歌浏览器的驱动Chromedriver,下载地址:, 需要找到与谷歌浏览器对应的版本下载,下载完成后放入到/usr/bin或/usr/local/bin目录即可完成安装

sudo mv ./chromedriver /usr/bin

之后在terminal终端输入chromedriver,若显示如下信息,证明安装成功

charles.:~/ $ chromedriver                                                                          [10:21:40]
Starting ChromeDriver 81.0.4044.138 (8c6c7ba89cc9453625af54f11fd83179e23450fa-refs/branch-heads/4044@{#999}) on port 9515
Only local connections are allowed.
Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code.

再通过pip命令安装Selenium

pip install selenium

通过以下代码测试是否可以成功驱动谷歌浏览器

from selenium import webdriver
browser = webdriver.Chrome()
browser.get('http://www.baidu.com')

若可以正常打开浏览器并访问百度首页,证明安装成功

安装MongoDB

在Mac下安装MongoDB可采用Homebrew方式

brew install mongodb

然后创建一个新的文件夹/data/db,用于存放数据

启动MongoDB

brew services start mongodb
sudo mongod

停止和重启MongoDB命令

brew services stop mongodb
brew services restart mongodb

(一)编程实现通用爬虫

无需登录或可抓包模拟登陆

原先我们爬每一个网站都需要单独写一个spider, 这个spider中存在name ,start_urls ,allowed_domains,rules等字段,我们通常要完成下面几步:

  • 通过scrapy命令新建一个spider
  • 定义一个爬取字段信息的字典
  • 通过分析网页找到定位规则
  • 实现parse()解析字段的函数

而这些步骤相对比较固定,因此我们可以新建一个universal爬虫,通过读取配置 文件的方式来动态生成一个spider,省去了重复创建spider的麻烦

1、在终端启动爬虫命令 在终端输入以下命令,就可以启动爬虫

python3 run.py  spider_name

2、读取配置文件,启动爬虫进程 run()函数读取对应spider_name的配置文件后,启动相应的爬虫进程

import sys
from scrapy.utils.project import get_project_settings
from spds.utils import get_config
from scrapy.crawler import CrawlerProcess

def run():
    # 获取爬虫名称
    name = sys.argv[1]

    # 用户配置,一个json文件
    custom_settings = get_config(name)

    # 使用通用爬虫
    spider = custom_settings.get('spider', 'universal')

    # 项目配置
    project_settings = get_project_settings()
    settings = dict(project_settings.copy())
    # 合并用户配置和项目配置
    settings.update(custom_settings.get('settings'))

    # 启动爬虫
    process = CrawlerProcess(settings)
    process.crawl(spider, **{'name': name})
    process.start()

if __name__ == '__main__':
    run()

3、run()函数需要调用get_config()方法 读取配置文件的方法为utils.py中的get_config(),它会根 据传入的爬虫名name,去config目录下寻找对应的爬虫的json文件

from os.path import realpath, dirname
import json

def get_config(name):
    path = dirname(realpath(__file__)) + '/configs/' + name + '.json'
    with open(path, 'r', encoding='utf-8') as f:
        return json.loads(f.read())

4、json文件的结构

{"spider": "universal",
  "website": "第一财经网",
  "type": "新闻",
  "index": "http://yicai.com",
  "settings": {
    "USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36"
  },
  "start_urls":{
    "type": "static",
    "value" :["http://yicai.com/news", "http://yicai.com/data"]
  },
  "allowed_domains": [
    "yicai.com"
  ],

  "rules": "yicai",

  "item": {
    "class": "NewsItem",
    "loader": "ChinaLoader",
    "attrs": {
      "title": [
        {
          "method": "xpath",
          "args": [
            "//div[@class=\"title f-pr\"]/h1//text()"
          ]
        }
      ],
      "url":[
        {
          "method": "attr",
          "args": [
            "url"
          ]
        }
      ],
      "website":[
        {
          "method":"value",
          "args":[
            "第一财经网"
          ]
        }
      ]
    }
  }
}

Selenium + Webdriver

  • 信号

由于使用Selenium进行驱动常用的操作无非是滚动、点击、 输入数据的有序组合,那么我就把它们封装成函数,然后通过 信号值分别调用对应的操作,最后只需要规定它们的顺序来完成 用户想要的控制流程.

信号1 滚动
信号2 点击 
信号3 输入

所有操作浏览器的流程都封装在一个叫做SeleniumMiddleware的中间件来实现, 当驱动程序工作完成后,将采集到的数据返回给Spider中的parse_item()函数 进行解析,最后再通过MongoPipeline管道存入MongoDB数据库

  • 任务

和之前的通用爬虫一样,将所有可配置的参数都抽离出来,形成一个json文件,其中 不同之处在于这里的tasks字段

"tasks": [
    {
      "order": 1,
      "action": {
        "signal": 1,
        "args": "",
        "text": ""
      },
      "attrs": {}
    },

    {
      "order": 2,
      "action": {
        "signal": 2,
        "args": "//li[@data-anchor=\"#comment\" and contains(.,\"商品评价\")]"
      },
      "attrs": {
        "mark": "//div[@class=\"comment-percent\"]"
      }
    },
    {
      "order": 3,
      "action": {
        "signal": 2,
        "args": "//li[@data-anchor=\"#detail\" and contains(.,\"商品介绍\")]"
      },
      "attrs": {
        "product_name": "//ul[@class=\"parameter2 p-parameter-list\"]/li[contains(.,\"商品名称\")]",
        "product_code": "//ul[@class=\"parameter2 p-parameter-list\"]/li[contains(.,\"商品编号\")]",
        "product_weight": "//ul[@class=\"parameter2 p-parameter-list\"]/li[contains(.,\"商品毛重\")]"
      }
    }

  ]

所有的任务都规定在这个列表中,每个元素是一个字典类型,代表一个task任务,我们规定一个 task任务拥有以下3个属性

  • order 用来规定任务task发生的顺序
  • action 表示要采取的动作类型,通过signal来确定,每个信号对应的动作见信号
  • attrs表示动作结束后所需要获取的字段,它的值是一个字典类型,key为字段名称,值为提取该字段的xpath规则

(二)通过Scrapy框架中的Pipeline将数据写入MongoDB

1、在项目中定义一个MongoPipeline

# pipeline.py
class MongoPipeline():
    def __init__(self, mongo_uri, mongo_db, mongo_coll):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db
        self.mongo_coll = mongo_coll

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DB'),
            mongo_coll=crawler.settings.get('MONGO_COLL')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def process_item(self, item, spider):
        self.db[self.mongo_coll].insert(dict(item))
        return item

    def close_spider(self, spider):
        self.client.close()


class SpdsPipeline(object):
    def process_item(self, item, spider):
        return i

2、在settings.py中设定优先顺序

# settings.py
ITEM_PIPELINES = {
   # 'spds.pipelines.SpdsPipeline': 300,
     'spds.pipelines.MongoPipeline': 301,
}

3、添加MongoDB参数

# MONGO_URI = 'mongodb://jizhu:jizhu123@119.29.214.139:27017' # 腾讯云服务器
MONGO_URI = 'localhost'
MONGO_DB = 'articles'
MONGO_COLL = 'yicai'

(三)搭建本地Elasticsearch集群

其实在Elasticsearch搭建本地集群非常简单,只需要多开几个终端进程,在命令行附带一些额外信息即可

bin/elasticsearch (默认端口9200)
bin/elasticsearch -Ehttp.port=8200 -Epath.data=node2
bin/elasticsearch -Ehttp.port=7200 -Epath.data=node3

通过上面的命令就可以在端口为920082007200上各开一个Elasticsearch节点

(四)编写程序将MongoDB数据写入Elasticsearch

1、最终的数据需要进行清洗、分词等处理并放入Elasticsearch中进行索引

# lagou_es.py
...
client = pymongo.MongoClient('127.0.0.1', 27017)
    db = client['lagou']
    collection_list = [ 'jobs', 'quanzhangongchengshi','qukuailian', 'tuxiangchuli', 'tuxiangshibie', 'yuyinshibie']
    for coll in collection_list:
        print('正在转换::' + coll)
        collection = db[coll]
        ls = list(collection.find())
        for item in ls:
            item['website'] = '拉勾网'
            job = JobType()
            job.website = item['website']
            job.url = item.get('url','')
            job.job_name = item.get('job_name','')
            job.company = item.get('company','')
            job.salary = item.get('salary','')
            job.city = item.get('city','')
            job.experience = item.get('experience','')
            job.education = item.get('education','')
            job.job_type = item.get('job_type','')
            job.publish_time = item.get('publish_time','')
            job.job_advantage = item.get('job_advantage','')
            job.job_desc = item.get('job_desc','')
            job.job_addr = item.get('job_addr','')
            job.c_area = item.get('c_area','')
            job.c_dev = item.get('c_dev','')
            job.c_size = item.get('c_size','')
            job.c_investment = item.get('c_investment','')
            job.c_homepage = item.get('c_homepage','')

            # 新增城市地理位置和工作地地理位置
            job.city_location = getGeoPoint(item.get('city',''))
            job.job_addr_location = getGeoPoint(item.get('job_addr',''))

            job.suggest = gen_suggests(JobType._doc_type.index, ((job.job_name,10),(job.city,3)))
            job.save()
...

2、插入Elasticsearch前的额外工作 由于之后需要通过地理信息绘制热力图,需要使用到经纬度信息,所以需要提前将具体的地理位置转换成经纬度信息,这里使用高德地图的地理/逆地理编码API,实现一个getGeoPoint()方法. (点击这里查看高德API开发文档)

# -*- coding:utf-8 -*-
# Author: Zhu Chen 
# Create Time: 2020/06  All rights reserved

import requests
import json
import re
def getGeoPoint(addr):
    parameters = {
        'key': '84040e9a1a5c324110dbddf00f5d4477',
        'address': addr,
        'city': '',
        'output':'json'
    }

    url = 'https://restapi.amap.com/v3/geocode/geo'

    res = requests.get(url=url, params=parameters)
    try:
        result = json.loads(res.text).get('geocodes')[0].get('location')
    except:
        result = ''

    # 对经纬度逆置
    invert = ''
    matched = re.match('(.*),(.*)', result)
    if matched:
        invert = matched.group(2) + ',' + matched.group(1)

    return invert



if __name__ == '__main__':

    res = getGeoPoint('北京市朝阳区阜通东大街6号')
    print(res)

这里需要注意一点:一般情况下API都是先经度后纬度,但是Elasticseach中Mapping定义的GeoPoint字段要求的是先纬度后经度,因此需要做一下逆置处理,这一点很容易被人忽略

(五)基于Elasticsearch检索服务实现一个全文搜索引擎

Python Web服务器

配置Django环境,创建项目

1、安装django
pip install django 

2、创建目录
mkdir ~/projects/django

3、创建项目
django-admin startproject WebService

4、启动项目
cd WebService
python manage.py runserver  

5、创建app
python manage.py startapp Search 

项目结构

├── Search
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── urls.py
│   └── views.py
├── WebService
│   ├── __init__.py
│   ├── __pycache__
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── manage.py
├── static
│   ├── admin
│   ├── css
│   ├── img
│   └── js
└── template
    ├── index.html
    └── result.html

前端功能实现

1、首页搜索提示栏的显示与隐藏

搜索建议是通过Ajax异步请求获取的,关键代码:

// 搜索建议
$(function(){
    $('.searchInput').bind(' input propertychange ',function(){
        var searchText = $(this).val();
        var tmpHtml = ""
        $.ajax({
            cache: false,
            type: 'get',
            dataType:'json',
            url:suggest_url+"?s="+searchText+"&s_type="+$(".searchItem.current").attr('data-type'),
            async: true,
            success: function(data) {
                for (var i=0;i<data.length;i++){
                    tmpHtml += '<li><a  href="'+search_url+'?q='+data[i]+'">'+data[i]+'</a></li>'
                }
                $(".dataList").html("")
                $(".dataList").append(tmpHtml);
                if (data.length == 0){
                    $('.dataList').hide()
                }else {
                    $('.dataList').show()
                }
            }
        });
    } );
})

hideElement($('.dataList'), $('.searchInput'));

2、结果页面的侧边栏显示隐藏

实现代码:

$('.subfieldContext .more').click(function(e){
   var $more = $(this).parent('.subfieldContext').find('.more');
   if($more.hasClass('show')){

      if($(this).hasClass('define')){
         $(this).parent('.subfieldContext').find('.more').removeClass('show').find('.text').text('自定义');
      }else{
         $(this).parent('.subfieldContext').find('.more').removeClass('show').find('.text').text('更多'); 
      }
      $(this).parent('.subfieldContext').find('li:gt(2)').hide().end().find('li:last').show();
    }else{
      $(this).parent('.subfieldContext').find('.more').addClass('show').find('.text').text('收起');
      $(this).parent('.subfieldContext').find('li:gt(2)').show();    
   }

});

$('.sideBarShowHide a').click(function(e) {
   if($('#main').hasClass('sideBarHide')){
      $('#main').removeClass('sideBarHide');
      $('#container').removeClass('sideBarHide');
   }else{
      $('#main').addClass('sideBarHide');    
      $('#container').addClass('sideBarHide');
   }

   });

3、“我的搜索”使用本地缓存

关键代码:

var searchArr;
//定义一个search的,判断浏览器有无数据存储(搜索历史)
if(localStorage.search){
//如果有,转换成 数组的形式存放到searchArr的数组里(localStorage以字符串的形式存储,所以要把它转换成数组的形式)
    searchArr= localStorage.search.split(",")
}else{
//如果没有,则定义searchArr为一个空的数组
    searchArr = [];
}
//把存储的数据显示出来作为搜索历史
MapSearchArr();

function add_search(){
    var val = $(".searchInput").val();
    if (val.length>=2){
        //点击搜索按钮时,去重
        KillRepeat(val);
        //去重后把数组存储到浏览器localStorage
        localStorage.search = searchArr;
        //然后再把搜索内容显示出来
        MapSearchArr();
    }
window.location.href=search_url+'?q='+val+"&s_type="+$(".searchItem.current").attr('data-type')

}

4、关键词去重处理

关键代码:

function KillRepeat(val){
    var kill = 0;
    for (var i=0;i<searchArr.length;i++){
        if(val===searchArr[i]){
            kill ++;
        }
    }
    if(kill<1){
        searchArr.unshift(val);
    }else {
        removeByValue(searchArr, val)
        searchArr.unshift(val)
    }
}

后端功能实现

1、View视图层的部分实现代码:

class IndexView(View):
    #首页
    def get(self, request):
        _topn_search = redis_kw.zrevrangebyscore("search_keywords_set", "+inf", "-inf", start=0, num=5)
        topn_search = []
        for item in _topn_search:
            topn_search.append(item.decode())
        return render(request, "index.html", {"topn_search":topn_search})
        #return render(request, "index.html")

class SearchSuggest(View):
#搜索建议功能
    def get(self, request):
        key_words = request.GET.get('s','')
        re_datas = []
        if key_words:
            s = ArticleType.search()
            s = s.suggest('my_suggest', key_words, completion={
                "field":"suggest", "fuzzy":{
                    "fuzziness":2
                },
                "size": 10
            })
            suggestions = s.execute_suggest()
            for match in suggestions.my_suggest[0].options:
                source = match._source
                re_datas.append(source["title"])
        return HttpResponse(json.dumps(re_datas), content_type="application/json")


class SearchView(View):
#结果页
    def get(self, request):
        key_words = request.GET.get("q","")
        s_type = request.GET.get("s_type", "article")

        redis_kw.zincrby("search_keywords_set", 1, key_words)

        _topn_search = redis_kw.zrevrangebyscore("search_keywords_set", "+inf", "-inf", start=0, num=5)
        topn_search = []
        for item in _topn_search:
            topn_search.append(item.decode())

        page = request.GET.get("p", "1")
        try:
            page = int(page)
        except:
            page = 1

        try:
            yicai_count = redis_cnt.get("articles").decode()
        except:
            yicai_count = 9999

        start_time = datetime.now()
        response = client.search(
            index= "yicai_new",
            body={
                "query":{
                    "multi_match":{
                        "query":key_words,
                        "fields":["intro", "title", "contents"]
                    }
                },
                "from":(page-1)*10,
                "size":10,
                "highlight": {
                    "pre_tags": ['<span class="keyWord">'],
                    "post_tags": ['</span>'],
                    "fields": {
                        "title": {},
                        "contents": {},
                        "intro":{}
                    }
                }
            }
        )

        end_time = datetime.now()
        last_seconds = (end_time-start_time).total_seconds()
        total_nums = response["hits"]["total"]
        if (page%10) > 0:
            page_nums = int(total_nums/10) +1
        else:
            page_nums = int(total_nums/10)
        hit_list = []
        for hit in response["hits"]["hits"]:
            hit_dict = {}
            if "title" in hit["highlight"]:
                hit_dict["title"] = "".join(hit["highlight"]["title"])
            else:
                hit_dict["title"] = hit["_source"]["title"]
            if "contents" in hit["highlight"]:
                hit_dict["contents"] = "".join(hit["highlight"]["contents"])[:1000]
            else:
                hit_dict["contents"] = hit["_source"]["contents"][:1000]

            hit_dict["time"] = hit["_source"]["time"]
            hit_dict["url"] = hit["_source"]["url"]
            hit_dict["score"] = hit["_score"]
            hit_dict["source"] = hit["_source"]["source"]
            hit_dict["website"] = hit["_source"]["website"]

            hit_list.append(hit_dict)

        return render(request, "result.html", {"page":page,
                                               "all_hits":hit_list,
                                               "key_words":key_words,
                                               "total_nums":total_nums,
                                               "page_nums":page_nums,
                                               "last_seconds":last_seconds,
                                               "yicai_count":yicai_count,
                                               "topn_search":topn_search

                                               })

2、Model层的部分实现代码:

class ArticleType(DocType):
    #第一财经文章类型
    suggest = Completion(analyzer=ik_analyzer)
    title = Text(analyzer="ik_max_word")
    url = Keyword()
    time = Date()
    author = Keyword()
    intro = Text(analyzer='ik_max_word')
    source = Keyword()
    contents = Text(analyzer='ik_max_word')
    editor = Keyword()
    website = Keyword()

    class Meta:
        index = "yicai"
        doc_type = "article"

3、Url路由的配置代码:

urlpatterns = [
    path('', IndexView.as_view(), name='index'),
    path('suggest/', SearchSuggest.as_view(), name='suggest'),
    path('search/', SearchView.as_view(), name='search'),
]

4、全局路由配置urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(app_urls))
]

(六)用Kibana实现数据分析和数据可视化

DevTools

Elasticsearch 提供了 REST HTTP 接口,对于开发和测试也极其方便,在实际使用时,很多人会使用命令行 curl 或者 Postman之类图形化 http 请求工具来完成对 Elasticsearch 的请求。官方有 Kibana中的Dev Tools Console插件这类神兵利器,用来替代效率低下的命令行,用过就再也回不去命令行了

Mapping 映射

为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。

Elasticsearch 支持如下简单域类型:

  • 字符串: string
  • 整数 : byte, short, integer, long
  • 浮点数: float, double
  • 布尔型: boolean
  • 日期: date

Elasticsearch还提供了一些高级的类型,比如Elasticsearch提供了 两种表示地理位置的方式:用纬度-经度表示的坐标点使用 geo_point 字段类型, 以 GeoJSON 格式定义的复杂地理形状,使用 geo_shape 字段类型。我们在绘制热点城市地图的时候定义的job_addr_location使用的就是geo_point类型

Aggregation 聚合

聚合允许我们向数据提出一些复杂的问题。虽然功能完全不同于搜索,但它使用相同的数据结构。这意味着聚合的执行速度很快并且就像搜索一样几乎是实时的。

这对报告和仪表盘是非常强大的。它实时显示客户数据,便于立即回应,而不是对数据进行汇总( 需要一周时间去运行的 Hadoop 任务 ),报告随着数据变化而变化,而不是预先计算的、过时的和不相关的。

聚合的两个主要的概念:

  • 桶(Buckets) 满足特定条件的文档的集合
  • 指标(Metrics) 对桶内的文档进行统计计算

聚合实例:

汽车经销商可能会想知道哪个颜色的汽车销量最好,用聚合可以轻易得到结果,用 terms 桶操作:

posted @ 2023-02-24 18:37  GaoYanbing  阅读(92)  评论(0编辑  收藏  举报