搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

Python分布式爬虫打造搜索引擎

基于Scrapy、Redis、elasticsearch和django打造一个完整的搜索引擎网站

​​ ​https://github.com/mtianyan/ArticleSpider​​ 未来是什么时代?是数据时代!数据分析服务、互联网金融,数据建模、自然语言处理、医疗病例分析……越来越多的工作会基于数据来做,而爬虫正是快速获取数据最重要的方式,相比其它语言,Python爬虫更简单、高效

一、基础知识学习:

1. 爬取策略的深度优先和广度优先

目录:

  1. 网站的树结构
  2. 深度优先算法和实现
  3. 广度优先算法和实现

网站url树结构分层设计:

  • bogbole.com
  • blog.bogbole.com
  • python.bogbole.com
  • python.bogbole.com/123

环路链接问题:

从首页到下面节点。

但是下面的链接节点又会有链接指向首页

所以:我们需要对于链接进行去重

1. 深度优先

2. 广度优先

跳过已爬取的链接

对于二叉树的遍历问题

深度优先(递归实现):

顺着一条路,走到最深处。然后回头

广度优先(队列实现):

分层遍历:遍历完儿子辈。然后遍历孙子辈

Python实现深度优先过程code:

登录后复制

def depth_tree(tree_node):

if tree_node is not None:

print (tree_node._data)

if tree_node._left is not None:

return depth_tree(tree_node.left)

if tree_node._right is not None:

return depth_tree(tree_node,_right)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

Python实现广度优先过程code:

登录后复制

def level_queue(root):

#利用队列实现树的广度优先遍历

if root is None:

return

my_queue = []

node = root

my_queue.append(node)

while my_queue:

node = my_queue.pop(0)

print (node.elem)

if node.lchild is not None:

my_queue.append(node.lchild)

if node.rchild is not None:

my_queue.append(node.rchild)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

2. 爬虫网址去重策略

  1. 将访问过的url保存到数据库中
  2. 将url保存到set中。只需要O(1)的代价就可以查询到url

100000000*2byte*50个字符/1024/1024/1024 = 9G

  1. url经过md5等方法哈希后保存到set中,将url压缩到固定长度而且不重复
  2. 用bitmap方法,将访问过的url通过hash函数映射到某一位
  3. bloomfilter方法对bitmap进行改进,多重hash函数降低冲突

scrapy去重使用的是第三种方法:后面分布式scrapy-redis会讲解bloomfilter方法。

3. Python字符串编码问题解决:

  1. 计算机只能处理数字,文本转换为数字才能处理,计算机中8个bit作为一个字节,

    所以一个字节能表示的最大数字就是255

  2. 计算机是美国人发明的,所以一个字节就可以标识所有单个字符

    ,所以ASCII(一个字节)编码就成为美国人的标准编码

  3. 但是ASCII处理中文明显不够,中文不止255个汉字,所以中国制定了GB2312编码

    ,用两个字节表示一个汉字。GB2312将ASCII也包含进去了。同理,日文,韩文,越来越多的国家为了解决这个问题就都发展了一套编码,标准越来越多,如果出现多种语言混合显示就一定会出现乱码

  4. 于是unicode出现了,它将所有语言包含进去了。
  5. 看一下ASCII和unicode编码:
  1. 字母A用ASCII编码十进制是65,二进制 0100 0001
  2. 汉字”中” 已近超出ASCII编码的范围,用unicode编码是20013二进制是01001110 00101101
  3. A用unicode编码只需要前面补0二进制是 00000000 0100 0001
  1. 乱码问题解决的,但是如果内容全是英文,unicode编码比ASCII编码需要多一倍的存储空间,传输也会变慢。
  2. 所以此时出现了可变长的编码”utf-8” ,把英文:1字节,汉字3字节,特别生僻的变成4-6字节,如果传输大量的英文,utf8作用就很明显。

**读取文件,进行操作时转换为unicode编码进行处理** **保存文件时,转换为utf-8编码。以便于传输** 读文件的库会将转换为unicode *python2 默认编码格式为`ASCII`,Python3 默认编码为 `utf-8`*

