环境:
Python3.7
scrapy2.4
官方文档(英文)scrapy2.4:https://docs.scrapy.org/en/latest/intro/overview.html
官方文档(中文)scrapy2.3:https://www.osgeo.cn/scrapy/intro/overview.html
笔记参照:"从零开始学scrapy网络爬虫" 书籍; 作者:张涛;
在我理解来看,爬虫无非就三个步骤:1、发出请求。2、解析数据。3、存储数据。
1、Scrapy 框架结构及执行流程
1.1、组件
-
引擎(Engine)
引擎犹如总指挥,是整个系统的大脑,只会其他组件协同工作。
-
调度器(Scheduler)
调度器接收引擎发过来的请求,按照先后顺序,压入队列中,同时去除重复的请求。
-
下载器(Downloader)
下载器用于下载页面内容,并将网页内容返回给爬虫(Scrapy下载器是建立在 twisted 这个高效的异步模型上的)。
-
爬虫(Spiders)
爬虫作为最核心的组件,用于从特定的网页中提取需要的信息,即所谓的实体(Item)。用户也可以从中提取出链接,让 Scrapy 继续抓取下一个页面。
-
项目管道(Item Pipelines)
项目管道负责处理爬虫从网页中抽取的实体。主要的功能是持久化实体、验证实体的有效性、清除不需要的数据等。
-
下载器中间件(Downloader Middlewares)
下载器中间件介于引擎和爬虫和之间,下载器中间件介于引擎和下载器之间的请求及响应。
-
爬虫中间件(Spider Middlewares)
爬虫中间件介于引擎和爬虫之间,主要工作是处理爬虫的响应输入和输出。
1.2、数据流
Scrapy 中定义的 Request 和 Response 类,用于保存请求和响应数据;Item 类保存解析后的结构数据。
1.3、Scrapy 执行流程
下面从数据流的角度介绍 Scrapy 的结构数据:
- 爬虫(Spider)使用 URL(要抓取页面的网址)构造一个请求(request)对象,提交给引擎(Engine)。如果请求要伪装成浏览器,或者设置代理 IP,可以先在爬虫中间件中设置,再发送给引擎。
- 引擎将请求安排给调度器,调度器根据请求的优先级确定执行顺序。
- 引擎从调度器获取即将要执行的请求。
- 引擎通过下载器中间件,将请求发送给下载器下载页面。
- 页面完成后,下载器会生成一个响应(Response)对象并将其发送给引擎。下载后的数据会保存于响应对象中。
- 引擎接收来自下载器的响应对象后,通过爬虫中间件,将其发送给爬虫(Spider)进行处理。
- 爬虫将抽取到的一条数据实体(Item)和新的请求(如下一页的链接)发送给引擎。
- 引擎将爬虫获取到 Item 发送给项目管道(Item Pipelines),项目管道实现数据持久化等功能。同时将新的请求发送给调度器,再从第 2 步开始重复执行,直到调度器中没有更多的请求,引擎关闭该网站。
2、基础
第一个网络爬虫
2.1、创建项目
scrapy startproject qidian_hot # qidian_hot 是项目名称
目录下就会生成一个文件夹,这个文件夹就是项目了。
2.2、编写代码
解析数据主要写在 spiders 文件夹里面,现在我们在文件夹里面创建一个新的 .py 文件。
# spiders/qidian_hot_spider.py
# -*- coding:utf-8 -*-
from scrapy import Request
from scrapy.spiders import Spider
class HotSalesSpider(Spider):
# 定义爬虫名称
name = 'hot' # 这个名字用于启动 Scrapy 项目的。
# 起始的 URL 列表
start_urls = ['https://www.qidian.com/rank/hotsales?style=1']
# 如果想要抓取多页数据,可这样写
# start_urls = [
# 'https://www.qidian.com/rank/hotsales?style=1',
# 'https://www.qidian.com/rank/hotsales?style=1&page=2', # page是翻页的参数
# 'https://www.qidian.com/rank/hotsales?style=1&page=3',
# 'https://www.qidian.com/rank/hotsales?style=1&page=4',
# 'https://www.qidian.com/rank/hotsales?style=1&page=5'
# ]
# 解析函数
def parse(self, response, **kwargs):
# 使用 xpath 定位到小说内容的 div 元素,保存到列表中
list_selector = response.xpath('//div[@class="book-mid-info"]')
# 依次读取每部小说的元素,从中获取名称、作者、类型和形式
for one_selector in list_selector:
# 获取小说名
name = one_selector.xpath('h4/a/text()').extract()[0]
# 获取作者
author = one_selector.xpath('p[1]/a[1]/text()').extract()[0]
# 获取类型
type = one_selector.xpath('p[1]/a[2]/text()').extract()[0]
# 获取形式(连载/完本)
form = one_selector.xpath('p[1]/span/text()').extract()[0]
# 将爬取到的一部小说保存到字典中
hot_dict = {
'name': name, # 小说名称
'author': author, # 作者
'type': type, # 类型
'form': form # 形式
}
# 使用 yield 返回字典
yield hot_dict
上面代码注意事项:
- **name:**必填项,name 是区分不同爬虫的唯一标识,因为一个 Scrapy可以有多个爬虫。不同的爬虫,name 值不能相同。
- **start_urls:**存放要爬取的的目标网页地址的列表。
- **start_requests():**爬虫启动时,引擎自动调用该方法,并且只会被调用一次,用于生成初始化的请求对象(Request)。start_requests() 方法读取 start_urls 列表中的 URL 并生成 Request 对象,发送给引擎。引擎再指挥其他组件向网站服务器发送请求,下载网页。代码中之所以没看到 start_requests() 方法,是因为我们没有重写它,直接使用了基类的功能。
- **parse():**Spider 类的核心方法。引擎将下载好的页面作为参数传递给 parse() 方法,parse() 方法执行从页面中解析数据的功能。
2.3、运行项目
这里运行项目的名称就是上面代码中 name = ‘hot’ 的 name 值。
为了使我们的蜘蛛工作,请转到项目的顶级目录并运行:
scrapy crawl hot
scrapy crawl hot -o hot.csv # 可以将内容输出成 csv文件
2.4、重写 start_requests() 方法
- 重写 start_requests() 方法可以给浏览器添加一些参数,比如这里的浏览器UA。
# spiders/qidian_hot_spider.py
# -*- coding:utf-8 -*-
from scrapy import Request
from scrapy.spiders import Spider
class HotSalesSpider(Spider):
# 定义爬虫名称
name = 'hot' # 这个名字用于启动 Scrapy 项目的。
# 设置浏览器头/设置User-Agent/请求头
qidian_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/79.0.3945.88 Safari/537.36'}
# 获取初始化 Request
def start_requests(self): # 有了这个函数就不需要有 start_urls 属性去设置 URL 了
url = 'https://www.qidian.com/rank/hotsales?style=1'
# 生成请求对象,设置 url、headers、callback
yield Request(url, headers=self.qidian_headers, callback=self.qidian_parse)
# 解析函数
def qidian_parse(self, response): # 只是换了名字,里面的内容没变。(这个名字不是非要换)
# 使用 xpath 定位到小说内容的 div 元素,保存到列表中
list_selector = response.xpath('//div[@class="book-mid-info"]')
# 依次读取每部小说的元素,从中获取名称、作者、类型和形式
for one_selector in list_selector:
# 获取小说名
name = one_selector.xpath('h4/a/text()').extract()[0]
# 获取作者
author = one_selector.xpath('p[1]/a[1]/text()').extract()[0]
# 获取类型
type = one_selector.xpath('p[1]/a[2]/text()').extract()[0]
# 获取形式(连载/完本)
form = one_selector.xpath('p[1]/span/text()').extract()[0]
# 将爬取到的一部小说保存到字典中
hot_dict = {
'name': name, # 小说名称
'author': author, # 作者
'type': type, # 类型
'form': form # 形式
}
# 使用 yield 返回字典
yield hot_dict
start_requests() 参数说明:
- **url:**请求访问的网址。
- **headers:**请求头信息。
- **callback:**回调函数。这里确定解析数据的函数为 qidian_parse()。引擎会将下载好的页面(Response 对象)发送给该方法,执行数据解析功能。
2.5、Request 对象
Request 对象用来描述一个 HTTP 请求,它通常在 Spider 中生成并由下载器执行。
# Request 对象的源码
class Request(object_ref):
def __init__(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None, flags=None, cb_kwargs=None):
**1、**以下参数用于设置网站发送的 HTTP 请求的内容:
- **url:必填。**HTTP 请求的网址。
- **method:**HTTP 请求的方法,如 GET、POST、PUT等,默认是 GET。必须是大写英文。
- **headers:**HTTP 的 请求头,类型为字典型。请求头包含的内容:
- **Accept:**浏览器端可以接收的媒体类型。
2. **Accept-Encoding:**浏览器接受的编码方式。
3. **Accept-Language:**浏览器所接受的语言种类。
4. **Connection:**表示是否需要持久连接。
5. **Cookie:**有时也可以用复数 Cookies。这个是为了辨别用户身份、进行会话跟踪而储存在用户本地的数据。
6. **Host:**指定被请求资源的 Internet 主机和端口号,通常从 URL 中提取。
7. **User-Agent:**模拟浏览器头。
- **body:**HTTP 的请求体,类型为 str 或 unicode。
- **cookies:**请求的 Cookie 值,类型为字典型或列表型,可以实现自动登录的效果。
- **encoding:**请求的编码方式,默认为 utf-8。
**2、**以下参数设置 Scrapy 框架内部的事务:
- **meta:**字典类型,用于数据的传递。它可以将数据传递给其他组件,也可以传递给 Response 对象。
- **priority:**请求的优先级,默认为 0,优先级越高的请求会优先下载。
- **dont_filter:**如果对同一个 url 多次提交相同的请求,可以使用此项来忽略重复的请求,避免重复下载,其默认值为 False。如果设置为 True,即使是重复的请求,也会强制下载,例如爬取实时变化的股票信息数据。
- **errback:**在处理请求时引发任何异常时调用的函数,包括 HTTP 返回的 404。
- **flags:**发送到请求的标志可用于日志记录或类似用途。
- **cb_kwargs:**具有任意数据的dict,将作为关键字参数传递到请求的回调。
2.6、递归下一页链接
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response, **kwargs):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').getall(),
'author': quote.css('span small::text').getall(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a::attr(href)').get() # 获取下一页链接
if next_page is not None:
next_page = response.urljoin(next_page) # 因为上面获取到的链接是相对路径的,urljoin方法可以补齐头部路径。
yield scrapy.Request(next_page, callback=self.parse) # 跳转到下一页链接,发出请求,callback=回调到这个函数。
Request() 方法只支持绝对路径的链接。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response, **kwargs):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').getall(),
'author': quote.css('span small::text').getall(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a::attr(href)').get() # 获取下一页链接
if next_page is not None:
yield response.follow(next_page, callback=self.parse) # 跳转到下一页链接,发出请求,callback=回调到这个函数。
- for 循环迭代下一页
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response, **kwargs):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').getall(),
'author': quote.css('span small::text').getall(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
url = 'http://quotes.toscrape.com'
for i in range(2, 11):
yield scrapy.Request(url + '/page/{}/'.format(i), callback=self.parse) # 跳转到下一页链接,发出请求,callback=回调到这个函数。
follow() 方法支持相对路径的链接。
# 也可以将选择器直接传递给 follow()
for href in response.css('ul.pager a::attr(href)'):
yield response.follow(href, callback=self.parse)
# 也可以直接把 a 标签给到 follow() 方法,它会自动去识别 href 属性的值。
for a in response.css('ul.pager a'):
yield response.follow(a, callback=self.parse)
# 或者更简单的写法
yield from response.follow_all(css='ul.pager a', callback=self.parse)
更多例子
import scrapy
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response, **kwargs):
author_page_links = response.css('.author + a')
yield from response.follow_all(author_page_links, self.parse_author)
pagination_links = response.css('li.next a')
yield from response.follow_all(pagination_links, self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).get(default='').strip()
yield {
'name': extract_with_css('h3.author-title::text'),
'birthdate': extract_with_css('.author-born-date::text'),
'bio': extract_with_css('.author-description::text'),
}
这个蜘蛛将从主页开始,它将跟踪所有指向作者页面的链接,调用 parse_author 它们的回调,以及与 parse 像我们以前看到的那样回拨。
这里,我们把回电传递给 response.follow_all 作为使代码更短的位置参数;它也适用于 Request .
这个 parse_author 回调定义了一个助手函数,用于从CSS查询中提取和清理数据,并用作者数据生成python dict。
这个蜘蛛展示的另一个有趣的事情是,即使同一作者引用了很多话,我们也不需要担心多次访问同一作者页面。默认情况下,scrapy过滤掉对已经访问过的URL的重复请求,避免了由于编程错误而太多地访问服务器的问题。这可以通过设置进行配置 DUPEFILTER_CLASS .
3、解析器
- getall():返回的是列表类型,返回值可能存在很多个结果。
- get():返回的是字符串类型,返回结果只有第一个。注意:如果没有匹配到,将返回 None。
Xpath
常用的一些示例:
response.xpath('//title')
>> [<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
response.xpath('//title/text()').get()
>> 'Quotes to Scrape'
获取 id 为"firstHeading"的 h1 标签下 span 中的 text。
//h1[@id="firstHeading"]/span/text()
获取 id 为"toc"的 div 标签内的无序列表(ul)中所有链接的 URL。
//div[@id="toc"]/ul//a/@href
获取 class 属性包含"ltr"以及 class 属性包含"skin-vector"的任意元素
内所有标题元素(h1)中的文本。这两个字符串可能在同一个 class 中,也可
能在不同的 class 中。
//*[contains(@class,"ltr") and contains(@class,"skin-vector")]//h1//text() # contains 匹配一个属性值中包含的字符串
选择 class 属性值为"infobox"的表格中第一张图片的 URL。
//table[@class="infobox"]//img[1]/@src
选择 class 属性以"reflist"开头的 div 标签中所有链接的 URL。
//div[starts-with(@class,"reflist")]//a/@href # starts-with 匹配一个属性开始位置的关键字
选择子元素包含文本"References"的元素之后的div元素中所有链接的URL。
//*[text()="References"]/../following-sibling::div//a
选择文本为 “百度搜索” 的 a 标签。
//a[text()='百度搜索'] # text() 匹配的是显示文本信息,此处也可以用来做定位用
常用路径表达式:
表达式 | 描述 | 实例 |
---|---|---|
nodename | 选取此节点的所有子节点 | div,p,h1 |
/ | 从根节点选取(描述绝对路径) | /html |
// | 不考虑位置,选择页面中所有子孙节点 | //div |
. | 选取当前节点(描述相对路径) | ./div |
… | 选取当前节点的父节点(描述相对路径) | h1/…/ |
@属性名 | 获取属性的值 | @href,@id |
text() | 获取元素中的文本节点 | //h1/text() |
小技巧
response.xpath('div[@class="price"]/p[2]/text()').getall()
这行代码输出的应该是列表,但是有意思的是,输出的列表里的多个值也可以使用 text() 获取文本功能。而已列表里的值可以直接输出,scrapy会自动把括号去掉,多个值用逗号隔开
估计 css 也可以这样用
CSS
response.css('title') 返回 title 标签的对象
>> [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
response.css('title::text').getall() 返回 title 文本内容
>> ['Quotes to Scrape']
response.css('title').getall() 返回 title 标签
>> ['<title>Quotes to Scrape</title>']
response.css('title::text').get() 返回 title 文本内容 ::text
>> 'Quotes to Scrape'
response.css('title::text')[0].get() 同上
>> 'Quotes to Scrape'
response.css('title.asd').getall() 返回 class 属性值为 asd 的 title 标签
例子:<div class="dnskj asd"></div>
response.css('.dnskj.asd') 如果 class 的值存在空格,那么就用 . 符号来代替空格
response.css('a span') 返回 a 标签下的 span 标签
response.css('li.next a::attr(href)') 抓取class属性值为next的li标签下的a标签的href属性值 ::attr(href)
response.css('li.next a').attrib['href'] 抓取class属性值为next的li标签下的a标签的href属性值 .attrib['href'],这个在xpath也可以使用
>> '/page/2/'
常用的CSS表达式
表达式 | 描述 | 实例 |
---|---|---|
* | 选取所有元素 | * |
E | 选取 E 元素 | div |
E1,E2 | 选取 E1和E2元素 | div,p |
E1>E2 | 选择E1的子元素E2 | div>h1 |
E1 E2 | 选取 E1 子孙中的 E2元素 | div h1 |
.class | 选取 class 属性的值为 class的元素 | .author |
#id | 选取ID属性的值为 div 的元素 | .name |
[ATTR] | 选取包含ATTR属性的元素 | [href] |
[ATTR=value] | 选取属性 ATTR 的值为 value 元素 | [class=author] |
E:nth-child(n) | 选取E元素且该元素是其父元素的第n个子元素 | p:nth-child(1) |
E:nth-last-child(n) | 选取E元素且该元素是其父元素的倒数第n和子元素 | p:nth-last-child(1) |
E::text | 获取E元素的文本 | h1::text |
解析器 + re
response.css('title::text').re(r'Quotes.*')
>> ['Quotes to Scrape']
response.css('title::text').re(r'Q\w+')
>> ['Quotes']
response.css('title::text').re(r'(\w+) to (\w+)')
>> ['Quotes', 'Scrape']
解析器注意事项
- 使用的时候尽量使用相对路径,而不是绝对路径。
- 使用的时候定位 ID 值往往是最可靠的。
4、命令行工具
创建项目
scrapy startproject myproject
创建爬虫
首先进入到项目的根目录,再执行下面命令
scrapy genspider baidu baidu.com # baidu 是文件名也是爬虫name的名,baidu.com 就是要抓取的网站了
就会在项目路径下的 spiders 文件夹下创建新的 .py 文件。
更多命令
scrapy -h 可以查看有什么命令
project命令:(只能在项目内部使用)
bench Run quick benchmark test(翻译:运行快速基准测试)
check Check spider contracts(翻译:检查卡盘合同)
list List available spiders(翻译:列出可用的蜘蛛)
edit Edit spider(翻译:编辑蜘蛛)
parse Parse URL (using its spider) and print the results(翻译:解析URL(使用其spider)并打印结果)
crawl Run a spider(翻译:跑蜘蛛)
全局命令:
fetch Fetch a URL using the Scrapy downloader(翻译:使用Scrapy下载器获取一个URL)
genspider Generate new spider using pre-defined templates(翻译:使用预定义的模板生成新的spider)
runspider Run a self-contained spider (without creating a project)(翻译:运行自包含的spider(不创建项目))
settings Get settings values(翻译:获取设置值)
shell Interactive scraping console(翻译:交互式抓取控制台)
startproject Create new project(翻译:创建新项目)
version Print Scrapy version(翻译:打印废版)
view Open URL in browser, as seen by Scrapy(翻译:在浏览器中打开URL,如Scrapy所见)
详细的这里可以看:https://www.osgeo.cn/scrapy/topics/commands.html#
5、spider 提取数据
- 具体用法在第 2、3 章已经实战过了。
spider是定义一个特定站点(或一组站点)如何被抓取的类,包括如何执行抓取(即跟踪链接)以及如何从页面中提取结构化数据(即抓取项)。换言之,spider是为特定站点(或者在某些情况下,一组站点)定义爬行和解析页面的自定义行为的地方。
对于蜘蛛来说,抓取周期是这样的:
-
首先生成对第一个URL进行爬网的初始请求,然后指定一个回调函数,该函数使用从这些请求下载的响应进行调用。
要执行的第一个请求是通过调用 start_requests()(默认)生成的方法 Request 对于中指定的URL start_urls 以及 parse 方法作为请求的回调函数。
-
在回调函数中,解析响应(网页)并返回 item objects , Request 对象,或这些对象中的一个不可重复的对象。这些请求还将包含回调(可能相同),然后由scrappy下载,然后由指定的回调处理它们的响应。
-
在回调函数中,解析页面内容,通常使用 选择器 (但您也可以使用beautifulsoup、lxml或任何您喜欢的机制)并使用解析的数据生成项。
-
最后,从spider返回的项目通常被持久化到数据库(在某些 Item Pipeline) 或者使用 Feed 导出.
scrapy.Spider 类的一些属性和方法:
类属性(待补充)
- name: 定义此蜘蛛名称的字符串。spider名称是scrappy定位(和实例化)spider的方式,因此它必须是唯一的。但是,没有什么可以阻止您实例化同一个蜘蛛的多个实例。这是最重要的蜘蛛属性,也是必需的。如果蜘蛛 爬取 一个域,通常的做法是在域后命名蜘蛛,无论有没有 TLD . 例如,一只爬行的蜘蛛 mywebsite.com 经常被称为 mywebsite .(就是name的值尽量是网址的域名,这样好分辨)
- allowed_domains:包含允许此蜘蛛爬行的域的字符串的可选列表。对于不属于此列表(或其子域)中指定的域名的URL请求,如果 OffsiteMiddleware 启用。假设您的目标URL是
https://www.example.com/1.html
然后添加'example.com'
列在名单上。 - start_urls:
官方文档有补充
6、Item 封装数据
在 spider 提取数据是字典类型的,字典使用虽然方便,但是也有它的缺陷:
- 字段名拼写容易出错,且无法检测到这些错误。
- 返回的数据类型无法确保一致性。
- 不便于将数据传给其他组件(如传递给用于数据处理的 pipeline 组件)。
定义 Item 和 Field
Item 是一个简单的容器,用于收集抓取到的数据,其提供了类似于字典(dictionary-like)的API,并具有用于声明可用字段的简单语法。
# items.py
import scrapy
class QidianHotItem(scrapy.Item):
name = scrapy.Field() # 小说名称
author = scrapy.Field() # 作者
type = scrapy.Field() # 类型
form = scrapy.Field() # 形式
将已知需要抓取的数据,用字段声明。
Field 对象用于指定每个字段的元数据,并且 Field 对象对接收的数据没有任何限制。
# quotes_spider.py
# -*- coding:utf-8 -*-
from scrapy import Request
from scrapy.spiders import Spider
from ..items import QidianHotItem # 导入模块
class HotSalesSpider(Spider):
# 定义爬虫名称
name = 'hot' # 这个名字用于启动 Scrapy 项目的。
# 设置浏览器头/设置User-Agent/请求头
qidian_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/79.0.3945.88 Safari/537.36'}
# 获取初始化 Request
def start_requests(self): # 有了这个函数就不需要有 start_urls 属性去设置 URL 了
url = 'https://www.qidian.com/rank/hotsales?style=1'
# 生成请求对象,设置 url、headers、callback
yield Request(url, headers=self.qidian_headers, callback=self.qidian_parse)
# 解析函数
def qidian_parse(self, response): # 只是换了名字,里面的内容没变。(这个名字不是非要换)
# 使用 xpath 定位到小说内容的 div 元素,保存到列表中
list_selector = response.xpath('//div[@class="book-mid-info"]')
# 依次读取每部小说的元素,从中获取名称、作者、类型和形式
for one_selector in list_selector:
# 获取小说名
name = one_selector.xpath('h4/a/text()').extract()[0]
# 获取作者
author = one_selector.xpath('p[1]/a[1]/text()').extract()[0]
# 获取类型
type = one_selector.xpath('p[1]/a[2]/text()').extract()[0]
# 获取形式(连载/完本)
form = one_selector.xpath('p[1]/span/text()').extract()[0]
item = QidianHotItem() # 实例化
item['name'] = name # 小说名称
item['author']= author # 作者
item['type']= type # 类型
item['form']= form # 形式
# 使用 yield 返回字典
yield item
- 首先导入 quotes_spider.items 下的 QidianHotItem 模块。
- 生成 QidianHotItem 的对象 item,用于保存一部小说信息。
- 将从页面中提取到的各个字段赋予给 item。赋值方法跟 Python 字典一样,使用 key-value 的形式。key 要在于 QidianHotItem 中定义的名称一致,否则会报错,value 为各字段。Item 复制了标准的字典的 API,因此可以按照字典的形式赋值。
使用 ItemLoader 填充容器
当项目很大、提取的字段数以百计时,数据的提取规则也会越来越多,再加上还要对提取到的数据做转换处理,代码就会变得庞大,维护起来十分困难。
为了解决这个问题,scrapy 提供了项目加载器(ItemLoader)这样一个填充容器。通过填充容器,可以配置 Item 中各个字段的提取规则,并通过函数分析原始数据,最后对Item 字段赋值,使用起来非常便捷。
Item 和 ItemLoader 的区别在于:
- Item 提供了保存抓取到的数据的容器,需要手动将数据保存于容器中。
- ItemLoader 提供的是填充容器的机制。
# quotes_spider.py
# -*- coding:utf-8 -*-
from scrapy import Request
from scrapy.spiders import Spider
from scrapy.loader import ItemLoader # 导入 ItemLoader 类
from ..items import QidianHotItem
class HotSalesSpider(Spider):
# 定义爬虫名称
name = 'hot' # 这个名字用于启动 Scrapy 项目的。
# 设置浏览器头/设置User-Agent/请求头
qidian_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/79.0.3945.88 Safari/537.36'}
# 获取初始化 Request
def start_requests(self): # 有了这个函数就不需要有 start_urls 属性去设置 URL 了
url = 'https://www.qidian.com/rank/hotsales?style=1'
# 生成请求对象,设置 url、headers、callback
yield Request(url, headers=self.qidian_headers, callback=self.qidian_parse)
# 解析函数
def qidian_parse(self, response): # 只是换了名字,里面的内容没变。(这个名字不是非要换)
# 使用 xpath 定位到小说内容的 div 元素,保存到列表中
list_selector = response.xpath('//div[@class="book-mid-info"]')
# 依次读取每部小说的元素,从中获取名称、作者、类型和形式
for one_selector in list_selector:
# 生成 ItemLoader 实例
# tiem 接收 QidianHotItem 实例,selector 接收一个选择器
novel = ItemLoader(item=QidianHotItem(), selector=one_selector)
# 使用 xpath 选择器
novel.add_xpath("name", "h4/a/text()")
novel.add_xpath("author", "p[1]/a[1]/text()")
novel.add_xpath("type", "p[1]/a[2]/text()")
# 使用 css 选择器
novel.add_css("form", ".author span::text")
# 将提取好的数据 load 出来,并使用 yield 返回
yield novel.load_item()
ItemLoader.add 填充数据有三种:
- add_xpath():用 xpath 选择器提取数据。
- add_css():用 css 选择器提取数据。
- add_value():直接传值。
add_value()使用方法:
novel.add_value('form', "连载")
当提取到的数据被填充到 ItemLoader 后,还需要调用 load_item() 方法给 Item 对象赋值。
Item 和 ItemLoader 的区别在于
- Item 提供了保存抓取到的数据的容器,需要手动将数据保存于容器中。
- ItemLoader 提供的是填充容器的机制。
进一步的清洗数据
-
使用 ItemLoader 提取的数据,也是保存于列表中的,以前可以通过 extract_first() 或者 extract() 获取列表中的数据,但是在 ItemLoader 中如何实现?看一下 ItemLoader 生成的数据格式:
{'author': ['防守打'], 'form': ['连载'], 'name': ['修炼'], 'type': ['玄幻']}
-
很多时候我们还需要做额外的数据处理,比如:去除空格、提取数字或格式化数据等。这些又如何处理?
解决的办法就是,使用输入处理器(input_processor)和输出处理器(ouput_processor)对数据的输入和输出进行解析。
例子:
# items.py
import scrapy
from scrapy.loader.processors import TakeFirst
# 定义一个转换小说形式的函数
def form_convert(form):
if form[0] == '连载':
return 'LZ'
else:
return 'WJ'
class QidianHotItem(scrapy.Item):
name = scrapy.Field(output_processor=TakeFirst()) # 小说名称
author = scrapy.Field(output_processor=TakeFirst()) # 作者
type = scrapy.Field(output_processor=TakeFirst()) # 类型
form = scrapy.Field(input_processor=form_convert, output_processor=TakeFirst()) # 形式
# input_processor、output_processor输入和输出处理器,
# TakeFirst()函数是scrapy内置的处理器,用来获取集合中第一个非空值。
执行流程:
form 为小说形式的字段,scrapy.Field() 的参数 input_processor 绑定了函数 form_convert()。但 ItemLoader 通过选择器(xpath或css)提取某字段数据后,就会将其发送给输入处理器进行处理,然后将处理完的数据发送给输出处理器做最后一次处理。最后调用 load_item() 函数将数据填充进 ItemLoader,并得到填充后的 Item 对象。
- 提取数据
- 输入处理器(input_processor )处理数据
- 输出处理器(output_processor)处理数据
- 调用load_item() 填充 ItemLoader
- 得到填充后的 Item 对象
然后我们来运行一下这个案例。就可以发现 form 字段的值(就是 form 字段那一栏)已经被替换了,原本的连载替换成了 LZ,完结的替换成了 WJ。
7、使用 Pipeline 处理数据
有时候可能还需要对数据进行处理,例如过滤掉重复数据、验证数据的有效性,以及将数据存入数据库等。接下来学习如何处理,
scrapy 的 Item Pipeline(项目管道)是用于处理数据的组件。
Item Pipeline 介绍
**当 Spider 将收集到的数据封装为 Item 后,将会被传递到 Item Pipeline(项目管道)组件中等待进一步处理。scrapy 犹如一个爬虫流水线,Item Pipeline 是流水线的最后一道工序,但它是可选的,默认关闭,使用时需要将它激活。**如果需要,可以定义多个 Item Pipeline 组件,数据会依次访问每个组件,执行相应的数据处理功能。
Item Pipeline 典型的应用:
- 清理数据。
- 验证数据的有效性。
- 查重并丢弃。
- 将数据按照自定义的格式存储到文件中。
- 将数据保存到数据库中。
Item Pipeline 方法
Item Pipeline 中,除了必须要有的 process_item() 方法外,还有3个比较常用的方法,可根据需求选择实现。
- **open_spider(self, spider)方法:**当 Spider 开启时(爬取数据之前),该方法被调用,参数 spider 为被开启的 Spider。该方法通常用于在数据之前完成某些初始化工作,如打开文件和连接数据库等。
- **close_spider(self, spider)方法:**当 Spider 被关闭时(所有数据被爬取完毕),该方法被调用,参数 spider 为被关闭的 Spider。该方法通常用于在数据处理完毕后,完成某些清理工作,如关闭文件和关闭数据库等。
- **from_crawler(cls, crawler)方法:**该方法被调用时,会创建一个新的 Item Pipeline 对象,参数 crawler 为使用当前管道的项目。该方法通常用于提供对 Scrapy 核心组件的访问,如访问项目配置设置文件 settings.py。
编写 Item Pipeline
编写自己的 Item Pipeline 组件其实很简单,它只是一个实现了几个简单方法的 Python 类。当建立一个项目后,这个类就已经自动创建了。
例子:将小说的形式以首字母展现。
# pipeline.py
class QidianHotPipeline:
def process_item(self, item, spider):
# 判断小说形式是连载还是完结
if item['form'] == '连载':
item['form'] = 'LZ'
else:
item['form'] = 'WJ'
return item
QidianHotPipeline 是自动生成的 Item Pipeline 类,它无须继承特定的类,只需要实现某些特定的方法,如 process_item()、open_item() 和 close_spider()。注意:方法名不可改变。
process_item() 方法是 Item Pipeline 类的核心方法,必须要实现,用于处理 Spider 爬取到得每一条数据(Item)。他有两个参数:
- item:待处理的 Item 对象。
- spider:爬取此数据的 Spider 对象。
方法的返回值是处理后的 Item 对象,返回的数据会传递给下一级的 Item Pipeline(如果有的情况下)继续处理。
启用 Item Pipeline
编写完了 Item Pipeline ,就需要去启用它了,因为它默认是关闭的。
启动它也很简单,只需要在 settings.py 里将代码的注释打开就好。
# settings.py
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'qidian_hot.pipelines.QidianHotPipeline': 300,
}
ITEM_PIPELINES 是一个字典,将想要启用的 Item Pipeline 添加到这个字典中。其中,键是 Item Pipeline 类的导入路径,值是一个整数值。如果启用多个 Item Pipeline,这些值就决定了它们运行的顺序,数值越小,优先级越高。
多个 Item Pipeline
场景:如果有这样一个需求,同一个作者只能上榜一部作品,而爬取到的数据中可能有多同一作者的作品。因此可以实现一个去重处理的 Item Pipeline,将重复数据过滤掉。
在 pipeline.py 中再定义一个去重的 Item Pipeline 类的 DuplicatesPipeline:
# pipelines.py
from scrapy.exceptions import DropItem
class DuplicatesPipeline:
def __init__(self):
# 定义一个保存作者姓名的集合
self.author_set = set()
def process_item(self, item, spider):
if item['author'] in self.author_set:
# 抛弃重复的 Item 项
raise DropItem('查找到重复姓名的项目:%s' % item)
else:
self.author_set.add(item['author'])
return item
# settings.py
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'qidian_hot.pipelines.DuplicatesPipeline': 100,
'qidian_hot.pipelines.QidianHotPipeline': 300,
}
在构造函数 __init__() 中定义一个保存作者姓名的集合 author_set()。
在 process_item() 方法中,判断 item 中的 author 字段是否已经存在于集合 author_set 中,如果不存在,则将 item 中的 author 字段存入 author_set 集合中;如果存在,就是重复数据,使用 raise 抛出一个 DropItem 异常,该 Item 就会被抛弃,不会传递给后面的 Item Pipeline 继续处理,更不会导出到文件中。
最后 settings 配置里的 ITEM_PIPELINES:
DuplicatesPipeline 设置的值比 QidianHotPipeline 小,因此优先执行 DuplicatesPipeline ,过滤掉重复项,再将非重复项传递 QidianHotPipeline 继续处理。
- 得出的一点心得:Item 应该就是数据了。数据是先经过 items.py 文件,然后再到 pipelines.py。
保存为其他类型文件
用命令行将数据保存到本地的格式有 CSV、JSON、XML等。但是如果有特殊要求,比如这里要求保存为 TXT 文件,并且字段之间使用其他间隔符(例如分号),这样就需要自己实现了。
下面通过 Item Pipeline 实现上面的那个特殊要求:
# pipelines.py
class SaveToTextPipeline:
file_name = "hot.txt" # 文件名称
file = None # 文件对象
# Spider 开启时,执行打开文件操作
def open_spider(self, spider):
# 以追加形式打开文件
self.file = open(self.file_name, 'a', encoding='utf-8')
# 数据处理
def process_item(self, item, spider):
# 获取 item 中的各个字段,将其连接成一个字符串
# 字段之间用分号隔开
# 字符串末尾要有换行符 \n
novel_str = item['name'] + ";" + item['author'] + ";" + \
item['type'] + ";" + item['form'] + "\n"
# 将字符串写入文件中
self.file.write(novel_str)
return item
# Spider 关闭时,执行关闭文件操作
def close_spider(self, spider):
# 关闭文件
self.file.close()
分析 SaveToTextPipeline 类的几个方法:
- 属性 file_name 定义文件名称,属性 file 定义文件对象,便于操作文件。
- open_spider() 方法实现文件的打开操作,在数据处理前执行一次。
- close_spider() 方法实现文件的关闭操作,在数据处理后执行一次。
- process_item() 方法实现数据的写入操作。首先,获取 item 中的所有字段,将它们连成字符串,字段之间用分号间隔,而且字符串末尾要加上换行符(\n),实现一行显示一条数据;然后,使用 Python 的 write() 函数将数据写入文件中。
然后去 settings.py 里添加这个 Pipeline。
# settings.py
ITEM_PIPELINES = {
'qidian_hot.pipelines.DuplicatesPipeline': 100,
'qidian_hot.pipelines.QidianHotPipeline': 300,
'qidian_hot.pipelines.SaveToTextPipeline': 400, # new
}
运行命令:scrapy crawl hot
就可以输出文件了
统一将配置信息都写在 settings 里
为了便于管理,scrapy 中将各种配置信息放在配置文件 settings.py 中。上面的文件夹名也可以转移到 settings.py 中配置,下面来看看修改方法:
# settings.py
FILE_NAME = 'hot.txt'
# pipelines.py
class SaveToTextPipeline:
# file_name = "hot.txt" # 文件名称
file = None # 文件对象
@classmethod
def from_crawler(cls, crawler):
# 获取配置文件中的 FILE_NAME 的值
# 如果获取失败,就使用默认值 'hot2.txt'
cls.file_name = crawler.settings.get('FILE_NAME', 'hot2.txt')
return cls()
# Spider 开启时,执行打开文件操作
def open_spider(self, spider):
# 以追加形式打开文件
self.file = open(self.file_name, 'a', encoding='utf-8')
# 数据处理
def process_item(self, item, spider):
# 获取 item 中的各个字段,将其连接成一个字符串
# 字段之间用分号隔开
# 字符串末尾要有换行符 \n
novel_str = item['name'] + ";" + item['author'] + ";" + \
item['type'] + ";" + item['form'] + "\n"
# 将字符串写入文件中
self.file.write(novel_str)
return item
# Spider 关闭时,执行关闭文件操作
def close_spider(self, spider):
# 关闭文件
self.file.close()
SaveToTextPipeline 类增加了 from_crawler() 方法,用于获取配置文件中的文件名。注意,该方法是一个类方法(@classmethod),scrapy 会调用该方法来创建 SaveToTextPipeline 对象,它有两个参数:
- cls:SaveToTextPipeline 对象。
- crawler:scrapy 中的核心对象,通过它可以访问配置文件(settings.py)。
from_crawler() 方法必须返回一个新生成的 SaveToTextPipeline 对象(return cls)。
8、数据库存储
MySQL 数据库
python 访问 MySQL 数据库
Python3:mysqlclient 模块。
连接 MySQL 数据库服务器
import MySQLdb
db_conn = MySQLdb.connect(
db = 'qidian', # 数据库名
host = 'localhost', # 主机
user = 'root',
password = '1234',
charset = 'utf8' # 编码格式
) # 返回的 db_conn 是 Connection 对象。
获取操作游标
db_cursor = db_conn.cursor() # 返回的是 cursor 对象,用于执行 SQL 语句。
执行 SQL 语句(增、删、改、查)
- 新增数据
sql = 'insert into hot(name, author, type, form)values("道君", "未知", "仙侠", "连载")'
db_cursor.execute(sql)
- 修改数据
sql = 'update hot set author = "跃千愁" where name = "道君"'
db_cursor.execute(sql)
- 查询表 hot 中 type 为仙侠的数据
sql = 'select * from hot where type="仙侠"'
db_cursor.execute(sql)
- 删除表中 type 为 仙侠的数据
sql = 'delete from hot where type="仙侠"'
db_cursor.execute(sql)
回滚
在数据库执行更新操作(update、insert、delete)的过程中,如果遇到错误,可以使用 collback() 方法将数据恢复到更新前的状态。这就是所谓的原子性,即要么完整地被执行,要么完全不执行。
db_conn.rollback()
需要注意:回滚操作一定要在 commit() 方法之前执行,否则就无法恢复了。
需要注意:回滚操作一定要在 commit() 方法之前执行,否则就无法恢复了。
需要注意:回滚操作一定要在 commit() 方法之前执行,否则就无法恢复了。
提交数据
调用 Connection 对象的 commit() 方法实现数据的提交。前面虽然通过 execute() 方法执行 SQL 语句完成了对数据库的更新操作,但是并未真正更新到数据库中,需要通过 commit() 方法实现对数据库的永久修改。
db_conn.commit()
关闭游标及数据库
当执行完对数据库的所有操作后,不要忘了关闭游标和数据库对象:
db_cursor.close() # 关闭游标
db_conn.close() # 关闭数据库
项目案例
以起点小说为例,将小说信息存储到 MySQL 中。
在 pipelines.py 中,定义一个 MySQLPipeline 类,用于实现对 MySQL 数据库的操作。
- 需要自己先创建数据库,然后创建数据库、数据表。
# settings.py
MYSQL_DB_NAME = "db" # 数据库名
MYSQL_HOST = "localhost"
MYSQL_USER = "root"
MYSQL_PASSWORD = "19971996"
# pipelines.py
import MySQLdb
class MySQLPipeline:
# Spider 开启时,执行连接数据库功能
def open_spider(self, spider):
# 获取 settings.py 配置信息
db_name = spider.settings.get("MYSQL_DB_NAME", "db")
host = spider.settings.get("MYSQL_HOST", "localhost") # 或许 settings 配置的信息
user = spider.settings.get("MYSQL_USER", "root")
pwd = spider.settings.get("MYSQL_PASSWORD", "19971996")
# 连接 MySQL 数据库服务器
self.db_conn = MySQLdb.connect(db=db_name, host=host, user=user,
password=pwd, charset='utf8')
# 使用 cursor() 方法获取操作游标
self.db_cursor = self.db_conn.cursor()
# 数据处理
def process_item(self, item, spider):
# 获取 item 中的各个字段,并保存成元祖
values = (item['name'], item['author'], item['type'], item['form'])
# 设计插入操作的 sql 语句
sql = 'insert into hot(name, author, type, form)values(%s, %s, %s, %s)'
# 执行 SQL 语句,实现插入功能
self.db_cursor.execute(sql, values)
return item
# Spider 关闭时,执行数据库关闭工作
def close_spider(self, spider):
# 关闭文件
self.db_conn.commit() # 提交数据
self.db_cursor.close() # 关闭游标
self.db_conn.close() # 关闭数据库
# settings.py
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'qidian_hot.pipelines.DuplicatesPipeline': 100,
'qidian_hot.pipelines.QidianHotPipeline': 300,
'qidian_hot.pipelines.MySQLPipeline': 400, # 记得设置这个,设置了才会启动。
}
MongoDB 数据库(待补充)
Python 访问 MongoDB 数据库
- pip install pymongo
连接数据库
import pymongo
# 方式一:默认的host 和port
db_client = pymongo.MonClient()
# 方式二:自定义 host 和 port 参数
db_client = pymongo.MonClient(host='localhost', port=27017)
# 方式三:使用标准的 URI 连接语法
db_client = pymongo.MonClient('mongodb://localgost:207017/')
以上三种方式都可以连接 MongoDB 数据库。
指定数据库
MongoDB 可以建立多个数据库,因此需要指定要操作的数据库:
db = db_client['qidian']
**db_client 是上一步中连接数据库服务器得到的(client)对象,db是返回的数据库对象。**当然还可以这样指定数据库:
db = db_client.qidian
上面两种方式都可以使用。注意:如果指定的数据库不存在,会自动创建一个该名称的数据库,这是 MongoDB 比较灵活的地方。
指定集合(相当于关系型数据库中的表)
MongoDB 中的一个数据库可以包含多个集合(一个数据库可以有多个集合),这跟关系型数据库中的表一个道理(一个数据库可以有多个表),所以也需要指定要操作的集合:
db_collection = db['hot']
**db 是上一步中得到的数据库对象,db_collection 是返回的集合(Collection)对象。**对集合的各种操作,都是通过这个 Collection 对象来完成的。注意:如果指定的集合不存在,在操作集合时就会自动创建一个集合。
插入文档
插入与条件匹配的单个文档。
如果想添加一个小说文档到集合 hot 中,可以先将数据存储与字典中:
novel = {
'name': '太初',
'author': '高楼大夏',
'form': '连载',
'type': '玄幻'
}
然后调用 db_collection 的 insert_one() 方法将新文档插入集合 hot 中:
result = db_collection.insert_one(novel)
print(result)
print(result.inserted_id)
在添加文档时,如果没有显式指明,该方法会为每个文档添加一个 ObjectId 类型的 _id 字段,作为文档的唯一识别号。可以使用 result 的 inserted_id 属性来获取 _id 的值。
然后运行结果就可以发现,result 是一个 InsertOneResult 类型的对象。
插入与条件匹配的所有文档。
还可以使用 insert_many() 方法,一次插入多个文档:
novel1 = {
'name': '太初1',
'author': '高楼大夏',
'form': '连载',
'type': '玄幻'
}
novel2 = {
'name': '太初2',
'author': '高楼大夏',
'form': '连载',
'type': '玄幻'
}
result = db_collection.insert_many([novel1, novel2])
print(result)
方法 insert_many() 返回一个 InsertOneResult 对象。可以调用该对象的 inserted_ids 属性来获取插入的所有文档的 _id 列表。
注意事项。
- 创建集合:如果当前的集合不存在,插入操作会自动创建集合。
- _id 字段:在 MongoDB 中,存储在集合中的每个文档都需要一个唯一的 _id 字段作为主键。如果插入的文档省略了该 _id 字段,MongoDB 驱动程序会自动为该字段生成 ObjectId 类型的 _id。
- 原子性:MongoDB 中的所有写入操作都是单个文档级别的原子操作。
查询文档
可以使用 find_one() 或 find() 方法查询集合中的文档记录。find_one() 方法返回单个文档记录,而 find() 方法则返回一个游标对象,用于查询多个文档记录。下面来了解查询文档的各种用法:
查询与条件匹配的单个文档
要查询集合 hot 中,name 为 “帝国的崛起” 的文档记录,可以将查询条件存储与字典中,并作为参数传递给 find_one() 方法:
result = db_collection.find_one({'name': '帝国的崛起'})
print(result)
该操作对应以下 SQL 语句:
SELECT * FROM hot WHERE name = "帝国的崛起" LIMIT 0, 1
运行,就会以字典的格式输出结果了。
注:find_one() 方法仅返回符合条件的第一个文档,如果没有符合的文档,则返回 None。
查询所有文档
如果要查询集合中所有的文档,可将空字典作为参数传递给 find() 方法:
cursor = db_collection.find({})
该操作对应以下 SQL 语句:
SELECT * FROM hot
查询与条件匹配的所有文档
查询集合 hot 中,所有 type 为 “历史” 的文档记录:
cursor = db_collection.find({'type': '历史'})
print(cursor) # 这个应该是列表了
for one in cursor: # 遍历所有文档
print(one)
find() 方法返回一个 Cursor 类型的对象 cursor,它可以通过 for 循环遍历所有取得的结果。
该操作对应与以下 SQL 语句:
SELECT * FROM hot WHERE type = "历史"
更新文档
Redis 数据库
其他
1、将所使用的包导出来
- 注意:会将编译器所有的包都导出来,所以在创建项目的时候,尽量使用虚拟环境。
# 安装模块
pip install pipreqs
# 切换到项目的目录里,然后执行命令,就会在项目的目录下新建一个requriements文本,里面会包含所有的包。
pip freeze > requriements.txt
# 安装文件里所有的包
pip install -r requriements.txt
# 使用清华源去下载(待测试,理论上可以执行)
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requriements.txt
2、关闭检查 robots
# settings.py
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
3、使用 py 文件去执行 scrapy
运行爬虫时每次都要打开命令行输入是不是很麻烦?
scrapy 提供了一个 cmdline 库,可以非常方便的运行爬虫程序。
首先在项目的根目录新建一个.py文件,我这里是名字起个 start.py,然后写以下代码:
# start.py
from scrapy import cmdline
cmdline.execute("scrapy crawl hot".split()) # 中间的参数就是命令了
更多例子
1、抓取小说存储到csv
import scrapy
from scrapy import Request
class QuotesSpider(scrapy.Spider):
name = "ww"
qidian_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/79.0.3945.88 Safari/537.36'}
# 获取初始化 Request
def start_requests(self): # 有了这个函数就不需要有 start_urls 属性去设置 URL 了
url = 'https://www.shuwulou.com/shu/48045.html'
# 生成请求对象,设置 url、headers、callback
yield Request(url, headers=self.qidian_headers, callback=self.parse)
def parse(self, response, **kwargs):
cssList = '.zjlist dd'
list_title = response.css(cssList)
for l in list_title:
newURL = l.css('a::attr(href)').get()
newURL = 'https://www.shuwulou.com' + newURL
title = l.css('a::text').get()
# 将需要存储的数据,通过 meta参数传递给最后一个函数,只有最后一个函数才能将所有数据存储完
yield response.follow(newURL, callback=self.context, meta={'title': title, 'newURL': newURL})
def context(self, response):
title = response.request.meta['title'] # 接收上一个函数传递过来的数据
newURL = response.request.meta['newURL'] # 接收上一个函数传递过来的数据
title1 = response.css('h1::text').get()
zuozhe = response.css('.read-titlelinke span::text').get()
yield {
'标题': title,
'链接': newURL,
'书内标题': title1,
'作者': zuozhe
}
2、爬取乐有家全国的租房信息
# item.py
import scrapy
class LeyoujiaItem(scrapy.Item):
title = scrapy.Field() # 标题
HouseType = scrapy.Field() # 户型
orientation = scrapy.Field() # 朝向
built_up_area = scrapy.Field() # 建筑面积
Inside_area = scrapy.Field() # 套内面积
renovation = scrapy.Field() # 装修程度
floor = scrapy.Field() # 楼层
HouseYear = scrapy.Field() # 房子年份
residential_quarters = scrapy.Field() # 住宅区
Lot = scrapy.Field() # 地段
other = scrapy.Field() # 其他
rent = scrapy.Field() # 租金
Payment_mode = scrapy.Field() # 付款模式;压几付几
Rental_form = scrapy.Field() # 租房形式;整组/合租
# spiders/lyj.py
import scrapy
from scrapy import Request
from leyoujia.items import LeyoujiaItem
class LyjSpider(scrapy.Spider):
name = 'lyj'
# allowed_domains = ['shenzhen.leyoujia.com/zf/']
url = 'https://guangzhou.leyoujia.com'
head = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/86.0.4240.183 Safari/537.36'}
def start_requests(self):
# 初始访问
yield Request(self.url, headers=self.head, callback=self.parse_url)
def parse_url(self, response, **kwargs):
# 获取全国的链接,每个地方的链接都不一样
chengshi = response.css('.city-opts a::attr(href)').getall()
for i in chengshi:
yield Request(i+'/zf/', headers=self.head, callback=self.parse, meta={'purl': i}) # 将地方的链接传递给 parse 函数,方便翻页使用
def parse(self, response, **kwargs):
purl = response.request.meta['purl']
homeList = response.xpath('//li[@class="item clearfix"]')
for home in homeList:
item = LeyoujiaItem()
item['title'] = home.xpath('div[@class="text"]/p[1]/a/text()').getall()
item['HouseType'] = home.xpath('div[@class="text"]/p[2]/span[1]/text()').getall()
item['orientation'] = home.xpath('div[@class="text"]/p[2]/span[2]/text()').getall()
item['built_up_area'] = home.xpath('div[@class="text"]/p[2]/span[3]/text()').getall()
item['Inside_area'] = home.xpath('div[@class="text"]/p[2]/span[4]/text()').getall()
item['renovation'] = home.xpath('div[@class="text"]/p[3]/span[1]/text()').getall()
item['floor'] = home.xpath('div[@class="text"]/p[3]/span[2]/text()').getall()
item['HouseYear'] = home.xpath('div[@class="text"]/p[3]/span[3]/text()').getall()
item['residential_quarters'] = home.xpath('div[@class="text"]/p[4]/span[1]/a/text()').getall()
item['Lot'] = home.xpath('div[@class="text"]/p[4]/span[2]/a/text()').getall()
item['other'] = home.xpath('div[@class="text"]/p[5]/span/text()').getall()
item['rent'] = home.xpath('div[@class="price"]/p[1]/span/text()').getall()
qita = home.xpath('div[@class="price"]/p[2]/text()').get().split('|')
item['Payment_mode'] = qita[1].strip()
item['Rental_form'] = qita[0].strip()
yield item
new_page = response.xpath("//a[text()='下一页 ']")
if new_page is not None:
urls = purl + new_page.attrib['href'] # 获取 href 属性
yield scrapy.Request(urls, headers=self.head, callback=self.parse, meta={'purl': purl})
# 如果需要回调到 parse 这个函数,那么 purl 属性也需要传递回给自身,否则下一页将会获取不到 purl 了