Python分布式爬虫打造搜索引擎-scrapy爬取知名技术文章网站


 一、项目基础环境

  •  python3.6.0
  • pycharm2018.2
  • mysql+navicat

 

二、scrapy爬取知名技术文章网站

 1、使用虚拟环境 ,创建虚拟环境:

mkvirtualenv --python=C:\python3.6\python.exe articlespider   #python3.6版本

 2、进入articlespider目录,安装scrapy:

pip install -i https://pypi.douban.com/simple/ scrapy  #豆瓣源安装速度快

 安装时报错:

 

 

需要手动下载安装Twisted:https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

放到 articlespider目录下 ,安装:

pip install Twisted-18.7.0-cp36-cp36m-win_amd64.whl

 3、创建爬虫项目:

1)创建项目:

scrapy startproject ArticleSpider

创建成功:

 

 

2)项目中创建spider模块:

 进入ArticleSpider ,创建spider:

#jobbole:spider名称 ,  blog.jobbole.com:要爬取数据的url
scrapy genspider jobbole blog.jobbole.com

创建成功:

  

在pycharm中打开我们的ArticleSpider项目,可以看到spiders下多了一个py文件:jobbole.py

 

scrapy目录结构:

  • scrapy.cfg:配置文件
  • setings.py:基本设置
    SPIDER_MODULES = ['ArticleSpider.spiders'] #存放spider的路径
    NEWSPIDER_MODULE = 'ArticleSpider.spiders' 
  • pipelines.py:做跟数据存储相关的东西
  • middilewares.py:自己定义的middlewares 定义方法,处理响应的IO操作
  • init.py:项目的初始化文件
  • items.py:定义我们所要爬取的信息的相关属性。Item对象是种类似于表单,用来保存获取到的数据

 此时,pycharm下articlespiders项目还没完全配置好:

 ①、首先需要在pycharm的setting中的project interpreter下配置python:使用虚拟环境articlespider下的python3.6

 ②、接着,爬虫项目不像Django项目,没有自动配置好调试或运行相关,需要我们手动生成一个main.py文件,作为启动文件:

   1)在articlespiders项目下新建main.py文件:

# ArticleSpiders/main

from scrapy.cmdline import execute
import sys,os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))  # 父路径
execute(['scrapy','crawl','jobbole'])  #执行指令:scrapy crawl jobbole ,执行后会跳到jobbole.py中执行JobboleSpider类

   2)settings.py下设置不遵守reboots协议 :

    ROBOTSTXT_OBEY 设为False

ROBOTSTXT_OBEY = False

 

执行main.py文件,报错:ModuleNotFoundError: No module named 'win32api',在虚拟环境下安装 pypiwin32 :

pip install -i https://pypi.douban.com/simple/ pypiwin32

 

再debug运行main.py文件,此时运行成功:

 


 

4、 xpath的使用

1)为什么要使用xpath?

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

2)xpath节点关系

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

3)xpath语法:

 

 

 

简单使用xpath基本语法爬取数据:

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

import scrapy


class JobboleSpider(scrapy.Spider):
    name = 'jobbole'
    allowed_domains = ['blog.jobbole.com']
    start_urls = ['http://blog.jobbole.com/']

    def parse(self, response):
        re_selector = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1/text()") # 不支持这种绝对路径,容易出错
        re2_selector = response.xpath('//*[@id="post-110287"]/div[1]/h1/text()')
        re3_selector = response.xpath('//div[@class="entry-header"]/h1/text()')

 

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

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)

 

小demo:

tag_list=['职场','2 评论','今昔']
[element for element in tag_list if not element.strip().endswith('评论')]

# 结果['职场', '今昔']

以上都是在pycharm中debug调试进行的,如觉麻烦可在终端开启调试模式:

scrapy shell http://blog.jobbole.com/110287/  # 开启控制台调试

 


 

5、CSS选择器  

  

 

 

 

 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)

 


爬取 http://blog.jobbole.com/all-posts/ 所有文章 :

 yield关键字:

#yield Request:会将url交给scrapy引擎进行下载数据,,下载完成后会调用回调方法parse_detail()提取文章内容中的字段
yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)

 

scrapy下的request下载网页,并执行回调函数:

from scrapy.http import Request
Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)
urllib模块下的parse.urljoin函数能拼接网址,原因是爬取到的网址可能是个相对路径,不是绝对路径,使用parse.urlkoin可以避免一些错误(具体看源码):
urljoin(参数1,参数2):如果没有参数1,url=参数二,反过来,url=参数1.如果参数1/2都存在,会提取出参数1域名,与参数2比较,再合并成一个完整url
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/

 

