Scrapy框架实战:构建校园二手平台数据爬虫与数据处理管道
2026/9/5 21:19:37 网站建设 项目流程

简介:本资源是一套基于Python Scrapy框架实现的校园集市数据采集系统源码,面向Python初学者、爬虫入门学习者及高校信息分析实践者,解决校园社区平台结构化数据自动化获取难题,适用于课程设计、科研数据预处理与轻量级舆情分析等场景。压缩包共48个文件(120KB),含24个Python脚本(涵盖spiders爬虫逻辑、pipelines数据管道、database数据库交互、utils工具函数等核心模块)、11个文本说明文档(含环境配置、使用指南与结构说明)、2个SQL建表脚本(mysql_struct.sql与mysql_create.sql)及必要配置与许可文件,目录分层清晰,模块职责明确,便于理解Scrapy工程化开发范式。目前已有51人学习下载,提供完整可运行项目骨架、多策略热度计算(如pop_value_cal.py、calculate_hot.py)、情感评分(sentiment_Rating.py)与词向量/语义相关性分析(correlation_cal_w2c.py、correlation_cal_bert.py)等扩展能力,是掌握Scrapy实战开发与校园数据挖掘的良好范例。

1. 项目缘起:为什么需要“赞噢校园集市”爬虫?

做校园二手交易平台的数据分析,最头疼的就是数据从哪里来。直接找平台要API?对于绝大多数校园集市类应用来说,这几乎不可能。它们要么没有开放接口,要么接口权限申请流程繁琐,对于学生团队或个人开发者而言,门槛太高。于是,自己动手写一个爬虫,就成了获取一手数据最直接、也最可控的方式。

“赞噢校园集市”作为一个典型的校园内信息聚合平台,上面沉淀了大量有价值的非结构化数据:商品品类分布、价格区间、发布时间规律、用户活跃时段、热门关键词等等。这些数据对于分析校园内的消费趋势、优化商品发布策略、甚至作为课程设计或毕业设计的素材,都有着极高的价值。但网页上的数据是给人看的,不是给程序直接用的,这就需要爬虫来扮演“数据搬运工”的角色。

选择Python的Scrapy框架来做这件事,几乎是业内共识。它不是一个简单的requests+BeautifulSoup脚本,而是一个为大规模、结构化数据抓取而生的“工业级”框架。这意味着从项目一开始,你考虑的就不仅仅是“怎么把这一页数据扒下来”,而是“如何高效、稳定、可维护地抓取成千上万条商品信息,并处理可能遇到的翻页、反爬、数据清洗等一系列问题”。Scrapy提供了一套完整的解决方案,包括请求调度、数据解析、管道处理、中间件扩展等组件,让你能像搭积木一样构建一个健壮的爬虫系统,而不是在几百行的脚本里和一堆try...except与正则表达式搏斗。

2. 核心架构设计:Scrapy项目是如何组织的?

一个标准的Scrapy项目,其目录结构本身就体现了清晰的分层设计思想。当你执行scrapy startproject zanao_market命令后,会生成如下骨架:

zanao_market/ ├── scrapy.cfg # 项目部署配置文件 └── zanao_market/ # 项目Python模块 ├── __init__.py ├── items.py # 定义要抓取的数据结构 ├── middlewares.py # 下载器中间件和爬虫中间件 ├── pipelines.py # 数据后处理管道(清洗、去重、存储) ├── settings.py # 项目全局设置(并发、延迟、中间件开关等) └── spiders/ # 爬虫文件存放目录 ├── __init__.py └── zanao_spider.py # 我们即将编写的核心爬虫

