首页 热点专区 小学知识 中学知识 出国留学 考研考公
您的当前位置:首页正文

《python分布式爬虫打造搜索引擎》----学习笔记

2024-12-07 来源:要发发知识网

第一章 windows(以及linux)下的环境搭建

1. python打造分布式爬虫:

开发工具IDE:pycharm
数据库: mysql,redis,elasticsearch
开发环境:virtualenv虚拟环境

2. 需要学会的是:

pycharm的安装和使用
mysql和navicat的安装和使用
virtualenv和virtualenvwrapper的安装和配置

3. 关于Linux下启动pycharm的两种方法:

百度进入pycharm的官网后选择download并且选择相应的linux版本,下载下来的文件是绿色的。

  • (1) 在linux下使用命令可以直接启动,找到bin文件夹中的pycharm.sh文件,运行。具体linux下的操作命令如图:
    linux下运行pycharm的命令
  • (2) 除了这种方法以外,还有另外一个快捷键通过命令就可以直接启动,首先在linux下面可以建立自己的命令,使用vim命令编辑用户总目录下面的bashrc文件,按shift+G到.bashrc文件的最后一行。

    :wq命令是退出这个文件,编辑完成之后,一定要运行source ~/.bashrc,命令让这个配置文件生效,生效之后直接运行pycharm命令就可以启动pycharm软件了。

4. pycharm的使用之项目虚拟环境的设置方法

关于创建项目的虚拟环境

5. 检查windows上是否已安装mysql的方法

  1. 在cmd中输入mysql,如果提示找不到命令,除了没安装,还有可能是mysql的bin路径没有设置到变量path中
  2. 确定是安装好的,输入mysql还是报错,是因为没有指定用户名以及输入密码,使用命令mysql -uroot -p,接下来会提示输入命令。

一般关于mysql的操作很少使用cmd控制台的,可以使用navicat软件(轻量级,用于mysql的连接和管理)。

  • 关于mysql的补充,在官网上点击mysql installer可以下载,下载网址:,找到msi,需要注意的是下载200多兆比较大的。
5.1 在navicat中关于mysql使用:
  • 在navicat中连接好mysql就可以新建数据库了,注意字符集和排序方法的选择:


5.2 navicat中数据库内容的传输有两种方法:
  1. 直接传输
  1. 转化成sql文件

6. 在linux下安装mysql

  1. 直接使用命令sudo ~到主目录中,再使用命令sudo apt-get install mysql-server,前面必须加sudo,因为这是系统软件,否则是没有权限的,刚安装的时候都需要设置用户名和密码的。

  2. 在linux下检查mysql是否正常启动,使用命令ps aux|grep mysqld,前面的ps aux是检查所有进程的,grep是过滤出mysql相关的进程。进入mysql后命令exit;退出。

7. 如何配置虚拟机的mysql与外部系统navicat连接:

(1) 关于如何配置mysql,使得外部的navicat可以和虚拟机内linux的mysql相互连接。默认情况下是连接不进来的,需要修改配置,方法如下:

:wq退出文件,接下来使用命令sudo service mysql restart重启mysql服务。再用上面的方法检查是否启动成功,如果有相应的进程号代表启动成功。

(2) 使用虚拟机的外部windows上的navicat连接时,需要知道虚拟机的ip,使用命令ifconfig可以查看到,在navicat设置连接时报错。

(3) Linux下设置mysql的用户权限,其相关命令是:

8. Windows下安装Python2和python3以及虚拟环境的配置

如果同时安装了python2和python3,在cmd中输入python还是2的环境,这是因为环境变量的path里面,只配置了2.7的路径,如果想要变成3,那么就需要将python3的路径加入到path变量中,有两个版本会引起冲突,所以删除2的路径,就可以将python默认的版本为3了。

如果是在linux环境下,可以直接运行命令sudo apt-get install python3.5,可以直接指明python的版本,在下载最新的linux版本时候,python2和3都已经默认安装上的,只不过默认的版本是python2.7。如果需要使用python3,在linux环境下,只需要输入命令python3就切换成3的环境了。

8.1 Windows python虚拟环境的安装和配置:
  • 虚拟环境最大的好处就是可以将开发环境相互隔离,而不互相影响,比如说有的项目是使用2来开发的,有的使用3来开发的,用虚拟环境下就能很好的解决。

在安装开发包之前,说一个pip的技巧,某些开发包在下载过程中会很慢,经常出现timeout的情况,解决这个问题,可以配置一下下载第三方包的镜像,可以加速下载和安装。百度搜索python豆瓣源就可以搜索到镜像,并且还提示了第三方包的安装方法:


  1. 在cmd中输入命令pip install virtualenv安装虚拟环境
  2. 学会使用豆瓣源,用-i指定镜像源,安装django,命令pip install -i django
  3. 安装了虚拟环境之后,就可以新建虚拟环境了,命令virtualenv scrapytest,后面为虚拟环境的名称,这个虚拟环境会被安装到对应的cmd目录下,比如C:\Users\mtudou>,就可以找到scrapytest的一个文件夹,里面存放的python最初的包。
  4. 激活虚拟环境,进入scripts文件夹,使用命令activate.bat完成激活。使用deactivate.bat退出当前虚拟环境。

如何指定新建的虚拟环境的python版本,使用命令virtualenv -p ...\python.exe scrapypython3,前面的...表示python对应版本的安装路径,即python.exe的路径。

8.2 Linux安装python虚拟环境:
1. 命令`sudo apt-get install python-virtualenv`安装
2. 命令`virtualenv pythonha2`,新建一个虚拟空间
3. 命令`cd pythonha2`进入虚拟环境目录
4. 这里和windows不一样,**它的脚本在bin目录下**,windows是在scripts目录下,所以命令`cd bin/`
5. 命令`source activate`启动虚拟环境
8.3 Windows下安装virtualenvwrapper

