import time
def heavy_task(iterable):
for i in iterable:
time.sleep(5)
yield i
start1 = time.time()
heavy_task([1,2,3]) # 只返回一个生成器,几乎瞬间执行完毕
time.time() - start13.886222839355469e-05
但总之,使用框架仍然利大于弊,能使用框架还是尽量使用框架。
开始之前,需要介绍一个 Python 特性: yield。
生成器是一个可迭代的对象,更具体的说是一类特殊的迭代器。 生成器返回值虽然是可迭代的,但和列表不同,元素具体是什么我们是不知道的。 只有在进行迭代到某个元素时,这个元素的值才从生成器中生成。
之前已经使用过一些类似于生成器的东西:
range() 函数不是直接返回一个列表,而是一个生成器。enumerate() 函数也是返回一个生成器,每次访问时,生成一个元组,包含列表元素的序号和其本身。这个关键字类似于 return,用于“返回”一些值,只不过它并不是立刻返回,而是只有当迭代到这个元素时才返回。
import time
def heavy_task(iterable):
for i in iterable:
time.sleep(5)
yield i
start1 = time.time()
heavy_task([1,2,3]) # 只返回一个生成器,几乎瞬间执行完毕
time.time() - start13.886222839355469e-05
start2 = time.time()
[x for x in heavy_task([1,2,3])] # 迭代访问生成器中的元素,每次都花费了较长时间
time.time() - start215.009274005889893
遇到几次 yield 就会生成几个元素。
def down(n):
while n > 0:
yield n
n = n - 1
list(down(10))[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
def fibonacci(max: int=100):
a, b = 1, 1
c = a + b
while a < max:
yield a
a, b = b, c
c = a + b
list(fibonacci())[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
生成器作为一种迭代器,有一些迭代器本身的优势:
生成器还有一些额外的优势:
__iter__() 和 __next__() 两个函数。Scrapy 是一个快速的高级网页抓取和网页爬取框架,用于抓取网站并从其页面中提取结构化数据。它可用于多种用途,从数据挖掘到监控和自动化测试。
文档: https://docs.scrapy.org/en/latest/
官方示例: https://github.com/scrapy/quotesbot
在命令行中安装需要首先创建一个虚拟环境
python -m venv .venv
./.venv/bin/Activate.ps1然后安装 scrapy
pip install scrapy创建项目(project 替换为实际项目名)
scrapy startproject projectscrapy.cfg 配置文件project 项目目录
items.py 定义将爬取获得的数据项类型middlewares.py 中间件pipelines.py 数据处理流水线,可以对爬取的每一个数据项进一步加工settings.py 设置文件spiders
我们主要需要编写的是 spiders 目录下的爬虫脚本。 但爬虫之前,最好先定义一下我们这次爬取的目标。 以爬取房天下郑州市所有小区为例,我们要爬取的就是小区的列表。 因此,先在 items.py 文件中定义一个“小区类”。
import scrapy
class CommunityItem(scrapy.Item):
''' 小区
'''
name = scrapy.Field()
link = scrapy.Field()
category = scrapy.Field()
district = scrapy.Field()
region = scrapy.Field()
address = scrapy.Field()
price_psm = scrapy.Field()
parking = scrapy.Field()现在我们的目标就是获取列表,从第一页开始,地址是
https://zz.esf.fang.com/housing/
在 spiders 目录下创建一个 community.py 文件,先实例化一个爬虫对象
from scrapy import Spider
class CommunityListSpider(Spider):
name = "community_list" # 爬虫的名字,稍后会用到
start_urls = ["https://zz.esf.fang.com/housing/"] # 初始要爬取的链接框架会为我们自动发送请求,响应也解析为 HTML 格式。 但是如何从响应中提取数据,是我们需要在 parse 成员函数中实现的。
class CommunityListSpider(Spider):
def parse(self, response: Response):
pass下面我们就要分析如何获取数据。
参数 response 是解析后的页面,提供了 CSS 选择器和 Xpath 选择器。 在页面上打开开发者工具,可以看到:
div.houseList 元素显示了一个小区列表
div.list 显示每个小区的信息
a.plotTit 显示小区名span.plotFangType 显示类型dd p:last-child a 两个元素分别显示所在行政区和商圈p.priceAverage 显示房价div.fenye 显示了有那些页
a:nth-last-child(2) 是下一页根据以上信息,我们可以将第一页解析出来。 使用 response 的 css() 方法使用 CSS 选择器,get() 函数获取信息。 框架提供了一个特殊的伪类选择器 ::text 选择文本内容。
def parse(self, response: Response):
for item in response.css("div.houseList div.list"):
yield CommunityItem(
name=item.css("a.plotTit::text").get(),
link=item.css("a.plotTit::attr(href)").get(),
category=item.css("span.plotFangType::text").get(),
district=item.css("dd p:nth-of-type(2) a:nth-of-type(1)::text").get(),
region=item.css("dd p:nth-of-type(2) a:nth-of-type(2)::text").get(),
price_psm=item.css("p.priceAverage span::text").get()
)然后可以先运行一下爬虫,测试是否正确
scrapy crawl community_list如果能正确爬取数据,那么下面就要研究如何爬取下一页。 我们首先找到“下一页”这个按钮。 为了避免意外这里获取了分页器中的每个按钮,并找到了内容是“下一页”的那个。
pagers: list = response.css("div.fanye a")
next_page = [p for p in pagers if len(p.re("下一页")) > 0]然后根据链接,使用 response 的 follow() 方法,访问下一页。 callback 参数是处理响应的方法,由于下一页和第一页的处理完全相同,我们这里直接传入 parse() 函数。
if len(next_page) > 0:
sleep(1 + random.uniform(0, 1))
next_page_link = next_page[0].css("::attr(href)").get()
yield response.follow(next_page_link, callback=self.parse)为了避免爬虫太快被服务器封禁,每次爬取后暂停 1 秒多。 暂停时间设计了一个随机数,也是为了避免太过规律被服务器识破。
现在爬虫应该已经完成了。现在我们为爬虫指定一个输出文件格式,可以采用 JSON 格式。 或者 JSONL 格式,该格式每行是一个 JSON 格式表示的数据。
class CommunityListSpider(Spider):
name = "community_list"
custom_settings = {
"FEEDS": {
"community_list.json": {
"format": "jsonl",
}
}
}运行一下爬虫,就可以看到开始不断爬取了。
scrapy crawl community_list如果中间断了,我们可以根据日志找到最后一次爬取的页面,重新设置 start_url 运行爬虫,结果会被追加到输出文件中。
完整代码
from time import sleep
from random import uniform
from scrapy import Spider
from fang.items import CommunityItem
class CommunityListSpider(Spider):
name = "community_list"
custom_settings = {
"FEEDS": {
"community_list.json": {
"format": "json",
"overwrite": False
}
}
}
start_urls = ["https://zz.esf.fang.com/housing/"]
def parse(self, response):
for item in response.css("div.houseList div.list"):
yield CommunityItem(
name=item.css("a.plotTit::text").get(),
link=item.css("a.plotTit::attr(href)").get(),
category=item.css("span.plotFangType::text").get(),
district=item.css("dd p:nth-of-type(2) a:nth-of-type(1)::text").get(),
region=item.css("dd p:nth-of-type(2) a:nth-of-type(2)::text").get(),
price_psm=item.css("p.priceAverage span::text").get()
)
pagers: list = response.css("div.fanye a")
next_page = [p for p in pagers if len(p.re("下一页")) > 0]
if len(next_page) > 0:
sleep(1 + uniform(0, 1))
next_page_link = next_page[0].css("::attr(href)").get()
yield response.follow(next_page_link, callback=self.parse)ItemLoader ,用于方便地加载数据def parse(self, response):
l = ItemLoader(item=Product(), response=response)
l.add_xpath("name", '//div[@class="product_name"]')
l.add_css("stock", "p#stock")scrapy shell <url> 可以启动一个命令行,<url> 的响应将作为 response 变量,可以交互式地试验和测试爬虫脚本。