爬取所有文章完整代码:

import scrapy
from scrapy.http import Request
from urllib import parse


class JobboleSpider(scrapy.Spider):
    name = 'jobbole'
    allowed_domains = ['blog.jobbole.com']
    start_urls = ['http://blog.jobbole.com/all-posts/']

    def parse(self, response):
        """
                1. 获取文章列表页中的文章url并交给scrapy下载后并进行解析
                2. 获取下一页的url并交给scrapy进行下载, 下载完成后交给parse
                流程:执行parse函数 → request下载,回调parse_detail函数 → 提取下一页交给scrapy进行下载,回调parse函数,
                则又执行request下载,回调parse_detail函数,如此循环,直到获取不到下一页
                """
        # 解析列表页中的所有文章url并交给scrapy下载后并进行解析
        post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()
        for post_url in post_urls:
            #request下载完成之后,回调parse_detail进行文章详情页的解析
            yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)

        # 提取下一页并交给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)




    def parse_detail(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)

        #通过css选择器提取字段
        front_image_url = response.meta.get("front_image_url", "")  #文章封面图
        title = response.css(".entry-header h1::text").extract()[0]
        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)
View Code


 

6、items设计 

 数据爬取的任务就是从非结构的数据中提取出结构性的数据。
items 可以让我们自定义自己的字段(类似于字典,但比字典的功能更齐全)

6.1、当我们爬取数据,获取数据后需要将数据传给下个函数调用,则可以利用scrapy request下的meta属性,meta接收的是字典类型,当使用request爬取数据时,将需要传递给下个函数的数据放到meta中,meta中的数据会传给下个函数的response中去,可以被下个函数使用

 1)关于scrapy request的meta:

Request中meta参数的作用是传递信息给下一个函数,使用过程可以理解成:

把需要传递的信息赋值给这个叫meta的变量,
但meta只接受字典类型的赋值,因此
要把待传递的信息改成“字典”的形式,即:
meta={'key1':value1,'key2':value2}

如果想在下一个函数中取出value1,
只需得到上一个函数的meta['key1']即可,
因为meta是随着Request产生时传递的,
下一个函数得到的Response对象中就会有meta,
即response.meta,




meta是一个dict,主要是用解析函数之间传递值,一种常见的情况:在parse中给item某些字段提取了值,但是另外一些值需要在parse_item中提取,这时候需要将parse中的item传到parse_item方法中处理,显然无法直接给parse_item设置而外参数。 Request对象接受一个meta参数,一个字典对象,同时Response对象有一个meta属性可以取到相应request传过来的meta
request meta

 

 2)关于urllib parse.urljoin(url1,url2)

parse.url:能将相对路径,自动补充域名补全路径,前提主域名response能获取域名路径

一般爬取网上数据时,存在网页某些url是相对路径形式展示的,通过js代码等操作补全,此时我们要爬取完整url路径,就需要用到parse.urljoin方法了

如果你是相对路径,没有补全域名,我就从response里取出来补全,如果你有域名我则不起作用

 

 获取文章url及封面图url,爬取下载页面相应数据:

 

调用callback函数,meta里的数据可以传递到parse_detail函数中调用:

  

项目创建成功,在项目ArticleSpider/ArticleSpider目录下有个items.py文件,如上述所说,我们能将爬取到的数据通过自定义自己的字段(类似字典),保存起来,之后可以通过pipelines.py文件,将数据保存到数据库等地方去。

6.2、items.py创建类 JobBoleArticleItem ,继承于scrapy.Itme:

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()

 

6.3、在jobbole.py parse_detail 中实例化 JobBoleArticleItem , 并将爬取到的数据放入 JobBoleArticleItem 对象中:

 from ArticleSpider.items import JobBoleArticleItem
def parse_detail(self, response):
  article_item = JobBoleArticleItem()    # 实例化
   article_item["title"] = title    # 保存获取到的数据到Item 中
        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

 

 接着在 parse_detail 中添加代码:

  article_item数据会传到pipelines当中去

yield article_item

 

 同时,setting中添加代码:

# setting.py

ITEM_PIPELINES = {
   'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
}

ITEM_PIPELINES:item的管道,配置好ArticlespiderPipeline路径,当执行代码 yield article_item 时,程序会转到pipelines.py文件中执行  ArticlespiderPipeline 类,

也就是通过 yield article_item 将article_item数据传给pipelines调用。并执行pipelines中的相关类,如: ArticlespiderPipeline  ,在 ArticlespiderPipeline 类中,我们可以通过代码编写,将数据存到数据库中去

