Scrapy分布式爬虫打造搜索引擎—— scrapy 爬取伯乐在线
Posted lxr1995
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Scrapy分布式爬虫打造搜索引擎—— scrapy 爬取伯乐在线相关的知识,希望对你有一定的参考价值。
1.开发环境准备
-
1.爬取策略
- 目标:爬取“伯乐在线”的所有文章
- 策略选择:由于“伯乐在线”提供了全部文章的索引页 ,所有不需要考虑url的去重方法,直接在索引页开始,一篇文章一篇文章地进行爬取,一直进行到最后一页即可。
- 索引页地址:http://blog.jobbole.com/all-posts/
-
2. 搭建python3虚拟环境
- 打开cmd,进入命令行,输入workon,查看当前存在的虚拟环境: workon
- 为爬虫项目,新建python3虚拟环境: mkvirtualenv -p python3 ArticleSpider_Env
- 成功新建python3虚拟环境后,输入: workon ,可以看到现在虚拟环境 ArticleSpider_Env 已存在
-
PS C:UsersGoFree> workon Pass a name to activate one of the following virtualenvs: ============================================================================== ArticleSpider_Env env_python2.7 env_python3.6 PycharmProjects PS C:UsersGoFree>
-
-
3.在虚拟环境中,安装scrapy包
- 进入ArticleSpider_Env 虚拟环境,输入 :C:UsersGoFree>workon ArticleSpider_Env
- 显示信息如下:
C:UsersGoFree>workon ArticleSpider_Env (ArticleSpider_Env) C:UsersGoFree>
- 显示信息如下:
- 安装scrapy包,输入: pip install scrapy --upgrade
- 部分安装成功的信息如下:
Installing collected packages: attrs, pyasn1, pyasn1-modules, six, idna, asn1crypto, pycparser, cffi, cryptography, pyOpenSSL, service-identity, w3lib, lxml, cssselect, parsel, queuelib, PyDispatcher, zope.interface, constantly, incremental, Automat, hyperlink, Twisted, scrapy Successfully installed Automat-0.6.0 PyDispatcher-2.0.5 Twisted-18.4.0 asn1crypto-0.24.0 attrs-18.1.0 cffi-1.11.5 constantly-15.1.0 cryptography-2.2.2 cssselect-1.0.3 hyperlink-18.0.0 idna-2.7 incremental-17.5.0 lxml-4.2.1 parsel-1.4.0 pyOpenSSL-18.0.0 pyasn1-0.4.3 pyasn1-modules-0.2.1 pycparser-2.18 queuelib-1.5.0 scrapy-1.5.0 service-identity-17.0.0 six-1.11.0 w3lib-1.19.0 zope.interface-4.5.0 (ArticleSpider_Env) C:UsersGoFree>
- 部分安装成功的信息如下:
- 进入ArticleSpider_Env 虚拟环境,输入 :C:UsersGoFree>workon ArticleSpider_Env
-
4.在虚拟环境中,在指定位置创建scrapy项目
- 定位到想要创建项目的文件夹,输入:scrapy startproject ArticleSpider
- 创建成功,显示信息如下:(输入: dir ,能看到新创建了ArticleSpider文件夹)
(ArticleSpider_Env) E:myGit>scrapy startproject ArticleSpider New Scrapy project ‘ArticleSpider‘, using template directory ‘c:\\users\\gofree\\.virtualenvs\\articlespider_env\\lib\\site-packages\\scrapy\\templates\\project‘, created in: E:myGitArticleSpider You can start your first spider with: cd ArticleSpider scrapy genspider example example.com (ArticleSpider_Env) E:myGit>dir 驱动器 E 中的卷是 新加卷 卷的序列号是 D609-D119 E:myGit 的目录 2018/06/11 18:59 <DIR> . 2018/06/11 18:59 <DIR> .. 2018/06/11 18:59 <DIR> ArticleSpider 2018/06/05 20:28 <DIR> ArticleSpider_origion 2018/06/08 18:46 <DIR> machine-learning-lxr 2018/06/08 22:48 <DIR> Search-Engine-Implementation-Using-Python 0 个文件 0 字节 6 个目录 197,621,055,488 可用字节 (ArticleSpider_Env) E:myGit>
- 创建成功,显示信息如下:(输入: dir ,能看到新创建了ArticleSpider文件夹)
- 定位到想要创建项目的文件夹,输入:scrapy startproject ArticleSpider
-
5.使用PyCharm打开新建项目
- 打开PyCharm,在顶部标签栏File -> open ,选择新建文件夹ArticleSpider 打开,选择在新窗口打开
- 选择 File -> Settings ,为本项目添加前面创建的虚拟环境。选择如下图所示:(注意,解释器需要选择 ArticleSpider_EnvScriptspython.py 文件)
-
6.初始化爬取“伯乐在线”文章页 http://blog.jobbole.com/ 的爬虫文件
- cmd进入 ArticleSpider 文件夹内 :对我来说,输入: cd ArticleSpider ,就进入了项目文件夹,具体信息如下:
-
(ArticleSpider_Env) E:myGit>cd ArticleSpider (ArticleSpider_Env) E:myGitArticleSpider>
-
- cmd中输入: scrapy genspider jobbole blog.jobbole.com ,详细信息如下:
-
(ArticleSpider_Env) E:myGitArticleSpider>scrapy genspider jobbole blog.jobbole.com
Created spider ‘jobbole‘ using template ‘basic‘ in module:
ArticleSpider.spiders.jobbole
(ArticleSpider_Env) E:myGitArticleSpider>
-
- 生成 jobbole.py文件,在下图所示的路径中:
- cmd进入 ArticleSpider 文件夹内 :对我来说,输入: cd ArticleSpider ,就进入了项目文件夹,具体信息如下:
-
7. 启动爬取“伯乐在线”的爬虫
- 输入命令: scrapy crawl jobbole ,出现错误信息如下:
- ModuleNotFoundError: No module named ‘win32api‘
- 根据错误信息的提示,在当前虚拟环境中,安装pypiwin32包
- 输入: pip install pypiwin32 ,详细信息如下:
-
(ArticleSpider_Env) E:myGitArticleSpider>pip install pypiwin32 Collecting pypiwin32 Downloading https://files.pythonhosted.org/packages/d0/1b/2f292bbd742e369a100c91faa0483172cd91a1a422a6692055ac920946c5/pypiwin32-223-py3-none-any.whl Collecting pywin32>=223 (from pypiwin32) Downloading https://files.pythonhosted.org/packages/9f/9d/f4b2170e8ff5d825cd4398856fee88f6c70c60bce0aa8411ed17c1e1b21f/pywin32-223-cp36-cp36m-win_amd64.whl (9.0MB) 100% |████████████████████████████████| 9.0MB 5.9kB/s Installing collected packages: pywin32, pypiwin32 Successfully installed pypiwin32-223 pywin32-223 (ArticleSpider_Env) E:myGitArticleSpider>
- 输入命令: scrapy crawl jobbole ,出现错误信息如下:
-
8.Pycharm 断点调试
-
Pycharm 断点调试基础
- 参考:
- https://www.cnblogs.com/lijunjiang2015/p/7689822.html
- https://blog.csdn.net/weixin_39198406/article/details/78873120
- https://blog.csdn.net/u011331731/article/details/72801449
- 总结如下:
- 设置断点:在行号后单击(双击取消)
- 两种模式
- console模式:类似于命令行的出输,可以直观的看到程序每行代码运行的效果。
- Alt + Shift + F9 运行debug模式
- Debuger 模式:即断点调试模式
- console模式:类似于命令行的出输,可以直观的看到程序每行代码运行的效果。
- F6: 按顺序往下执行
- F7:进入
- F8:跳过。下一步但仅限于设置断点的文件
- F9:只在断点和交互处停止,快速调式
- F10:显示目前项目所有断点
- Shift+F8:跳出。当单步执行到子函数内时,用step out就可以执行完子函数余下部分,并返回到上一层函数。
- Alt+F9:直接跳到下一个断点
- 参考:
-
-
-
创建main.py文件,调用 jobbole.py,用作调试
- 在文件夹 ArticleSpider 的根目录下,创建main.py文件。目录结构和main.py的代码分别如下:(注意,execute([])中的字符需放在列表中,连起来就是在cmd中启动爬取jobbole的命令)
-
# -*- coding : utf-8 -*- __author__ = ‘lxr‘ from scrapy.cmdline import execute import sys import os # os.path.dirname() : 返回传入文件的父目录路径 # os.path.abspath(__file__) : 返回当前文件的路径 sys.path.append(os.path.dirname(os.path.abspath(__file__))) execute(["scrapy","crawl","jobbole"])
-
- 在文件夹 ArticleSpider 的根目录下,创建main.py文件。目录结构和main.py的代码分别如下:(注意,execute([])中的字符需放在列表中,连起来就是在cmd中启动爬取jobbole的命令)
-
-
-
将 ROBOTSTXT_OBEY 设置为False
- Robots 协议作用:将过滤不符合robots协议的URL。
- 在后续开发过程中,需要将ROBOTSTXT_OBEY 设置为False。否则,在开启爬虫时,爬虫会因为URL被过滤掉而早早停掉
- 协议位置如图,更改setting 文件中的 ROBOTSTXT_OBEY = False:
-
在jobbole.py文件中打断点。如下图所示:
-
对main.py 进行debug(红色圈围起来的是debug按钮)
-
2.使用xpath方法爬取页面内容
-
1.xpath基础
-
1.简介
- 1. xpath 使用路径表达式在 xml 和 Html 中进行导航
- 2. xpath包含标准函数库
- 3. xpath是一个w3c的标准
-
2.节点关系
- 1.父节点
- 2.子节点
- 3.同胞节点
- 4.先辈节点
- 5.后代节点
-
3.语法
-
-
2.具体爬取前的必要说明
- 现在,以爬取文章 http://blog.jobbole.com/107275/ 为例进行说明。
-
1.更换 jobbole.py 中的 start_urls,改为 start_urls = [‘http://blog.jobbole.com/107275/‘]
-
为了更换说明,现在以爬取文章的标题为例
-
2.在Firefox浏览器上获取当前文章的xpath路径
- 打开文章,进入文章页面
- 按F12, 显示页面代码
- 点击红圈围住的按钮。作用,如点击标题,可以定位到对应的Html代码处
- 在对应标题的代码处,点击“右键”,选择复制xpath路径。
- 得到xpath路径 :/html/body/div[1]/div[3]/div[1]/div[1]/h1/span
- 打开文章,进入文章页面
-
3.获取chrome浏览器的xpath路径
- 方法与从Firefox类似
- 得到xpath路径://*[@id="post-107275"]/div[1]/h1
-
4. 编写 jobbole.py ,用两种xpath 去获取标题
- 代码截图如下:
- 运行debug,注意红线圈出的爬取结果
- 爬取结果
- Firefox 获取的xpath,未能返回爬取数据,返回为空
- Chrome获取的xpath,成功爬取了希望的标题数据
- 原因解释:
- Firefox 按f12获取的HTML代码是页面生成后显示的代码
- Chrome 按f12获取的HTML代码就是生成此页面的原始代码
- 结论:在获取xpath 或下节要介绍的 CSS选择器 时,使用 Chrome 进去获取
- 代码截图如下:
-
5.为什么使用SelectorList作为返回值,而不是直接返回节点类型
- 返回值类型如同所示
- 解释:
- 如果获取的不是节点
- 或者获取的元素内还嵌套其他的节点,还希望对获取的元素做进一步的xpath 筛选
- 如果返回节点,就不能进行select筛选
- 所有scrapy对返回值作了一定封装,让我们可以在嵌套的select筛选。
- 返回值类型如同所示
-
3.使用scrapy shell 调试
- 原因:在cmd 命令行下,进行scrapy 调试,速度更快,占用的资源更少
- 用法:可以把在shell 中调试成功的语句粘贴到Pycharm中
- 启动方式:
- 打开cmd
- 进入虚拟环境: workon ArticleSpider_Env
- 进入 ArticleSpider 项目中 : (ArticleSpider_Env) E:myGitArticleSpider>
- 启动 scrapy shell 调试: scrapy shell http://blog.jobbole.com/107275/ ,后面跟的URL就是打算爬取的页面地址
- 开启成功,显示信息如下:
(ArticleSpider_Env) E:myGitArticleSpider>scrapy shell http://blog.jobbole.com/107275/ 2018-06-12 15:15:53 [scrapy.utils.log] INFO: Scrapy 1.5.0 started (bot: ArticleSpider) 2018-06-12 15:15:53 [scrapy.utils.log] INFO: Versions: lxml 4.2.1.0, libxml2 2.9.5, cssselect 1.0.3, parsel 1.4.0, w3lib 1.19.0, Twisted 18.4.0, Python 3.6.2 |Continuum Analytics, Inc.| (default, Jul 20 2017, 12:30:02) [MSC v.1900 64 bit (AMD64)], pyOpenSSL 18.0.0 (OpenSSL 1.1.0h 27 Mar 2018), cryptography 2.2.2, Platform Windows-10-10.0.17134-SP0 2018-06-12 15:15:53 [scrapy.crawler] INFO: Overridden settings: {‘BOT_NAME‘: ‘ArticleSpider‘, ‘DUPEFILTER_CLASS‘: ‘scrapy.dupefilters.BaseDupeFilter‘, ‘LOGSTATS_INTERVAL‘: 0, ‘NEWSPIDER_MODULE‘: ‘ArticleSpider.spiders‘, ‘SPIDER_MODULES‘: [‘ArticleSpider.spiders‘]} 2018-06-12 15:15:53 [scrapy.middleware] INFO: Enabled extensions: [‘scrapy.extensions.corestats.CoreStats‘, ‘scrapy.extensions.telnet.TelnetConsole‘] 2018-06-12 15:15:54 [scrapy.middleware] INFO: Enabled downloader middlewares: [‘scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware‘, ‘scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware‘, ‘scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware‘, ‘scrapy.downloadermiddlewares.useragent.UserAgentMiddleware‘, ‘scrapy.downloadermiddlewares.retry.RetryMiddleware‘, ‘scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware‘, ‘scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware‘, ‘scrapy.downloadermiddlewares.redirect.RedirectMiddleware‘, ‘scrapy.downloadermiddlewares.cookies.CookiesMiddleware‘, ‘scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware‘, ‘scrapy.downloadermiddlewares.stats.DownloaderStats‘] 2018-06-12 15:15:54 [scrapy.middleware] INFO: Enabled spider middlewares: [‘scrapy.spidermiddlewares.httperror.HttpErrorMiddleware‘, ‘scrapy.spidermiddlewares.offsite.OffsiteMiddleware‘, ‘scrapy.spidermiddlewares.referer.RefererMiddleware‘, ‘scrapy.spidermiddlewares.urllength.UrlLengthMiddleware‘, ‘scrapy.spidermiddlewares.depth.DepthMiddleware‘] 2018-06-12 15:15:54 [scrapy.middleware] INFO: Enabled item pipelines: [] 2018-06-12 15:15:54 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6026 2018-06-12 15:15:54 [scrapy.core.engine] INFO: Spider opened 2018-06-12 15:15:54 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://blog.jobbole.com/107275/> (referer: None) [s] Available Scrapy objects: [s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc) [s] crawler <scrapy.crawler.Crawler object at 0x0000020E3ED38FD0> [s] item {} [s] request <GET http://blog.jobbole.com/107275/> [s] response <200 http://blog.jobbole.com/107275/> [s] settings <scrapy.settings.Settings object at 0x0000020E41439898> [s] spider <JobboleSpider ‘jobbole‘ at 0x20e416e29e8> [s] Useful shortcuts: [s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed) [s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help) [s] view(response) View response in a browser >>>
-
4.爬取文章的具体信息
- 注意,在进行具体爬取时,xpath路径应该根据页面的html结构计算得出。不要直接在chrome上选择复制xpath,chrome生成的xpath是根据当前URL生成的,放在其他页面将不能使用。
-
1.爬取标题
- extract() : 把 selector 对象 转换为数组(注意观察:想要获取的文本在数组中的第一个位置,通常为extract()[0])
- 注意,在xpath 地址最后,添加 /text() :表示只获取文本,不需要前后的HTML标签
- 获取标题的代码如下:
- title = response.xpath(‘//div[@class="entry-header"]/h1/text()‘).extract()[0]
-
2.爬取发表日期
- strip():可以除去一段字符串中的空格、回车、换行。
- replace("想替换的字符1",“用来替换的字符2”):把一段字符串中的1字符用2字符替换
- 代码: create_date = response.xpath(‘//div[@class="entry-meta"]/p/text()‘).extract()[0].strip().replace("·","").strip()
-
3.爬取点赞数
- 代码: praise_nums = response.xpath(‘//div[@class="post-adds"]/span[1]/h10/text()‘).extract()[0] ,获得结果为: ‘1‘
- 考虑为空的情况,即没有,需要使用正则表达式和if-else结果
- 所以,使用正则表达式:导入 re
- result = re.match("正则表达式",“匹配文本”)
- result.group():对应方法查看 https://www.cnblogs.com/lxr1995/p/9148794.html
- 考虑结果为空,使用if-else语句手动赋值点赞数为0
- 考虑有匹配的‘1’是字符串,不是数字,使用 int() 进行强制类型转换
- contains用法: span[contains(@class,"vote-post-up")] :表示取class名包含“vote-post-up”的span元素
- 代码如下:
praise_nums = response.xpath(‘//div[@class="post-adds"]/span[1]/h10/text()‘).extract()[0] match_re = re.match(‘.*?(d+).*‘, praise_nums) if match_re: praise_nums = int(match_re.group(1)) else: praise_nums = 0
-
-
4.爬取收藏数
- 和点赞数的情况类似,使用正式表达式和if-else语句
- 代码如下:
-
fav_nums = response.xpath(‘//div[@class="post-adds"]/span[2]/text()‘).extract()[0] match_re = re.match(‘.*?(d+).*‘,fav_nums) if match_re: fav_nums = int(match_re.group(1)) else: fav_nums = 0
-
-
5.爬取评论数
- 使用结构可与点赞数、收藏数类比
- 代码如下:
-
comment_nums = response.xpath(‘//div[@class="post-adds"]/a/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
-
-
6.爬取正文
- 由于不同网站正文的排版是不同的,所有正文元素分析是一个比较复杂的内容,这里暂时把所有的html元素都保存,以后需要做进一步提取或者样式分析时可以使用
- 代码如下:
- content = response.xpath(‘//div[@class="entry"]‘).extract()[0]
-
7.爬取标签
- 返回的是数组,需要将数组中的值连接起来,生成一个标签字符串,使用 ‘‘,".join()
- 备忘:lamda表达式:tag_list = [elem for elem in tag_list if not elem.strip().endwith("评论")]
- 作用:删除列表找以“评论”结尾的项
- 代码如下:
-
tag_list = response.xpath(‘//p[@class="entry-meta-hide-on-mobile"]/a/text()‘).extract() tag = ",".join(tag_list)
-
-
8.总结
- 在jobboler.py中的添加的代码如下:
-
# -*- coding: utf-8 -*- import scrapy import re class JobboleSpider(scrapy.Spider): name = ‘jobbole‘ allowed_domains = [‘blog.jobbole.com‘] start_urls = [‘http://blog.jobbole.com/107275/‘] # start_urls = [‘http://blog.jobbole.com/114107/‘] def parse(self, response): #使用xpath #标题 title = response.xpath(‘//div[@class="entry-header"]/h1/text()‘).extract()[0] #发表日期 create_date = response.xpath(‘//div[@class="entry-meta"]/p/text()‘).extract()[0].strip().replace("·","").strip() #点赞数 praise_nums = response.xpath(‘//div[@class="post-adds"]/span[1]/h10/text()‘).extract()[0] match_re = re.match(‘.*?(d+).*‘, praise_nums) if match_re: praise_nums = int(match_re.group(1)) else: praise_nums = 0 #收藏数 fav_nums = response.xpath(‘//div[@class="post-adds"]/span[2]/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(‘//div[@class="post-adds"]/a/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] #标签 tag_list = [elem for elem in tag_list ] #在tag_list中有不是标签的项时,过滤使用 tag_list = response.xpath(‘//p[@class="entry-meta-hide-on-mobile"]/a/text()‘).extract() tag = ",".join(tag_list) pass
-
- 在jobboler.py中的添加的代码如下:
-
3.使用CSS选择权爬取页面内容
-
1.CSS基本语法
-
2.爬取具体文章
- 代码样式类似,用CSS选择器地址替换xpath地址
- response.xpath 替换成 response.css
- 输出文本:添加伪类选择器 ::text
- 对含有多个class的标签,取class名唯一的,来代表该标签
- 具体代码如下:
-
# -*- coding: utf-8 -*- import scrapy import re class JobboleSpider(scrapy.Spider): name = ‘jobbole‘ allowed_domains = [‘blog.jobbole.com‘] start_urls = [‘http://blog.jobbole.com/107275/‘] # start_urls = [‘http://blog.jobbole.com/114107/‘] def parse(self, response): #使用CSS选择器 #标题 title = response.css(‘.entry-header h1::text‘).extract()[0] #发表日期 create_date = response.css(‘.entry-meta-hide-on-mobile ::text‘).extract()[0].strip().replace("·","").strip() #点赞数 praise_nums = response.css(‘.vote-post-up h10::text‘).extract()[0] match_re = re.match(‘.*?(d+).*‘, praise_nums) if match_re: praise_nums = int(match_re.group(1)) else: praise_nums = 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(‘.entry‘).extract()[0] #标签 tag_list = response.css(‘.entry-meta-hide-on-mobile a ::text‘).extract() tag = ",".join(tag_list) pass
-
- extract()[0] 优化:
- 对于确定使用 extract()[0] 的时候,可以用 extract_first() 替换
- extract_first() 取空值时,会返回一个默认值,默认值可在 () 中指定 "" 为空,而不用抛出异常
4.xpath和CSS选择器总结
哪种方式适合自己就可以选择哪一种方式,两种方法没有高下之分。
5.编写spider爬取伯乐在线的所有文章
-
1.逻辑梳理
- 1.获取文章列表页中的文章url,并交给scrapy下载后并进行解析
- 2.获取下一页的URL并交给scrapy进行下载,下载完成后交给parse
-
2.获取文章列表页中的文章url,并交给scrapy下载后并进行解析
-
1. 伪类选择器 ::attr(属性):提取属性的值
-
2.提取文章列表页的所有文章url
- 1. 更改开始url 为所有文章列表: start_urls = [‘http://blog.jobbole.com/all-posts/‘]
- 2. 获取文章列表中的所有url,返回结果是一个数组 :post_urls = response.css(‘#archive .floated-thumb .post-thumb a::attr(href)‘).extract()
- 3. 通过for循环,遍历得到数组中的每个URL,以便做后续处理 :for post_url in post_urls:
-
3.把提取到的url交给scrapy下载并进行解析
- 1.导入scrapy的Request方法:from scrapy.http import Request
- 2.把通过xpath或css选择器提取字段的代码封装成一个parse_detail方法:(我这里使用CSS选择器)
-
def parse_detail(self, response): #提取文章具体字段 #使用CSS选择器 #标题 title = response.css(‘.entry-header h1::text‘).extract()[0] #发表日期 create_date = response.css(‘.entry-meta-hide-on-mobile ::text‘).extract()[0].strip().replace("·","").strip() #点赞数 praise_nums = response.css(‘.vote-post-up h10::text‘).extract()[0] match_re = re.match(‘.*?(d+).*‘, praise_nums) if match_re: praise_nums = int(match_re.group(1)) else: praise_nums = 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(‘.entry‘).extract()[0] #标签 tag_list = response.css(‘.entry-meta-hide-on-mobile a ::text‘).extract() tag = ",".join(tag_list) pass
-
- 3. 在for 循环内,调用Request(url,callback)
- url 赋值为 提取到的文章URL
- callback 赋值 刚封装的parse_detail方法,用来解析具体文章url中的内容
- 由于parse_detail 在 jobbole类内,使用 self.parse_detail 调用,不需要传参数
- URL优化:由于传入的post_url 可能为 /107275/ ,所以需要和主域名http://blog.jobbole.com 拼接成完整的URL
- 方法:导入 from urllib import parse
- 修改url : parse.urljoin(response,post_url) ,会自动提取response的主域名和post_url的子域名进行拼接
- 详细代码为:Request(url = parse.urljoin(response.url,post_url),callback = self.parse_detail)
- 4. 交给scrapy进行下载:
- 使用关键字yield ,详细代码:yield Request(url = parse.urljoin(response.url,post_url),callback = self.parse_detail)
-
-
3.提取下一页url,交给scrapy进行下载
- 1. 用两个(多个)类指定同一个类的CSS选择性方法:去除空格即可
- css(".next.page-numbers)
- 2. 分析“下一页”的CSS选择器,提取“下一页”URL
- 具体代码:next_url = response.css(".next.page-numbers ::attr(href)").extract_first("")
- 3.如果提取到下一页url,就加给scrapy进行处理
- 使用 if 判断是否取到 下一页url
- 取到,用yield 传递给 scrapy 进行下载
- 代码如下:
-
if next_url: yield Request(url=parse.urljoin(response.url,next_url), callback=self.parse)
-
- 1. 用两个(多个)类指定同一个类的CSS选择性方法:去除空格即可
-
4.完成全部文章爬取,jobbole.py的代码如下:
-
# -*- coding: utf-8 -*- import scrapy import re 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 """ #获取文章列表页中的文章url,并交给scrapy下载后并进行解析 post_urls = response.css(‘#archive .floated-thumb .post-thumb a::attr(href)‘).extract() for post_url in post_urls: yield Request(url = parse.urljoin(response.url, post_url),callback = self.parse_detail) #提取下一页URL,并交给scrapy进行下载 next_url = response.css(".next.page-numbers ::attr(href)").extract_first("") if next_url: yield Request(url=parse.urljoin(response.url,next_url), callback=self.parse) def parse_detail(self, response): #提取文章具体字段 #使用CSS选择器 #标题 title = response.css(‘.entry-header h1::text‘).extract()[0] #发表日期 create_date = response.css(‘.entry-meta-hide-on-mobile ::text‘).extract()[0].strip().replace("·","").strip() #点赞数 praise_nums = response.css(‘.vote-post-up h10::text‘).extract()[0] match_re = re.match(‘.*?(d+).*‘, praise_nums) if match_re: praise_nums = int(match_re.group(1)) else: praise_nums = 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(‘.entry‘).extract()[0] #标签 tag_list = response.css(‘.entry-meta-hide-on-mobile a ::text‘).extract() tag = ",".join(tag_list) # #使用xpath # #标题 # title = response.xpath(‘//div[@class="entry-header"]/h1/text()‘).extract()[0] # #发表日期 # create_date = response.xpath(‘//div[@class="entry-meta"]/p/text()‘).extract()[0].strip().replace("·","").strip() # #点赞数 # praise_nums = response.xpath(‘//div[@class="post-adds"]/span[1]/h10/text()‘).extract()[0] # match_re = re.match(‘.*?(d+).*‘, praise_nums) # if match_re: # praise_nums = int(match_re.group(1)) # else: # praise_nums = 0 # #收藏数 # fav_nums = response.xpath(‘//div[@class="post-adds"]/span[2]/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(‘//div[@class="post-adds"]/a/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] # #标签 tag_list = [elem for elem in tag_list ] #在tag_list中有不是标签的项时,过滤使用 # tag_list = response.xpath(‘//p[@class="entry-meta-hide-on-mobile"]/a/text()‘).extract() # tag = ",".join(tag_list)
-
6.items 设计
-
1.数据爬取的主要目的
- 从非结构性的数据源提取到结构性的数据
-
2.提取数据后,如何把数据返回?
- 最简单的方式:将提取到的字段分别放到字典当中,然后通过字典返回给scrapy。
- 缺点:字典虽然好用,但是缺少一些结构性的东西,比如:容易打错字段的名字。
- 解决:为了将这些东西进行完整的格式化,scrapy提供了item类。
- 最简单的方式:将提取到的字段分别放到字典当中,然后通过字典返回给scrapy。
-
3.item简介
- 作用:
- 类似字典,但是比字典的功能齐全
- 可以让我们自己指定字段。
- 运行流程:当我们对item进行实例化,在spider中做yield时,当scrapy发现这是一个item实例,就会直接把这个item路由到pipelines中。
- 好处:在pipelines中集中处理数据的保存、去重等等操作。
- 作用:
-
4.补充:爬取所有文章列表页面中,每篇文章的封面
- 修改爬取本页面所有文章url的方式:
- 理由:由于需要获取文章封面的url,所以改成先获取文章节点,在通过for循环,分布提取文章url、文章封面url。
- 添加for循环下的 Request()方法的参数: meta={"front_image_url":image_url}
- 其中image_url为提取到的封面url,front_image_url为自定义的名称。
- 通过yield Request() 方式,把image_url 传递到 具体解析文章的方法中并保存。
- 在具体解析文章的方法parse_detail()中,保存image_url。
- 代码: front_image_url = response.meta.get("front_image_url","")
- 其中,传递过来的meta是字典类型
- 使用get方法,第1个参数是传递过来的图片url的字典名称,第2个参数""是默认参数空,避免取空封面url时抛异常。
- 修改爬取本页面所有文章url的方式:
-
5.添加item定义
- 1.在items.py文件中,定义JobboleArticleItem类,需要基础scrapy的item。
- 2.在类中,具体定义爬虫的字段,指定为scrapy.Field,表示传递任何参数都可以,一共11个字段
- 添加原来就有的字段:标题,发布日期,点赞数,收藏数,评论数,标签,正文
- 添加封面url字段
- 如果封面图已经在本地保存,添加 本地存储的封面地址字段
- 添加博客url字段
- 因为现在的博客url字段时变长的,使用md5等压缩算法,添加 博客url定长字段。
- 3.现在item.py的代码如下:注意,Field() : Field 后面 必须加 (),否则无法赋值
-
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class ArticlespiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass class JobboleArticleItem(scrapy.Item): url = scrapy.Field() #博客url url_object_id =scrapy.Field() #url经过MD5等压缩成固定长度 front_image_url = scrapy.Field() # 封面url front_image_path = scrapy.Field() # 本地存储的封面路径 title = scrapy.Field() create_date = scrapy.Field() praise_nums= scrapy.Field() fav_nums= scrapy.Field() comment_nums= scrapy.Field() content= scrapy.Field() tag= scrapy.Field()
-
-
6.把爬取的值填充到item项中
- 1.在jobbole.py中导入刚定义的JobboleArticleItem类
- 2.在parse_detail()中实例化JobboleArticleItem对象: article_item = JobboleArticleItem()
- 3.以字典方式将解析得到的值传递给item,如:article_item["title"] = title
- 4.全部赋值后,用 yield item 把 item 传递给 pipeline : yield article_item
- 完整代码如下:
article_item = JobboleArticleItem() article_item["title"] = title article_item["create_date"] = create_date article_item["praise_nums"] = praise_nums article_item["fav_nums"] = fav_nums article_item["comment_nums"] = comment_nums article_item["content"] = content article_item["tags"] = tags article_item["url"] = response.url article_item["front_image_url"] = front_image_url # article_item["url_object_id"] = # article_item["front_image_path"] = yield article_item
- 完整代码如下:
- 5.为了使第4步生效,在setting中,对这三行去注释:
-
7.完善item
-
1. 下载图片到本地
- 1.新建images文件夹,存放图片,位置如图:
- 2. 修改setting:
-
ITEM_PIPELINES = { ‘ArticleSpider.pipelines.ArticlespiderPipeline‘: 300, ‘scrapy.pipelines.images.ImagesPipeline‘:1, } IMAGES_URLS_FIELD = "front_image_url" project_dir = os.path.abspath(os.path.dirname(__file__)) IMAGES_STORE = os.path.join(project_dir,"images")
- 解释:‘scrapy.pipelines.images.ImagesPipeline‘:1 :开启图片下载的方法,数字代表优先级,越低优先级越高
-
IMAGES_URLS_FIELD:指向保存封面地址的变量
-
IMAGES_STORE: 指定下载图片的目录
- 考虑程序会运行在远程,使用相对路径存放图片
- os.path.abspath():返回当前文件的绝对路径
- os.path.dirname(): 返回当前文件的文件名
- os.path.join():连接文件和文件名
-
- 3.虚拟环境安装pillow: (ArticleSpider_Env) E:myGitArticleSpider>pip install pillow
- 4.在运行时,报错,把图片地址转换成数组: article_item["front_image_url"] = [front_image_url]
- 5.点击”运行“,在images文件夹中生成了图片
- 1.新建images文件夹,存放图片,位置如图:
-
2. 完善封面本地存储路径 front_image_path
- 1.在 pipelines.py 中,添加语句: from scrapy.pipelines.images import ImagesPipeline
- 2.在 pipelines.py 中,添加 class ArticleImagePipeline(ImagesPipeline): ,该类继承 ImagesPipeline方法
- 3.在添加的类中,重载 def item_completed(self, results, item, info): 方法
- 4.在 setting.py中,添加 ArticleImagePipeline ,代码为: ‘ArticleSpider.pipelines.ArticleImagePipeline‘: 1,
- 设置的优先级高,先处理该方法,赋值item 的图片本地路径,在处理整个item.
- 5.pipeline.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 class ArticlespiderPipeline(object): def process_item(self, item, spider): return item class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): for ok , value in results: images_file_path = value["path"] item["front_image_path"] = images_file_path return item
- 注意,最后要返回 item.
-
- 6.setting.py的具体代码如下:
-
# Configure item pipelines # See https://doc.scrapy.org/en/latest/topics/item-pipeline.html ITEM_PIPELINES = { ‘ArticleSpider.pipelines.ArticlespiderPipeline‘: 300, # ‘scrapy.pipelines.images.ImagesPipeline‘:1, ‘ArticleSpider.pipelines.ArticleImagePipeline‘: 1, } IMAGES_URLS_FIELD = "front_image_url" project_dir = os.path.abspath(os.path.dirname(__file__)) IMAGES_STORE = os.path.join(project_dir,"images")
-
-
3.完善博客url MD5 压缩定长地址 url_object_id
- 1. 进行utils 项目文件夹,并在其中新建python 文件 common.py
- 2. 在 common.py 中
- 1.导入hashlib ,import hashlib
- 2.编写 def get_md5(url): ,将 url 经过md5方法压缩
- 3.common.py代码如下:
-
# -*- coding : utf-8 -*- __author__ = "lxr" import hashlib def get_md5(url): # 判断 url 是否为 unicode ,是,则转换成 utf-8 if isinstance(url,str): # str代表unicode url = url.encode("utf-8") m = hashlib.md5() m.update(url) return m.hexdigest() # 返回 抽取的摘要 if __name__ == "__main__" : print(get_md5("http://jobbole.com".encode("utf-8")))
- 3. 在jobbole.py 中
- 1.导入 get_md5() : from ArticleSpider.utils.common import get_md5
- 2.使用get_md5(),为 item 项中的url md5 压缩值 进行赋值:
- article_item["url_object_id"] = get_md5(response.url) # url经过md5压缩
- 1. 进行utils 项目文件夹,并在其中新建python 文件 common.py
-
7.数据表设计和保存item到json文件
-
1.保存item到json文件
-
1. jobbole.py 修改 def parse_detail(self, response):,添加日期的格式转换(字符串——>日期), import datetime
-
try: create_date = datetime.datetime.strptime(create_date, "%Y/%m/%d").date() #将字符串格式的日期转换成日期格式 except Exception as e: create_date = datetime.datetime.now().date() article_item["create_date"] = create_date
-
-
2. pipeline.py 添加 item信息存储json的方法
-
# -*- 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 scrapy.exporters import JsonItemExporter import codecs import json class ArticlespiderPipeline(object): def process_item(self, item, spider): return item class JsonWithEncodingPipeline(object): # 自定义json文件的导出 def __init__(self): self.file = codecs.open(‘article.json‘,‘w‘,encoding=‘utf-8‘) # 写方式打开json文件 def process_item(self, item, spider): lines = json.dumps(dict(item), ensure_ascii=False) + " " # item强制转为字典,再解析为json串 ; arcii设置false self.file.write(lines) # json串写入文件 return item def spider_closed(self,spider): self.file.close() #关闭文件 class JsonExporterPipeline(object): # 调用scrapy 提供的 json exporter ,导出json文件 def __init__(self): self.file = open(‘articleexport.json‘, ‘wb‘) # b二进制 self.exporter = JsonItemExporter(self.file, encoding=‘utf-8‘, ensure_ascii=False) self.exporter.start_exporting() # 开始导出json文件 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 class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): for ok , value in results: images_file_path = value["path"] item["front_image_path"] = images_file_path return item
-
-
-
-
3.setting.py 修改:
-
ITEM_PIPELINES = { ‘ArticleSpider.pipelines.JsonExporterPipeline‘: 2, # ‘scrapy.pipelines.images.ImagesPipeline‘:1, ‘ArticleSpider.pipelines.ArticleImagePipeline‘: 1, }
-
-
-
2.数据库表设计
-
1. 新建article_spider数据库:
-
2.新建表 article:
-
8.通过pipeline保存数据到mysql
-
1.虚拟环境安装mysql驱动
- (ArticleSpider_Env) C:UsersGoFree>pip install mysqlclient
-
2.使用Twisted框架,实现mysql的异步存取,pipeline.py添加如下代码:
-
import MySQLdb.cursors from twisted.enterprise import adbapi class MysqlTwistedPipeline(object): def __init__(self, dbpool): self.dbpool = dbpool @classmethod def from_settings(cls, settings): # 将setting.py中的值导入 dbparms = dict( host = settings["MYSQL_HOST"], db = settings["MYSQL_DBNAME"], user = settings["MYSQL_USER"], passwd = settings["MYSQL_PASSWORD"], charset = "utf8", use_unicode = True, cursorclass = MySQLdb.cursors.DictCursor, ) 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) # 处理异常 def handle_error(self, failure): # 处理异步插入的异常 print(failure) def do_insert(self, cursor, item): # 执行具体操作 insert_sql = """ insert into jobbole_article(title, create_date, url, url_object_id, front_image_url, front_image_path, comment_nums, fav_nums, praise_nums, tags, content) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ cursor.execute(insert_sql, (item["title"], item["create_date"], item["url"], item["url_object_id"], item["front_image_url"], item["front_image_path"], item["comment_nums"], item["fav_nums"],item["praise_nums"], item["tags"], item["content"]))
-
-
3. 修改setting.py配置:
-
ITEM_PIPELINES = { # ‘ArticleSpider.pipelines.JsonExporterPipeline‘: 2, # ‘scrapy.pipelines.images.ImagesPipeline‘:1, ‘ArticleSpider.pipelines.ArticleImagePipeline‘: 1, ‘ArticleSpider.pipelines.MysqlTwistedPipeline‘: 2, } # 添加mysql的连接参数 MYSQL_HOST="localhost" MYSQL_DBNAME="article_spider" MYSQL_USER = "root" MYSQL_PASSWORD = "root"
-
9.scrapy item loader机制
-
1. 使用item loader 改写 jobbole.py 中 关于 item 的赋值部分
- jobbole.py代码如下:
-
# -*- coding: utf-8 -*- import scrapy import re from scrapy.http import Request from urllib import parse from ArticleSpider.items import JobboleArticleItem,ArticleItemLoader from ArticleSpider.utils.common import get_md5 import datetime from scrapy.loader import ItemLoader 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 """ #获取文章列表页中的文章url,并交给scrapy下载后并进行解析 post_nodes = response.css(‘#archive .floated-thumb .post-thumb a‘) for post_node in post_nodes: image_url = post_node.css("img::attr(src)").extract_first("") post_url = post_node.css("::attr(href)").extract_first("") yield Request(url = parse.urljoin(response.url, post_url),meta={"front_image_url":image_url},callback = self.parse_detail) #提取下一页URL,并交给scrapy进行下载 next_url = response.css(".next.page-numbers ::attr(href)").extract_first("") if next_url: yield Request(url=parse.urljoin(response.url,next_url), callback=self.parse) def parse_detail(self, response): # #提取文章具体字段 # #使用CSS选择器 # 通过item loader加载 item item_loader = ArticleItemLoader(item=JobboleArticleItem(), response=response) front_image_url = response.meta.get("front_image_url", "") # 封面 item_loader.add_css("title", ".entry-header h1::text") item_loader.add_css("create_date", ".entry-meta-hide-on-mobile ::text") item_loader.add_value("url", response.url) item_loader.add_value("url_object_id", get_md5(response.url)) 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", ".entry") item_loader.add_css("tags", ".entry-meta-hide-on-mobile a ::text") article_item = item_loader.load_item() yield article_item
-
2.将传入数据的预处理和输出放在item.py中
- item.py代码如下:
-
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy from scrapy.loader import ItemLoader from scrapy.loader.processors import MapCompose, TakeFirst, Join import datetime import re class ArticlespiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass 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): match_re = re.match(‘.*?(d+).*‘, value) if match_re: nums = int(match_re.group(1)) else: nums = 0 return nums def remove_comment_tags(value): if "评论" in value : return "" else: return value def return_value(value): return value class ArticleItemLoader(ItemLoader): # 自定义Item loader default_output_processor = TakeFirst() class JobboleArticleItem(scrapy.Item): url = scrapy.Field() #博客url url_object_id =scrapy.Field() #url经过MD5等压缩成固定长度 front_image_url = scrapy.Field( output_processor=MapCompose(return_value) ) # 封面url front_image_path = scrapy.Field() # 本地存储的封面路径 title = scrapy.Field( # input_processor = MapCompose(add_jobbole) # 传值的预处理 #也可使用lamda表达式 MapCompose(lamda x : x + "-jobbole") ) create_date = scrapy.Field( input_processor=MapCompose(date_convert), ) 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(",") )
-
3.完善front_image_url 的异常处理(没有封面),修改 pipeline.py:
- 修改 class ArticleImagePipeline(ImagesPipeline):
-
class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): if "front_image_url" in item: for ok , value in results: images_file_path = value["path"] item["front_image_path"] = images_file_path return item
以上是关于Scrapy分布式爬虫打造搜索引擎—— scrapy 爬取伯乐在线的主要内容,如果未能解决你的问题,请参考以下文章
Python分布式爬虫必学框架scrapy打造搜索引擎???
第三百五十三节,Python分布式爬虫打造搜索引擎Scrapy精讲—scrapy的暂停与重启
python分布式爬虫打造搜索引擎--------scrapy实现
python分布式爬虫打造搜索引擎--------scrapy实现