由于我们看到对于每一个虚拟环境,我们都必须记住它的路径,并且要找到相应的scripts目录或者bin目录,才来启动很麻烦,所以有一个专门的虚拟环境管理工具virtualwrapper。

  1. 命令pip install virtualenvwrapper,安装管理工具
  2. 命令workon,列出所有的虚拟环境
  3. 命令mkvirtualenv 名称,新建一个新的虚拟环境,默认是放在Users/administrator/envs里面的,实际上是可以修改这个目录的,在高级系统环境变量设置中,新建一个WORKON_HOME变量,就可以自己指定路径了。如果新建了新的envs地址,使用workon命令就找不到之前的虚拟环境了,可以把原先的虚拟环境全部拷贝过去。所有通过mkvirtualenv创建的虚拟环境都会到work_home这个目录下。
  4. 命令workon 名称,直接进入该虚拟环境,再也不需要考虑虚拟环境的路径了,命令“deactivate”直接退出该虚拟环境。
  5. 如果同时安装了python2和3,要指定虚拟环境使用那个版本,命令mkvirtualenv --python=python.exe路径 名称
  • 有时候安装包的时候报错,记住这个网址:,很多时候安装失败都可以在这里找到对应的版本,主要是windows的版本,找到对应的python版本的msi文件,就可以直接下载下来。进入该文件所在的目录,使用pip intsall 文件名完成安装。如果是安装到某个虚拟环境,要先进入指定的虚拟环境
8.4 Linux下安装virtualenvwrapper
  1. pip install virtualenvwrapper
  2. 安装好了之后不能像windows下一样直接使用mkvirtualenv的,因为没有配置,使用命令sudo find / -name virtualwrapper.sh,找到这个文件
  3. 找到文件位置之后,就需要配置source文件,使用命令vim ~/.bashrc打开总管理文件,在最后一行,配置其中两条路径。

第二章 爬虫相关基础知识

2.1 scrapy简介

做爬虫的时候,经常都会听到scrapy VS requests + beautifulsoup的组合,在本次分布式爬虫实现中只用scrapy而不用后者的原因是:

  1. requests和beautifulsoup都是库,scrapy是一个框架,框架中可以应用requests等,可以集合很多第三方库
  2. scrapy基于twisted,性能是最大的优势,异步IO框架
  3. scrapy方便扩展,提供了很多内置的功能,提高开发速度
  4. scrapy内置的css和xpath selector(对html或者xml进行分析)非常方便,beautifulsoup缺点就是慢

实践中还是会用到requests,但是不会用到beautifulsoup,因为它的功能可以直接使用scrapy的select完成。

2.2 互联网上网页分类

静态网页,事先在服务器端生成好的页面,内容是不会变的
动态网页,从服务器端取数据返回
webservice(restapi),也是属于动态网页的一种,只是通过ajax方式和后台交互的一种技术

2.3 爬虫能做什么

  1. 搜索引擎-百度,google,垂直领域搜索引擎(有一个目标,知道自己到底爬什么数据)
  2. 推荐引擎-今日头条(根据浏览习惯猜测感兴趣的内容进行推送)
  3. 机器学习的数据样本
  4. 数据分析-金融数据分析,舆情分析

2.4 正则表达式

提问:为什么有css或者xpath selector还要学正则表达式,有时候根据selector获得了整个标签内的内容,但是还要进行进一步的筛选,比如里面的数字信息等

  1. 可以帮我们判断某个字符串是否符合某一个模式
  2. 提取整个字符串里面的重要的部分信息
正则表达式里面的特殊字符
正则表达式中常用字符的用法介绍:
^ : 以什么字符开头
$ : 以什么字符结尾
. : 任意字符
* :出现任意次数,0次或者更多次
():还提取按模式取出来的子串。例如,".*(b.*b).*"表示不管前后是什么的两个b之间的子串
? :非贪婪匹配模式,默认的情况下,匹配是贪婪模式,匹配最大长度
    对于 "bobby123"这个待匹配的,结果就是bb,而不是bobb,所以这就是贪婪,反向匹配(或者理解成直到结束符合的最后一个结果)
    非贪婪匹配就是从左边开始,只需要出现一个结果就可以了,".*?(b.*?b).*"表示对两个b从左到右只要出现一次就可
    ".*?(b.*b).*"第二个b不要问好,那么第二个b就是贪婪模式,会持续匹配到最后一个b
+ :字符至少出现一次
{1}:前面的字符出现一次
{3,}: 要求前面的字符必须出现3次以上
{2,5}:前面的字符至少出现2次,最少出现5次
| : 或的关系
[] : 中括号里面的内容只要满足任何一个即可,也可以是一个区间,中括号里面的^表示不等于,中括号里面的符号就是符号,不是特殊符号的含义
\s :表示空格符
\S : 刚好与小s的意思相反,只要不是空格都可以
\w : 表示[A-Za-z0-9_]其中的任意一个字符
\W : 与\w的意思刚好相反
[\u4E00-\u9FA5] : unicode编码,含义是汉字,意思是只要出现汉字就可以。
\d : 表示数字

代码实例:

import re
"""
line = "study in 南京大学" # 待匹配的字符串
# # if line == "boooooooobaaby123":  # 一般匹配方法
regex_str = ".*?([\u4E00-\u9FA5]+大学)" # 定义模式,用到了^.*三种特殊字符
match_obj = re.match(regex_str,line)  # 接收匹配的结果
if match_obj:  # 参数第一个是模式字符串,第二个是待匹配的字符串
    print(match_obj.group(1))  # 匹配子串结果

"""

"""
? :非贪婪匹配模式,匹配最大长度,默认的情况下,匹配是贪婪模式
():还提取按模式取出来的子串。例如,".*(b.*b).*"表示不管前后是什么的两个b之间的子串,使用group()方法可以得到第一种子串的结果
    如果还有子串,则group(2)表示第二个子串结果,最外层括号为第一个
    对于 "bobby123"这个待匹配的,结果就是bb,而不是bobb,所以这就是贪婪,反向匹配
    非贪婪匹配就是从左边开始,只需要出现一个结果就可以了,".*?(b.*?b).*"表示对两个b从左到右只要出现一次就可
    ".*?(b.*b).*"第二个b不要?,那么第二个b就是贪婪模式,会持续匹配到最后一个b
"""