ArticlespiderPipeline  代码:

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

 

item中有个key:_value , _value中存放着我们爬取到的所有数据

 


 

 所以目前完整爬虫执行顺序是:创建main.py,执行main函数 → 跳到spiders目录下的jobbole.py 执行 JobboleSpider 类 ,类中有parse 、parse_detail等函数 → JobboleSpider类中获取到数据,实例化 JobBoleArticleItem ,将数据保存到Items 中 ,执行 yield article_item → 跳转到pipelines.py中执行ArticlespiderPipeline 类


 

6.4、图片下载保存

有时候我们爬取到图片或文件等,希望把它保存到本地或数据库中

首先进入setting中 ITEM_PIPELINES  配置图片下载pipeline:

ITEM_PIPELINES={
'scrapy.pipelines.images.ImagesPipeline': 1,
}  

  

Scrapy中的pipelines提供了默认的文件、图片、媒体等下载保存方式,如上所述,将路径配置上去就会相应的执行scrapy/pipelines的相关函数 ,

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

接下来我们新建一个images文件夹用于保存图片,在setting中配置images文件夹的路径:

project_dir = os.path.abspath(os.path.dirname(__file__)) 
IMAGES_STORE = os.path.join(project_dir, 'images')  # 图片文件路径

 

指定某字段(我们获取图片路径的字段)为我们的图片文件处理

IMAGES_URLS_FIELD = "front_image_url"   # 指定爬取的某(图片)字段作为图片处理

 

此时执行会报错:

 

原因是因为图片保存等需要pillow库来操作,解决方法就是在虚拟环境中安装pillow:pip install pillow

  

重新执行又报错:

 

 

原因是'scrapy.pipelines.images.ImagesPipeline' 中对图片的要求是数组格式,而我们目前传递的图片类型(front_image_url)是字符串格式,因此我们需要将爬取的图片类型改成数组类型获取

解决方法:

article_item["front_image_url"] = front_image_url
# ↓改成数组形式
article_item["front_image_url"] = [front_image_url]

 接着,重新运行项目,成功爬取到数据及下载图片成功保存:

 

额外知识:

 scrapy下的pipelines目录下有files.py、images.py、media.py ,用于处理文件、图片、用户上传下载媒体等数据

 

 关于imager.py目前用到参数(常用参数可看源码):

IMAGES_URLS_FIELD = "front_image_url"  # 指定该字段作为图片处理对象

IMAGES_STORE = os.path.join(project_dir, 'images')  # 图片存放路径

IMAGES_MIN_HEIGHT = 100  # 图片最小高度
IMAGES_MIN_WIDTH = 100   # 图片最小宽度

 使用scrapy自带的image.py处理图片,只需要按要求命名一致,以及在ITEM_PIPELINES 配置好所使用的类即可:ImagesPipeline , 

使用files.py、media.py也是一样的道理。


 

6.5、上面我们已经爬取到图片并保存到我们的本地中,现在我们想办法将保存到服务器或本地的图片重新绑定个路径,方便调用

1)在pipelines.py中新建自定义 ArticleImagePipeline 类,继承于ImagePipeline ,重载Scrapy下的image.py中的 item_completed方法,

 item_completed方法参数中有一个名为results的参数,该参数内部数据是个list,list内部是元组的类型,每个元组有两个子数据,第一个是状态,成功则返回True,另一个子数据是个字典类型,字典内部有个path参数,path参数存放的就是我们服务端图片保存的路径。如下:

  result = {(True, {'url':'http://......'} ) }

 

 

# pipelines.py
from scrapy.pipelines.images import ImagesPipeline  
class ArticleImagePipeline(ImagesPipeline):  # 继承ImagePipeline
    def item_completed(self, results, item, info):
        if "front_image_url" in item:  # 
            for ok, value in results:  # results[0] = ‘ok’
                image_file_path = value["path"]  
            item["front_image_path"] = image_file_path

        return item  # 需返回item

 

2)items.py中JobBoleArticleItem 类,添加字段:

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()

3)在setting.py中配置我们自定义的pipelines类路径:

ITEM_PIPELINES = {
   'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
    'scrapy.pipelines.images.ImagesPipeline': 1, # scrapy自带图片处理
    'ArticleSpider.pipelines.ArticleImagePipeline': 2, #新增 , 自定义ArticleImagePipeline ,需配置好管道路径
}

 

