简介:本资源是一套面向房地产数据分析初学者与行业研究者的Python链家二手房及租房数据爬虫实战源码,解决房产市场原始数据获取难、结构化处理门槛高的实际问题,适用于市场调研、投资分析、教学实践等场景。压缩包共20个文件,含8个核心Python脚本(如core.py主控逻辑、woaiwojialib.py定制化反爬交互、model.py数据建模)、2个CSV数据文件(houseinfo.csv与community.csv)、2个PNG可视化图示、2个文本配置/日志文件,以及Jupyter笔记本(lianjia.ipynb)用于交互式分析演示,整体大小10.85MB。已有704人学习下载。读者可直接运行scrawl.py等模块完成链家网页数据抓取,借助settings.py灵活配置参数,通过jupyter笔记快速复现清洗、统计与可视化全流程;目录结构分层清晰,含独立工具库(shlib.py)、杂项函数(misc.py)与完整开源许可,兼顾可读性、可维护性与合规性。
1. 项目概述与核心价值
最近在做一个数据分析项目,需要大量城市二手房和租房市场的实时数据。找了一圈公开的数据源,要么更新不及时,要么字段不全,要么就是收费昂贵。于是,很自然地就想到了链家——这个国内最大的房产信息平台之一,它上面的房源数据可以说是最全、最活跃的。手动去抄?不现实,一个城市每天新增的房源信息就成千上万条。所以,自己动手写一个爬虫就成了最直接有效的解决方案。
这个项目,就是基于Python设计并实现一个针对链家二手房和租房信息的在线数据爬虫。它的核心目标很明确:自动化、高效率、稳定地抓取链家网站上公开的房源列表页和详情页数据,并将这些非结构化的网页信息,转化为结构化的、可供分析的数据集。无论是想研究某个区域的房价走势、租金回报率,还是想做个比价工具,或者仅仅是学习网络爬虫技术,这个项目都能提供一个从零到一的完整实战案例。
我选择Python,是因为它在数据抓取和处理领域的生态实在是太成熟了。requests负责网络请求,BeautifulSoup或lxml负责解析HTML,pandas负责数据清洗和存储,再加上一些用于控制爬取速度、处理异常的库,几乎可以应对所有常见的爬虫场景。链家网站的结构相对清晰,虽然有一定反爬措施,但并非坚不可摧,非常适合作为中级爬虫练手和实际应用的项目。
注意:任何爬虫项目都必须严格遵守目标网站的
robots.txt协议,并尊重网站的服务压力。本项目仅用于技术学习和研究,所爬取的数据应为适度、低频的公开信息,严禁用于商业爬取、恶意攻击或任何干扰网站正常运营的行为。
2. 爬虫整体架构与设计思路
设计一个健壮的爬虫,不能一上来就写代码。我们需要先想清楚整个数据流转的流程,以及每个环节可能遇到的问题。我的设计思路主要分为四个核心模块,它们共同构成了爬虫的骨架。
2.1 核心模块划分
- 请求调度器 (Request Scheduler):这是爬虫的“大脑”。它负责生成需要爬取的URL队列。对于链家,我们需要同时处理二手房和租房两个频道,每个频道下又有不同的城市、区域、甚至排序方式。调度器要能灵活地配置这些起始点,并管理URL的优先级与去重。
- 网页下载器 (Downloader):这是爬虫的“双手”。它根据调度器给出的URL,去实际访问网页并获取HTML内容。这里的关键在于模拟正常浏览器访问,包括设置合理的请求头(User-Agent、Referer等)、处理Cookie、以及应对可能出现的反爬机制(如IP限制、验证码)。
- 数据解析器 (Parser):这是爬虫的“眼睛”。下载下来的HTML是杂乱无章的,解析器的任务就是从中精准地“抠”出我们需要的信息。比如房源标题、总价、单价、面积、户型、朝向、楼层、小区名、经纪人、发布时间等等。链家的页面结构会不时调整,所以解析器的代码需要有一定的容错性和可维护性。
- 数据存储器 (Storage):这是爬虫的“仓库”。解析出来的结构化数据需要被持久化保存。根据数据量和使用场景,可以选择保存为CSV文件、JSON文件,或者直接存入MySQL、MongoDB等数据库。我通常会先存为CSV,方便快速查看和用Excel/Pandas处理,如果数据量大或需要复杂查询,再考虑导入数据库。
2.2 技术栈选型与考量
为什么选这些库?每个选择背后都有实际考量:
requestsvsurllib:requests的API更加人性化,会话保持、代理设置、超时处理都非常简洁,社区活跃,是Python HTTP客户端的首选。BeautifulSoup4vslxml:这是一个经典选择。BeautifulSoup的语法更接近自然语言,容错性好,适合初学者和快速开发。lxml的解析速度极快,适合处理大量页面。在本项目中,由于链家页面结构不算特别复杂但页面量可能大,我推荐使用lxml作为解析引擎,因为速度优势在批量爬取时非常明显。我们可以用BeautifulSoup(html, ‘lxml’)的方式来结合两者的优点。pandas:它不仅是数据分析神器,其DataFrame对象也是临时存储和清洗数据的完美容器,并且能一键导出为CSV/Excel,极其方便。fake-useragent:用于随机生成不同的浏览器User-Agent字符串,是绕过基于User-Agent反爬的基础手段。time/random:用于在请求间插入随机延时,这是体现“礼貌爬虫”的关键,能有效降低被封IP的风险。
2.3 反爬策略初步分析
在动手之前,我们必须先“侦察”一下链家的反爬措施。通过手动浏览和简单测试,我发现主要有以下几点:
- 请求头校验:检查
User-Agent,没有或不常见的可能会被拒绝或返回简版页面。 - 访问频率限制:短时间内发起大量请求,IP地址可能会被暂时封锁。
- Cookie/Session验证:一些列表页可能需要携带特定的Cookie才能访问完整信息。
- 页面结构动态化:虽然链家主体内容仍是服务端渲染,但部分数据可能通过接口获取,需要分析XHR请求。
针对这些,我们的应对策略是:
- 伪装请求头:使用
fake-useragent库随机生成。 - 控制爬取速度:在每个请求之间设置随机间隔(如2-5秒)。
- 使用会话:利用
requests.Session()保持连接和自动管理Cookie。 - 优先分析列表页接口:如果发现列表数据是通过Ajax加载的,直接抓取接口数据会更高效、更稳定。
3. 核心细节解析与实操要点
明确了架构,我们来深入每个环节的细节。这部分是爬虫稳定运行的关键,很多坑都藏在这里。
3.1 目标URL分析与构造
链家二手房和租房的列表页URL有规律可循,这是我们爬虫的入口。
- 二手房列表页示例:
https://[城市拼音].lianjia.com/ershoufang/[区域拼音]/pg[页码]/- 例如,北京海淀区的第二页:
https://bj.lianjia.com/ershoufang/haidian/pg2/ - 我们可以通过拼接城市、区域、页码来批量生成URL。
- 例如,北京海淀区的第二页:
- 租房列表页示例:
https://[城市拼音].lianjia.com/zufang/[区域拼音]/pg[页码]/ - 详情页URL: 通常可以在列表页中每个房源卡片里找到
href属性,是相对路径,需要拼接基础域名。
实操心得:不要硬编码URL模式。最好将城市、区域的映射关系(如中文名到拼音)保存在一个配置字典或JSON文件中,这样爬虫更容易扩展和维护。另外,链家可能会对URL进行小幅调整,定期检查是必要的。
3.2 网页下载器的稳健性实现
一个健壮的下载器不能只会发请求。以下是核心代码框架和要点:
import requests from fake_useragent import UserAgent import time import random class LianJiaDownloader: def __init__(self): self.session = requests.Session() self.ua = UserAgent() # 设置一个基础的请求头模板 self.base_headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', } self.request_count = 0 def get_page(self, url, max_retries=3): """下载页面,包含重试机制""" for i in range(max_retries): try: # 每次请求使用随机User-Agent headers = self.base_headers.copy() headers['User-Agent'] = self.ua.random # 重要:添加Referer,模拟从链家站内跳转 headers['Referer'] = 'https://www.lianjia.com/' response = self.session.get(url, headers=headers, timeout=10) response.raise_for_status() # 检查HTTP状态码,非200会抛出异常 # 检查返回内容是否有效,有时被封会返回空页面或错误信息 if len(response.content) < 5000: # 简单通过长度判断,阈值可根据实际情况调整 print(f"警告:页面内容过短,可能被拦截。URL: {url}") return None if response.encoding is None: response.encoding = 'utf-8' # 确保编码正确 self.request_count += 1 # 礼貌性延迟,并加入随机性 time.sleep(random.uniform(2, 4)) return response.text except requests.exceptions.RequestException as e: print(f"请求失败 (尝试 {i+1}/{max_retries}): {url}, 错误: {e}") if i < max_retries - 1: sleep_time = random.uniform(5, 10) # 失败后等待更久 print(f"等待 {sleep_time:.2f} 秒后重试...") time.sleep(sleep_time) else: print(f"URL {url} 重试{max_retries}次后仍失败。") return None return None关键点解析:
- 使用Session:
self.session可以自动处理Cookie,保持多次请求间的状态,比单次requests.get更接近真实浏览器行为。 - 随机User-Agent:
UserAgent().random每次生成一个主流浏览器的UA,避免因单一UA被识别。 - 设置Referer:这是一个容易被忽略但很重要的反爬点。告诉服务器你是从链家站内跳转过来的,更像真人行为。
- 异常处理与重试:网络请求充满不确定性。
try...except块捕获超时、连接错误等异常,并实现重试逻辑,提高爬虫的鲁棒性。 - 礼貌延迟:
time.sleep(random.uniform(2, 4))是爬虫的“道德底线”。固定延时容易被识别,随机延时更安全。这个时间间隔需要根据目标网站的承受能力和自身需求权衡。 - 内容有效性检查:简单的长度检查可以快速过滤掉因被反爬而返回的错误页面或空页面。
3.3 数据解析器的精准定位
解析是爬虫中最“脏”最累的活,因为你要面对不断变化的HTML结构。我们的原则是:使用属性组合定位,避免使用易变的类名或结构索引。
以链家二手房详情页为例,我们需要提取房源标题、总价等。首先要用浏览器的开发者工具(F12)仔细分析元素。
from bs4 import BeautifulSoup import re class LianJiaParser: @staticmethod def parse_list_page(html): """解析列表页,获取房源链接和基本信息""" if not html: return [] soup = BeautifulSoup(html, 'lxml') house_list = [] # 定位房源列表项,通常有特定的class items = soup.find_all('li', class_=re.compile('clear LOGCLICKDATA')) for item in items: house_info = {} # 提取详情页链接 link_tag = item.find('a', class_='img') if link_tag and link_tag.get('href'): house_info['detail_url'] = 'https://bj.lianjia.com' + link_tag['href'] # 提取标题 title_tag = item.find('div', class_='title') if title_tag: house_info['title'] = title_tag.text.strip() # 提取价格等信息 - 这里需要更精细的定位 price_tag = item.find('div', class_='totalPrice') if price_tag: house_info['total_price'] = price_tag.text.strip() # ... 提取其他列表页可见信息,如小区、户型、面积等 house_list.append(house_info) return house_list @staticmethod def parse_detail_page(html): """解析详情页,获取完整信息""" if not html: return {} soup = BeautifulSoup(html, 'lxml') detail_info = {} # 1. 提取标题 title_tag = soup.find('h1', class_='main') detail_info['title'] = title_tag.text.strip() if title_tag else '' # 2. 提取总价和单价 - 通常在一个class为price的div里 price_div = soup.find('div', class_='price') if price_div: total_price_span = price_div.find('span', class_='total') unit_price_span = price_div.find('span', class_='unitPriceValue') detail_info['total_price'] = total_price_span.text.strip() if total_price_span else '' detail_info['unit_price'] = unit_price_span.text.strip() if unit_price_span else '' # 3. 提取房源基本信息(面积、户型、楼层等)- 通常在class为introContent的div里 intro_div = soup.find('div', class_='introContent') if intro_div: base_items = intro_div.find_all('li') for li in base_items: text = li.text.strip() if '房屋户型' in text: detail_info['layout'] = text.split(':')[-1] elif '建筑面积' in text: detail_info['area'] = text.split(':')[-1] elif '所在楼层' in text: detail_info['floor'] = text.split(':')[-1] # ... 同理提取其他字段 # 4. 提取小区信息 community_tag = soup.find('div', class_='communityName') if community_tag: a_tag = community_tag.find('a') detail_info['community'] = a_tag.text.strip() if a_tag else '' # 5. 提取经纪人信息 agent_tag = soup.find('div', class_='brokerInfoText') if agent_tag: detail_info['agent'] = agent_tag.text.strip() # 6. 提取房源标签 tags = [] tag_div = soup.find('div', class_='tags') if tag_div: tags = [span.text.strip() for span in tag_div.find_all('span')] detail_info['tags'] = ','.join(tags) return detail_info解析技巧与避坑指南:
- 使用正则表达式匹配类名:
class_=re.compile('clear LOGCLICKDATA')可以匹配类名中包含该字符串的元素,即使类名有增减(如clear LOGCLICKDATA noresult)也能匹配到,比精确匹配更健壮。 - 层层防御:每次使用
.find()或.find_all()后,都要判断结果是否为None,避免后续调用.text或[‘href’]时抛出AttributeError。 - 组合定位:不要只依赖一个类名或标签。例如找价格,先找到特征明显的父元素(
class=’price’的div),再在其内部寻找具体的价格标签,这样即使内部结构微调,也不容易出错。 - 文本清洗:
.text.strip()是基本操作,但链家的数据里可能包含空格、换行符、特殊字符。有时需要用re.sub(‘\s+’, ‘ ‘, text)来合并多余空白。 - 应对结构变化:链家前端的类名和结构可能会改版。最稳妥的方法是寻找具有唯一性的
>import pandas as pd from downloader import LianJiaDownloader from parser import LianJiaParser import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s') class LianJiaCrawler: def __init__(self, start_urls, max_pages_per_area=100): self.downloader = LianJiaDownloader() self.parser = LianJiaParser() self.start_urls = start_urls # 起始URL列表,例如不同区域的列表页第一页 self.max_pages = max_pages_per_area self.all_house_data = [] # 临时存储所有房源数据 def crawl(self): """主爬取流程""" for start_url in self.start_urls: logging.info(f"开始爬取区域: {start_url}") current_page = 1 while current_page <= self.max_pages: # 1. 构造列表页URL list_url = self._generate_list_url(start_url, current_page) if not list_url: break logging.info(f"爬取列表页: {list_url}") # 2. 下载列表页 list_html = self.downloader.get_page(list_url) if not list_html: logging.warning(f"列表页下载失败,可能已无更多页面或触发反爬。URL: {list_url}") break # 如果列表页都下载失败,可能该区域爬完了或IP受限,跳出循环 # 3. 解析列表页,获取详情页链接 house_previews = self.parser.parse_list_page(list_html) if not house_previews: logging.info(f"列表页 {list_url} 未解析到房源,可能已到末页。") break # 当前页没有房源,说明已爬完所有页面 # 4. 遍历每个房源,爬取详情页 for preview in house_previews: detail_url = preview.get('detail_url') if not detail_url: continue # 可选:检查该详情页是否已爬取过(去重) # if self._is_duplicate(detail_url): # continue logging.info(f"爬取详情页: {detail_url}") detail_html = self.downloader.get_page(detail_url) if detail_html: detail_data = self.parser.parse_detail_page(detail_html) # 将列表页的部分信息合并进来 detail_data.update({k: v for k, v in preview.items() if k != 'detail_url'}) detail_data['source_url'] = detail_url # 记录来源 detail_data['crawl_time'] = pd.Timestamp.now() # 记录爬取时间 self.all_house_data.append(detail_data) # 每爬取N条保存一次,防止数据丢失 if len(self.all_house_data) % 20 == 0: self._save_to_csv() else: logging.warning(f"详情页下载失败: {detail_url}") # 详情页之间的延时已在downloader中实现 # 5. 判断是否还有下一页(可选,更智能的方式) # 可以通过解析列表页的“下一页”按钮是否存在来判断 # 这里我们使用简单的页码递增,配合“解析不到房源即停止”的机制 current_page += 1 # 一个区域爬取完成后,保存一次数据 self._save_to_csv() logging.info("所有区域爬取完成!") def _generate_list_url(self, base_url, page): """根据基础URL和页码生成列表页URL""" # 假设base_url是类似 `https://bj.lianjia.com/ershoufang/haidian/` 的形式 if page == 1: return base_url else: # 确保base_url以'/'结尾 base = base_url.rstrip('/') return f"{base}/pg{page}/" def _save_to_csv(self, filename='lianjia_houses.csv'): """将数据保存到CSV文件""" if not self.all_house_data: return df = pd.DataFrame(self.all_house_data) # 如果文件已存在,则追加写入,不包含表头 try: existing_df = pd.read_csv(filename) df.to_csv(filename, mode='a', header=False, index=False, encoding='utf-8-sig') except FileNotFoundError: # 文件不存在,首次写入,包含表头 df.to_csv(filename, index=False, encoding='utf-8-sig') logging.info(f"数据已保存至 {filename},当前累计 {len(self.all_house_data)} 条记录。") # 清空临时列表,避免内存占用过大(如果数据量极大) # self.all_house_data.clear()4.2 数据存储与持久化方案
上面代码中使用了CSV文件进行存储,这是最简单直接的方式。
pandas的to_csv方法非常强大,encoding=’utf-8-sig’可以确保用Excel打开时中文不乱码。对于更复杂的场景,可以考虑以下方案:
数据库存储 (推荐用于大规模数据):
- SQLite:轻量级,无需安装服务器,适合单机爬虫。Python内置
sqlite3模块。 - MySQL/PostgreSQL:功能强大,适合多机协作或需要复杂查询的场景。可以使用
pymysql或psycopg2库。 - MongoDB:文档型数据库,schema灵活,非常适合存储结构可能变化的爬虫数据。使用
pymongo库。
- SQLite:轻量级,无需安装服务器,适合单机爬虫。Python内置
增量爬取与去重: 爬虫不应该每次都从头爬。我们可以在数据库中为每条数据增加一个唯一标识(如详情页URL的MD5值),并在爬取前先查询该标识是否存在。也可以记录每条数据的爬取时间,方便后续只爬取更新过的数据。
数据清洗与标准化: 爬取下来的原始数据往往是“脏”的。在存储前或存储后,需要进行清洗:
- 价格字段:去除“万”、“元/平米”等字符,转换为数值类型(
float)。 - 面积字段:提取数字部分。
- 户型字段:拆分为室、厅、卫等独立字段。
- 楼层字段:分离出总楼层和当前楼层。
- 处理缺失值:对于解析失败或页面没有的字段,填充为
None或空字符串。
这部分工作可以在
Parser类中做,也可以在存储到DataFrame后,用pandas的向量化操作批量处理,效率更高。- 价格字段:去除“万”、“元/平米”等字符,转换为数值类型(
5. 高级策略与性能优化
当基本爬虫跑通后,我们可能会面临效率瓶颈和更严格的反爬。这时就需要引入一些高级策略。
5.1 并发爬取提升效率
单线程爬取速度太慢。我们可以使用
concurrent.futures库的ThreadPoolExecutor来实现多线程,加速详情页的爬取。from concurrent.futures import ThreadPoolExecutor, as_completed class LianJiaCrawlerConcurrent(LianJiaCrawler): def crawl_details_concurrently(self, detail_urls, max_workers=5): """并发爬取一批详情页""" with ThreadPoolExecutor(max_workers=max_workers) as executor: # 提交任务 future_to_url = {executor.submit(self._crawl_single_detail, url): url for url in detail_urls} # 获取结果 for future in as_completed(future_to_url): url = future_to_url[future] try: detail_data = future.result() if detail_data: self.all_house_data.append(detail_data) except Exception as exc: logging.error(f'详情页 {url} 爬取过程中产生异常: {exc}') self._save_to_csv() def _crawl_single_detail(self, detail_url): """包装单页爬取逻辑,供线程池调用""" detail_html = self.downloader.get_page(detail_url) if detail_html: return self.parser.parse_detail_page(detail_html) return None重要提醒:并发爬取是一把双刃剑。
- 优点:极大提升爬取速度。
- 缺点:对目标网站压力剧增,极易触发反爬机制(IP封禁、请求限制)。同时,请求间隔难以控制。
- 建议:务必谨慎使用。即使使用并发,也必须在
Downloader的get_page方法中保留全局性的、线程安全的延时控制(这需要更复杂的锁机制),或者使用更高级的限速队列。对于链家这类商业网站,强烈不建议开启过高并发(如超过3个线程),礼貌爬取是第一原则。
5.2 应对更复杂的反爬机制
如果基础的伪装和延时仍然导致频繁被封,可能需要考虑以下方案:
代理IP池:这是应对IP封锁最有效的方法。你需要一个稳定的代理IP来源(付费服务通常更可靠)。在
requests.get中通过proxies参数设置代理。proxies = { 'http': 'http://your-proxy-ip:port', 'https': 'https://your-proxy-ip:port', } response = self.session.get(url, headers=headers, proxies=proxies, timeout=10)需要实现一个代理IP管理器,负责IP的获取、验证、失效剔除和轮换。
浏览器自动化工具:对于JavaScript渲染严重或反爬极强的网站,
requests可能无法获取到有效内容。这时可以考虑Selenium或Playwright。它们能控制真实浏览器,可以执行JS,但速度慢、资源消耗大,应作为最后手段。from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options = webdriver.ChromeOptions() options.add_argument('--headless') # 无头模式,不显示浏览器窗口 driver = webdriver.Chrome(options=options) driver.get(url) # 等待特定元素加载出来 element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "main")) ) html = driver.page_source driver.quit() # 然后用BeautifulSoup解析html模拟登录:如果需要爬取“我的关注”等需要登录后才能查看的数据,就需要模拟登录。这通常涉及分析登录接口的POST请求,处理可能存在的Token、验证码等。成功登录后,使用
Session对象保持登录状态即可爬取受限页面。
6. 常见问题与排查技巧实录
在实际操作中,你一定会遇到各种各样的问题。下面是我踩过的一些坑和解决方法。
6.1 问题排查清单
问题现象 可能原因 排查步骤与解决方案 返回状态码403 IP被封锁、请求头不完整、Cookie无效。 1. 检查 User-Agent,Referer是否设置且合理。
2. 暂停爬虫,等待一段时间(如半小时)再试。
3. 更换代理IP。
4. 检查Session是否保持了必要的Cookie(可尝试手动浏览器登录后复制Cookie)。返回内容为空或极短 触发了反爬,返回了错误页或验证页。 1. 打印返回的HTML内容,查看是否是验证码页面或提示“访问过于频繁”的页面。
2. 大幅增加请求间隔时间(如10-30秒)。
3. 检查请求参数是否完整,某些页面可能需要特定的查询参数。解析不到数据,但浏览器能看到 页面结构已更新,或数据由JavaScript动态加载。 1. 使用浏览器开发者工具,对比当前爬取的HTML和浏览器中“查看网页源代码”的HTML,看目标数据是否存在。
2. 如果不存在,说明是JS加载。需要分析网络请求,找到真正的数据接口(通常是XHR请求),直接爬取接口URL(通常是JSON格式)。
3. 如果必须从渲染后页面获取,则需使用Selenium。数据字段错乱或缺失 解析规则写得太“死”,无法适应页面微小变化。 1. 使用更宽松的定位方式,如 re.compile匹配部分类名。
2. 增加更多的try...except块,对每个字段单独处理,避免一个解析失败导致整个条目丢失。
3. 定期运行测试用例,确保解析器对新页面依然有效。爬取速度越来越慢,最后停止 可能触发了网站的滑动验证码或更复杂的反爬策略。 1. 这是最棘手的情况。首先检查是否是IP被彻底封禁(换一个网络环境试试)。
2. 考虑引入更复杂的策略:降低并发数至1,使用高质量住宅代理IP,模拟更真实的人类行为(如随机滑动鼠标、随机浏览时间)。
3.评估成本与收益:对于链家,如果只是学习或小规模研究,遵守规则、低速爬取是唯一可持续的方式。大规模商业爬取面临极高的法律和技术风险。6.2 调试与日志记录
良好的日志是调试爬虫的生命线。不要只用
print。import logging import sys # 配置日志,同时输出到控制台和文件 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('crawler.log', encoding='utf-8'), logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger(__name__) # 在代码中不同级别记录 logger.debug(f"正在解析URL: {url}") # 详细信息,调试用 logger.info(f"成功爬取列表页,共找到 {len(items)} 个房源。") logger.warning(f"页面内容长度异常: {len(html)}") logger.error(f"请求失败: {url}, 错误: {e}")运行爬虫时,打开
crawler.log文件,结合控制台输出,可以清晰地看到爬虫的运行状态、成功和失败记录,方便定位问题。6.3 代码维护与可持续性
- 配置化:将城市列表、区域列表、请求头模板、数据库连接信息、爬取间隔等参数提取到配置文件(如
config.yaml或config.py)中,避免硬编码。 - 模块化:正如本项目设计,将下载、解析、存储、调度分离成独立模块,方便单独测试和替换。例如,更换解析引擎或存储后端时,只需修改对应模块。
- 异常处理:网络爬虫运行在复杂的环境中,必须假设一切外部调用都可能失败。为每一个网络请求、文件操作、数据库操作都加上异常处理,确保单点失败不会导致整个程序崩溃。
- 尊重
robots.txt:在爬取前,访问https://[城市].lianjia.com/robots.txt,查看网站允许和禁止爬取的路径。这是一个法律和道德上的最佳实践。
这个基于Python的链家爬虫项目,从设计到实现,涉及了网络请求、HTML解析、数据清洗、并发控制、反爬应对等多个知识点。它不是一个一劳永逸的工具,而是一个需要根据目标网站变化而持续维护的系统。最宝贵的收获不是最终的数据集,而是在解决一个个具体问题过程中积累的经验和对HTTP协议、Web技术的更深理解。记住,爬虫的世界里,稳健和礼貌远比速度重要。
本文还有配套的精品资源,点击获取