# 关于一个匹配多种格式输入能找出生日的模式
str = "XXX出生于2001年6月1日"
str = "XXX出生于2001/6/1"
str = "XXX出生于2001-6-1"
str = "XXX出生于2001-06-01"
str = "XXX出生于2001-06"
regex_str = ".*出生于(\d{4}[年/-]\d{1,2}([月/-]\d{1,2}|[月/-]$|$))"
match_obj = re.match(regex_str,str)
if match_obj:
    print(match_obj.group(1))

2.5 深度优先和广度优先算法

这一节主要了解网站的树结构,深度优先算法和实现,广度优先算法和实现

爬虫的基本原理,一个网站的url设计是分层的,树形结构,能够让我们爬取网站的时候更加有策略。在设计网站url时候是不会有环路的,但是在真实网站url链接的结构中,是有环路的。比如,从首页到达某个页面,这个页面上会有返回首页的链接。如果一直进入这个死循环,那么其他页面就爬取不到内容了。所以需要用到网页的去重。伯乐在线网站的文章爬取其中获取到的文章url是不会重复的,就不需要去重。但大多数文章都需要去重。


scrapy默认使用深度优先实现的,深度优先使用递归实现的广度优先是采用队列来实现的


2.6 爬虫去重策略

  1. 将访问过的url保存到数据库中,获取url时查询一下是否爬过了。虽然数据库中有缓存,但是每次都查询效率很低。
  2. 将url保存到set中。只需要O(1)的代价就可以查询到url,但是内存占用会越来越大
    假设有1亿条url,那么就需要1亿 x 2byte x 50字符/1024/1024/1024=8G
  3. url经过md5等方法哈希后保存到set中,将url压缩到固定长度而且不重复,scrapy实际上就是应用这种方法
  4. 用bitmap方法,将访问过的url通过hash函数映射到某一位,对内存压缩更大,缺点是冲突比较高
  5. bloomfilter方法对bitmap进行改进,多重hash函数降低冲突可能性。即减少内存,又减少冲突。

2.7 弄清楚unicode编码和utf8编码

字符串编码,写文件以及网络传输过程中,调用某些函数,经常碰到提示编码错误。

看一下ASCII和unicode编码:

  • 字母A用ASCII编码十进制是65,二进制 0100 0001
  • 汉字"中" 已近超出ASCII编码的范围,用unicode编码是20013二进制是01001110 00101101
  • A用unicode编码只需要前面补0二进制是 00000000 0100 0001

乱码问题解决了,但是如果内容全是英文,unicode编码比ASCII编码需要多一倍的存储空间,传输也会变慢。所以此时出现了可变长的编码utf-8,把英文:1字节,汉字3字节,特别生僻的变成4-6字节,如果传输大量的英文,utf8作用就很明显。Unicode编码虽然占用空间但是因为占用空间大小等额,在内存中处理会简单一些。

关于windows下编码格式问题:



关于linux下的编码问题:


第三章 网站内容爬取之创建项目

因此对于这种爬取内容有一个总链接的话,就不需要采用深度优先或者广度优先策略,只需要将这个总链接下的每一页的内容取出即可。说到每一页,查看url特点,发现就是在链接后面修改了页数,但是不能用这个方法,因为网站上文章数发生变化时,就必须要去修改源码。
如果是对每个分页上的写一页的链接进行跟踪,那么有多少页都无所谓了。

3.1 通过命令行创建pycharm scrapy项目步骤:

  1. 创建一个虚拟环境article_spider:mkvirtualenv article_spider
  2. 在这个虚拟环境内安装scrapy: pip install -i scrapy
    注意安装的时候可能会报错,twisted找不到,那么就去下载安装包,手动安装,安装的时候必须也是在这个虚拟环境内
  3. 建立scrapy项目,pycharm里面没有scrapy的工程,所以需要在命令行内手动安装
    进入pycharm工作目录cd d:\PycharmProjects
    进入虚拟环境workon article_spider
    创建scrapy项目专门爬取伯乐在线的文章scrapy startproject ArticleSpider,如下图,即为创建成功
  4. 在pycharm中打开刚创建的项目

3.2 pycharm项目工程文件的理解

scrapy.cfg: 类似于django的配置,它大量的借鉴了django的设计理念
settings.py: 包含了很多scrapy的配置,工程名字,spider_modules也指明了存放spiders的路径
pipelines.py: 做跟数据存储相关的
middlewares.py: 可以存放自己定义的middlewares,让scrapy变得更加可控
items.py: 有点类似于django里面的form,定义数据保存的格式
spiders文件夹:里面存放具体某个网站的爬虫,scrapy会去这个文件夹里面找有多少个爬虫文件,只需要在这里面继承了spiders,就会被scrapy找到

3.3 创建具体的spider方法

以上部分是关于scrapy虚拟环境的搭建,以及如何通过命令来新建一个scrapy工程。

在pycharm打开article_spider项目之后,需要导入之前创建的虚拟环境,即配置解释器。项目默认的是python3环境的python.exe,但是如果你创建了虚拟环境,虚拟环境的文件夹的scripts文件夹下也有python.exe。

3.4 启动spider的两种方法

  1. 启动一个spider的命令是scrapy crawl spider名称,运行命令尝试一下是否能正确启动jobbole这个spider,出现如下情况,解决好安装完成win32包之后就能正常启动了。启动sipder报错No module named "Win32api"解决办法如下“:
  1. 但是我们要在pycharm中完成爬虫,而不是每次都打开命令行,因此编写mian.py文件(放在主项目下),来调用spider命令。右键运行这个文件就可以啦。