这个结构的关键在于“各司其职”:

  • items.py:定义你的“数据容器”。这就像数据库的表结构,提前声明好你要抓取哪些字段(如商品标题、价格、描述、发布时间、卖家信息等),确保后续处理的数据格式统一。
  • spiders/:这里是爬虫的“大脑”。每个爬虫文件负责定义如何发起请求(起始URL)、如何解析响应(提取数据)、以及如何跟进链接(翻页或深入详情页)。
  • pipelines.py:数据离开爬虫后的“加工流水线”。在这里,你可以对抓取到的Item进行清洗(比如去除价格字符串中的“元”字,转为浮点数)、去重(基于商品ID)、验证(检查关键字段是否为空),并最终存储到文件(如JSON、CSV)或数据库(如MySQL、MongoDB)中。
  • settings.py:项目的“控制中心”。几乎所有行为都可以在这里配置,例如:
    • CONCURRENT_REQUESTS:并发请求数,控制爬取速度,避免对目标服务器造成过大压力。
    • DOWNLOAD_DELAY:下载延迟,两次请求之间的等待时间,是应对反爬的初级但有效手段。
    • USER_AGENT:用户代理,模拟真实浏览器访问。
    • ITEM_PIPELINES:启用哪些数据管道及其执行顺序。
  • middlewares.py:提供“插件”能力。你可以在这里编写自定义中间件,在请求发出前或响应返回后插入逻辑,例如自动更换代理IP、处理Cookie、解析动态加载的内容(需结合Selenium或Playwright)等。

对于“赞噢校园集市”这类页面结构相对规整的网站,我们的核心工作将集中在spiders/items.py,并辅以简单的pipelines进行数据清洗和存储。settings.py中的基础反爬配置是必须的,而middlewares.py在初期可以保持默认,待遇到更复杂的反爬策略时再启用。

3. 爬虫实战:从页面解析到数据抓取

假设“赞噢校园集市”的商品列表页URL模式为https://market.zanao.com/list?page={page},商品详情页为https://market.zanao.com/item/{item_id}。我们的爬虫目标是抓取所有列表页上的商品基本信息,并尽可能进入详情页抓取更完整的描述和卖家信息。

3.1 定义数据模型(items.py)

首先,在items.py中定义我们想要抓取的数据字段。这步很关键,它让后续的数据处理有据可依。

import scrapy class ZanaoMarketItem(scrapy.Item): # 从列表页即可获取的基础信息 item_id = scrapy.Field() # 商品ID,通常可作为唯一标识 title = scrapy.Field() # 商品标题 price = scrapy.Field() # 价格(字符串,如“50元”) main_image_url = scrapy.Field() # 主图链接 list_page_url = scrapy.Field() # 列表页来源URL publish_time = scrapy.Field() # 发布时间(列表页显示) category = scrapy.Field() # 商品分类 # 需要进入详情页才能获取的扩展信息 detail_url = scrapy.Field() # 详情页URL description = scrapy.Field() # 商品详细描述 seller_name = scrapy.Field() # 卖家昵称 contact_info = scrapy.Field() # 联系方式(需注意隐私合规) view_count = scrapy.Field() # 浏览量 location = scrapy.Field() # 交易地点 status = scrapy.Field() # 商品状态(已售/在售)

3.2 编写核心爬虫(spiders/zanao_spider.py)

接下来是重头戏。我们将创建一个名为ZanaoSpider的爬虫类。