登录后复制

#python3

import sys

sys.getdefaultencoding()

s.encoding('utf-8')

  • 1.
  • 2.
  • 3.
  • 4.

登录后复制

#python2

import sys

sys.getdefaultencoding()

s = "我和你"

su = u"我和你"

~~s.encode("utf-8")#会报错~~

s.decode("gb2312").encode("utf-8")

su.encode("utf-8")

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

二、伯乐在线爬取所有文章

1. 初始化文件目录

基础环境

  1. python 3.5.1
  2. JetBrains PyCharm 2016.3.2
  3. mysql+navicat

为了便于日后的部署:我们开发使用了虚拟环境。

登录后复制

pip install virtualenv

pip install virtualenvwrapper-win

安装虚拟环境管理

mkvirtualenv articlespider3

创建虚拟环境

workon articlespider3

直接进入虚拟环境

deactivate

退出激活状态

workon

知道有哪些虚拟环境

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

注意:

默认workon的windows路径位于C:\Users\Administrator\Envs,建议将workon的虚拟列表路径改为非C盘,所以通过设置WORKON_HOME指定其他的路径,如我改为了:E:\Envs(不用再添加到path中)

如何创建指定python版本的虚拟环境:

登录后复制

virtualenv -p /usr/bin/python2.7 env

  • 1.

在安装scrapy时:

如:pip install scrapy 时出现:

​​​error: Microsoft Visual C++ 14.0 is required. Get it with “Microsoft Visual C++ Build Tools”: http://landinghub.visualstudio.com/visual-cpp-build-tools​

解决办法

方法一(前提你的网络够快): 安装 Microsoft visual c++ 14.0

​https://964279924.ctfile.com/fs/1445568-239446865​​​ 或

​https://pan.baidu.com/s/1q2Nj41Xk85CHHv7_zOhQIA​​ 密码:qbba

方法二:​ ​https://www.jb51.net/article/125081.htm​

通过在虚拟环境中

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

然后再安装scrapy就可以了:

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

scrapy项目初始化介绍

自行官网下载py35对应得whl文件进行pip离线安装

Scrapy 1.3.3

**命令行创建scrapy项目**

登录后复制

cd desktop

scrapy startproject ArticleSpider #在虚拟环境中输入

  • 1.
  • 2.
  • 3.

**scrapy目录结构** scrapy借鉴了django的项目思想

  • ​scrapy.cfg​​:配置文件。
  • ​setings.py​​:设置

登录后复制

SPIDER_MODULES = ['ArticleSpider.spiders'] #存放spider的路径

NEWSPIDER_MODULE = 'ArticleSpider.spiders'

  • 1.
  • 2.

pipelines.py:

做跟数据存储相关的东西

middilewares.py:

自己定义的middlewares 定义方法,处理响应的IO操作

__init__.py:

项目的初始化文件。

items.py:

定义我们所要爬取的信息的相关属性。Item对象是种类似于表单,用来保存获取到的数据

**创建我们的spider**

登录后复制

cd ArticleSpider

scrapy genspider jobbole blog.jobbole.com

  • 1.
  • 2.

可以看到直接为我们创建好的空项目里已经有了模板代码。如下:

登录后复制

# -*- coding: utf-8 -*-

import scrapy

class JobboleSpider(scrapy.Spider):

name = "jobbole"

allowed_domains = ["blog.jobbole.com"]

# start_urls是一个带爬的列表,

#spider会为我们把请求下载网页做到,直接到parse阶段

start_urls = ['http://blog.jobbole.com/']

def parse(self, response):

pass

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

scray在命令行启动某一个Spyder的命令:

登录后复制

scrapy crawl jobbole

  • 1.

注意:因为使用了虚拟环境,所以将项目导入pycharm时要将项目的project interpreter切换为虚拟环境下的python.exe

比如我的虚拟环境中的python.exe位于 E:\Envs\pyscrapy3\Scripts

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