通过上述三步操作,就已经把下载后的图片绝对路径保存到我们的item中去了


 6.6、我们每爬取一个文章,或者说一个url,我们就应该给该url进行去重处理,这样可以避免我们在爬取数据时遇到同一个url时会重新爬一次数据。所有爬取成功的url都逐个存于response.url中,

我们现在要做的是将response中的url进行md5加密处理,之后存于数据库

1)首先,新建utils包,用于处理一些额外的函数等,在utils中新建common.py,编写get_md5函数:

关于md5加密:python3中数据默认都是Unicode类型,进行md5加密时,需要将数据转行成utf8才能进行加密,否则报错:

 

# utils/common.py
import hashlib

def get_md5(url):
    if isinstance(url, str): # python3中数据都是Unicode类型,需要转换成utf8才能被hash加密,否则会报错,py3中str即为Unicode
        url = url.encode("utf-8")  # 如果是Unicode类型则encode成utf8
    m = hashlib.md5()
    m.update(url)
    return m.hexdigest()

 

 2)在jobbole.py中parse_detail函数中,将response.url 进行md5加密处理并保存到item中:

from ArticleSpider.utils.common import get_md5

def parse_detail(self, response):

    article_item["url_object_id"] = get_md5(response.url)   # 新增
    article_item["title"] = title
    article_item["url"] = response.url
    .
    .
    .

 

 3)同时,在items.py下的 JobBoleArticleItem 中新增字段:

url_object_id = scrapy.Field()

 6.7数据保存相关

1)保存到本地json文件中

方式一:

在pipelines.py文件中新建类:JsonWithEncodingPipeline

打开文件时不直接使用 open ,用python自带的codecs包,可以避免一些编码方面的问题出现

使用json.dumps时,不使用ensure_ascii=False ,默认输出中文是ASCII字符码,要想正确输出中文,需要指定ensure_ascii=False

pipelines.py/JsonWithEncodingPipelines:

import codecs

class JsonWithEncodingPipeline(object):
    #自定义json文件的导出
    def __init__(self):
        self.file = codecs.open('article.json', 'w', encoding="utf-8")
    def process_item(self, item, spider):  #yield item后,跳到pipelines.py中执行对应类时,默认会执行此方法
        #将item转换为dict,然后生成json对象,false避免中文出错
        lines = json.dumps(dict(item), ensure_ascii=False) + "\n"
        self.file.write(lines)
        return item
    #scrapy.signals中的信号量状态,spider_closed:当spider关闭爬虫时调用
    def spider_closed(self, spider):
        self.file.close()

 

接着在setting中的ITEM_PIPELINES 配置好:

ITEM_PIPELINES = {
   'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
}

 

当爬虫爬取数据,保存到 Item 中,经过yield Item ,转到pipelines中执行里面的类

方式二:

使用scrapy中自带的类保存文件:

# scrapy/exporters

['BaseItemExporter',
'PprintItemExporter',
'PickleItemExporter',
'CsvItemExporter',
'XmlItemExporter',
'JsonLinesItemExporter',
'JsonItemExporter', # 保存json文件
'MarshalItemExporter']

 

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

 

接着在setting中配置 ITEM_PIPELINES:

'ArticleSpider.pipelines.JsonExporterPipleline ': 6

 

2)将 数据保存到mysql中

首先,在Navicat中新建数据库article_spider ,再新建表,之后填充数据类型:

 

其中,

 1、create_date ,在数据库中是datetime类型,我们在爬取数据时是以字符串形式保存,因此需要更改下item['create_date']类型:

import datetime
 try:
            create_date = datetime.datetime.strptime(create_date, "%Y/%m/%d").date()
        except Exception as e:
            create_date = datetime.datetime.now().date()

注:

 datetime.datetime.strptime():将字符串转换成日期格式

 datetime.datetime.strftime():将日期格式转换成字符串

 datetime.datetime.strftime().date():日期

 datetime.datetime.strftime().time():时间

 datetime.datetime.now():当前日期时间

 datetime.datetime.now().date():当前日期

 

 2、需要设定一个主键,我们用url_object_id 来做主键

  

准备工作弄好,需要在虚拟环境下安装:mysqlclient

pip install mysqlclient

 

安装成功即可以开始我们的数据保存到mysql操作了

方式一: 利用pipelines保存数据到数据库(同步)

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()

useUnicode=true&characterEncoding=UTF-8 作用:

添加的作用是:指定字符的编码、解码格式。

     例如:mysql数据库用的是gbk编码,而项目数据库用的是utf-8编码。这时候如果添加了useUnicode=true&characterEncoding=UTF-8
,那么作用有如下两个方面:

1. 存数据时:
     数据库在存放项目数据的时候会先用UTF-8格式将数据解码成字节码,然后再将解码后的字节码重新使用GBK编码存放到数据库中。

2.取数据时:
     在从数据库中取数据的时候,数据库会先将数据库中的数据按GBK格式解码成字节码,然后再将解码后的字节码重新按UTF-8格式编码数据,最后再将数据返回给客户端
View Code

 

 接着,在setting中的 ITEM_PIPELINES 配置:

'ArticleSpider.pipelines.MysqlPipeline ': 7

 

 

方式二:利用pipelines保存数据到数据库(twisted异步)

因为我们的爬取速度可能大于数据库存储的速度,最好是异步操作(异步化操作mysql)

首先,我们将数据库连接的相关配置参数转到setting.py中配置:

MYSQL_HOST = "127.0.0.1"
MYSQL_DBNAME = "article_spider"
MYSQL_USER = "root"
MYSQL_PASSWORD = "123456"

 

接着在pipelines.py中新建类:MysqlTwistedPipline ,编写代码如下:

执行顺序:from_settings → init → pross_item

import MySQLdb
import MySQLdb.cursors
from twisted.enterprise import adbapi

class MysqlTwistedPipeline(object):
    def __init__(self,dbpool):
        self.dbpool = dbpool

    @classmethod
    def from_settings(cls,settings):
        # 读取配置文件信息,在类初始化(实例)时调用
        dbparm = dict(
            host = settings["MYSQL_HOST"],
            db = settings["MYSQL_DBNAME"],
            user = settings["MYSQL_USER"],
            passwd = settings["MYSQL_PASSWORD"],
            charset = 'utf8',
            cursorclass = MySQLdb.cursors.DictCursor,  # 字典类型,还有一种json类型
            use_unicode = True,
        )
        dbpool = adbapi.ConnectionPool("MySQLdb",**dbparm) #tadbapi.ConnectionPool:wisted提供的一个用于异步化操作的连接处(容器)。将数据库模块,及连接数据库的参数等传入即可连接mysql
        return cls(dbpool) # 实例化 MysqlTwistedPipeline

    def process_item(self,item,spider):
        # 操作数据时调用
        query = self.dbpool.runInteraction(self.sql_insert,item) #在连接池执行mysql语句相应操作 ,异步操作
        query.addErrback(self.handle_error,item,spider)  # 异常处理

    def handle_error(self,failure,item,spider):
        # 处理异常
        print("异常:",failure)

    def sql_insert(self,cursor,item):
        # 数据插入操作
        insert_sql = """
                    insert into article_spider(title, url, create_date, fav_nums,url_object_id)
                    VALUES (%s, %s, %s, %s,%s)
                """
        cursor.execute(insert_sql,(item["title"],item["url"],item["create_date"],item["fav_nums"],item["url_object_id"]))

 

 

最后,切记一定要在setting中的ITEM_PIPELINES 配置好路径:

ITEM_PIPELINES = {

    'ArticleSpider.pipelines.MysqlTwistedPipeline': 3,
}

 

关于mysql数据增删改查,也可以使用django ORM模式,需下载相关依赖包,具体参考:

 https://github.com/scrapy-plugins/scrapy-djangoitem


7、scrapy item loader机制

使用scrapy的itemloader来维护提取代码,

itemloadr提供了一个容器,让我们配置某一个字段该使用哪种规则

from scrapy.loader import ItemLoader
# 三种方法
    add_css
    add_value  # 与其他两种不同,这种是直接获取value。如获取request爬取的url:add_value(“url”,response.url)
    add_xpath

 

使用scrapy itemloader的方法,获取到的数据都是列表类型,如果我们要拿的是具体value数据,就需要做进一步处理。

 item.py中定义的类中的字段可以带两个参数,我们可以通过scrapy自带的MapCompose来处理函数,再把结果以参数形式传给item下类的各字段:

MapCompose:用来处理函数

scrapy.Field:可接收两个参数(也可不填,按默认):

  input_processor:预处理,当数据传进来时可以将数据进一步处理

  output_processor:输出,经预处理后的数据再输出

from scrapy.loader.processors import MapCompose
def add_mtianyan(value): #自定义函数,用于MapCompose调用。被调用时参数value即为调用方传入的value,如下面函数中调用,参数value即为title中的数据,接着对value进一步处理后再将处理好数据返回给title
    return value+"-mtianyan"