import scrapy from urllib.parse import urljoin from zanao_market.items import ZanaoMarketItem class ZanaoSpider(scrapy.Spider): name = 'zanao' # 爬虫的唯一标识,用于命令行启动 allowed_domains = ['market.zanao.com'] # 限制爬虫只爬取该域名下的链接 start_urls = ['https://market.zanao.com/list?page=1'] # 起始URL # 基础反爬配置,也可以在settings.py中统一设置 custom_settings = { 'DOWNLOAD_DELAY': 1, # 每次请求间隔1秒,礼貌爬取 'CONCURRENT_REQUESTS_PER_DOMAIN': 2, # 对同一域名并发数 'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', # 模拟Chrome } def parse(self, response): """ 解析商品列表页。 1. 提取当前页所有商品条目。 2. 对于每个商品,提取基础信息,并构造详情页请求。 3. 寻找并跟进“下一页”链接。 """ # 假设每个商品在页面上由一个 class='item' 的 div 包裹 item_nodes = response.css('div.item') for item_node in item_nodes: # 初始化一个Item对象 market_item = ZanaoMarketItem() # 从列表页节点提取基础信息(这里以CSS选择器为例,XPath同样强大) market_item['item_id'] = item_node.css('::attr(data-id)').get() market_item['title'] = item_node.css('h3.title::text').get().strip() price_text = item_node.css('span.price::text').get() market_item['price'] = price_text.strip() if price_text else None market_item['main_image_url'] = item_node.css('img.thumb::attr(src)').get() market_item['list_page_url'] = response.url market_item['publish_time'] = item_node.css('span.time::text').get() market_item['category'] = item_node.css('span.category::text').get() # 构建详情页URL detail_path = item_node.css('a::attr(href)').get() if detail_path: detail_url = urljoin(response.url, detail_path) market_item['detail_url'] = detail_url # 关键步骤:生成一个对详情页的新请求,并将初步填充的item通过meta传递过去 # callback指定详情页的解析函数 yield scrapy.Request( url=detail_url, callback=self.parse_detail, meta={'market_item': market_item} ) else: # 如果没有详情页链接,直接yield当前的基础信息item yield market_item # 翻页处理:查找“下一页”按钮的链接 next_page_url = response.css('a.next-page::attr(href)').get() if next_page_url: # 将相对URL转为绝对URL next_page_absolute_url = urljoin(response.url, next_page_url) # 将下一页的请求交回给parse函数自身处理,实现递归爬取 yield scrapy.Request(url=next_page_absolute_url, callback=self.parse) def parse_detail(self, response): """ 解析商品详情页,补充Item的扩展信息。 """ # 从请求的meta中取出之前传递过来的item对象 market_item = response.meta['market_item'] # 使用更精确的选择器提取详情页信息 market_item['description'] = '\n'.join(response.css('div.detail-content ::text').getall()).strip() market_item['seller_name'] = response.css('div.seller-info span.name::text').get() # 注意:联系方式可能被保护,需要特殊处理或遵守robots.txt market_item['contact_info'] = response.css('div.contact::text').get() market_item['view_count'] = response.css('span.view-count::text').re_first(r'\d+') # 用正则提取数字 market_item['location'] = response.css('span.location::text').get() market_item['status'] = '已售' if response.css('span.sold-out') else '在售' # 详情页信息补充完毕,返回完整的item给引擎,引擎会将其送入pipelines yield market_item

代码要点解析:

  1. parseparse_detail的分工:这是Scrapy处理分页+详情页抓取的经典模式。parse负责“广度”遍历(列表与翻页),parse_detail负责“深度”挖掘(单个商品详情)。通过yield scrapy.Request并指定callback,实现了请求的调度。
  2. meta参数的使用:在从列表页跳转到详情页时,我们将已经提取到的部分数据(market_item)通过Requestmeta字典传递下去。这样在详情页解析函数中,可以拿到同一个item对象进行信息补充,避免了数据割裂。
  3. 选择器的使用:示例中主要使用了CSS选择器(response.css),它比XPath更易读,尤其在处理class、id时。::text用于提取节点文本,::attr(attr_name)用于提取属性值。get()获取第一个匹配结果,getall()获取所有匹配结果的列表。
  4. 翻页逻辑:通过查找并跟进“下一页”链接,并将回调函数指向自身(callback=self.parse),爬虫可以自动遍历所有列表页,直到找不到下一页链接为止。

3.3 配置与优化(settings.py)

settings.py中,我们需要开启一些关键配置以支持爬虫运行和数据持久化。

# 遵守robots协议,对于正规项目建议设为True,但某些情况下可能需要关闭以抓取数据 ROBOTSTXT_OBEY = False # 并发与延迟设置,根据目标网站承受能力和自身需求调整 CONCURRENT_REQUESTS = 16 # 全局并发请求数 CONCURRENT_REQUESTS_PER_DOMAIN = 4 # 对单个域名的并发数 DOWNLOAD_DELAY = 0.5 # 下载延迟(秒),0.5秒/请求 # 用户代理池(可选,更高级的反爬策略) # USER_AGENT = '...' # 如果只用一个,在这里设置 # 或者安装scrapy-fake-useragent库,实现自动轮换 # 启用并调整内置的去重过滤器 DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter' # 配置Item Pipelines,数字代表优先级(1-1000),越小越先执行 ITEM_PIPELINES = { 'zanao_market.pipelines.ZanaoMarketPipeline': 300, } # 日志级别,开发调试时可设为DEBUG,生产环境设为INFO或WARNING LOG_LEVEL = 'INFO'

