震惊了李彦宏 , 看哭了马化腾
基于Scrapy、Redis、elasticsearch和django打造一个完整的搜索引擎网站
本教程一共八章:从零开始,直到搭建一个搜索引擎。
项目github代码地址:https://github.com/mtianyan/ArticleSpider
未来是什么时代?是数据时代!数据分析服务、互联网金融,数据建模、自然语言处理、医疗病例分析……越来越多的工作会基于数据来做,而爬虫正是快速获取数据最重要的方式,相比其它语言,Python爬虫更简单、高效
一、基础知识学习:
1. 爬取策略的深度优先和广度优先
目录:
- 网站的树结构
- 深度优先算法和实现
- 广度优先算法和实现
网站url树结构分层设计:
- bogbole.com
- blog.bogbole.com
- python.bogbole.com
- python.bogbole.com/123
环路链接问题:
从首页到下面节点。
但是下面的链接节点又会有链接指向首页
所以:我们需要对于链接进行去重
1. 深度优先
2. 广度优先
跳过已爬取的链接
对于二叉树的遍历问题
深度优先(递归实现):
顺着一条路,走到最深处。然后回头
广度优先(队列实现):
分层遍历:遍历完儿子辈。然后遍历孙子辈
Python实现深度优先过程code:
1 | def depth_tree(tree_node): |
Python实现广度优先过程code:
1 | def level_queue(root): |
2. 爬虫网址去重策略
- 将访问过的url保存到数据库中
- 将url保存到set中。只需要O(1)的代价就可以查询到url
100000000*2byte*50个字符/1024/1024/1024 = 9G
- url经过md5等方法哈希后保存到set中,将url压缩到固定长度而且不重复
- 用bitmap方法,将访问过的url通过hash函数映射到某一位
- bloomfilter方法对bitmap进行改进,多重hash函数降低冲突
scrapy去重使用的是第三种方法:后面分布式scrapy-redis会讲解bloomfilter方法。
3. Python字符串编码问题解决:
- 计算机只能处理数字,文本转换为数字才能处理,计算机中8个bit作为一个字节,
所以一个字节能表示的最大数字就是255- 计算机是美国人发明的,所以一个字节就可以标识所有单个字符
,所以ASCII(一个字节)编码就成为美国人的标准编码- 但是ASCII处理中文明显不够,中文不止255个汉字,所以中国制定了GB2312编码
,用两个字节表示一个汉字。GB2312将ASCII也包含进去了。同理,日文,韩文,越来越多的国家为了解决这个问题就都发展了一套编码,标准越来越多,如果出现多种语言混合显示就一定会出现乱码- 于是unicode出现了,它将所有语言包含进去了。
- 看一下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编码进行处理
保存文件时,转换为utf-8编码。以便于传输
读文件的库会将转换为unicode
python2 默认编码格式为ASCII
,Python3 默认编码为 utf-8
1 | #python3 |
1 | #python2 |
二、伯乐在线爬取所有文章
1. 初始化文件目录
基础环境
- python 3.5.1
- JetBrains PyCharm 2016.3.2
- mysql+navicat
为了便于日后的部署:我们开发使用了虚拟环境。
1 | pip install virtualenv |
scrapy项目初始化介绍
自行官网下载py35对应得whl文件进行pip离线安装
Scrapy 1.3.3
命令行创建scrapy项目
1 | cd desktop |
scrapy目录结构
scrapy借鉴了django的项目思想
scrapy.cfg
:配置文件。setings.py
:设置
1 | SPIDER_MODULES = ['ArticleSpider.spiders'] #存放spider的路径 |
pipelines.py:
做跟数据存储相关的东西
middilewares.py:
自己定义的middlewares 定义方法,处理响应的IO操作
init.py:
项目的初始化文件。
items.py:
定义我们所要爬取的信息的相关属性。Item对象是种类似于表单,用来保存获取到的数据
创建我们的spider
1 | cd ArticleSpider |
可以看到直接为我们创建好的空项目里已经有了模板代码。如下:
1 | # -*- coding: utf-8 -*- |
scray在命令行启动某一个Spyder的命令:
1 | scrapy crawl jobbole |
在windows报出错误
ImportError: No module named 'win32api'
1 | pip install pypiwin32#解决 |
创建我们的调试工具类*
在项目根目录里创建main.py
作为调试工具文件
1 | # _*_ coding: utf-8 _*_ |
settings.py的设置不遵守reboots协议
ROBOTSTXT_OBEY = False
在jobble.py打上断点:
1 | def parse(self, response): |
可以看到他返回的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抓取实例
为什么要使用xpath?
- xpath使用路径表达式在xml和html中进行导航
- xpath包含有一个标准函数库
- xpath是一个w3c的标准
- xpath速度要远远超beautifulsoup。
xpath节点关系
- 父节点
*上一层节点*
- 子节点
- 兄弟节点
*同胞节点*
- 先辈节点
*父节点,爷爷节点*
- 后代节点
*儿子,孙子*
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抓取误区
取某一个网页上元素的xpath地址
在标题处右键使用firebugs查看元素。
然后在<h1>2016 腾讯软件开发面试题(部分)</h1>
右键查看xpath
1 | # -*- coding: utf-8 -*- |
调试debug可以看到
1 | re_selector =(selectorlist)[] |
可以看到返回的是一个空列表,
列表是为了如果我们当前的xpath路径下还有层级目录时可以进行选取
空说明没取到值:
我们可以来chorme里观察一下
chorme取到的值
//*[@id="post-110287"]/div[1]/h1
chormexpath代码
1 | # -*- coding: utf-8 -*- |
可以看出此时可以取到值
分析页面,可以发现页面内有一部html是通过JavaScript ajax交互来生成的,因此在f12检查元素时的页面结构里有,而xpath不对
xpath是基于html源代码文件结构来找的
xpath可以有多种多样的写法:
1 | re_selector = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1/text()") |
推荐使用id型。因为页面id唯一。
推荐使用class型,因为后期循环爬取可扩展通用性强。
通过了解了这些此时我们已经可以抓取到页面的标题,此时可以使用xpath利器照猫画虎抓取任何内容。只需要点击右键查看xpath。
开启控制台调试
scrapy shell http://blog.jobbole.com/110287/
完整的xpath提取伯乐在线字段代码
1 | # -*- coding: utf-8 -*- |
css选择器的使用:
1 | # 通过css选择器提取字段 |
3. 爬取所有文章
yield关键字
1 | #使用request下载详情页面,下载完成后回调方法parse_detail()提取文章内容中的字段 |
scrapy.http import Request下载网页
1 | from scrapy.http import Request |
parse拼接网址应对herf内有可能网址不全
1 | from urllib import parse |
class层级关系
1 | next_url = response.css(".next.page-numbers::attr(href)").extract_first("") |
twist异步机制
Scrapy使用了Twisted作为框架,Twisted有些特殊的地方是它是事件驱动的,并且比较适合异步的代码。在任何情况下,都不要写阻塞的代码。阻塞的代码包括:
- 访问文件、数据库或者Web
- 产生新的进程并需要处理新进程的输出,如运行shell命令
- 执行系统层次操作的代码,如等待系统队列
实现全部文章字段下载的代码:
1 | def parse(self, response): |
全部文章的逻辑流程图
4. scrapy的items整合字段
数据爬取的任务就是从非结构的数据中提取出结构性的数据。
items 可以让我们自定义自己的字段(类似于字典,但比字典的功能更齐全)
在当前页,需要提取多个url
原始写法,extract之后则生成list列表,无法进行二次筛选:
1 | post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract() |
改进写法:
1 | post_nodes = response.css("#archive .floated-thumb .post-thumb a") |
在下载网页的时候把获取到的封面图的url传给parse_detail的response
在下载网页时将这个封面url获取到,并通过meta将他发送出去。在callback的回调函数中接收该值
1 | yield Request(url=parse.urljoin(response.url,post_url),meta={"front_image_url":image_url},callback=self.parse_detail) |
urljoin的好处
如果你没有域名,我就从response里取出来,如果你有域名则我对你起不了作用了
编写我们自定义的item并在jobboled.py中填充。
1 | class JobBoleArticleItem(scrapy.Item): |
import之后实例化,实例化之后填充:
1 | 1. from ArticleSpider.items import JobBoleArticleItem |
yield article_item将这个item传送到pipelines中
pipelines可以接收到传送过来的item
将setting.py中的pipeline配置取消注释
1 | # Configure item pipelines |
当我们的item被传输到pipeline我们可以将其进行存储到数据库等工作
setting设置下载图片pipeline
1 | ITEM_PIPELINES={ |
H:\CodePath\pyEnvs\articlespider3\Lib\site-packages\scrapy\pipelines
里面有三个scrapy默认提供的pipeline
提供了文件,图片,媒体。
ITEM_PIPELINES是一个数据管道的登记表,每一项具体的数字代表它的优先级,数字越小,越早进入。
setting设置下载图片的地址
1 | # IMAGES_MIN_HEIGHT = 100 |
设置下载图片的最小高度,宽度。
新建文件夹images在
1 | IMAGES_URLS_FIELD = "front_image_url" |
安装PILpip install pillow
定制自己的pipeline使其下载图片后能保存下它的本地路径
get_media_requests()接收一个迭代器对象下载图片
item_completed获取到图片的下载地址
继承并重写item_completed()
1 | from scrapy.pipelines.images import ImagesPipeline |
setting中设置使用我们自定义的pipeline,而不是系统自带的
1 | ITEM_PIPELINES = { |
图片url的md5处理
新建package utils
1 | import hashlib |
不确定用户传入的是不是:
1 | def get_md5(url): |
在jobbole.py中将url的md5保存下来
1 | from ArticleSpider.utils.common import get_md5 |
5. 数据保存到本地文件以及mysql中
保存到本地json文件
import codecs打开文件避免一些编码问题,自定义JsonWithEncodingPipeline实现json本地保存
1 | class JsonWithEncodingPipeline(object): |
setting.py注册pipeline
1 | ITEM_PIPELINES = { |
scrapy exporters JsonItemExporter导出
scrapy自带的导出:
- 'CsvItemExporter',
- 'XmlItemExporter',
- 'JsonItemExporter'
from scrapy.exporters import JsonItemExporter
1 | class JsonExporterPipleline(object): |
设置setting.py注册该pipeline
1 | 'ArticleSpider.pipelines.JsonExporterPipleline ': 2 |
保存到数据库(mysql)
数据库设计数据表,表的内容字段是和item一致的。数据库与item的关系。类似于django中model与form的关系。
日期的转换,将字符串转换为datetime
1 | import datetime |
数据库表设计
- 三个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(同步)编写
1 | import MySQLdb |
保存到数据库的(异步Twisted)编写
因为我们的爬取速度可能大于数据库存储的速度。异步操作。
设置可配置参数
seeting.py设置
1 | MYSQL_HOST = "127.0.0.1" |
代码中获取到设置的可配置参数
twisted异步:
1 | import MySQLdb.cursors |
- 所有值变成了list
- 对于这些值做一些处理函数
item.py中对于item process处理函数
MapCompose可以传入函数对于该字段进行处理,而且可以传入多个
1 | from scrapy.loader.processors import MapCompose |
注意:此处的自定义方法一定要写在代码前面。
1 | create_date = scrapy.Field( |
只取list中的第一个值。
自定义itemloader实现默认提取第一个
1 | class ArticleItemLoader(ItemLoader): |
list保存原值
1 | def return_value(value): |
下载图片pipeline增加if增强通用性
1 | class ArticleImagePipeline(ImagesPipeline): |
自定义的item带处理函数的完整代码
1 | class JobBoleArticleItem(scrapy.Item): |
三、知乎网问题和答案爬取
1. 基础知识
session和cookie机制
cookie:
浏览器支持的存储方式
key-valuehttp无状态请求,两次请求没有联系
session的工作原理
(1)当一个session第一次被启用时,一个唯一的标识被存储于本地的cookie中。
(2)首先使用session_start()函数,从session仓库中加载已经存储的session变量。
(3)通过使用session_register()函数注册session变量。
(4)脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中.
request模拟知乎的登录
http状态码
获取crsftoken
1 | def get_xsrf(): |
python模拟知乎登录代码:
1 | # _*_ coding: utf-8 _*_ |
2. scrapy创建知乎爬虫登录
1 | scrapy genspider zhihu www.zhihu.com |
因为知乎我们需要先进行登录,所以我们重写它的start_requests
1 | def start_requests(self): |
下载首页然后回调login函数。
login函数请求验证码并回调login_after_captcha函数.此处通过meta将post_data传送出去,后面的回调函数来用。
1 | def login(self, response): |
- login_after_captcha函数将验证码图片保存到本地,然后使用PIL库打开图片,肉眼识别后在控制台输入验证码值
然后接受步骤一的meta数据,一并提交至登录接口。回调check_login检查是否登录成功。
1 | def login_after_captcha(self, response): |
- check_login函数,验证服务器的返回数据判断是否成功
scrapy会对request的URL去重(RFPDupeFilter),加上dont_filter则告诉它这个URL不参与去重.
源码中的startrequest:
1 | def start_requests(self): |
我们将原本的start_request的代码放在了现在重写的,回调链最后的check_login
1 | def check_login(self, response): |
###3. 知乎数据表设计
上图为知乎答案版本1
上图为知乎答案版本2
设置数据表字段
问题字段 | 回答字段 |
---|---|
zhihu_id | zhihu_id |
topics | url |
url | question_id |
title | author_id |
content | content |
answer_num | parise_num |
comments_num | comments_num |
watch_user_num | create_time |
click_num | update_time |
crawl_time | crawl_time |
知乎url分析
点具体问题下查看更多。
可获得接口:
重点参数:offset=43
isend = true
next
href=”/question/25460323”
1 | all_urls = [parse.urljoin(response.url, url) for url in all_urls] |
- 从首页获取所有a标签。如果提取的url中格式为 /question/xxx 就下载之后直接进入解析函数parse_question
如果不是question页面则直接进一步跟踪。
1 | def parse(self, response): |
- 进入parse_question函数处理
创建我们的item
item要用到的方法ArticleSpider\utils\common.py:
1 | def extract_num(text): |
setting.py中设置SQL_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" SQL_DATE_FORMAT = "%Y-%m-%d"
使用:
1 | from ArticleSpider.settings import SQL_DATETIME_FORMAT |
知乎的问题 item
1 | class ZhihuQuestionItem(scrapy.Item): |
知乎问题回答item
1 | class ZhihuAnswerItem(scrapy.Item): |
有了两个item之后,我们继续完善我们的逻辑
1 | def parse_question(self, response): |
处理问题回答提取出需要的字段
1 | def parse_answer(self, reponse): |
知乎提取字段流程图:
深度优先:
- 提取出页面所有的url,并过滤掉不需要的url
- 如果是questionurl就进入question的解析
- 把该问题的爬取完了然后就返回初始解析
将item写入数据库
pipelines.py错误处理
插入时错误可通过该方法监控
1 | def handle_error(self, failure, item, spider): |
改造pipeline使其变得更通用
原本具体硬编码的pipeline
1 | def do_insert(self, cursor, item): |
改写后的:
1 | def do_insert(self, cursor, item): |
可选方法一:
1 | if item.__class__.__name__ == "JobBoleArticleItem": |
推荐方法:
把sql语句等放到item里面:
jobboleitem类内部方法
1 | def get_insert_sql(self): |
知乎问题:
1 | def get_insert_sql(self): |
知乎回答:
1 | def get_insert_sql(self): |
第二次爬取到相同数据,更新数据
1 | ON DUPLICATE KEY UPDATE content=VALUES(content), answer_num=VALUES(answer_num), comments_num=VALUES(comments_num), |
调试技巧
1 | if match_obj: |
1 | #方便调试 |
错误排查
[key error] title
pipeline中debug定位到哪一个item的错误。
四、通过CrawlSpider对招聘网站拉钩网进行整站爬取
推荐工具cmder
http://cmder.net/
下载full版本,使我们在windows环境下也可以使用linux部分命令。
配置path环境变量
1. 设计拉勾网的数据表结构
2. 初始化拉钩网项目并解读crawl源码
scrapy genspider --list
查看可使用的初始化模板
ailable templates:
- basic
- crawl
- csvfeed
- xmlfeed
1 | scrapy genspider -t crawl lagou www.lagou.com |
cmd与pycharm不同,mark root
setting.py 设置目录
crawl模板
1 | class LagouSpider(CrawlSpider): |
源码阅读剖析
https://doc.scrapy.org/en/1.3/topics/spiders.html#crawlspider
提供了一些可以让我们进行简单的follow的规则,link,迭代爬取
rules:
规则,crawel spider读取并执行
parse_start_url(response):
example:
rules是一个可迭代对象,里面有Rule实例->LinkExtractor的分析allow=('category\.php', ), callback='parse_item',
allow允许的url模式。callback,要回调的函数名。
因为rules里面没有self,无法获取到方法。
1 | import scrapy |
分析拉勾网模板代码
- 将http加上s
- 重命名parse_item为我们自定义的parse_job
- 点击
class LagouSpider(CrawlSpider):
的CrawlSpider,进入crawl源码 class CrawlSpider(Spider):
可以看出它继承于spider- 入口:
def start_requests(self):
- alt+左右方向键,不同代码跳转
- 5->之后默认parse CrawlSpider里面有parse函数。但是这次我们不能向以前一样覆盖
Crawl.py核心函数parse。
parse函数调用_parse_response
1 | def parse(self, response): |
_parse_response
- 判断是否有callback即有没有self.parse_start_url
- 我们可以重载parse_start_url加入自己的处理
- 把参数传递给函数,并调用process_results函数
_parse_response函数
1 | def _parse_response(self, response, callback, cb_kwargs, follow=True): |
parse_start_url的return值将会被process_results方法接收处理
如果不重写,因为返回为空,然后就相当于什么都没做
1 | def process_results(self, response, results): |
点击followlink
1 | def set_crawler(self, crawler): |
如果setting中有这个参数,则可以进一步执行到parse
_requests_to_follow
- 判断传入的是不是response,如果不是直接returns
- 针对当前response设置一个空set,去重
- 把self的rules通过enumerate变成一个可迭代对象
- 跳转rules详情
- 拿到link通过link_extractor.extract_links抽取出具体的link
- 执行我们的process_links
- link制作完成发起Request,回调_response_downloaded函数
- 然后执行parse_respose
1 | def _requests_to_follow(self, response): |
_compile_rules
- 在我们初始化时会调用_compile_rules
copy.copy(r) for r in self.rules]
将我们的rules进行一个copy- 调用回调函数get_method。
- 调用rules里面我们定义的process_links
- 调用rules里面我们定义的process_request
1 | def _compile_rules(self): |
self.process_links = process_links
self.process_request = process_request
可以通过在rules里面传入我们自己的处理函数,实现对url的自定义。
达到负载均衡,多地不同ip访问。
_response_downloaded
通过rule取到具体的rule
调用我们自己的回调函数
1 | def _response_downloaded(self, response): |
- allow :符合这个url我就爬取
- deny : 符合这个url规则我就放弃
- allow_domin : 这个域名下的我才处理
- allow_domin : 这个域名下的我不处理
- restrict_xpaths:进一步限定xpath
1 | self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(), |
extract_links
如果有restrict_xpaths,他会进行读取执行
1 | def extract_links(self, response): |
get_base_url:
urllib.parse.urljoin替我们拼接好url
1 | def get_base_url(text, baseurl='', encoding='utf-8'): |
编写rule规则
1 | rules = ( |
3. 设计lagou的items
需要用到的方法
1 | from w3lib.html import remove_tags |
定义好的item
1 | class LagouJobItem(scrapy.Item): |
重写的itemloader
设置默认只提取第一个
1 | class LagouJobItemLoader(ItemLoader): |
4. 提取字段值并存入数据库
1 | def parse_job(self, response): |
获得的拉勾网item数据
5. items中添加get_insert_sql实现存入数据库
1 | def get_insert_sql(self): |
五、爬虫与反爬虫
1. 基础知识
如何使我们的爬虫不被禁止掉
爬虫:
自动获取数据的程序,关键是批量的获取
反爬虫:
使用技术手段防止爬虫程序的方法
误伤:
反爬虫技术将普通用户识别为爬虫,效果再好也不能用
学校,网吧,出口的公网ip只有一个,所以禁止ip不能用。
ip动态分配。a爬封b
成本:
反爬虫人力和机器成本
拦截:
拦截率越高,误伤率越高
反爬虫的目的:
爬虫与反爬虫的对抗过程:
使用检查可以查看到价格,而查看网页源代码无法查看到价格字段。
scrapy下载到的网页时网页源代码。
js(ajax)填充的动态数据无法通过网页获取到。
2. scrapy架构及源码介绍
- 我们编写的spider,然后yield一个request发送给engine
- engine拿到什么都不做然后给scheduler
- engine会生成一个request给engine
- engine拿到之后通过downloadermiddleware 给downloader
- downloader再发送response回来给engine。
- engine拿到之后,response给spider。
- spider进行处理,解析出item & request,
- item->给itempipeline;如果是request,跳转步骤二
path:articlespider3\Lib\site-packages\scrapy\core
- engine.py:
- scheduler.py
downloader
item
- pipeline
- spider
engine.py:重要函数schedule
- enqueue_request:把request放scheduler
_next_request_from_scheduler
:从调度器拿。
1 | def schedule(self, request, spider): |
articlespider3\Lib\site-packages\scrapy\core\downloader\handlers
支持文件,ftp,http下载(https).
后期定制middleware:
- spidermiddlewire
- downloadmiddlewire
django和scrapy结构类似
3. scrapy的两个重要类:request和response
类似于django httprequest
1 | yield Request(url=parse.urljoin(response.url, post_url)) |
request参数:
1 | class Request(object_ref): |
cookies:
Lib\site-packages\scrapy\downloadermiddlewares\cookies.py
1 | cookiejarkey = request.meta.get("cookiejar") |
- priority: 优先级,影响调度顺序
- dont_filter:我的同样的request不会被过滤
- errback:错误时的回调函数
https://doc.scrapy.org/en/1.2/topics/request-response.html?highlight=response
errback example:
1 | class ErrbackSpider(scrapy.Spider): |
response类
1 | def __init__(self, url, status=200, headers=None, body=b'', flags=None, request=None): |
response的参数:
request:yield出来的request,会放在response,让我们知道它是从哪里来的
4. 自行编写随机更换useagent
- setting中设置
1 | user_agent_list = [ |
然后在代码中使用。
1 | from settings import user_agent_list |
1 | import random |
但是问题:每个request之前都得这样做。
5. middlewire配置及编写fake UseAgent代理池
取消DOWNLOADER_MIDDLEWARES的注释状态
1 | DOWNLOADER_MIDDLEWARES = { |
articlespider3\Lib\site-packages\scrapy\downloadermiddlewares\useragent.py
1 | class UserAgentMiddleware(object): |
重要方法process_request
配置默认useagent为none
1 | DOWNLOADER_MIDDLEWARES = { |
使用fakeuseragentpip install fake-useragent
settinf.py设置随机模式RANDOM_UA_TYPE = "random"
1 | from fake_useragent import UserAgent |
6. 使用西刺代理创建ip代理池保存到数据库*
ip动态变化:重启路由器等
ip代理的原理:
不直接发送自己真实ip,而使用中间代理商(代理服务器),那么服务器不知道我们的ip也就不会把我们禁掉
setting.py设置
1 | class RandomProxyMiddleware(object): |
使用西刺代理创建代理池保存到数据库
1 | # _*_ coding: utf-8 _*_ |
使用scrapy_proxies创建ip代理池
pip install scrapy_proxies
收费,但是简单
https://github.com/scrapy-plugins/scrapy-crawlera
tor隐藏。vpn
http://www.theonionrouter.com/
7. 通过云打码实现验证码的识别
1 | # _*_ coding: utf-8 _*_ |
8. cookie的禁用。& 设置下载速度
http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/autothrottle.html
setting.py:
1 | # Disable cookies (enabled by default) |
设置下载速度:
1 | # The initial download delay |
给不同的spider设置自己的setting值
1 | custom_settings = { |
六、scrapy进阶开发
1. Selenium动态页面抓取
Selenium (浏览器自动化测试框架)
Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。测试系统功能——创建回归测试检验软件功能和用户需求。支持自动录制动作和自动生成 .Net、Java、Perl等不同语言的测试脚本
安装pip install selenium
文档地址:
http://selenium-python.readthedocs.io/api.html
安装webdriver.exe
天猫价格获取
1 |
|
知乎模拟登录
1 | from selenium import webdriver |
微博模拟登录
微博开放平台api
1 | from selenium import webdriver |
模拟JavaScript鼠标下滑
1 | from selenium import webdriver |
页面不加载图片
1 | from selenium import webdriver |
phantomjs无界面的浏览器获取天猫价格
1 | #phantomjs, 无界面的浏览器, 多进程情况下phantomjs性能会下降很严重 |
2.selenium集成进scrapy
如何集成
创建中间件。
1 | from selenium import webdriver |
使用selenium集成到具体spider中
信号量:
dispatcher.connect 信号的映射,当spider结束该做什么
1 | from scrapy.xlib.pydispatch import dispatcher |
python下无界面浏览器
pip install pyvirtualdisplay
linux使用:
1 | from pyvirtualdisplay import Display |
错误:cmd=[‘xvfb’,’help’]
os error
sudo apt-get install xvfb
pip install xvfbwrapper
scrapy-splash:
支持分布式,稳定性不如chorme
selenium grid
支持分布式
splinter
https://github.com/cobrateam/splinter
scrapy的暂停重启
scrapy crawl lagou -s JOBDIR=job_info/001
pycharm进程直接杀死 kiil -9
一次 ctrl+c可接受信号
Lib\site-packages\scrapy\dupefilters.py
先hash将url变成定长的字符串
然后使用集合set去重
telnet
远程登录
telnet localhost 6023
连接当前spiderest()
命令查看spider当前状态
spider.settings["COOKIES_ENABLED"]
Lib\site-packages\scrapy\extensions\telnet.py
数据收集 & 状态收集
Scrapy提供了方便的收集数据的机制。数据以key/value方式存储,值大多是计数值。 该机制叫做数据收集器(Stats Collector),可以通过 Crawler API 的属性 stats 来使用。在下面的章节 常见数据收集器使用方法 将给出例子来说明。
无论数据收集(stats collection)开启或者关闭,数据收集器永远都是可用的。 因此您可以import进自己的模块并使用其API(增加值或者设置新的状态键(stat keys))。 该做法是为了简化数据收集的方法: 您不应该使用超过一行代码来收集您的spider,Scrpay扩展或任何您使用数据收集器代码里头的状态。
http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/stats.html
状态收集,数据收集器
1 | # 收集伯乐在线所有404的url以及404页面数 |
七、scrapy-redis 分布式爬虫
1. 分布式爬虫设计及redis介绍
多个爬虫如何进行调度,一个集中的状态管理器
优点:
- 利用多机器带宽
- 利用多ip加速爬取速度
两个问题:
- request队列的集中管理
- 去重集中管理
分布式。
2. redis命令
hexists course_dict mtianyan
hexists course_dict mtianyan2
Redis HEXISTS命令被用来检查哈希字段是否存在。
返回值
回复整数,1或0。
- 1, 如果哈希包含字段。
- 0 如果哈希不包含字段,或key不存在。
hdel course_dict mtianyan
Redis HDEL命令用于从存储在键散列删除指定的字段。如果没有这个哈希中存在指定的字段将被忽略。如果键不存在,它将被视为一个空的哈希与此命令将返回0。
返回值回复整数,从散列中删除的字段的数量,不包括指定的但不是现有字段。
hgetall course_dict
Redis Hgetall 命令用于返回哈希表中,所有的字段和值。
在返回值里,紧跟每个字段名(field name)之后是字段的值(value),所以返回值的长度是哈希表大小的两倍。
hset course_dict bobby “python scrapy”
Redis Hset 命令用于为哈希表中的字段赋值 。
如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。
如果字段已经存在于哈希表中,旧值将被覆盖。
hkey course_dict
Redis Keys 命令用于查找所有符合给定模式 pattern 的 key 。。
hvals course_dict
Redis Hvals 命令返回哈希表所有字段的值。
lpush mtianyan “scary”
rpush mtianyan “scary”
存入key-value
lrange mtianyan 0 10
取出mtianyan的0到10
命令 | 说明 |
---|---|
lpop/rpop | 左删除/右删除 |
llen mtianyan | 长度 |
lindex mtianyan 3 | 第几个元素 |
sadd | 集合做减法 |
siner | 交集 |
spop | 随机删除 |
srandmember | 随机选择多个元素 |
smembers | 获取set所有元素 |
srandmember | 随机选择多个元素 |
zadd | 每个数有分数 |
zcount key 0 100 | 0-100分数据量统计 |
3. scrapy-redis搭建分布式爬虫
需要的环境:
Python 2.7, 3.4 or 3.5
Redis >= 2.8
Scrapy >= 1.1
redis-py >= 2.10
pip install redis
setting.py设置
1 | SCHEDULER = "scrapy_redis.scheduler.Scheduler" |
要继承redisspider
1 | from scrapy_redis.spiders import RedisSpider |
启动spider
scrapy runspider myspider.py
push urls to redis:放置初始url进入队列
redis-cli lpush myspider:start_urls http://google.com
搭建示例
- 创建新的scrapy项目
- 去github拷贝scrapy-redis源码
不同spider使用不同redis list
将队列从内存放入redis中
next_requests
所有的yield出去的request会被
ScrapyRedisTest\scrapy_redis\scheduler.py
的以及重写的enqueue_request接收
八、elasticsearch搭建搜索引擎
elasticsearch介绍:一个基于lucene的搜索服务器,分布式多用户的全文搜索引擎 java开发的 基于restful web接口
自己搭建的网站或者程序,添加搜索功能比较困难
所以我们希望搜索解决方案要高效
零配置并且免费
能够简单的通过json和http与搜索引擎交互
希望搜索服务很稳定
简单的将一台服务器扩展到多台服务器
内部功能:
分词 搜索结果打分 解析搜索要求
全文搜索引擎:solr sphinx
很多大公司都用elasticsearch 戴尔 Facebook 微软等等
elasticsearch对Lucene进行了封装,既能存储数据,又能分析数据,适合与做搜索引擎
关系数据搜索缺点:
无法对搜素结果进行打分排序
没有分布式,搜索麻烦,对程序员的要求比较高
无法解析搜索请求,对搜索的内容无法进行解析,如分词等
数据多了,效率低
需要分词,把关系,数据,重点分出来nosql数据库:
文档数据库 json代码,在关系数据库中数据存储,需要存到多个表,内部有多对多等关系之类的,需要涉及到多个表才能将json里面的内容存下来,nosql直接将一个json的内容存起来,作为一个文档存档到数据库。
mongodb:
1. elasticsearch安装与配置
- java sdk安装
- elasticsearch安装官网下载 不使用官网的版本,提供原始的插件不多
- elasticsearc-rtf github搜索,中文发行版,已经安装了很多插件 https://github.com/medcl/elasticsearch-rtf
- 运行elasticsearch的方法,在bin文件目录下进入命令行,执行elasticsearch.bat
5.配置文件:elasticsearch-rtf\elasticsearch-rtf-master\config\elasticsearch.yml
2. elasticsearch两个重要插件:head和kibana的安装
head插件相当于Navicat,用于管理数据库,基于浏览器
https://github.com/mobz/elasticsearch-head
1 | Running with built in server |
配置elasticsearch与heade互通
kibana.bat
2. elasticsearch基础概念
- 集群:一个或多个节点组织在一起
- 节点:一个集群中的一台服务器
- 分片:索引划分为多份的能力,允许水平分割,扩展容量,多个分片响应请求
- 副本:分片的一份或多分,一个节点失败,其他节点顶上
|index | 数据库|
|type | 表|
|document | 行|
|fields | 列|
集合搜索和保存:增加了五种方法:
OPTIONS & PUT & DELETE & TRACE & CONNECT
3. 倒排索引:
倒排索引待解决的问题:
4. elasticsearch命令
PUT lagou/job/1
1为idPUT lagou/job/
不指明id自动生成uuid。修改部分字段
POST lagou/job/1/_update
DELETE lagou/job/1
elasticserach批量操作:
查询index为testdb下的job1表的id为1和job2表的id为2的数据
1 | GET _mget |
index已经指定了,所有在doc中就不用指定了
1 | GET testdb/_mget{ |
连type都一样,只是id不一样
1 | GET testdb/job1/_megt |
或者继续简写
1 | GET testdb/job1/_megt |
elasticsearch的bulk批量操作:可以合并多个操作,比如index,delete,update,create等等,包括从一个索引到另一个索引:
- action_and_meta_data\n
- option_source\n
- action_and_meta_data\n
- option_source\n
- ….
- action_and_meta_data\n
- option_source\n
每个操作都是由两行构成,除了delete除外,由元信息行和数据行组成
注意数据不能美化,即只能是两行的形式,而不能是经过解析的标准的json排列形式,否则会报错
1 | POST _bulk |
elasticserach的mapping映射
elasticserach的mapping映射:创建索引时,可以预先定义字段的类型以及相关属性,每个字段定义一种类型,属性比mysql里面丰富,前面没有传入,因为elasticsearch会根据json源数据来猜测是什么基础类型。M挨批评就是我们自己定义的字段的数据类型,同时告诉elasticsearch如何索引数据以及是否可以被搜索。
作用:会让索引建立的更加细致和完善,对于大多数是不需要我们自己定义
相关属性的配置
- String类型: 两种text keyword。text会对内部的内容进行分析,索引,进行倒排索引等,为设置为keyword则会当成字符串,不会被分析,只能完全匹配才能找到String。 在es5已经被废弃了
- 日期类型:date 以及datetime等
- 数据类型:integer long double等等
- bool类型
- binary类型
- 复杂类型:object nested
- geo类型:geo-point地理位置
- 专业类型:ip competition
- object :json里面内置的还有下层{}的对象
- nested:数组形式的数据
elasticserach查询:
大概分为三类:
- 基本查询:
- 组合查询:
- 过滤:查询同时,通过filter条件在不影响打分的情况下筛选数据
match查询:
后面为关键词,关于python的都会提取出来,match查询会对内容进行分词,并且会自动对传入的关键词进行大小写转换,内置ik分词器会进行切分,如python网站,只要搜到存在的任何一部分,都会返回
GET lagou/job/_search
1 | { |
term查询
区别,对传入的值不会做任何处理,就像keyword,只能查包含整个传入的内容的,一部分也不行,只能完全匹配
terms查询
title里传入多个值,只要有一个匹配,就会返回结果
控制查询的返回数量
1 | GET lagou/_serach |
通过这里就可以完成分页处理洛,从第一条开始查询两条
match_all 返回所有
1 | GET lagou/_search |
python系统,将其分词,分为词条,满足词条里面的所有词才会返回结果,slop参数说明两个词条之间的最小距离
multi_match查询
比如可以指定多个字段,比如查询title和desc这两个字段包含python的关键词文档
1 | GET lagou/_search |
query为要查询的关键词 fileds在哪些字段里查询关键词,只要其中某个字段中出现了都返回
^3的意思为设置权重,在title中找到的权值为在desc字段中找到的权值的三倍
指定返回字段
1 | GET lagou/_search{ |
通过sort把结果排序
1 | GET lagou/_search |
sort是一个数组,里面是一个字典,key就是要sort的字段,asc desc是升序降序的意思
查询范围 range查询
1 | GET lagou/_search |
range是在query里面的,boost是权重,gte lte是大于等于 小于等于的意思
对时间的范围查询,则是以字符串的形式传入
wildcard模糊查询,可以使用通配符*
组合查询:bool查询
bool查询包括了must should must_not filter来完成
格式如下:
1 | bool:{ |
5. 把爬取的数据保存至elasticsearch
1 | class ElasticsearchPipeline(object): |
elasticsearch-dsl-py
High level Python client for Elasticsearch
pip install elasticsearch-dsl
items.py 中将数据保存至es
1 | def save_to_es(self): |
6. elasticsearch结合django搭建搜索引擎
获取elasticsearch的查询接口
1 | body={ |
使django与其交互。