伯乐在线爬取所有文章
使用scrapy对于伯乐在线的文章内容评论数,收藏数等进行爬取。
包含从环境配置,软件安装,项目初始化,xpath,图片下载,数据保存到本地文件以及mysql(同步异步)等一系列内容。
二、伯乐在线爬取所有文章
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): |