4. 数据处理与持久化:Pipeline的实战应用

爬虫抓取到的原始数据往往是“脏”的,需要清洗后才能使用。同时,我们需要将数据保存下来。这些工作都在pipelines.py中完成。

4.1 数据清洗管道

import re from itemadapter import ItemAdapter class ZanaoMarketPipeline: def process_item(self, item, spider): """ 对每一个yield出来的item进行处理。 """ adapter = ItemAdapter(item) # 1. 价格清洗:将“50元”、“面议”、“免费送”等转换为标准格式 price_field = adapter.get('price') if price_field: # 尝试提取数字 num_match = re.search(r'(\d+(\.\d+)?)', price_field) if num_match: adapter['price_num'] = float(num_match.group(1)) # 新增一个清洗后的数字字段 adapter['price_unit'] = '元' elif '面议' in price_field or '议价' in price_field: adapter['price_num'] = None adapter['price_unit'] = '面议' elif '免费' in price_field or '送' in price_field: adapter['price_num'] = 0.0 adapter['price_unit'] = '免费' else: adapter['price_num'] = None adapter['price_unit'] = '未知' # 2. 描述信息去空白和冗余字符 description = adapter.get('description') if description: # 替换多个换行和空格为单个空格 cleaned_desc = re.sub(r'\s+', ' ', description).strip() adapter['description'] = cleaned_desc # 3. 关键字段空值检查(可选,可记录日志或丢弃) required_fields = ['title', 'item_id'] for field in required_fields: if not adapter.get(field): spider.logger.warning(f"Item {adapter.get('item_id')} missing required field: {field}") # 可以选择在此处丢弃该item: raise DropItem(f"Missing {field}") return item

4.2 数据存储管道

清洗后的数据需要存储。我们可以写多个管道,分别存储到不同地方。这里以存储为JSON行文件(JSON Lines)和MySQL数据库为例。

JSON文件存储管道:

import json from scrapy.exporters import JsonItemExporter class JsonExportPipeline: def open_spider(self, spider): # 爬虫启动时打开文件 self.file = open('zanao_items.jl', 'wb') # .jl 表示 JSON Lines格式 self.exporter = JsonItemExporter(self.file, encoding='utf-8', ensure_ascii=False) self.exporter.start_exporting() 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

MySQL存储管道(需要安装pymysqlmysqlclient):

import pymysql class MySQLPipeline: def __init__(self, mysql_settings): self.mysql_settings = mysql_settings self.conn = None self.cursor = None @classmethod def from_crawler(cls, crawler): # 从settings.py中读取数据库配置 return cls( mysql_settings=crawler.settings.get('MYSQL_SETTINGS') ) def open_spider(self, spider): # 建立数据库连接 self.conn = pymysql.connect(**self.mysql_settings) self.cursor = self.conn.cursor() # 创建表(如果不存在) self._create_table() def close_spider(self, spider): # 关闭连接 if self.cursor: self.cursor.close() if self.conn: self.conn.close() def _create_table(self): create_table_sql = """ CREATE TABLE IF NOT EXISTS zanao_items ( id INT AUTO_INCREMENT PRIMARY KEY, item_id VARCHAR(50) UNIQUE, title TEXT, price_num FLOAT, price_unit VARCHAR(10), description LONGTEXT, seller_name VARCHAR(100), category VARCHAR(50), publish_time DATETIME, location VARCHAR(100), status VARCHAR(20), detail_url TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) CHARSET=utf8mb4; """ self.cursor.execute(create_table_sql) self.conn.commit() def process_item(self, item, spider): # 构造插入或更新语句 sql = """ INSERT INTO zanao_items (item_id, title, price_num, price_unit, description, seller_name, category, publish_time, location, status, detail_url) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE title=VALUES(title), price_num=VALUES(price_num), description=VALUES(description), status=VALUES(status); """ values = ( item.get('item_id'), item.get('title'), item.get('price_num'), item.get('price_unit'), item.get('description'), item.get('seller_name'), item.get('category'), item.get('publish_time'), item.get('location'), item.get('status'), item.get('detail_url') ) try: self.cursor.execute(sql, values) self.conn.commit() except pymysql.Error as e: spider.logger.error(f"Error inserting item {item.get('item_id')}: {e}") self.conn.rollback() return item