class JobBoleArticleItem(scrapy.Item):
    title = scrapy.Field(
        input_processor=MapCompose(lambda x:x+"mtianyan",add_mtianyan),  # 使用MapCompose处理函数,每个数据传入时都会调用一次MapCompose,再经过MapCompose调用设定的函数,有几个函数就调用几个函数
      output_processor=TakeFirst()  # 该字段返回列表类型,此行代码表示:取第一个数据
  ) )

 


itemloader使用

1)在item.py中自定义 ArticleItemLoader 类,继承于 ItemLoader :

class ArticleItemLoader(ItemLoader):
    #自定义itemloader中output_processor默认方法,实现默认提取第一个数据
    default_output_processor = TakeFirst()

 

2)在jobbole.py中的parse_detail 方法中,实例化ArticleItemLoader ,并使用ItemLoader自带方法爬取数据,并放置到item中:

#from scrapy.loader import ItemLoader
from pipelines import ArticleItemLoader
# 通过item loader加载item
        front_image_url = response.meta.get("front_image_url", "")  # 文章封面图
       # item_loader = ItemLoader(item=JobBoleArticleItem(), response=response)
        item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response) # 使用自定义ItemLoader实例化JobBoleArticleItem
        item_loader.add_css("title", ".entry-header h1::text")
        item_loader.add_value("url", response.url)
        item_loader.add_value("url_object_id", get_md5(response.url))
        item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text")
        item_loader.add_value("front_image_url", [front_image_url])
        item_loader.add_css("praise_nums", ".vote-post-up h10::text")
        item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")
        item_loader.add_css("fav_nums", ".bookmark-btn::text")
        item_loader.add_css("tags", "p.entry-meta-hide-on-mobile a::text")
        item_loader.add_css("content", "div.entry")
        #最后,一定要调用这个方法来对规则进行解析生成item对象
        article_item = item_loader.load_item()

     yield article_item

 

自定义ArticleItemLoader的好处:

  爬虫爬取数据时,使用ItemLoader机制获取到的数据都是列表类型,如果每个字段都加output_processor参数,显得冗余,通过自定义ArticleItemLoader类,实现默认output_processor方法,之后每个字段的output_processor都会默认使用ArticleItemLoader的默认output方法

scrapy.Field自带的两个参数,除了output_processor外,另外一个参数input_processor可以对字段进行一些另外的处理,再将数据返回给字段。比如说,要将爬取到的图片路径(字符串格式)转换成日期格式:

def get_date(value):# value为爬取到的url(字符串格式)
     try:
        create_date = datetime.datetime.strptime(value,'%Y/%m/%d').date() # 将字符串格式的url转换成日期格式,再传回给该字段
        except Exception as e:
            create_date = datetime.date.now().date()
     return create_date

 

 接着,在item.py下的 JobBoleArticleItem类中处理:

  scrapy 自带的MapCompose模块,可以处理函数相关

from scrapy.loader.processors import MapCompose

class JobBoleArticleItem(scrapy.Item):
    create_date = scrapy.Field(
        input_processor=MapCompose(get_date), # 此时获取到的数据即为日期格式
    )

 

这样就可以注释掉一堆爬虫爬取数据的冗余代码了:

 


 

爬取伯乐技术网站所有文章完整代码

1、setting.py:

import os

BOT_NAME = 'ArticleSpider'

SPIDER_MODULES = ['ArticleSpider.spiders']
NEWSPIDER_MODULE = 'ArticleSpider.spiders'

ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
   'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
   'ArticleSpider.pipelines.ArticleImagePipeline': 2,  # 用于图片下载时调用
   # 'ArticleSpider.pipelines.JsonWithEncodingPipeline': 3,  # 方式一:用于保存item 数据 ,在图片下载之后再调用
   'ArticleSpider.pipelines.MysqlTwistedPipeline': 4,  # 方式三:异步数据库保存item数据
   # 'ArticleSpider.pipelines.JsonExporterPipleline': 3,  # 方式二:使用scrapy提供的JsonItemExporter保存json文件,用于保存item 数据
    # 'scrapy.pipelines.images.ImagesPipeline':1  # scrapy中的pipelines自带的ImagesPipeline,用于图片下载,另外还有图片、媒体下载

}
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
IMAGES_STORE = os.path.join(BASE_DIR,'images')  # 名称是固定写法,文件保存路径
IMAGES_URLS_FIELD = "acticle_image_url" # 名称是固定写法。设定acticle_image_url字段为图片url,下载图片时找此字段对应的数据

ITEM_DATA_DIR = os.path.join(BASE_DIR,"item_data")  # item数据保存到当地item_data文件夹

# mysql配置
MYSQL_HOST = "127.0.0.1"
MYSQL_DBNAME = "article_spider"
MYSQL_USER = "root"
MYSQL_PASSWORD = "******"
setting.py

 

