Python Scrapy爬虫实战:抓取房天下房产信息并存储到MySQL数据库
简介:本文介绍如何使用Python的Scrapy框架爬取房天下网站的房产信息,并将抓取的数据存储到MySQL数据库中。Scrapy是一个功能强大的网络爬虫框架,MySQL则用于结构化数据存储。通过本项目实战,读者可掌握爬虫项目搭建、网页解析、模拟登录、数据存储等关键技术,适用于数据分析、数据采集等场景。项目还涉及反爬应对、数据清洗和异常处理等内容,具备完整的爬虫开发流程。 
1. Scrapy框架环境搭建与项目初始化
在进行网络爬虫开发之前,搭建一个稳定且高效的Scrapy开发环境是至关重要的。本章将从Python环境的配置入手,逐步引导读者完成Scrapy框架的安装、项目初始化及基础结构解析。通过本章学习,您将掌握创建Scrapy项目的完整流程,并理解各目录文件的作用,为后续爬取房天下网站数据奠定坚实基础。
1.1 Python环境准备
在安装Scrapy之前,确保系统中已正确安装Python环境。推荐使用Python 3.8及以上版本,以获得更好的兼容性和性能支持。
操作步骤:
-
下载安装Python
访问 Python官网 ,根据操作系统选择对应版本进行安装。 -
验证Python安装
打开终端(或命令行工具),输入以下命令查看Python版本:
bash python --version # 或 python3 --version
- 安装pip包管理器 (如未内置)
使用以下命令确保pip已安装:
bash python -m ensurepip --upgrade
环境建议:
- 推荐使用虚拟环境(如
venv或conda)隔离项目依赖,提升开发与部署效率。
2. 房天下网站结构分析与页面解析
在构建一个高效的网络爬虫之前,首先需要对目标网站的结构进行深入分析。房天下作为中国知名的房地产信息平台,其网站结构复杂,页面内容动态加载频繁,对于爬虫开发者来说具有一定的挑战性。本章将从网站结构概述、页面解析技术以及内容抓取策略三个方面入手,全面解析房天下的页面结构与数据获取方式,为后续的数据提取与处理打下坚实基础。
2.1 网站结构概述
2.1.1 房天下网站页面布局与URL结构
房天下的网站主要分为多个功能模块,包括新房、二手房、租房、资讯等。每个模块的URL结构都有一定的规律,理解这些结构有助于我们构建高效的爬虫逻辑。
以新房频道为例,其主要页面结构如下:
| 页面类型 | 示例URL | 结构说明 |
|---|---|---|
| 首页 | https://newhouse.fang.com/ |
新房首页,展示各城市新房入口 |
| 城市列表页 | https://newhouse.fang.com/house-s/ |
包含全国主要城市的链接 |
| 城市新房列表页 | https://bj.newhouse.fang.com/house-s/ |
北京地区新房列表 |
| 楼盘详情页 | https://bj.newhouse.fang.com/house/1234567890/ |
具体楼盘的详细信息 |
通过观察可以发现,URL结构通常遵循以下模式:
- 域名结构:
{城市缩写}.newhouse.fang.com - 页面路径:
/house-s/表示列表页,/house/{楼盘ID}/表示详情页
这种结构化的设计便于我们通过程序自动生成目标URL,从而实现自动化的数据抓取。
2.1.2 静态页面与动态加载内容的识别
在进行网页抓取时,必须识别页面中哪些内容是静态加载的,哪些是通过JavaScript动态加载的。房天下的页面结构中,部分数据是通过AJAX请求动态获取的,这对我们直接使用Scrapy进行抓取提出了挑战。
例如,在楼盘详情页中,部分楼盘参数(如价格、面积等)是通过异步请求获取的,如下图所示的请求:
graph TD
A[用户访问详情页] --> B[浏览器发起初始HTML请求]
B --> C[服务器返回基础HTML]
C --> D[浏览器解析HTML并执行JS]
D --> E[发起AJAX请求获取楼盘参数]
E --> F[服务器返回JSON数据]
F --> G[渲染最终页面]
面对这种情况,我们可以采用以下策略:
- 直接分析AJAX请求 :使用浏览器的开发者工具(F12)查看Network面板,找到数据源接口,模拟请求获取JSON数据。
- 使用Selenium或Playwright :如果动态内容较多且难以通过接口获取,可以借助无头浏览器工具来渲染页面。
下面是一个使用Scrapy抓取静态页面的简单示例代码:
import scrapy
class FangSpider(scrapy.Spider):
name = 'fang_spider'
start_urls = [
'https://bj.newhouse.fang.com/house-s/'
]
def parse(self, response):
# 提取楼盘名称列表
for house in response.css('div.nlc_details'):
yield {
'name': house.css('div.houses_text a::text').get(),
'url': house.css('div.houses_text a::attr(href)').get()
}
# 提取下一页链接并继续爬取
next_page = response.css('a.next::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
代码逐行解析:
1. 导入scrapy模块。
2. 定义一个Spider类,设置起始URL。
3.parse方法用于解析响应内容。
4. 使用CSS选择器提取楼盘名称和链接。
5. 提取下一页链接并使用response.follow进行递归爬取。
2.2 页面解析技术
2.2.1 使用浏览器开发者工具分析HTML结构
在进行页面解析之前,熟练掌握浏览器开发者工具的使用是必不可少的。Chrome浏览器的开发者工具提供了强大的DOM查看与元素定位功能,帮助我们快速找到目标数据所在的HTML节点。
以房天下楼盘列表页为例:
- 打开页面并按下F12,进入开发者工具。
- 点击“元素”标签,使用鼠标选择器工具点击页面中的楼盘名称。
- 在DOM结构中定位到对应的
<a>标签。 - 查看其父级节点结构,确认CSS路径。
例如,楼盘名称所在的HTML结构可能如下:
<div class="nlc_details">
<div class="houses_text">
<a href="/house/1234567890/" target="_blank">北京·万科新里程</a>
</div>
</div>
对应的CSS选择器为: div.nlc_details div.houses_text a::text
2.2.2 XPath与CSS选择器的初步使用
Scrapy支持XPath和CSS选择器两种方式来提取页面数据。两者各有优势,XPath更擅长处理复杂的层级结构,而CSS选择器则更简洁易读。
CSS选择器示例:
# 提取楼盘名称
response.css('div.nlc_details div.houses_text a::text').get()
# 提取所有楼盘链接
response.css('div.nlc_details div.houses_text a::attr(href)').getall()
XPath示例:
# 提取楼盘名称
response.xpath('//div[@class="nlc_details"]//div[@class="houses_text"]/a/text()').get()
# 提取所有楼盘链接
response.xpath('//div[@class="nlc_details"]//div[@class="houses_text"]/a/@href').getall()
对比说明:
- CSS选择器语法更简洁,适合初学者。
- XPath支持更复杂的路径表达式,如//div[contains(text(), '万科')]。
- 推荐在Scrapy中优先使用CSS选择器,复杂结构再使用XPath辅助。
下面是一个结合CSS与XPath的混合使用示例:
def parse(self, response):
for item in response.css('div.nlc_details'):
name = item.css('div.houses_text a::text').get()
link = item.css('div.houses_text a::attr(href)').get()
price = item.xpath('.//div[@class="price_average"]/text()').get()
yield {
'name': name,
'link': link,
'price': price.strip() if price else None
}
代码说明:
-item.css(...)用于提取当前div下的子元素。
-.xpath(...)使用.开头表示在当前节点下继续查找,避免重复解析全文档。
2.3 页面内容的抓取策略
2.3.1 列表页与详情页的区分与提取
在房天下的网站结构中,列表页通常展示多个楼盘的基本信息,而详情页则包含详细参数。抓取策略通常为“列表页抓取链接,详情页抓取数据”。
列表页抓取示例:
def parse_list(self, response):
# 提取所有楼盘详情页链接
links = response.css('div.nlc_details div.houses_text a::attr(href)').getall()
for link in links:
yield response.follow(link, callback=self.parse_detail)
详情页抓取示例:
def parse_detail(self, response):
yield {
'name': response.css('h1.title::text').get(),
'price': response.css('span.price::text').get(),
'area': response.css('div.area span::text').get(),
'developer': response.xpath('//div[@class="developer"]/text()').get()
}
策略说明:
- 使用response.follow将链接加入请求队列。
-callback指定回调函数,分别处理列表与详情页。
- 避免在同一个函数中处理多个页面类型,提升代码可维护性。
2.3.2 多级页面跳转的处理方式
某些页面结构中存在多级跳转,例如:列表页 → 楼盘页 → 项目页 → 参数页。这种情况下,需要递归调用 response.follow ,逐级抓取。
示例代码:
def parse_list(self, response):
for link in response.css('div.list a::attr(href)').getall():
yield response.follow(link, callback=self.parse_project)
def parse_project(self, response):
detail_link = response.css('div.project a.detail::attr(href)').get()
if detail_link:
yield response.follow(detail_link, callback=self.parse_detail)
def parse_detail(self, response):
yield {
'name': response.css('h1::text').get(),
'price': response.css('span.price::text').get(),
'address': response.xpath('//div[@id="address"]/text()').get()
}
执行逻辑说明:
-parse_list处理一级页面,提取项目页链接。
-parse_project处理二级页面,提取详情页链接。
-parse_detail最终提取详细数据。
- 每次跳转都使用response.follow,保证请求队列正确调度。
状态保持与数据传递:
在多级跳转中,有时需要在页面之间传递中间数据。可以使用 meta 参数实现:
yield response.follow(detail_link, callback=self.parse_detail, meta={'project_name': project_name})
在回调函数中读取:
def parse_detail(self, response):
project_name = response.meta.get('project_name')
...
本章从网站结构分析入手,详细讲解了房天下网站的页面布局、URL结构、静态与动态内容识别,并深入介绍了使用Scrapy进行页面解析的技术手段,包括CSS选择器与XPath的使用技巧。最后,针对多级页面跳转的抓取策略进行了实践演示,为后续章节的数据提取与处理奠定了坚实基础。
3. 使用ItemLoader与XPath/CSS选择器提取数据
在数据抓取过程中,提取结构化数据是整个流程中最关键的一步。Scrapy 提供了 ItemLoader 工具来协助我们以一种更清晰、更可维护的方式组织字段提取逻辑。结合 XPath 和 CSS选择器 的强大定位能力,我们可以实现高效、精准的数据提取。
本章将深入探讨 ItemLoader 的使用方式、XPath 与 CSS 选择器的实战应用,并进一步讨论数据提取过程中的优化策略,包括字段清洗、去重、标准化等关键处理手段。
3.1 ItemLoader的使用方法
ItemLoader 是 Scrapy 提供的一个封装类,用于构建 Item 实例。它支持链式调用,使得字段提取逻辑更清晰,也便于后续的字段处理(如数据清洗、格式转换等)。
3.1.1 ItemLoader的初始化与字段映射
在 Scrapy 爬虫中,通常通过如下方式创建 ItemLoader 实例:
from scrapy.loader import ItemLoader
from myproject.items import HouseInfoItem
def parse(self, response):
loader = ItemLoader(item=HouseInfoItem(), response=response)
loader.add_xpath('name', '//h1[@class="house-title"]/text()')
loader.add_css('price', '.price span::text')
loader.add_value('url', response.url)
yield loader.load_item()
逐行解释 :
- 第1~2行:导入ItemLoader和自定义的HouseInfoItem。
- 第4行:在parse()方法中,传入response创建ItemLoader实例。
- 第5行:使用add_xpath()方法将字段name映射到 XPath 表达式。
- 第6行:使用add_css()方法提取价格字段。
- 第7行:使用add_value()方法添加静态值(如当前 URL)。
- 第8行:调用load_item()返回构建好的Item对象。
这种方式将字段提取逻辑集中在一个对象中,避免了在 Spider 中手动构造字典的混乱。
3.1.2 使用Input Processor与Output Processor处理原始数据
Scrapy 提供了输入处理器(Input Processor)和输出处理器(Output Processor)来处理字段值。默认情况下, MapCompose 作为输入处理器, TakeFirst 作为输出处理器。
例如,我们可以在 ItemLoader 中自定义字段的处理流程:
from scrapy.loader.processors import MapCompose, TakeFirst
from w3lib.html import remove_tags
class HouseInfoLoader(ItemLoader):
default_output_processor = TakeFirst()
name_in = MapCompose(remove_tags, str.strip)
price_in = MapCompose(str.strip, lambda x: x.replace('万', '0000'))
参数说明 :
-name_in:定义name字段的输入处理器,先移除 HTML 标签,再去除前后空格。
-price_in:定义price字段的输入处理器,去除空格后将“万”替换为“0000”,实现价格标准化。
-default_output_processor:设置默认输出处理器为TakeFirst,即取第一个非空值。
这种设计使得数据清洗逻辑可以模块化、复用性强,极大提升了代码的可读性与可维护性。
3.2 XPath与CSS选择器的实践应用
XPath 和 CSS 选择器是网页数据提取的两大核心工具。XPath 更适合复杂结构的节点提取,CSS 选择器则在类名、属性匹配上更具优势。
3.2.1 提取房屋名称、价格、面积等关键字段
以房天下网站为例,假设我们要提取如下字段:
| 字段名称 | 提取方式 | 示例 |
|---|---|---|
| 房屋名称 | XPath | //h1[@class="house-title"]/text() |
| 价格 | CSS | .price span::text |
| 面积 | XPath | //span[@class="area"]/text() |
| 户型 | CSS | .house-type::text |
我们可以将这些提取逻辑统一放入 ItemLoader :
loader = ItemLoader(item=HouseInfoItem(), response=response)
loader.add_xpath('name', '//h1[@class="house-title"]/text()')
loader.add_css('price', '.price span::text')
loader.add_xpath('area', '//span[@class="area"]/text()')
loader.add_css('type', '.house-type::text')
逻辑分析 :
- 使用add_xpath和add_css方法分别绑定字段与选择器。
- 字段名应与HouseInfoItem中定义的字段保持一致。
- 通过这种方式,字段提取与数据清洗可以完全解耦,便于后续维护。
3.2.2 多种选择器混合使用的技巧
在某些复杂页面中,可能需要结合 XPath 和 CSS 进行嵌套提取。例如:
loader.add_xpath('price', '//div[@class="price"]/span[@itemprop="price"]/text()')
loader.add_css('price', '.price span[itemprop="price"]::text')
分析 :
- 第一种写法使用 XPath 的嵌套路径,精确匹配price下的span。
- 第二种写法使用 CSS 属性选择器,同样可以实现目标。
- 两种方式在提取效果上等价,但在可读性和结构上略有差异。
此外,我们还可以在选择器中使用正则表达式来提取特定模式的内容:
loader.add_xpath('area', '//span[@class="area"]/text()', re='(\d+\.?\d*)㎡')
说明 :
- 使用re参数传入正则表达式,仅提取xx㎡格式中的数字部分。
- 此方式可以有效过滤无关文本,提升字段准确性。
3.3 数据提取的优化策略
在实际爬虫项目中,数据提取过程常面临字段缺失、数据重复、格式混乱等问题。因此,必须采用一定的策略进行优化。
3.3.1 数据重复与缺失的应对方式
数据重复问题
数据重复通常出现在分页抓取或跳转抓取过程中。解决方法包括:
- 使用指纹去重 :Scrapy 内置了
RFPDupeFilter,可以基于请求指纹判断是否重复。 - 自定义去重逻辑 :如基于
url或house_id建立缓存,避免重复采集。
数据缺失问题
字段缺失可能导致后续处理失败。我们可以使用如下方式处理:
- 设置默认值 :
loader.add_xpath('price', '//div[@class="price"]/text()', default='0')
- 使用默认处理器 :
class HouseInfoItem(scrapy.Item):
name = scrapy.Field(default='未知')
price = scrapy.Field(default=0)
分析 :
- 设置默认值可以防止字段为空,避免在后续 Pipeline 中抛出异常。
- 使用默认处理器时,需确保字段类型与默认值匹配,如str类型不能设置为0。
3.3.2 字段清洗与标准化处理
字段清洗是数据质量保障的关键步骤。以下是一些常见的清洗策略:
示例:价格字段标准化
def clean_price(value):
if '万' in value:
return float(value.replace('万', '')) * 10000
elif '元/㎡' in value:
return float(value.replace('元/㎡', '').strip())
else:
return float(value)
loader.add_css('price', '.price::text', MapCompose(clean_price))
说明 :
- 定义clean_price()函数统一处理不同格式的价格。
- 使用MapCompose()将清洗函数加入处理器链。
示例:面积字段处理
def clean_area(value):
return value.replace('㎡', '').strip()
loader.add_xpath('area', '//span[@class="area"]/text()', MapCompose(clean_area))
分析 :
- 移除单位“㎡”,确保面积字段为纯数字。
- 清洗后字段可直接用于数值比较或数据库存储。
示例:字段格式统一
针对地区字段,可能出现“北京市 | 朝阳区”、“北京市朝阳区”等不同格式,可统一为“北京市-朝阳区”:
def unify_location(value):
return value.replace('|', '-').replace(' ', '-')
loader.add_css('location', '.location::text', MapCompose(unify_location))
分析 :
- 统一字段格式,方便后续分析或数据库查询。
- 使用MapCompose可链式调用多个清洗函数。
补充:Mermaid 流程图说明数据提取流程
graph TD
A[开始解析页面] --> B{是否使用ItemLoader?}
B -->|是| C[创建ItemLoader实例]
C --> D[绑定字段与XPath/CSS选择器]
D --> E[应用Input Processor清洗数据]
E --> F[应用Output Processor输出字段值]
F --> G[构建Item对象]
G --> H[返回Item供Pipeline处理]
B -->|否| I[手动提取字段并构造字典]
I --> J[处理数据缺失与格式问题]
J --> H
流程说明 :
- 使用ItemLoader可以将字段提取与清洗逻辑模块化,提高代码可维护性。
- 未使用ItemLoader时,需要手动处理字段缺失、格式不统一等问题,逻辑分散,维护困难。
小结
本章详细讲解了如何利用 ItemLoader 提高字段提取的可维护性,结合 XPath 与 CSS 选择器进行高效数据提取,并通过处理器链实现字段清洗与标准化。同时,针对数据重复、缺失等问题,提出了实用的应对策略。这些内容为后续章节中数据的持久化与存储奠定了坚实基础。
下一章我们将介绍如何定义 HouseInfoItem 数据结构,以便在后续处理中统一数据格式。
4. HouseInfoItem数据结构定义
在构建Scrapy爬虫项目的过程中,数据结构的设计是至关重要的一步。Scrapy通过 Item 类来定义爬取数据的结构,类似于数据库中的表结构。良好的数据结构设计不仅有助于数据的提取与清洗,还能为后续的数据存储、分析和展示提供清晰的接口。本章将围绕 HouseInfoItem 这一具体的数据结构展开讲解,包括其定义方式、字段设计原则以及在Spider和Pipeline中的使用方式。
4.1 定义Item类
4.1.1 Scrapy中Item类的声明方式
Scrapy的 Item 类本质上是一个Python类,继承自 scrapy.Item ,并通过 scrapy.Field() 定义字段。每个字段代表一个数据属性,例如房屋名称、价格、面积等。
下面是一个基本的 HouseInfoItem 类定义示例:
import scrapy
class HouseInfoItem(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
area = scrapy.Field()
location = scrapy.Field()
publish_date = scrapy.Field()
代码逐行解读:
- 第1行:导入Scrapy模块。
- 第3行:定义
HouseInfoItem类,继承自scrapy.Item。 - 第4-8行:使用
scrapy.Field()定义字段,分别表示房屋名称、价格、面积、位置和发布时间。
字段说明:
name:房屋名称,通常为字符串类型。price:房屋价格,可能是字符串或浮点数,取决于数据源格式。area:房屋面积,单位通常为平方米,类型为字符串或浮点数。location:房屋所在位置,通常由区域和具体地址组成。publish_date:房源发布时间,类型为字符串或日期时间对象。
这种声明方式使得每个字段都有明确的语义,便于后续处理和映射到数据库表结构。
4.1.2 常见字段类型与默认值设置
虽然Scrapy的 Field() 本身不强制字段类型,但可以在字段定义时通过元数据(metadata)或后续处理(如ItemLoader的Processor)指定数据类型和默认值。
例如,我们可以为 price 字段设置默认值为 0 ,并在元数据中标注其类型为浮点型:
class HouseInfoItem(scrapy.Item):
price = scrapy.Field(
metadata={'type': 'float'},
default=0.0
)
area = scrapy.Field(
metadata={'type': 'float'},
default=0.0
)
location = scrapy.Field(default='未知')
字段说明:
metadata:用于附加字段元信息,例如类型、单位等。default:字段的默认值,当页面中未提取到该字段时使用。
通过这种方式,可以增强数据结构的健壮性和可维护性,尤其在数据清洗和存储阶段非常有用。
4.2 数据字段的逻辑梳理
4.2.1 房屋信息字段的完整性设计
为了确保抓取数据的完整性, HouseInfoItem 的字段设计应涵盖房源信息的核心要素。以下是一个较为完整的房屋信息字段清单:
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| name | string | 房屋名称或标题 |
| price | float | 房屋价格(元/平米或总价) |
| area | float | 建筑面积 |
| unit_price | float | 单价(元/平米) |
| location | string | 房屋所在位置 |
| community | string | 所属小区名称 |
| room_type | string | 户型(几室几厅) |
| floor | string | 楼层信息 |
| orientation | string | 房屋朝向 |
| publish_date | string | 发布时间 |
| source_url | string | 数据来源页面的URL |
字段设计逻辑分析:
price与unit_price区分:有些页面显示总价,有些显示单价,应分别保留。room_type与floor:属于房源的基本属性,便于用户筛选。source_url:用于数据溯源,便于后续调试或去重处理。
通过字段完整性设计,可以确保抓取数据在业务层面具备完整的描述能力。
4.2.2 数据类型与字段命名规范
字段命名应遵循清晰、简洁、一致的原则,通常使用小写字母和下划线分隔,如 publish_date 、 unit_price 。数据类型则应根据实际内容合理选择:
- 字符串类型(string) :适用于描述性字段,如名称、地址、时间等。
- 数值类型(float / int) :适用于价格、面积、楼层等可参与计算的字段。
- 布尔类型(boolean) :适用于状态类字段,如是否已售、是否置顶等。
此外,命名应避免歧义,例如:
- ❌
area:可能指建筑面积或使用面积,建议使用building_area或usable_area。 - ✅
unit_price:明确表示单价,避免混淆。
统一的命名规范不仅能提升代码可读性,也便于后续与其他系统(如数据库、前端)对接。
4.3 Item对象的使用与传递
4.3.1 在Spider中构建Item对象
在Spider中,我们通常通过解析响应(response)提取数据,并将结果填充到 Item 对象中。
以下是一个典型的Spider中构建 HouseInfoItem 的示例:
import scrapy
from tutorial.items import HouseInfoItem
class FangSpider(scrapy.Spider):
name = 'fang'
start_urls = ['https://www.fang.com/SoufunFamily.htm']
def parse(self, response):
for house in response.css('.house-list li'):
item = HouseInfoItem()
item['name'] = house.css('.title a::text').get()
item['price'] = house.css('.price::text').get()
item['area'] = house.css('.area::text').get()
item['location'] = house.css('.location::text').get()
item['publish_date'] = house.css('.time::text').get()
yield item
代码逻辑分析:
- 第1-2行:导入Scrapy模块与自定义Item类。
- 第4-7行:定义Spider类及起始URL。
- 第9行:定义
parse方法,用于处理响应。 - 第11行:使用CSS选择器遍历房源列表。
- 第12行:初始化一个
HouseInfoItem实例。 - 第13-17行:通过CSS选择器提取字段值,并赋值给Item对象。
- 第18行:使用
yield将Item对象传递给后续Pipeline处理。
这种方式使得Spider专注于数据提取,而数据处理逻辑则交由Pipeline完成,实现职责分离。
4.3.2 Pipeline中对Item的处理流程
Scrapy的Pipeline负责处理Spider提取的Item对象,通常包括数据清洗、验证、去重、持久化等操作。
下面是一个简单的Pipeline示例,用于清洗价格字段并存储到日志中:
class PriceCleanPipeline:
def process_item(self, item, spider):
if item['price']:
# 去除价格中的非数字字符
cleaned_price = ''.join(filter(str.isdigit, item['price']))
item['price'] = float(cleaned_price) if cleaned_price else 0.0
return item
代码逻辑分析:
process_item:每个Item都会经过该方法处理。- 使用
filter(str.isdigit, item['price'])过滤非数字字符。 - 将清洗后的字符串转换为浮点数,若为空则设为0.0。
另一个Pipeline示例用于将Item写入日志文件:
import logging
class LoggingPipeline:
def open_spider(self, spider):
logging.info("Spider started: %s", spider.name)
def process_item(self, item, spider):
logging.debug("Processed item: %s", dict(item))
return item
def close_spider(self, spider):
logging.info("Spider finished: %s", spider.name)
代码逻辑分析:
open_spider:Spider启动时记录日志。process_item:记录每个Item的内容。close_spider:Spider结束时记录日志。
Pipeline调用流程图:
graph TD
A[Spider yield Item] --> B[PriceCleanPipeline]
B --> C[LoggingPipeline]
C --> D[其他Pipeline或存储]
流程说明:
- Spider提取数据并生成Item对象。
- Item进入第一个Pipeline(如
PriceCleanPipeline)进行数据清洗。 - 清洗后的Item继续传递到后续Pipeline(如
LoggingPipeline)。 - 最终,Item可能被写入数据库、文件或进行其他处理。
通过Pipeline链式处理,可以实现模块化、可扩展的数据处理流程,提升代码的可维护性与灵活性。
总结
在本章中,我们详细讲解了Scrapy中 HouseInfoItem 的定义与使用方式,包括:
- 如何声明Item类并设置字段元数据与默认值;
- 如何设计完整的房屋信息字段结构,确保数据完整性;
- 如何在Spider中构建Item对象并将其传递给Pipeline;
- 如何在Pipeline中对Item进行清洗、记录等处理操作;
- 使用Mermaid流程图展示了Item对象在Spider与Pipeline之间的流转路径。
下一章将深入讲解如何使用 FormRequest 模拟登录房天下网站,突破需要登录才能访问的页面限制。
5. 使用FormRequest模拟登录房天下网站
在进行网络爬虫开发过程中,部分网站为了保护用户数据安全,设置了登录权限,只有登录后才能访问特定页面。Scrapy 提供了 FormRequest 类,能够帮助我们模拟表单提交,完成登录操作。本章将深入探讨如何通过 Scrapy 的 FormRequest 实现模拟登录房天下网站,并处理登录后的页面访问与异常处理机制。
5.1 登录机制分析
在模拟登录之前,首先需要对房天下网站的登录机制进行深入分析,包括请求方式、参数结构以及 Cookie 和 Session 的管理方式。
5.1.1 表单提交与请求方式识别
房天下网站的登录通常通过 HTML 表单实现,用户在输入用户名和密码后点击登录按钮,浏览器会将这些信息通过 POST 请求发送到服务器。
使用浏览器的开发者工具(F12),我们可以在“Network”选项卡中观察登录请求的详细信息:
- 请求地址(URL) :通常是
/login或类似的路径。 - 请求方法(Method) :POST。
- 请求头(Headers) :包含
Content-Type: application/x-www-form-urlencoded。 - 请求体(Body) :包含用户名(username)、密码(password)等字段。
例如,一个典型的登录请求体可能如下所示:
username=testuser&password=123456
5.1.2 Cookie与Session的获取与管理
登录成功后,服务器通常会返回一个 Session ID,并通过 Set-Cookie 响应头将其保存在浏览器中。后续请求时,浏览器会自动携带该 Cookie,服务器通过 Cookie 识别用户身份。
在 Scrapy 中,Spider 会自动管理 Cookie,即当使用 FormRequest 登录后,后续的请求会自动带上服务器返回的 Cookie,实现登录状态的维持。
5.2 使用FormRequest实现登录
Scrapy 提供了 FormRequest 类,可以方便地模拟 POST 表单提交,实现模拟登录。以下将详细介绍如何构建 FormRequest 请求并传递参数。
5.2.1 构建FormRequest请求
FormRequest 的基本使用方式如下:
import scrapy
class FangSpider(scrapy.Spider):
name = 'fang_login'
login_url = 'https://login.fang.com/login'
def start_requests(self):
yield scrapy.FormRequest(
url=self.login_url,
formdata={'username': 'your_username', 'password': 'your_password'},
callback=self.after_login
)
def after_login(self, response):
# 登录成功后的处理逻辑
self.log("登录成功")
# 可以继续发起其他请求,如访问个人中心页面
yield scrapy.Request(url='https://user.fang.com/dashboard', callback=self.parse_dashboard)
def parse_dashboard(self, response):
# 解析登录后的页面内容
self.log("访问个人中心页面成功")
# 提取所需数据
代码说明 :
-scrapy.FormRequest是用于模拟表单提交的类。
-formdata参数用于设置提交的表单字段,如用户名和密码。
-callback指定登录成功后调用的回调函数。
5.2.2 模拟POST请求与参数传递
在某些情况下,网站可能使用 JavaScript 动态生成表单参数,或者需要额外的 token、加密字段等。此时,我们可能需要手动构造请求体。
例如,若登录请求中包含加密字段 token ,可以通过如下方式传递:
yield scrapy.FormRequest(
url='https://login.fang.com/login',
formdata={
'username': 'your_username',
'password': 'your_password',
'token': 'encrypted_token'
},
callback=self.after_login
)
此外,还可以使用 scrapy.Request 手动构造 POST 请求:
yield scrapy.Request(
url='https://login.fang.com/login',
method='POST',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
body='username=your_username&password=your_password&token=encrypted_token',
callback=self.after_login
)
这种方式更灵活,适用于复杂表单结构。
5.3 登录后的页面访问与权限验证
登录成功后,我们需要验证是否能够正常访问受保护的页面,并确保登录状态的有效性。
5.3.1 登录成功后的页面抓取
在 after_login 回调函数中,我们可以继续发起新的请求,访问登录后才能查看的页面。例如:
def after_login(self, response):
# 检查是否登录成功
if "欢迎" in response.text:
self.log("登录成功")
yield scrapy.Request(
url='https://user.fang.com/myhouse',
callback=self.parse_myhouse
)
else:
self.log("登录失败")
在这个例子中,我们通过判断响应内容中是否包含“欢迎”关键词,来判断登录是否成功。
5.3.2 登录状态维持与异常处理
Scrapy 会自动维持 Cookie,因此在登录后发起的请求都会携带登录时获取的 Cookie。但为了提高程序的健壮性,建议添加异常处理机制:
def parse_myhouse(self, response):
if response.status == 200:
self.log("访问个人房源页面成功")
# 提取数据
else:
self.log(f"访问失败,状态码:{response.status}")
# 可以尝试重新登录
异常处理策略
- 登录失败重试机制 :可设置重试次数,或记录失败日志。
- 验证码识别 :部分网站登录时会要求输入验证码,需结合 OCR 或第三方识别服务。
- Session 过期处理 :长时间未操作可能导致 Session 过期,可通过中间件检测并重新登录。
- 请求失败重试 :使用
retry中间件,自动重试失败请求。
5.3.3 模拟登录流程图
以下是使用 FormRequest 模拟登录房天下的流程图:
graph TD
A[开始爬取] --> B[发送FormRequest登录请求]
B --> C{登录是否成功?}
C -->|是| D[进入个人中心页面]
C -->|否| E[记录失败日志/重新尝试登录]
D --> F[提取登录后页面数据]
E --> G[结束爬取]
F --> G
5.3.4 模拟登录请求参数对照表
以下是一个简单的登录请求参数对照表,用于理解表单字段的含义:
| 参数名 | 示例值 | 说明 |
|---|---|---|
| username | testuser | 用户名或手机号 |
| password | * * | 加密后的密码 |
| token | abc123xyz | 登录令牌,用于防重放攻击 |
| remember_me | on | 是否记住登录状态 |
5.3.5 登录后的数据抓取代码示例
以下是一个完整的登录后抓取个人房源信息的代码示例:
import scrapy
class FangLoginSpider(scrapy.Spider):
name = 'fang_login_spider'
login_url = 'https://login.fang.com/login'
dashboard_url = 'https://user.fang.com/myhouse'
def start_requests(self):
yield scrapy.FormRequest(
url=self.login_url,
formdata={'username': 'your_username', 'password': 'your_password'},
callback=self.after_login
)
def after_login(self, response):
if "欢迎" in response.text:
self.log("登录成功,准备访问房源页面")
yield scrapy.Request(url=self.dashboard_url, callback=self.parse_house_info)
else:
self.log("登录失败,响应内容:", response.text)
def parse_house_info(self, response):
houses = response.css('.house-list li')
for house in houses:
yield {
'title': house.css('.title::text').get(),
'price': house.css('.price::text').get(),
'area': house.css('.area::text').get(),
'link': house.css('a::attr(href)').get()
}
代码说明 :
- 使用FormRequest发起登录请求。
- 登录成功后访问个人房源页面。
- 使用 CSS 选择器提取房源标题、价格、面积和链接。
- 每个房源信息作为字典 yield 出去,供 Pipeline 处理。
5.3.6 登录逻辑优化建议
在实际项目中,为了增强爬虫的稳定性,建议对登录逻辑进行如下优化:
- 动态密码加密 :部分网站对密码进行加密传输,需在 Scrapy 中模拟加密算法。
- 验证码识别 :引入第三方 OCR 服务或手动输入验证码。
- User-Agent 随机化 :防止因固定 UA 被封禁。
- 设置下载延迟 :在
settings.py中配置DOWNLOAD_DELAY,防止频繁请求被封 IP。 - 使用代理 IP :构建代理池,轮换 IP,避免被网站封禁。
通过本章的学习,我们掌握了使用 Scrapy 的 FormRequest 实现模拟登录房天下网站的核心流程,包括登录机制分析、请求构造、参数传递、登录后页面访问与异常处理等内容。这些知识为后续章节中访问受保护资源、爬取登录后数据提供了坚实基础。
6. Scrapy爬虫数据持久化到MySQL数据库
在数据爬取完成后,如何将抓取到的信息高效、安全地存储到数据库中,是整个爬虫项目中至关重要的一个环节。Scrapy 提供了灵活的 Pipeline 机制,使得我们可以将爬取的数据写入到各种持久化存储系统中,如文件、MongoDB、MySQL 等。本章将重点讲解如何将爬取的房屋信息数据通过 Scrapy 的 Pipeline 机制,持久化到 MySQL 数据库中。
本章内容将从数据库设计、Scrapy 与 MySQL 的连接方式,到数据持久化实现的完整流程进行深入解析,并结合代码示例展示如何构建一个高效、稳定的数据写入流程。
6.1 数据库设计与建模
在将数据写入 MySQL 之前,首先需要完成数据库和数据表的设计,确保字段结构与爬虫抓取的数据字段一致,并具备良好的扩展性与查询性能。
6.1.1 MySQL数据库的创建与表结构设计
我们首先创建一个名为 house_info 的数据库,用于存储房天下网站抓取的房屋信息数据。接着设计一个名为 house_data 的数据表,用于存储房屋的关键信息。
创建数据库与数据表的 SQL 语句如下:
-- 创建数据库
CREATE DATABASE house_info;
-- 使用数据库
USE house_info;
-- 创建房屋信息表
CREATE TABLE house_data (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL COMMENT '房屋名称',
price DECIMAL(10, 2) NOT NULL COMMENT '房屋价格(万元)',
area DECIMAL(10, 2) NOT NULL COMMENT '建筑面积(平方米)',
location VARCHAR(255) COMMENT '房屋位置',
house_type VARCHAR(100) COMMENT '户型(如:三室两厅)',
publish_date DATE COMMENT '发布时间',
url VARCHAR(512) NOT NULL COMMENT '房屋详情页链接',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段说明表:
| 字段名 | 类型 | 是否为空 | 描述 |
|---|---|---|---|
| id | INT | 否 | 自增主键 |
| name | VARCHAR(255) | 否 | 房屋名称 |
| price | DECIMAL(10,2) | 否 | 房屋价格(单位:万元) |
| area | DECIMAL(10,2) | 否 | 建筑面积(单位:平方米) |
| location | VARCHAR(255) | 是 | 房屋位置 |
| house_type | VARCHAR(100) | 是 | 户型信息 |
| publish_date | DATE | 是 | 发布时间 |
| url | VARCHAR(512) | 否 | 房屋详情页链接 |
| create_time | TIMESTAMP | 否 | 记录创建时间 |
6.1.2 房屋信息字段与数据库字段的映射关系
接下来,我们需要将 Scrapy 爬虫抓取到的字段与数据库表字段进行映射。以我们定义的 HouseInfoItem 为例,其字段与数据库字段的对应关系如下:
class HouseInfoItem(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
area = scrapy.Field()
location = scrapy.Field()
house_type = scrapy.Field()
publish_date = scrapy.Field()
url = scrapy.Field()
映射关系如下:
| Scrapy Item字段 | MySQL字段名 |
|---|---|
| name | name |
| price | price |
| area | area |
| location | location |
| house_type | house_type |
| publish_date | publish_date |
| url | url |
这样我们就完成了数据库结构的设计与字段映射,接下来进入 Scrapy 与 MySQL 的连接配置。
6.2 Scrapy与MySQL的连接方式
Scrapy 本身并不直接提供与 MySQL 的连接功能,但可以通过 Python 的第三方库(如 pymysql 或 mysqlclient )来实现数据库操作。本节将介绍使用 pymysql 库实现数据库连接的方法。
6.2.1 使用pymysql库进行数据库连接
首先,确保已安装 pymysql :
pip install pymysql
接下来,我们可以在 Scrapy 的 settings.py 中配置数据库连接参数,以便在 Pipeline 中复用这些配置。
示例:在 settings.py 中添加如下配置:
MYSQL_CONFIG = {
'host': 'localhost',
'port': 3306,
'user': 'root',
'password': 'your_password',
'database': 'house_info',
'charset': 'utf8mb4',
}
示例:使用 pymysql 连接数据库的代码如下:
import pymysql
def connect_to_mysql():
conn = pymysql.connect(
host='localhost',
port=3306,
user='root',
password='your_password',
database='house_info',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
return conn
代码解析:
- host :数据库主机地址,默认为本地
localhost。 - port :MySQL 服务端口,默认为
3306。 - user/password :数据库登录用户名和密码。
- database :连接的数据库名。
- charset :设置字符集为
utf8mb4,支持中文和表情符号。 - cursorclass :指定返回字典格式的结果,便于后续处理。
6.2.2 数据插入语句的编写与优化
为了将数据写入 MySQL,我们需要构建插入语句。建议使用参数化 SQL 插入,以防止 SQL 注入攻击。
示例插入语句:
INSERT INTO house_data (name, price, area, location, house_type, publish_date, url)
VALUES (%s, %s, %s, %s, %s, %s, %s)
参数化插入示例代码:
def insert_house_data(cursor, item):
sql = """
INSERT INTO house_data (name, price, area, location, house_type, publish_date, url)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
values = (
item['name'],
item['price'],
item['area'],
item['location'],
item['house_type'],
item['publish_date'],
item['url']
)
cursor.execute(sql, values)
优化建议:
- 批量插入 :使用
executemany方法进行批量插入,提升性能。 - 事务控制 :使用
BEGIN/COMMIT/ROLLBACK来确保数据一致性。 - 连接池管理 :可结合
DBUtils或SQLAlchemy管理连接池,提高并发性能。
6.3 数据持久化的实现
Scrapy 提供了 Item Pipeline 机制,允许我们对抓取的数据进行清洗、验证和持久化操作。我们将通过自定义一个 MySQL Pipeline,将房屋信息写入数据库。
6.3.1 编写Pipeline实现数据写入
示例:创建 mysql_pipeline.py 文件
import pymysql
from scrapy.utils.project import get_project_settings
class MySQLPipeline:
def __init__(self):
settings = get_project_settings()
self.mysql_config = settings.get('MYSQL_CONFIG')
self.conn = None
self.cursor = None
def open_spider(self, spider):
"""在爬虫启动时建立数据库连接"""
self.conn = pymysql.connect(**self.mysql_config)
self.cursor = self.conn.cursor()
def close_spider(self, spider):
"""在爬虫结束时关闭数据库连接"""
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
def process_item(self, item, spider):
try:
self._insert_item(item)
self.conn.commit()
except Exception as e:
spider.logger.error(f"插入数据失败:{e}")
self.conn.rollback()
return item
def _insert_item(self, item):
sql = """
INSERT INTO house_data (name, price, area, location, house_type, publish_date, url)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
values = (
item['name'],
item['price'],
item['area'],
item['location'],
item['house_type'],
item['publish_date'],
item['url']
)
self.cursor.execute(sql, values)
说明:
open_spider():在爬虫启动时建立数据库连接。close_spider():在爬虫结束时关闭连接,释放资源。process_item():核心方法,处理每个 Item 数据,执行插入并提交事务。try-except:异常捕获机制,插入失败时回滚事务,防止脏数据。
在 settings.py 中启用 Pipeline:
ITEM_PIPELINES = {
'house_info.pipelines.MySQLPipeline': 300,
}
6.3.2 异常处理与事务控制
在数据写入过程中,可能会遇到字段缺失、数据库连接中断、主键冲突等问题。因此,合理的异常处理和事务控制机制至关重要。
常见异常处理策略:
- 字段校验 :在 Pipeline 中加入字段非空校验。
- 重试机制 :遇到数据库连接失败时可加入重试逻辑。
- 日志记录 :使用
spider.logger记录错误信息,便于排查问题。
事务控制示例:
def process_item(self, item, spider):
try:
self._insert_item(item)
self.conn.commit()
except pymysql.MySQLError as e:
spider.logger.error(f"MySQL 错误:{e}")
self.conn.rollback()
except Exception as e:
spider.logger.error(f"未知错误:{e}")
self.conn.rollback()
return item
流程图说明:
graph TD
A[开始处理Item] --> B{数据库连接正常?}
B -->|是| C[执行插入操作]
B -->|否| D[记录错误日志]
C --> E{插入成功?}
E -->|是| F[提交事务]
E -->|否| G[回滚事务]
F --> H[返回Item]
G --> I[记录错误日志]
D --> I
I --> J[返回Item]
通过本章的学习,我们了解了如何设计 MySQL 数据库表结构,配置 Scrapy 与 MySQL 的连接方式,并通过自定义 Pipeline 实现数据的持久化写入。下一章将围绕反爬策略展开,介绍如何在 Scrapy 中实现反爬对抗与请求优化。
7. 反爬策略应对方法与爬虫道德规范
在实际的网络爬虫开发过程中,尤其是面向如房天下这类具有较强反爬机制的网站时,爬虫开发者需要具备识别和应对常见反爬策略的能力,同时也必须遵守基本的网络爬虫道德规范,以避免被目标网站封禁IP、限制访问,甚至引发法律风险。
7.1 反爬策略分析与应对
7.1.1 请求频率限制与下载延迟设置
许多网站通过限制单位时间内来自同一IP的请求次数来防止爬虫行为。Scrapy 提供了 DOWNLOAD_DELAY 设置项,用于控制请求之间的最小间隔时间。
设置方式:
在 settings.py 中添加如下配置:
# 设置下载延迟为2秒
DOWNLOAD_DELAY = 2
# 随机延迟,避免固定间隔被识别
RANDOMIZE_DOWNLOAD_DELAY = True
参数说明:
-DOWNLOAD_DELAY: 每个请求之间的最小延迟(秒)
-RANDOMIZE_DOWNLOAD_DELAY: 如果为 True,则实际延迟为 0.5 * DOWNLOAD_DELAY 到 1.5 * DOWNLOAD_DELAY 之间的随机值
此外,还可以使用 AutoThrottle 插件动态调整下载延迟:
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
说明:该机制会根据服务器响应自动调整请求频率,以模拟更自然的用户行为。
7.1.2 IP代理池的构建与使用
当单一IP频繁访问目标网站时,极易被封禁。构建一个稳定的代理IP池是解决该问题的有效手段。
实现方式:
可以使用 scrapy-proxies 插件或自定义中间件来实现 IP 代理池功能。
示例:自定义代理中间件(proxies.py)
import random
class ProxyMiddleware:
def __init__(self):
self.proxies = [
'http://192.168.1.10:8080',
'http://192.168.1.11:8080',
'http://192.168.1.12:8080'
]
def process_request(self, request, spider):
proxy = random.choice(self.proxies)
request.meta['proxy'] = proxy
配置中间件:
在 settings.py 中启用中间件:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyMiddleware': 543,
}
提示:代理IP建议使用付费代理服务或定期更新的免费代理库,以保证可用性。
7.2 请求头与User-Agent管理
7.2.1 设置随机User-Agent
User-Agent 是服务器识别爬虫的重要依据之一。为了避免被识别为爬虫,我们可以通过随机更换 User-Agent 模拟浏览器访问行为。
实现方式:
可以使用 fake-useragent 库实现随机 User-Agent。
安装:
pip install fake-useragent
示例:自定义 User-Agent 中间件
from fake_useragent import UserAgent
class RandomUserAgentMiddleware:
def __init__(self):
self.ua = UserAgent()
def process_request(self, request, spider):
request.headers.setdefault('User-Agent', self.ua.random)
配置中间件:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomUserAgentMiddleware': 542,
}
说明:每次请求时,User-Agent 都会随机选择一个主流浏览器的标识,有效降低被识别为爬虫的风险。
7.2.2 请求头模拟浏览器行为
除了 User-Agent,还需要模拟浏览器的其他请求头,如 Accept、Accept-Language、Referer 等。
示例:完整请求头设置
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0 Safari/537.36',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Referer': 'https://www.baidu.com/',
'Connection': 'keep-alive'
}
在 Spider 中使用:
yield scrapy.Request(url, headers=headers, callback=self.parse)
7.3 遵守robots.txt规则与爬虫道德规范
7.3.1 robots.txt文件的解析与遵守
每个网站都会提供一个 robots.txt 文件,用于声明哪些页面允许爬取,哪些禁止爬取。
示例:房天下网站 robots.txt 文件片段
User-agent: *
Disallow: /house/
Disallow: /member/
说明:上述规则表示所有 User-Agent(爬虫)都不允许访问
/house/和/member/路径下的页面。
Scrapy 遵守 robots.txt 的配置:
# settings.py
ROBOTSTXT_OBEY = True
说明:启用该配置后,Scrapy 会自动解析目标网站的 robots.txt 文件,并跳过被禁止的路径。
7.3.2 合理设置爬取范围与频率,避免对服务器造成压力
爬虫开发者应具备良好的网络道德意识,合理设置爬取策略,避免对目标网站造成过大负载。
建议措施:
- 设置合理的下载延迟 :如前文所述,使用
DOWNLOAD_DELAY和AUTOTHROTTLE。 - 限制并发请求数量 :
python CONCURRENT_REQUESTS = 4 - 限制最大深度 :
python DEPTH_LIMIT = 2 - 避免重复请求 :
python DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
此外,开发者应避免在高峰期抓取数据,尽量选择服务器负载较低的时间段进行爬取。
简介:本文介绍如何使用Python的Scrapy框架爬取房天下网站的房产信息,并将抓取的数据存储到MySQL数据库中。Scrapy是一个功能强大的网络爬虫框架,MySQL则用于结构化数据存储。通过本项目实战,读者可掌握爬虫项目搭建、网页解析、模拟登录、数据存储等关键技术,适用于数据分析、数据采集等场景。项目还涉及反爬应对、数据清洗和异常处理等内容,具备完整的爬虫开发流程。
更多推荐



所有评论(0)