settings.py中启用这两个管道,并配置MySQL连接信息:

ITEM_PIPELINES = { 'zanao_market.pipelines.ZanaoMarketPipeline': 100, # 先清洗 'zanao_market.pipelines.JsonExportPipeline': 200, # 再导出JSON 'zanao_market.pipelines.MySQLPipeline': 300, # 最后存入数据库 } MYSQL_SETTINGS = { 'host': 'localhost', 'port': 3306, 'user': 'your_username', 'password': 'your_password', 'db': 'scrapy_data', 'charset': 'utf8mb4', }

5. 进阶:应对反爬策略与提升爬虫健壮性

校园网站的反爬措施可能相对简单,但养成良好的爬虫习惯至关重要。以下是一些实战中必须考虑的要点:

5.1 基础反爬措施

  1. 设置合理的DOWNLOAD_DELAY:这是最基本的礼貌。DOWNLOAD_DELAY = 1意味着每秒最多一个请求,能极大降低被封IP的风险。可以通过RANDOMIZE_DOWNLOAD_DELAY = True让延迟在一个区间内随机波动,模拟真人操作。
  2. 使用真实的User-Agent:在settings.py中设置一个常见的浏览器UA字符串。更高级的做法是使用中间件轮换多个UA。
  3. 启用CookiesCOOKIES_ENABLED = True。有些网站会通过会话Cookie来跟踪访问行为,保持Cookies有助于维持会话状态。
  4. 遵守robots.txt:对于公开数据抓取,建议ROBOTSTXT_OBEY = True。但需注意,robots.txt是君子协议,并非所有网站都严格遵循或定义清晰。

5.2 处理动态加载内容

如果“赞噢校园集市”的商品列表是通过JavaScript动态加载的(比如滚动翻页),直接请求HTML将获取不到数据。这时有几种方案:

  • 方案A:分析Ajax接口:使用浏览器的开发者工具(F12 -> Network -> XHR/Fetch),观察滚动时浏览器向后台发送了哪些请求。往往能找到一个返回JSON数据的API接口。直接模拟请求这个接口,效率更高,数据也更干净。
    • 优势:高效、数据结构化。
    • 挑战:接口参数可能加密,需要逆向分析。
  • 方案B:使用Selenium或Playwright中间件:让Scrapy驱动一个真实的浏览器来渲染页面,再提取渲染后的HTML。
    • 优势:能应对几乎所有前端渲染的页面。
    • 劣势:速度极慢,资源消耗大,不稳定。仅作为最后手段。
    • Scrapy-Playwright集成:这是目前更流行的方案。安装scrapy-playwright库,并在爬虫中启用Playwright。
    # 在爬虫的custom_settings中启用 custom_settings = { 'DOWNLOAD_HANDLERS': { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", }, 'TWISTED_REACTOR': "twisted.internet.asyncioreactor.AsyncioSelectorReactor", 'PLAYWRIGHT_BROWSER_TYPE': 'chromium', }
    然后在发起请求时,通过meta字典指定使用Playwright:
    yield scrapy.Request(url=url, callback=self.parse, meta={'playwright': True})

5.3 错误处理与重试

网络请求充满不确定性。Scrapy内置了强大的重试和错误处理机制。

  • 自动重试:在settings.py中设置RETRY_ENABLED = TrueRETRY_TIMES = 2(重试次数),RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429](对哪些HTTP状态码进行重试)。429状态码(Too Many Requests)尤其重要,遇到时应增加延迟。
  • 自定义错误处理:在爬虫中重写start_requests方法或使用中间件,可以更精细地控制请求失败后的逻辑,比如更换代理IP。