2、jobbole.py:

import scrapy
from scrapy.http import Request
from urllib import parse
from ArticleSpider.items import JobBoleActicleItem,ArticleItemLoader
from ArticleSpider.util.common import get_md5


class JobboleSpider(scrapy.Spider):
    name = 'jobbole'
    allowed_domains = ['blog.jobbole.com']
    # start_urls = ['http://blog.jobbole.com/']
    start_urls = ['http://blog.jobbole.com/all-posts/']

    def parse(self, response):
        # 爬取伯乐网所有的文章目标信息
        # 1、获取文章列表页的所有url,并交由scrapy进行逐个下载,通过回调函数:parse_detail解析并获取目标数据
        post_nodes = response.css('#archive .floated-thumb .post-thumb a') # 获取到的是个list,包含urls、imgs
        for post_node in post_nodes:
            post_url = post_node.css('::attr(href)').extract_first('')
            post_img = post_node.css('img::attr(src)').extract_first('') # 通过request内部meta,将img_url传给下个函数调用
            yield Request(url=parse.urljoin(response.url,post_url),meta={"acticle_image_url":post_img},callback=self.parse_detail)

        # 2、每下载并获取完一页的文章后,接着获取下一页的url,让parse方法接着处理下一页所有文章的目标数据
        next_url = response.css('.nex.page-numbers::attr(href)').extract_first('')
        print("nexturl",next_url)
        if next_url:
            yield Request(url=parse.urljoin(response.url,next_url),callback=parse)


    def parse_detail(self,response):
        acticle_image_url = response.meta.get("acticle_image_url", "")  # 文章封面图
        item_loader = ArticleItemLoader(item=JobBoleActicleItem(), response=response) # ArticleItemLoader:自定义item loader 。JobBoleActicleItem:实例化item对象
        item_loader.add_css("title", "div.entry-header h1::text")
        item_loader.add_value("url", response.url)
        item_loader.add_value("url_object_id", get_md5(response.url))
        item_loader.add_css("create_date", ".entry-meta-hide-on-mobile::text")
        item_loader.add_value("acticle_image_url", [acticle_image_url])
        item_loader.add_css("praise_nums", ".vote-post-up h10::text")
        item_loader.add_css("comment_nums", ".btn-bluet-bigger.href-style.hide-on-480::text")
        item_loader.add_css("fav_nums", "span.btn-bluet-bigger.href-style.bookmark-btn.register-user-only::text")
        item_loader.add_css("tags", ".entry-meta-hide-on-mobile a::text")
        item_loader.add_css("content", ".category-it-tech")
        # 最后,一定要调用这个方法来对规则进行解析生成item对象
        article_item = item_loader.load_item()


        yield article_item
jobbole.py

 

3、items.py:

from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose, TakeFirst, Join
import scrapy
import datetime
import re

class ArticleItemLoader(ItemLoader):
    #自定义itemloader
    default_output_processor = TakeFirst()


def return_value(value):
    return value


def date_convert(value):
    # 将str 转date格式
    try:
        create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
    except Exception as e:
        create_date = datetime.datetime.now().date()

    return create_date


def get_nums(value):
    # 匹配评论/点赞/收藏数
    match_re = re.match(".*?(\d+).*", value)
    if match_re:
        nums = int(match_re.group(1))
    else:
        nums = 0

    return nums


class JobBoleActicleItem(scrapy.Item):
    title = scrapy.Field()  # 标题
    create_date = scrapy.Field(  # 日期,date类型
        input_processor=MapCompose(date_convert),
    )
    url = scrapy.Field()  # 爬取的url
    url_object_id = scrapy.Field()  # 已经md5加密的爬取过的url
    acticle_image_url = scrapy.Field(  #封面图路径,下载时需确定为list
        output_processor=MapCompose(return_value)  # 直接返回value(列表类型),不做任何处理(默认获取第一个值)
    )
    praise_nums = scrapy.Field(  # 点赞数
        input_processor=MapCompose(get_nums)
    )
    comment_nums = scrapy.Field(   # 评论数
        input_processor=MapCompose(get_nums)
    )
    fav_nums = scrapy.Field(   # 收藏数
        input_processor=MapCompose(get_nums)
    )
    tags = scrapy.Field(   # 标签,list类型
        output_processor=MapCompose(return_value)
    )
    content = scrapy.Field()  # 内容
    article_image_path = scrapy.Field()  # 图片保存在服务端的地址
items.py

 