**在windows报出错误** `ImportError: No module named ‘win32api’`

登录后复制

pip install pypiwin32#解决

  • 1.

**创建我们的调试工具类*** 在项目根目录里创建main.py 作为调试工具文件

登录后复制

# _*_ coding: utf-8 _*_

__author__ = 'mtianyan'

__date__ = '2017/3/28 12:06'

from scrapy.cmdline import execute

import sys

import os

#将系统当前目录设置为项目根目录

#os.path.abspath(__file__)为当前文件所在绝对路径

#os.path.dirname为文件所在目录

#H:\CodePath\spider\ArticleSpider\main.py

#H:\CodePath\spider\ArticleSpider

sys.path.append(os.path.dirname(os.path.abspath(__file__)))

#执行命令,相当于在控制台cmd输入改名了

execute(["scrapy", "crawl" , "jobbole"])

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

**settings.py的设置不遵守reboots协议** `ROBOTSTXT_OBEY = False` 在jobble.py打上断点:

登录后复制

def parse(self, response):

pass

  • 1.
  • 2.

可以看到他返回的htmlresponse对象: 对象内部:

  • body:网页内容
  • _DEFAULT_ENCODING= ‘ascii’
  • encoding= ‘utf-8’

可以看出scrapy已经为我们做到了将网页下载下来。而且编码也进行了转换.

2. 提取伯乐在线内容

xpath的使用

xpath让你可以不懂前端html,不看html的详细结构,只需要会右键查看就能获取网页上任何内容。速度远超beautifulsoup。 目录:

登录后复制

1. xpath简介

2. xpath术语与语法

3. xpath抓取误区:javasrcipt生成html与html源文件的区别

4. xpath抓取实例

  • 1.
  • 2.
  • 3.
  • 4.

为什么要使用xpath?

  • xpath使用路径表达式在xml和html中进行导航
  • xpath包含有一个标准函数库
  • xpath是一个w3c的标准
  • xpath速度要远远超beautifulsoup。

**xpath节点关系**

  1. 父节点​​*上一层节点*​
  2. 子节点
  3. 兄弟节点​​*同胞节点*​
  4. 先辈节点​​*父节点,爷爷节点*​
  5. 后代节点​​*儿子,孙子*​​ xpath语法:

表达式

说明

article

选取所有article元素的所有子节点

/article

选取根元素article

article/a

选取所有属于article的子元素的a元素

//div

选取所有div元素(不管出现在文档里的任何地方)

article//div

选取所有属于article元素的后代的div元素,不管它出现在article之下的任何位置

//@class

选取所有名为class的属性

xpath语法-谓语:

表达式

说明