# pycharm里面没有关于scrapy的模板,所以是没法直接调试的,需要自己手动编写一个main文件
# 在这个文件中调用命令行,就可以完成调试了

from scrapy.cmdline import execute
# 调用这个函数可以执行scrapy的脚本

import sys
import os

# print(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# os.path.dirname(os.path.abspath(__file__)) 获取main文件路径的父目录
# os.path.abspath(__file__) 获取当前main文件的路径
# 需要设置工程的目录,必须在项目所在的目录下运行,再调用执行命令才会生效
# sys.path.append("D:\PycharmProjects\ArticleSpider") 如果路径设置死了,一旦项目位置发生变动,就需要改动很麻烦

execute(["scrapy","crawl","jobbole"])
# 调用execute()函数来执行spider命令,传入数组,即是执行启动spider的命令

# 一运行这个文件,如果在jobbole.py中parse()函数处设置断点,可以发现能够正常启动,并且返回了相关response变量
# 点开这个变量可以查看到,访问了某个网址,并且正常响应,也得到了body网页内容
# 下一步就是对其中的内容进行解析,获取想要爬取的内容啦。

第四章 网站内容爬取之xpath选择器在某个网页上的实践

如何通过xpath在html中取出想要的值:scrapy根据xml提供了一套简洁的通过xpath或者css选择器来提取数据的接口。

4.1 关于xpath

  • xpath简介:xpath使用路径表达式(就像windows系统文件的路径一样可以定位)在xml和html中进行导航,包含了很多标准函数库,xpath包含有一个标准函数库,xpath是一个w3c的标准
  • xpath术语
  • xpath语法

xpath节点关系:
html中被尖括号包起来的被称为一个节点。

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

4.2 xpath语法

表达式 说明
article 选取所有article元素的所有子节点
/article 选取根元素article(html中根元素都是html;xml可以自定义根节点)
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元素

4.3 xpath选择器在具体网站上的应用

可以看到,我们的标题标题在 html/body/div[1]/div[3]/div[1]/div[1]/h1 这个嵌套关系下,我们在用xpath解析的时候,不需要自己一个一个地看嵌套关系,在 F12下,在某个元素上面右键即copy->copy xpath就能获得该元素的xpath路径。
注意:在Firefox和chrom浏览器中右键copy xpath得到的结果可能不一样

在Firefox中,得到的路径是/html/body/div[1]/div[3]/div[1]/div[1]/h1
在chrom中,得到的是//*[@id="post-110287"]/div[1]/h1
可以发现两种路径不一样,经过测试,第一种路径不能获得标题,第二种可以,原因在于,一般元素检查看到的是动态的返回来的html信息,比如js生成的,然后有些节点可能是在后台返回信息时才创建的,对于静态的网页就是检查源代码,定位的结果可能不一样,采用第二种id确定的方式更容易标准的定位。

用xpath提取标题的案例:
# -*- coding: utf-8 -*-
import scrapy

class JobboleSpider(scrapy.Spider):
    name = "jobbole"
    allowed_domains = 
    start_urls = 

    def parse(self, response):
        # 取任何节点。id等于post-110287.
        # re_selector = response.xpath('/html/body/div[1]/div[3]/div[1]/div[1]/')
        # re_selector = response.css('#post-95104 > div.entry-header > h1')
        re_selector = response.xpath('//*[@id="post-110287"]/div[1]/h1')
        print(re_selector)
        pass

# 通过debug断点调试,可以看到返回来的re_selector是有正确信息的,定位到了h1这个节点

通过调试可以看到结果:

错误提示:

同一个页面的元素通过不同电脑的chrom浏览器进行源代码查看,标签结点信息发现不一样,在h1标签中多了个span标签,解决方法:清除浏览器缓存,以下是同一页面用一个内容的检查元素的对比图。

图1:未清除浏览器缓存前



图2:清除浏览器缓存后


4.4 使用shell命令调试及更多字段内容爬取

介绍一个技巧,每次调试启动scrapy是比较慢的,每次调试都请求了一次url,效率比较低

scrapy提供了一种shell模式,提高了调试的效率。具体操作方法是在命令行中,之前的启动scrapy的命令是scrapy crawl jobbole,现在可以在命令行中使用shell,命令为“scrapy shell 网址”,然后就进入了调试区域。步骤如下图,注意启动scrapy必须在命令行中进入相应的虚拟环境以及项目的工作目录