4、pipelines.py:

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

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy.pipelines.images import ImagesPipeline
from ArticleSpider.settings import ITEM_DATA_DIR
from scrapy.exporters import JsonItemExporter
import codecs
import os
import json

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

class ArticleImagePipeline(ImagesPipeline): # 继承ImagesPipeline,用于下载图片时进行一些处理
    def item_completed(self, results, item, info):  # 重载ImagesPipeline中的item_completed,下载完成时调用
        if "acticle_image_url" in item:  # 如果存在,表示item中有这个字段
            for complete_status,value in results:  # results是一个list,里面是元组类型,每个元组有两个数据,一个是状态true or false,另一个数据是个字典
                image_file_path = value["path"]   # value取出元组中的第二个数据(字典),该数据内的path保存中图片下载到服务器后的路径
            item["article_image_path"] = image_file_path  # 将图片存放路径存到item中
        return item

 #  --------------- item 数据保存 -----------------  #
# class JsonWithEncodingPipeline(object):
#     #方式一:使用json文件的方式,保存item数据
#     def __init__(self):
#         item_article_jsonfile = os.path.join(ITEM_DATA_DIR,'item_article.json')
#         self.file = codecs.open(item_article_jsonfile, 'w', encoding="utf-8") # 使用python自带的codecs打开文件,可以避免一些编码错误
#     def process_item(self, item, spider):  #yield item后,跳到pipelines.py中执行对应类时,默认会执行此方法
#         #item是JobBoleActicleItem类型,将item转换为dict,然后生成json对象,ensure_ascii=false避免中文出错
#         lines = json.dumps(dict(item), ensure_ascii=False) + "\n"
#         self.file.write(lines)
#         return item
#     #scrapy.signals中的信号量状态,spider_closed:当spider关闭爬虫时调用
#     def spider_closed(self, spider):
#         self.file.close()

# # item 数据保存,方式二:使用scrapy提供的JsonItemExporter保存json文件
# class JsonExporterPipleline(object):
#     #调用scrapy提供的json export导出json文件
#     def __init__(self):
#         item_article_jsonfile = os.path.join(ITEM_DATA_DIR, 'item_article_export.json')
#         self.file = open(item_article_jsonfile, '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

# item 数据保存,方式三:以异步方式写入数据库
import MySQLdb
import MySQLdb.cursors
from twisted.enterprise import adbapi

class MysqlTwistedPipeline(object):
    def __init__(self,dbpool):
        self.dbpool = dbpool

    @classmethod
    def from_settings(cls,settings):   # 用于读取配置文件信息,先于process_item调用
        dbparm = dict(
            host=settings["MYSQL_HOST"],
            db=settings["MYSQL_DBNAME"],
            user=settings["MYSQL_USER"],
            passwd=settings["MYSQL_PASSWORD"],
            charset='utf8',
            cursorclass=MySQLdb.cursors.DictCursor,  # 字典类型,还有一种json类型
            use_unicode=True,
        )
        dbpool = adbapi.ConnectionPool("MySQLdb",**dbparm)  # tadbapi.ConnectionPool:wisted提供的一个用于异步化操作的连接处(容器)。将数据库模块,及连接数据库的参数等传入即可连接mysql
        return cls(dbpool)  # 实例化 MysqlTwistedPipeline

    def process_item(self, item, spider):
        # 操作数据时调用
        query = self.dbpool.runInteraction(self.sql_insert, item)  # 执行mysql语句相应操作 ,异步操作
        query.addErrback(self.handle_error, item, spider)  # 异常处理

    def handle_error(self, failure, item, spider):
        # 处理异常
        print("异常:", failure)

    def sql_insert(self, cursor, item):
        # 数据插入操作
        insert_sql = """
                            insert into article_spider(title, url, create_date, fav_nums,url_object_id)
                            VALUES (%s, %s, %s, %s,%s)
                        """
        cursor.execute(insert_sql,
                       (item["title"], item["url"], item["create_date"], item["fav_nums"], item["url_object_id"]))
pipelines.py

 

5、main.py:运行主程序

from scrapy.cmdline import execute
import os,sys

sys.path.append(os.path.dirname(os.path.abspath(__file__)))  # 将父路径添加至sys path中
execute(['scrapy','crawl','jobbole',]) # 执行:scrapy crawl jobbole 。 其中'jobbole'是jobbole.py中JobboleSpider类的name字段数据
# execute(['scrapy','crawl','jobbole','--nolog']) # --nolog:表示不打印日志
main.py

 


 

posted on 2018-09-21 00:59  Eric_nan  阅读(369)  评论(0编辑  收藏  举报