/article/div[1

选取属于article子元素的第一个div元素

/article/div[last()]

选取属于article子元素的最后一个div元素

/article/div[last()-1]

选取属于article子元素的倒数第二个div元素

//div[@color]

选取所有拥有color属性的div元素

//div[@color=’red’]

选取所有color属性值为red的div元素

xpath语法:

表达式

说明

/div/*

选取属于div元素的所有子节点

//*

选取所有元素

//div[@*]

选取所有带属性的div 元素

//div/a 丨//div/p

选取所有div元素的a和p元素

//span丨//ul

选取文档中的span和ul元素

article/div/p丨//span

选取所有属于article元素的div元素的p元素以及文档中所有的 span元素

xpath抓取误区

​firebugs插件​

取某一个网页上元素的xpath地址

如:​ ​http://blog.jobbole.com/110287/​

在标题处右键使用firebugs查看元素。

然后在​​​<h1>2016 腾讯软件开发面试题(部分)</h1>​​右键查看xpath

登录后复制

# -*- coding: utf-8 -*-

import scrapy

class JobboleSpider(scrapy.Spider):

name = "jobbole"

allowed_domains = ["blog.jobbole.com"]

start_urls = ['http://blog.jobbole.com/110287/']

def parse(self, response):

re_selector = response.xpath("/html/body/div[3]/div[3]/div[1]/div[1]/h1")

# print(re_selector)

pass

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

调试debug可以看到

登录后复制

re_selector =(selectorlist)[]

  • 1.

可以看到返回的是一个空列表,

列表是为了如果我们当前的xpath路径下还有层级目录时可以进行选取

空说明没取到值:

我们可以来chorme里观察一下

chorme取到的值

​​​//*[@]/div[1]/h1​

chormexpath代码

登录后复制

# -*- coding: utf-8 -*-

import scrapy

class JobboleSpider(scrapy.Spider):

name = "jobbole"

allowed_domains = ["blog.jobbole.com"]

start_urls = ['http://blog.jobbole.com/110287/']

def parse(self, response):

re_selector = response.xpath('//*[@]/div[1]/h1')

# print(re_selector)

pass

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

可以看出此时可以取到值

分析页面,可以发现页面内有一部html是通过JavaScript ajax交互来生成的,因此在f12检查元素时的页面结构里有,而xpath不对

xpath是基于html源代码文件结构来找的

xpath可以有多种多样的写法:

登录后复制

re_selector = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1/text()")

re2_selector = response.xpath('//*[@]/div[1]/h1/text()')

re3_selector = response.xpath('//div[@class="entry-header]/h1/text()')

  • 1.
  • 2.
  • 3.

推荐使用id型。因为页面id唯一。

推荐使用class型,因为后期循环爬取可扩展通用性强。

通过了解了这些此时我们已经可以抓取到页面的标题,此时可以使用xpath利器照猫画虎抓取任何内容。只需要点击右键查看xpath。

开启控制台调试

​scrapy shell http://blog.jobbole.com/110287/​

完整的xpath提取伯乐在线字段代码

登录后复制

# -*- coding: utf-8 -*-

import scrapy

import re

class JobboleSpider(scrapy.Spider):

name = "jobbole"

allowed_domains = ["blog.jobbole.com"]

start_urls = ['http://blog.jobbole.com/110287/']

def parse(self, response):

#提取文章的具体字段

title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first("")

create_date = response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace("·","").strip()

praise_nums = response.xpath("//span[contains(@class, 'vote-post-up')]/h10/text()").extract()[0]

fav_nums = response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0]

match_re = re.match(".*?(\d+).*", fav_nums)

if match_re:

fav_nums = match_re.group(1)

comment_nums = response.xpath("//a[@href='#article-comment']/span/text()").extract()[0]

match_re = re.match(".*?(\d+).*", comment_nums)

if match_re:

comment_nums = match_re.group(1)

content = response.xpath("//div[@class='entry']").extract()[0]

tag_list = response.xpath("//p[@class='entry-meta-hide-on-mobile']/a/text()").extract()

tag_list = [element for element in tag_list if not element.strip().endswith("评论")]

tags = ",".join(tag_list)

pass

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

css选择器的使用:

登录后复制

# 通过css选择器提取字段

# front_image_url = response.meta.get("front_image_url", "") #文章封面图

title = response.css(".entry-header h1::text").extract_first()

create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·","").strip()

praise_nums = response.css(".vote-post-up h10::text").extract()[0]

fav_nums = response.css(".bookmark-btn::text").extract()[0]

match_re = re.match(".*?(\d+).*", fav_nums)

if match_re:

fav_nums = int(match_re.group(1))

else:

fav_nums = 0

comment_nums = response.css("a[href='#article-comment'] span::text").extract()[0]

match_re = re.match(".*?(\d+).*", comment_nums)

if match_re:

comment_nums = int(match_re.group(1))

else:

comment_nums = 0

content = response.css("div.entry").extract()[0]

tag_list = response.css("p.entry-meta-hide-on-mobile a::text").extract()

tag_list = [element for element in tag_list if not element.strip().endswith("评论")]

tags = ",".join(tag_list)

pass

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

3. 爬取所有文章

yield关键字

登录后复制

#使用request下载详情页面,下载完成后回调方法parse_detail()提取文章内容中的字段

yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)

  • 1.
  • 2.

scrapy.http import Request下载网页

登录后复制

from scrapy.http import Request

Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)

  • 1.
  • 2.

parse拼接网址应对herf内有可能网址不全

登录后复制

from urllib import parse

url=parse.urljoin(response.url,post_url)

parse.urljoin("http://blog.jobbole.com/all-posts/","http://blog.jobbole.com/111535/")

#结果为http://blog.jobbole.com/111535/

  • 1.
  • 2.
  • 3.
  • 4.

class层级关系

登录后复制

next_url = response.css(".next.page-numbers::attr(href)").extract_first("")

#如果.next .pagenumber 是指两个class为层级关系。而不加空格为同一个标签

  • 1.
  • 2.

twist异步机制

Scrapy使用了Twisted作为框架,Twisted有些特殊的地方是它是事件驱动的,并且比较适合异步的代码。在任何情况下,都不要写阻塞的代码。阻塞的代码包括:

  • 访问文件、数据库或者Web
  • 产生新的进程并需要处理新进程的输出,如运行shell命令
  • 执行系统层次操作的代码,如等待系统队列

实现全部文章字段下载的代码:

登录后复制

def parse(self, response):

"""

1. 获取文章列表页中的文章url并交给scrapy下载后并进行解析

2. 获取下一页的url并交给scrapy进行下载, 下载完成后交给parse

"""

# 解析列表页中的所有文章url并交给scrapy下载后并进行解析

post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()

for post_url in post_urls:

#request下载完成之后,回调parse_detail进行文章详情页的解析

# Request(url=post_url,callback=self.parse_detail)

print(response.url)

print(post_url)

yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)

#遇到href没有域名的解决方案

#response.url + post_url

print(post_url)

# 提取下一页并交给scrapy进行下载

next_url = response.css(".next.page-numbers::attr(href)").extract_first("")

if next_url:

yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

全部文章的逻辑流程图

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

4. scrapy的items整合字段

数据爬取的任务就是从非结构的数据中提取出结构性的数据。

items 可以让我们自定义自己的字段(类似于字典,但比字典的功能更齐全)

在当前页,需要提取多个url

原始写法,extract之后则生成list列表,无法进行二次筛选:

登录后复制

post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()

  • 1.

改进写法:

登录后复制

post_nodes = response.css("#archive .floated-thumb .post-thumb a")

for post_node in post_nodes:

#获取封面图的url

image_url = post_node.css("img::attr(src)").extract_first("")

post_url = post_node.css("::attr(href)").extract_first("")

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

在下载网页的时候把获取到的封面图的url传给parse_detail的response

在下载网页时将这个封面url获取到,并通过meta将他发送出去。在callback的回调函数中接收该值

登录后复制

yield Request(url=parse.urljoin(response.url,post_url),meta={"front_image_url":image_url},callback=self.parse_detail)

front_image_url = response.meta.get("front_image_url", "")

  • 1.
  • 2.
  • 3.

urljoin的好处

如果你没有域名,我就从response里取出来,如果你有域名则我对你起不了作用了

**编写我们自定义的item并在jobboled.py中填充。

登录后复制

class JobBoleArticleItem(scrapy.Item):

title = scrapy.Field()

create_date = scrapy.Field()

url = scrapy.Field()

url_object_id = scrapy.Field()

front_image_url = scrapy.Field()

front_image_path = scrapy.Field()

praise_nums = scrapy.Field()

comment_nums = scrapy.Field()

fav_nums = scrapy.Field()

content = scrapy.Field()

tags = scrapy.Field()

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

import之后实例化,实例化之后填充:

登录后复制

1. from ArticleSpider.items import JobBoleArticleItem

2. article_item = JobBoleArticleItem()

3. article_item["title"] = title

article_item["url"] = response.url

article_item["create_date"] = create_date

article_item["front_image_url"] = [front_image_url]

article_item["praise_nums"] = praise_nums

article_item["comment_nums"] = comment_nums

article_item["fav_nums"] = fav_nums

article_item["tags"] = tags

article_item["content"] = content

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

yield article_item将这个item传送到pipelines中

pipelines可以接收到传送过来的item

将setting.py中的pipeline配置取消注释

登录后复制

# Configure item pipelines

# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html

ITEM_PIPELINES = {

'ArticleSpider.pipelines.ArticlespiderPipeline': 300,

}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

当我们的item被传输到pipeline我们可以将其进行存储到数据库等工作

setting设置下载图片pipeline

登录后复制

ITEM_PIPELINES={

'scrapy.pipelines.images.ImagesPipeline': 1,

}

  • 1.
  • 2.
  • 3.

H:\CodePath\pyEnvs\articlespider3\Lib\site-packages\scrapy\pipelines

里面有三个scrapy默认提供的pipeline

提供了文件,图片,媒体。

ITEM_PIPELINES是一个数据管道的登记表,每一项具体的数字代表它的优先级,数字越小,越早进入。

setting设置下载图片的地址

登录后复制

# IMAGES_MIN_HEIGHT = 100

# IMAGES_MIN_WIDTH = 100

  • 1.
  • 2.

设置下载图片的最小高度,宽度。

新建文件夹images在

登录后复制

IMAGES_URLS_FIELD = "front_image_url"

project_dir = os.path.abspath(os.path.dirname(__file__))

IMAGES_STORE = os.path.join(project_dir, 'images')

  • 1.
  • 2.
  • 3.

安装PIL

​​​pip install pillow​

定制自己的pipeline使其下载图片后能保存下它的本地路径

get_media_requests()接收一个迭代器对象下载图片

item_completed获取到图片的下载地址

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

继承并重写item_completed()

登录后复制

from scrapy.pipelines.images import ImagesPipeline

class ArticleImagePipeline(ImagesPipeline):

#重写该方法可从result中获取到图片的实际下载地址

def item_completed(self, results, item, info):

for ok, value in results:

image_file_path = value["path"]

item["front_image_path"] = image_file_path

return item

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

setting中设置使用我们自定义的pipeline,而不是系统自带的

登录后复制

ITEM_PIPELINES = {

'ArticleSpider.pipelines.ArticlespiderPipeline': 300,

# 'scrapy.pipelines.images.ImagesPipeline': 1,

'ArticleSpider.pipelines.ArticleImagePipeline':1,

}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

图片url的md5处理

新建package utils

登录后复制

import hashlib

def get_md5(url):

m = hashlib.md5()

m.update(url)

return m.hexdigest()

if __name__ == "__main__":

print(get_md5("http://jobbole.com".encode("utf-8")))

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

不确定用户传入的是不是:

登录后复制

def get_md5(url):

#str就是unicode了

if isinstance(url, str):

url = url.encode("utf-8")

m = hashlib.md5()

m.update(url)

return m.hexdigest()

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

在jobbole.py中将url的md5保存下来

登录后复制

from ArticleSpider.utils.common import get_md5

article_item["url_object_id"] = get_md5(response.url)

  • 1.
  • 2.

5. 数据保存到本地文件以及mysql中

保存到本地json文件

import codecs打开文件避免一些编码问题,自定义JsonWithEncodingPipeline实现json本地保存

登录后复制

class JsonWithEncodingPipeline(object):

#自定义json文件的导出

def __init__(self):

self.file = codecs.open('article.json', 'w', encoding="utf-8")

def process_item(self, item, spider):

#将item转换为dict,然后生成json对象,false避免中文出错

lines = json.dumps(dict(item), ensure_ascii=False) + "\n"

self.file.write(lines)

return item

#当spider关闭的时候

def spider_closed(self, spider):

self.file.close()

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

setting.py注册pipeline

登录后复制

ITEM_PIPELINES = {

'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,

# 'scrapy.pipelines.images.ImagesPipeline': 1,

'ArticleSpider.pipelines.ArticleImagePipeline':1,

}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

scrapy exporters JsonItemExporter导出

scrapy自带的导出:

登录后复制

- 'CsvItemExporter',

- 'XmlItemExporter',

- 'JsonItemExporter'

  • 1.
  • 2.
  • 3.

​from scrapy.exporters import JsonItemExporter​

登录后复制

class JsonExporterPipleline(object):

#调用scrapy提供的json export导出json文件

def __init__(self):

self.file = open('articleexport.json', 'wb')

self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)

self.exporter.start_exporting()

def close_spider(self, spider):

self.exporter.finish_exporting()

self.file.close()

def process_item(self, item, spider):

self.exporter.export_item(item)

return item

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

设置setting.py注册该pipeline

登录后复制

'ArticleSpider.pipelines.JsonExporterPipleline ': 2

  • 1.
  • 1

保存到数据库(mysql)

数据库设计数据表,表的内容字段是和item一致的。数据库与item的关系。类似于django中model与form的关系。

日期的转换,将字符串转换为datetime

登录后复制

import datetime

try:

create_date = datetime.datetime.strptime(create_date, "%Y/%m/%d").date()

except Exception as e:

create_date = datetime.datetime.now().date()

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

数据库表设计

搜索引擎–基于Django/Scrapy/ElasticSearch的搜索引擎的实现

  • 三个num字段均设置不能为空,然后默认0.
  • content设置为longtext
  • 主键设置为url_object_id

数据库驱动安装

​​​pip install mysqlclient​

Linux报错解决方案:

ubuntu:

​​​sudo apt-get install libmysqlclient-dev​​​ centos:

​sudo yum install python-devel mysql-devel​

保存到数据库pipeline(同步)编写

登录后复制

import MySQLdb

class MysqlPipeline(object):

#采用同步的机制写入mysql

def __init__(self):

self.conn = MySQLdb.connect('127.0.0.1', 'root', 'password', 'article_spider', charset="utf8", use_unicode=True)

self.cursor = self.conn.cursor()

def process_item(self, item, spider):

insert_sql = """

insert into jobbole_article(title, url, create_date, fav_nums)

VALUES (%s, %s, %s, %s)

"""

self.cursor.execute(insert_sql, (item["title"], item["url"], item["create_date"], item["fav_nums"]))

self.conn.commit()

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

保存到数据库的(异步Twisted)编写

因为我们的爬取速度可能大于数据库存储的速度。异步操作。

设置可配置参数

seeting.py设置

登录后复制

MYSQL_HOST = "127.0.0.1"

MYSQL_DBNAME = "article_spider"

MYSQL_USER = "root"

MYSQL_PASSWORD = "123456"

  • 1.
  • 2.
  • 3.
  • 4.

代码中获取到设置的可配置参数

twisted异步:

登录后复制

import MySQLdb.cursors

from twisted.enterprise import adbapi

#连接池ConnectionPool

# def __init__(self, dbapiName, *connargs, **connkw):

class MysqlTwistedPipline(object):

def __init__(self, dbpool):

self.dbpool = dbpool

@classmethod

def from_settings(cls, settings):

dbparms = dict(

host = settings["MYSQL_HOST"],

db = settings["MYSQL_DBNAME"],

user = settings["MYSQL_USER"],

passwd = settings["MYSQL_PASSWORD"],

charset='utf8',

cursorclass=MySQLdb.cursors.DictCursor,

use_unicode=True,

)

#**dbparms-->("MySQLdb",host=settings['MYSQL_HOST']

dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)

return cls(dbpool)

def process_item(self, item, spider):

#使用twisted将mysql插入变成异步执行

query = self.dbpool.runInteraction(self.do_insert, item)

query.addErrback(self.handle_error, item, spider) #处理异常

def handle_error(self, failure, item, spider):

#处理异步插入的异常

print (failure)

def do_insert(self, cursor, item):

#执行具体的插入

#根据不同的item 构建不同的sql语句并插入到mysql中

insert_sql, params = item.get_insert_sql()

cursor.execute(insert_sql, params)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

可选django.items