获取发布时间:
response.xpath("//p[@class = 'entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace("·","").strip()

关于发布时间的标签设计:

在shell模式下的调试步骤:

获取点赞数:
int(response.xpath("//span[contains(@class,'vote-post-up')]/h10/text()").extract()[0])
# contains(属性名,内容)之所以用这个方法因为
#该span标签的class属性里面布置一个值,所以用contains()这个函数

在shell模式下的调试步骤:

获取收藏数:
response.xpath("//span[contains(@class,'bookmark-btn')]/text()").extract()[0]
# '  收藏'
# 收藏数的标签设置和点赞数不一样,直接是收藏前面有数字,这里没有数字,其实是0收藏的意思。
# 对于含数字的话,我们应该使用正则表达式将数字部分提取出来。

import re
match_re = re.match('.*?(\d+).*',' 收藏')
if match_re:
    fav_nums = int(match_re.group(1))
else:
    fav_nums = 0
# 正则表达式注意要有?表示非贪婪匹配,可以获取两位数等
# 还有一点就是老师没有考虑的,如果没有收藏数,即匹配不到数字,说明收藏数为0.
获取评论数:
# 评论数和收藏数的标签设计是一样的,只需要更改xpath即可
comment_nums = response.xpath("//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.xpath('//div[@class="entry"]').extract()[0]
# 对于文章内容,不同网站的设计不一样,我们一般保存html格式的内容

关于extract()方法和text()方法的区别:extract()是对一个selector的内容取出这个标签内的所有内容,包括当前的节点标签。text()方法一般是在xpath的路径内部,用于获取当前节点内的所有文本内容。

获取文章类型:
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)

4.5 xpath选择器在伯乐在线文章上的爬取实例

# -*- coding: utf-8 -*-
import scrapy
import re

class JobboleSpider(scrapy.Spider):
    name = "jobbole"
    allowed_domains = 
    start_urls = 

    def parse(self, response):
        # 取任何节点。id等于post-110287.
        #re_selector = response.css('#post-95104 > div.entry-header > h1')
        # re_selector = response.xpath('//*[@id="post-95104"]/div[1]/h1/text()')
        # 在xpath的路径后面添加一个text()方法就可以获得标签里面的标题内容了
        title = response.xpath('//*[@id="post-95104"]/div[1]/h1/text()').extract()[0]
        create_date = response.xpath("//p[@class = 'entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace("·", "").strip()
        praise_ums = response.xpath("//span[contains(@class,'vote-post-up')]/h10/text()").extract()[0]
        if praise_ums:
            praise_ums = int(praise_ums)
        else:
            praise_ums = 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 = int(match_re.group(1))
        else:
            fav_nums = 0

        comment_nums = response.xpath("//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.xpath('//div[@class="entry"]').extract()[0]
        # 对于文章内容,不同网站的设计不一样,我们一般保存html格式的内容

        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)

        print(title)
        print(create_date)
        print(praise_ums)
        print(fav_nums)
        print(comment_nums)
        print(content)
        print(tags)
        pass

第五章 网站内容爬取之css选择器在某个网页上的实践

css选择器:通过一定的语法定位到某一个元素,与xpath选择的功能是一样的

5.1 css选择器的常见用法

表达式 说明
* 选择所有节点
#container 选择id为container的节点
.container 选取所有class包含container的节点
li a 选取所有li下的所有a节点
ul + p (兄弟节点,后面的,不是子节点意思)选择ul后面的第一个p元素
div#container > ul (父子)选取id为container的div的第一个ul子元素
表达式 说明
ul ~ p 选取与ul相邻的所有p元素
a[title] 选取所有有title属性的a元素(属性有没有值都无所谓)
a[color="red"] 选取所有color属性为red值的a
a[href*="jobbole"] 选取所有href属性包含jobbole的a元素
a[href^="http"] 选取所有href属性值以http开头的a元素
a[hre$=".jpg"] 选取所有href属性值以.jpg结尾的a元素
input[type=radio]:checked input是元素,类型是radio,选取选中的radio的元素
表达式 说明
div:not(#container) 选取所有id非container的div元素
li:nth-child(3) 选取第三个li元素
tr:nth-child(2n) 第偶数个tr

几乎对于所有的元素来说,用xpath和css都是可以完成定位功能的,但对前端朋友来说比较熟悉前端的写法,scrapy提供两种方法。css的写法是比xpath更简短的,在浏览器中都能直接获取。对前端熟悉的人可以优先考虑使用css选择器来定位一个元素,对于之前用xpath做实例的网页全用css选择器,代码如下:

title = response.xpath("div.entry-header h1::text").extract()[0]
# '用 Vue 编写一个长按指令'
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# '2018/08/22'
praise_ums = response.css(".vote-post-up h10::text").extract_first()
if praise_ums:
    praise_ums = int(praise_ums)
else:
    praise_ums = 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]

tags = 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)

注意,提示一个新的函数,extract_first(),这个函数就是相当于之前的extract()[0],但是前者好处在于避免了当取出数组为空的情况,这时候取[0]元素是会报错的,不得不做异常处理。extract()函数可以传入参数,表示如果找到的数组为空,那么就返回默认值。比如extract("")就表示如果前面取出数组为空,那么就返回空字符串。

第六章 spider批量爬取

如何通过spider完成伯乐在线上大量文章的批量爬取

首先,我们需要通过列表页爬取所有文章的url,前面部分只爬取了一个页面,start_urls这个list中只有一个url,没有涉及到如何解析这个字段,通过文章分页一页一页的传递给scrapy,让scrapy自动去下载其他页面。

在scrapy中,不需要自己使用request去请求一个页面返回,所以问题是如何将众多的url传递给scrapy完成下载呢?

查看伯乐在线的文章布局如下:

要点提示:

在文章列表页中,每一篇文章就是一个div块。所以根据css选择器就能提取出文章列表中的每一篇的url,需要考虑的问题是,提取出来的url是否精确,没有混杂其他推荐文章的url,这就需要css选择器足够准确了

还要考虑的一个地方是,提取出来的url可能不是一个完整的网址,只是域名下的一部分,所以还需要将网址进行完善,比如加上域名部分,又或者原本是一个具体的文章网址,都需要处理。

初始化好request之后,如何交给scrapy下载,使用yield这个关键字就可以了。

代码如下
# jobbole.py
# -*- coding: utf-8 -*-
import scrapy
import re
from scrapy.http import Request
from urllib import parse  # python2中对应的是urlparse库

class JobboleSpider(scrapy.Spider):
    name = "jobbole"
    allowed_domains = 
    start_urls = 

    def parse(self, response):
        """
        1. 获取文章列表页的文章url,交给scrapy并交给解析函数进行具体字段的解析
        2. 获取下一页的url并交给scrapy进行下载,下载完成后交给parse
        """

        # 获取分页中某一页的所有文章url
        post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()
        # response.css(".floated-thumb .post-thumb a::attr(href)").extract()
        # 获取到的文章url有一部分是旁边的推荐链接,它们同样符合上面的css选择器,所以需要筛选
        # 所以修改css选择器,选择的更准确一点
        for post_url in post_urls:
            yield Request(url=parse.urljoin(response.url, post_url),callback=self.parse_details)
        #url=parse.urljoin(response.url, post_url)的作用是,对该页面的url不管是域名,还是具体文章,都可以提取出主域名,与post_url合在一起返回一个正确的域名。

    def parse_details(self,response):
        title = response.xpath('//div[@class='entry-header']/h1/text()').extract()[0]
        create_date = response.xpath("//p[@class = 'entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace("·", "").strip()
        praise_ums = response.xpath("//span[contains(@class,'vote-post-up')]/h10/text()").extract_first("")
        if praise_ums:
            praise_ums = int(praise_ums)
        else:
            praise_ums = 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 = int(match_re.group(1))
        else:
            fav_nums = 0

        comment_nums = response.xpath("//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.xpath('//div[@class="entry"]').extract()[0]
        # 对于文章内容,不同网站的设计不一样,我们一般保存html格式的内容

        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)

第七章 item设计-数据存入数据库-item loader机制

7.1 非结构性数据VS结构性数据

7.1.1 为何不使用dict数据类型:

数据爬取的主要目的就是从非结构的数据源得到结构性数据,解析完成的数据如何返回,最简单的就是将这些字段分别都放入一个字典里,返回给scrapy。虽然字典也很好用,但是dict缺少结构性的东西,比如字段的名字容易出错,比如fav_nums写成了fav_nums,那么dict的管理就会出错。

7.1.2 item类的作用:

为了将这些东西结构化,sccrapy提供了item类,可以像django一样指定字段,比如说,定义一个article_item,这个article_itemtitlecreat_date等字段,通过很多爬取到的item内容来实例化,就不会出错了。item类似于字典,但是比dict的功能强大,对item进行实例化和数据赋值之后,通过yeild传递给scrapy,scrapy发现这是一个item类,将item传递到pipeline中去,那么在pipeline中就可以集中处理数据的保存,去重等,这就是item的作用。

7.2 item类操作步骤
  1. 修改settings.py文件中的设置,使item传递给pipeline生效
  2. 在items.py文件中定义JobBoleArticleItem类,这个类要继承scrapy.Item,定义的内容就是有哪些字段,并且写明字段的类型,scrapy中只有Field()类型,所以定义字段的方法为:title = scrapy.Field(),其余同理
  3. 在jobbole.py文件中,引入JobBoleArticleItem类,并且实例化一个对象,article_item = JobBoleArticleItem(),当解析出来每一个字段值后,对这个对象的每一个属性或者说字段进行填充:article_item["title"] = title,注意都定义好后需要提交给scrapy:yield article_item
  4. 在pipelines.py文件中,如果字段中需要去下载文章封面图,并且保存到本地,获取保存到本地路径,就涉及到自定义pipeline,自己定义一个ArticleImagePipeline(ImagesPipeline)类,有继承关系,并且设置不同功能的pipeline执行的顺序,先下载图片保存本地,获取路径之后将其填充到item的front_image_path属性中,再将这个item提交给ArticlespiderPipeline(object),完成结构化数据的管理,比如存入数据库等等。
7.2.1 scrapy中使item传递给pipeline生效

为了使item传递给pipeline生效,必须在settings.py文件中将一段注释的代码取消注释,才能生效。设置好之后可以在pipelines中打断点,进行调试。

7.2.2 scrapy爬虫中完成图片下载到本地

将文章封面图片下载下来,并保存到本地,如何做?

scrapy提供了自动下载图片的机制,就只需要在settings.py中配置,在ITEM_PIPELINES中加一个scrapy的ImagePipeline就可以了,同时还要配置图片存放的地址IMAGES_STORE参数,以及下载图片的地址是item中的哪个字段IMAGES_URLS_FIELD参数

下载图片的pipeline配置
# settings.py 关于下载图片的pipeline的配置

# 这个ITEM_PIPELINES指的是在scrapy中配置item会流进这里的所有处理类,相当于item流进管道,后面的数字代表处理顺序,数字越小越早进入pipeline
ITEM_PIPELINES = {
    'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
    'scrapy.pipelines.images.ImagesPipeline':1,
    # 可以查看scrapy的源码,其中就有pipelines,提供了scrapy一些默认的pipline,可以加速编码过程
    # image.py里面就是存放的关于下载图片的pipline,其中ImagesPipeline这个配置好之后就可以自动下载图片
}
IMAGES_URLS_FIELD = "front_image_url"
# 告诉处理图片下载的pipeline到底应该去哪个地址下载图片,是item中的哪个字段

project_dir = os.path.abspath(os.path.dirname(__file__))
#scrapy提供了设置图片的保存路径,后面添加路径,可以是绝对路径,如果放到工作目录之后,就可以使用相对路径
IMAGES_STORE = os.path.join(project_dir,"images")
下载图片并且获取在本地的存放地址的pipeline配置
# settings.py 上面一段提供的仅仅是下载到本地,如果还要获取在本地的文件保存路径,还需要自己定义pipeline,并且完成顺序配置

import os #用来获取当前文件路径

BOT_NAME = 'ArticleSpider'

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

# Obey robots.txt rules
ROBOTSTXT_OBEY = False
# 如果这个参数是Ture的话,那么scrapy会默认去读取每个网站上的robots协议,会把不符合robots协议的url给过滤掉,如果不更改设置的话,爬虫很快就会被停掉

# 这个ITEM_PIPELINES指的是在scrapy中配置item会流进这里的所有处理类,相当于item流进管道,后面的数字代表处理顺序,数字越小越早进入pipeline
ITEM_PIPELINES = {
    'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
    #'scrapy.pipelines.images.ImagesPipeline':1,
    'ArticleSpider.pipelines.ArticleImagePipeline':1
    # 可以查看scrapy的源码,其中就有pipelines,提供了scrapy一些默认的pipeline,可以加速编码过程
    # image.py里面就是存放的关于下载图片的pipeline,其中ImagesPipeline这个配置好之后就可以自动下载图片,如果还要完成获取保存的文件路径,就需要自己定义一个新的pipeline来添加功能,并设置顺序
}
IMAGES_URLS_FIELD = "front_image_url"
# 这里下载图片的地址需要的是一个数组,而item中传过来的是个字符串,所以要改成数组传过来
# 告诉处理图片下载的pipeline到底应该去哪个地址下载图片,是item中的哪个字段

project_dir = os.path.abspath(os.path.dirname(__file__))
#scrapy提供了设置图片的保存路径,后面添加路径,可以是绝对路径,如果放到工作目录之后,就可以使用相对路径
IMAGES_STORE = os.path.join(project_dir,"images")

# 设置过滤图片的图片大小需要设置的参数有:
# IMAGES_MIN_HEIGHT = 100
# IMAGES_MIN_WIDTH = 100
报错信息:ModuleNotFoundError: No module named 'PIL'
# pipelines.py 处理获取下载到本地的图片的保存路径
# -*- coding: utf-8 -*-

from scrapy.pipelines.images import ImagesPipeline

class ArticlespiderPipeline(object):
    def process_item(self, item, spider):
        # item传递到pipline来之后,就可以进行很多操作,比如保存到数据库,drop扔掉这个item,主要是做数据存储
        return item
    # 到了这一个pipeline,需要做的就是跟数据库或者文件打交道,比如将数据保存到mysql,mangodb,或者本地文件,或者发送到es上面去

# scrapy中的imagepipline只有下载图片的功能,但是我们想将图片在本地保存的地址绑定在一起,并且传到item中
#定制自己的pipeline,去继承setting中配置的imagespipeline,让里面的某些功能变的可以定制
class ArticleImagePipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        # 文件最后下载完成的保存路径是存放在results里面的,通过断点调试可以查看这个results是个list,其中每个元素是一个tuple,存放了状态以及其余信息
        for ok, value in results:
            image_file_path = value["path"]
        item["front_image_path"] = image_file_path
        return item

7.3 将item数据保存到数据库或者json文件

7.3.1 保存item到json文件方法:

方法一: 在pipelines.py中,自定义pipeline类保存item为json文件,并且在settings.py文件中完成配置

# 将数据保存到json的pipeline
class JsonWithEncodingPipeline(object):
    def __init__(self):
        self.file = codecs.open('article.json','w',encoding='utf-8')
    # 保存到json文件,首先就要打开json文件,可以在初始化的时候就打开文件,先将需要写入的文件打开,用到python开发包codecs,与普通打开文件包open最大的区别在于文件的编码

    def process_item(self, item, spider):
        # 完成item的写入,首先将item转化为字符串
        lines = json.dumps(dict(item), ensure_ascii=False)+"\n"
        # ensure_ascii设为False,不然写入中文是会报错的,会直接把unicode编码写入文章中
        self.file.write(lines)
        return item
    # 内置的函数,spider关闭函数,同时将文件关闭
    def spider_closed(self,spider):
        self.file.close()

方法二: scrapy本身也提供了写入json的机制
scrapy提供了 field exporter机制,可以将item方便的导出成各种类型的文件,比如csv,xml,pickle,json等。使用方法,在pipelines.py中引入:from scrapy.exporters import JsonItemExporter

from scrapy.exporters import JsonItemExporter
# 调用scrapy提供的Json exporter导出json文件
class JsonExporterPipeline(object):
    # 一个关键的地方,在init时,可以直接使用open方法,并且需要传递exporter
    def __init__(self):
        self.file = open('articleexporter.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

7.4 item存入数据库

方法一:自定义pipeline完成存入mysql,同步机制
  1. 在navicat中建立article_spider数据库,并且相应的表和字段


  2. 在虚拟环境中安装mysql的驱动
    命令pip install mysqlclient,报错信息:error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": ,解决办法:去上面记住的那个网址下载mysqlclient手动安装,解决

    如果是在linux下,命令是sudo apt-get install libmysqlclient-devimp
    如果是在centos下,命令sudo yum install python-devel mysql-devel
  3. 编写mysqlpipeline
# 将item存入数据库
import MySQLdb
class MysqlPipeline(object):
    # 首先连接数据库,执行数据库具体操作是由cursor来完成的
    def __init__(self):
        #self.conn = MySQLdb.connect('host','user','password','dbname', charset = "utf8", user_unicode = True )
        self.conn = MySQLdb.connect('127.0.0.1', 'root', 'tudou1458', '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)
            VALUES (%s, %s)
        """
        # 传入插入的参数具体值
        self.cursor.execute(insert_sql,(item["title"],item["url"]))
        

方法2:用到twisted的异步机制

有了方法1,为什么还要方法2,spider解析的速度肯定是超过mysql数据入库的速度,如果后期爬取的item越来越多,插入速度很不上解析速度,就会堵塞。

Twisted这个框架提供了一种将mysql关系数据库插入异步化的操作,将mysql操作变成异步化操作,方法一中的execute()commit()是一种同步化的操作,意思就是execute不执行完,就不能往下执行,进行提交。在数据量不是很大的情况下还是可以采用方法1的,对于方法2,可以直接复制使用,需要修改的就是do_insert()函数中的内容。

Twisted框架提供了一种工具,连接池,将mysql操作变成异步操作,目前支持的是关系型数据库。

import MySQLdb.cursors
from scrapy.exporters import JsonItemExporter
# scrapy内置的item转化为各种文件的机制

class MysqlTwistedPipeline(object):
    def __init__(self,dbpool):
        self.dbpool = dbpool
    # 关键方法,通过classmethod来定义函数,在定义自己的主键和扩展的时候很有用
    # 这个方法会被scrapy调用,会将setting传递进来,方法名称是固定的
    # cls指的就是MysqlTwistedPipeline这个类
    @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
        )

        # 接下里如何使用twisted提供的异步api呢,实际上提供的是一个异步容器,有个库去连接mysql
        # 本身还是使用了mysqldb去连接数据库,twisted只是提供了一个异步容器
        dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
        # 第一个参数dbapiname实际就是mysqldb的模块名,第二个参数即为各种配置,注意这里采用可变化参数dict的传入方式,必须要使里面的变量名和默认的一样
        return cls(dbpool)

    # 接下来直接调用dbpool连接池来做数据插入
    def process_item(self, item, spider):
        # 使用twisted将mysql插入变成异步操作
        query = self.dbpool.runInteraction(self.do_insert,item)
        # 第一个参数是自己定义的函数,即具体的需要异步操作的动作,item是传入的参数
        # 有时候执行异步操作会有错误,可以直接处理错误,有专门的处理函数,通过返回来的对象的addErrorback()方法,第一个参数仍然是可自定义的错误处理函数
        query.addErrback(self.handle_error) #处理异常

    def handle_error(self,failure):
        print(failure)0

    def do_insert(self, cursor, item):
        # 执行具体的插入
        insert_sql = """
                    insert into jobbole_article(title, url)
                    VALUES (%s, %s)
                """
        cursor.execute(insert_sql, (item["title"], item["url"]))

7.5 scrapy item loader机制

当需要解析提取的字段越来越多,写了很多xpath和css选择器,后期维护起来就很麻烦,scrapy提供的item loader机制就可以将维护工作变得简单。

7.5.1 具体原理

item loader提供的是一种容器,可以在其中配置item的哪个字段需要怎么的选择器。

直接调用item_loader.load_item()方法,可以获得item,通过选择器获得的内容都为list,未经处理,比如是list的第一个值或者评论数需要正则表达式匹配之类,而scrapy中又提供了from scrapy.loader.processors import MapCompose类,可以在items.py定义item字段类型的时候,在Field中可以添加处理函数。具体操作过程如下:

7.5.2 重要代码块分析
# jobbole.py 解析字段,使用选择器
# 首先需要实例化一个ItemLoader类的对象
item_loader = ItemLoader(item=JobBoleArticleItem(),response = response) # 实例化一个对象

"""有三种重要的方法
item_loader.add_css() # 通过css选择器选择的
item_loader.add_xpath()
item_loader.add_value()  # 不是选择器选择的,而是直接填充
"""

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("fav_nums", ".bookmark-btn::text")
item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")
item_loader.add_css("content", "div.entry")
item_loader.add_css("tags","p.entry-meta-hide-on-mobile a::text")

# 获取article_item
article_item = item_loader.load_item()

"""
调用默认的load_item()方法有两个问题,第一个问题会将所有的值变成一个list,虽然听起来不合理,但是从另外的角度来看,也是合理的
因为通过css选择器取出来的极有可能就是一个list,不管是取第0个还是第1个,都是一个list,所以默认情况就是list
如何解决问题呢,list里面只取第一个,以及对某个字段的list加一些额外的处理过程
在item.py对字段进行定义,scrapy.Field()里面是有参数的,input_processor表示对输入的值预处理过程,后面MapCompose()类中可以传递很多函数名的参数,表示从左到右依次处理
title = scrapy.Field(
    input_processor = MapCompose(add_jobbole)
)
"""

# items.py 字段定义的时候加入处理过程

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

def add_jobbole(value):
    return value+"-jobbole"

def date_convert(value):
    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):
    # 对收藏数和评论数的正则处理
    try:
        match_re = re.match('.*?(\d+).*', value)
        if match_re:
            nums = int(match_re.group(1))
        else:
            nums = 0
    except:
        nums = 0

    return nums

def remove_comment_tags(value):
    #去掉tags中提取的评论项
    # 注意input_processor中的预处理是对list中的每个元素进行处理,所以只需要判断某一项是不是包含评论,置为空即可
    if "评论" in value:
        return ""
    else:
        return value

def return_value(value):
    # 这个函数是用于处理关于front_image_url字段的,本来传入就需要是list,所以不需要默认的输出处理
    # 如此一来,这个字段就是一个list模式的,所以在插入语句插入的时候,是字符串类型,那么就需要取第一个值进行插入
    return value

def image_add_http(value):
    if "http" not in value:
        value = "http:"+ value
    return value

class JobBoleArticleItem(scrapy.Item):
    title = scrapy.Field()
    # MapCompose这个类可以将传进来的值,从左到右,连续两个函数对它处理,可以传递任意多个函数,甚至可以是匿名函数
    create_date = scrapy.Field(input_processor = MapCompose(date_convert))
    url = scrapy.Field()
    # url实际是个变长的,可以对url做一个md5,让长度变成固定长度,把url变成唯一的长度固定的值
    url_object_id = scrapy.Field()
    front_image_url = scrapy.Field(input_processor = MapCompose(image_add_http),output_processor =MapCompose() )
    # 如果希望把封面图保存到本地中,把封面下载下来,记录一下在本地存放的路径
    front_image_path = scrapy.Field()
    # 在python中数据只有一种类型,Field类型,不想django可以指明字段是int类型的等等
    praise_nums = scrapy.Field(input_processor = MapCompose(get_nums))
    fav_nums = scrapy.Field(input_processor = MapCompose(get_nums))
    comment_nums = scrapy.Field(input_processor = MapCompose(get_nums))
    content = scrapy.Field()
    tags = scrapy.Field(input_processor = MapCompose(remove_comment_tags),output_processor = Join(","))


# 很多item的字段都是取list的第一个,是否需要在每个Field中都添加output_processor呢
# 可以通过自定义itemloader来解决,通过重载这个类,设置默认的输出处理设置,就可以统一处理了
class ArticleItemLoader(ItemLoader):
    # 自定义itemloader
    default_output_processor = TakeFirst()

7.6 整体代码调试:爬取伯乐在线文章并且将内容存入数据库

在实际保存到数据库的代码调试过程中,会遇到很多出其不意的问题,某个文章出现访问异常,或者没有封面图等异常情况,这种时候应该学会使用try_catch,捕获异常并且进行处理,从而处理个别异常文章。

报错信息:_mysql_exceptions.OperationalError: (1366, "Incorrect string value: '\xF0\x9F\x98\x8C\xE9\x99...' for column 'content' at row 1")
这个问题的原因来自于mysql的编码问题,解决办法为将mysql中数据库以及表的格式和连接数据库时的charset都要设置为utf8mb4格式,就解决了。

显示全文