5.4 分布式爬取与增量抓取

当数据量巨大时,单机爬取可能力不从心。

  • 分布式爬虫:结合Scrapy-Redis组件,可以轻松将爬虫改造成分布式。所有爬虫实例共享一个Redis队列中的请求,并利用Redis的集合进行去重,实现多机协同工作。
  • 增量抓取:每次全量抓取浪费资源。核心思路是识别数据的“更新标志”。对于商品,可以用item_idpublish_time组合判断。在管道中,只存储publish_time晚于上次抓取时间的商品,或者设计一个版本号字段。更复杂的增量抓取需要设计专门的数据同步逻辑。

6. 项目运行与调试

6.1 启动爬虫

在项目根目录(scrapy.cfg所在目录)下,执行:

scrapy crawl zanao -o output.json
  • crawl zanao:启动名为zanao的爬虫(即ZanaoSpider中定义的name)。
  • -o output.json:将抓取到的item输出到output.json文件(Scrapy会自动根据后缀选择导出器)。

6.2 常用调试命令

  • Shell交互调试:这是最强大的调试工具。在命令行输入:
    scrapy shell 'https://market.zanao.com/list?page=1'
    会进入一个交互式环境,变量response包含了请求的响应。你可以直接在这里测试选择器:
    response.css('div.item').get() response.xpath('//h3[@class="title"]/text()').get()
  • 查看爬取状态:运行爬虫时,控制台会实时输出日志。关注scraped items count(已抓取条目数)和日志中的WARNING/ERROR信息。
  • 限制抓取数量:调试时,可以使用-s CLOSESPIDER_ITEMCOUNT=10参数,让爬虫在抓取10个item后自动停止。

6.3 日志与监控

  • 日志级别:在settings.py中设置LOG_LEVEL = 'DEBUG'可以查看最详细的日志,包括每个请求和响应。生产环境建议设为INFOWARNING
  • 日志文件:通过-s LOG_FILE='scrapy.log'将日志输出到文件。
  • 数据统计:Scrapy内置了数据收集器,可以通过扩展(Extensions)将统计信息(如请求数、成功数、item数)发送到监控系统。

7. 伦理、法律与最佳实践

在享受爬虫带来的数据便利时,必须时刻绷紧合规这根弦。

  1. 尊重robots.txt:这是网站所有者表达爬虫抓取意愿的首要文件。务必先检查并遵守。对于明确禁止爬取的目录,不要强行抓取。
  2. 控制访问频率DOWNLOAD_DELAY不仅是技术需要,更是道德要求。不要对目标网站发起DDOS攻击般的请求。
  3. 识别并遵守网站条款:许多网站的“使用条款”或“服务协议”中会明确禁止未经授权的数据抓取。在法律层面,这比robots.txt更具约束力。
  4. 数据用途限制:抓取到的数据应用于个人学习、研究或公益目的。绝对禁止用于商业牟利、骚扰用户、侵犯隐私或任何非法活动。对于“赞噢校园集市”这类包含用户联系方式的平台,对联系信息的处理要格外谨慎。
  5. 版权与隐私:商品描述、图片可能涉及用户原创内容或版权。公开传播或商用需获得授权。用户昵称、联系方式属于个人隐私信息,收集和使用必须符合相关法律法规。
  6. 设置清晰的User-Agent:在你的User-Agent中留下联系方式(例如YourProjectBot/1.0 (+https://yourproject.com/bot-info)),方便网站管理员在必要时联系你。这是一种负责任的体现。

写一个能跑的爬虫不难,但写一个稳定、高效、可维护、且负责任的爬虫,才是区分新手和资深开发者的关键。基于Scrapy框架,你拥有了实现后者的坚实基础。从明确需求、设计数据结构开始,到编写解析逻辑、处理反爬、数据清洗入库,每一步都需要仔细考量。希望这份针对“赞噢校园集市”的爬虫设计与实现详解,能为你提供一个从零到一的完整蓝本,并在你未来面对更复杂的数据抓取任务时,带来有价值的参考。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询