ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

链家二手房爬取与MySQL存储:数据库课程设计完整实战

链家二手房爬取与MySQL存储:数据库课程设计完整实战 简介这份基于Python的链家二手房信息爬取与数据库存储设计源码面向希望学习网络爬虫与数据持久化的Python初学者也适合二手房产数据研究及课程设计场景。系统能够自动抓取链家网二手房列表中的标题、价格、地理位置等核心字段将结构化数据写入SQL数据库并把对应房屋图片保存到本地实现从采集到存储的完整链路。压缩包共33个文件含30张JPG图片、1个Python主脚本、1个SQL建表与导入脚本以及1个TXT说明文档整体约816KB结构清晰便于直接阅读与二次修改。已有173人学习下载。通过readme说明可快速部署运行调整SQL表结构和抓取字段即可适配不同需求既可用于小规模二手房信息备份也是理解requests解析、数据库交互等环节的实战参考。1. 链家二手房爬取与数据库存储这门数据库课程设计的正确拆法链家二手房最近成了数据库课程设计里的高频选题原因很直白房源列表自带价格、小区、户型、朝向、挂牌时间这类结构化字段入库后能直接跑聚合查询和排序爬取难度又刚好卡在入门与进阶之间。拿豆瓣电影top250练手会觉得太静态换懂车帝二手车要处理接口签名直接上boss直聘还会撞上token逆向链家拦人的方式主要是请求头校验和访问频次限制属于调参就能过的那一档。这篇文章按“基于Python的链家二手房信息爬取与数据库存储设计源码”这个题目的常规做法走一遍Python环境怎么配、列表页和详情页各取哪些字段、MySQL表怎么建、批量写入怎么去重和防止乱码最后补上入库后的校验手段整套代码可以直接搬进自己的课程设计项目。2. Python安装与请求前置Session复用、UA伪装和访问节奏2.1 用VSCode配置Python虚拟环境并装齐依赖Python版本选3.9以上即可不需要追新版本3.11最稳。装完Python之后打开VSCode装好Python扩展在项目目录里建虚拟环境并激活python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install requests beautifulsoup4 lxml pymysql fake-useragentrequests负责HTTP请求beautifulsoup4做页面解析lxml是bs4背后的C解析器pymysql用来连MySQLfake-useragent用来生成浏览器User-Agent。这里有个常见疏漏只装beautifulsoup4不装lxml解析时会退回python内置解析器链家页面本身是完整HTML不出错但速度明显慢所以五个依赖最好一次装齐。虚拟环境激活后先确认版本避免后续因为Python或pip版本差异多绕弯python -V pip listpip list输出里能看到requests、beautifulsoup4、lxml就说明环境就绪。Windows终端没有grep命令直接用pip list查看最省事。这里顺便说明一个选择不要用scrapy起步只做课程设计或几千条数据的小规模抓取requests加四个轻量依赖更容易写在论文里也更好debug。2.2 三个必调请求参数User-Agent、Referer和Cookie链家对请求头的校验比普通静态站严格但又没做到指纹识别。一个最近版本的ChromeUA、一个合理的Referer、一个完整Cookie就能绕过绝大多数请求头层面的拦截。参数常用取值作用User-AgentChrome或Safari开头的完整UA应付请求头校验不能带Python-requests字样Referer当前城市的链家二手房列表首页声明访问来源避免被当站外采集Cookielianjia_uuid、select_city等维持会话身份应对访问频率限制封装Session的常见做法如下import requests from fake_useragent import UserAgent def build_session() - requests.Session: # fallback 参数防止 fake_useragent 的在线数据源偶发抽风 ua UserAgent(fallbackMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36) session requests.Session() session.headers.update({ User-Agent: ua.random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Connection: keep-alive, Referer: https://sh.lianjia.com/ershoufang/, }) return sessionua.random每次请求随机取一个UA配合Session的keep-alive连接复用比手写固定UA更像浏览器。Referer这里填的是上海链家二手房首页如果抓北京就把域名换成bj.lianjia.com。注意Session只在一次运行内复用UA同一个UA连续请求几十页也会被识别所以推荐在翻页循环里每十次请求重新调用一次ua.random更新header。Cookie是最容易踩坑的一项。纯requests第一次访问链家能拿到200翻到第三五页却突然跳验证页多半是Cookie缺了。我一般会在浏览器无痕窗口手工打开一次链家二手房列表F12把Application里的Cookie整体复制直接塞进请求头session.headers[Cookie] lianjia_uuidxxx; select_city310000; l2_...;不要用requests的cookies参数逐条传直接放header里最省事也不会因为个别cookie的格式差异导致校验失败。2.3 随机sleep与分页节奏每页至少停两秒把抓取间隔设成固定2秒并不是好习惯请求间隔固定且数量均匀的访问从统计上看反而更容易被识别成脚本。常见做法是设一个区间用random.uniform生成浮动值import random import time def polite_wait(): # 每页之间休息区间不必太长但一定要随机 time.sleep(random.uniform(2.0, 5.5)) def get_page(session, url, retries3): for attempt in range(retries): try: resp session.get(url, timeout10) if resp.status_code 200 and sellListContent in resp.text: return resp.text except requests.RequestException: pass polite_wait() return Noneget_page返回None时不要继续翻页直接中断这一轮的抓取。链家二手房每个城市最多能翻100页比如上海大约3000条房源按每页30条、每页3.5秒算一轮抓完接近6分钟但持续不断气的请求一定触发频控。我在翻页循环里的策略是每页结束后立刻sleep连续两个超时请求就结束当前城市抓取并记录日志。另外get_page里的超时参数一定要写不写timeout的requests请求可能挂到几十分钟。3. 链家二手房页面解析列表页提字段详情页补数据3.1 房源卡片怎么定位BS4的select与.sellListContent列表页URL格式是https://sh.lianjia.com/ershoufang/pg2/第一页可以省略pg1。页面上每个房源是一个li挂在ul classsellListContent下每个li里有title、positionInfo、houseInfo、totalPrice四个区块。先用select把页面里所有房源li抽出再针对每个区块解析。from bs4 import BeautifulSoup def parse_list_html(html: str) - list[dict]: soup BeautifulSoup(html, lxml) items [] # .sellListContent 是链家列表容器li 是单个房源卡片 for li in soup.select(.sellListContent li): item {} title_a li.select_one(.title a) item[title] title_a.get_text(stripTrue) if title_a else None # house_id 从详情链接末尾解析这是后面去重的唯一键 item[house_id] None if title_a and href in title_a.attrs: href title_a[href] item[house_id] href.rstrip(/).split(/)[-1].replace(.html, ) # positionInfo 里通常有行政区和小区两个及以上链接 pos_links li.select(.positionInfo a) if pos_links: text_list [a.get_text(stripTrue) for a in pos_links] item[district] text_list[0] item[bizcircle] text_list[1] if len(text_list) 1 else None item[community] text_list[-1] else: item[district] item[bizcircle] item[community] None # houseInfo 的字段顺序之前变过用关键词定位比固定下标稳 parts li.select_one(.houseInfo).get_text( , stripTrue).split(|) if li.select_one(.houseInfo) else [] def pick(keyword): return next((p.strip() for p in parts if keyword in p), None) item[layout] pick(卫) # 2室1厅1卫 item[area] pick(平米) # 88.12平米 item[floor_info] pick(楼层) # 中楼层/共19层 item[orientation] pick(南) or pick(北) or pick(东) or pick(西) # 装修单独匹配避免和 positionInfo 里的文字混在一起 decoration next((p.strip() for p in parts if p.strip() in {精装, 简装, 毛坯, 豪装}), None) item[decoration] decoration # 总价区域有一个 span 保存数字单位是万 total_span li.select_one(.totalPrice span) total_text total_span.get_text(stripTrue) if total_span else None item[total_price] float(total_text) if total_text else None items.append(item) return items这段代码里最关键的两个点一是house_id从详情链接取链家的详情链接形如https://sh.lianjia.com/ershoufang/107103842443.html末尾那段纯数字就是全局唯一编号二是houseInfo的字段顺序不要写死链家改过页面结构早年的版本是楼型开头新版变成户型开头用pick(keyword)按关键词搜索字段顺序变了也不会取错。area字段建议在存入数据库前转成float保留两位小数。3.2 详情页需要单独抓的字段关注人数、挂牌时间、楼栋信息列表页能拿到房源标题、行政区、小区、户型、面积、朝向、装修、楼层、总价这九项但数据库课程设计如果想做得更完整通常还会补详情页里的挂牌时间和关注人数。这两个字段在列表页拿不到需要单独请求详情页面。import re def parse_detail_html(html: str) - dict: soup BeautifulSoup(html, lxml) item {} follow_match re.search(r(\d)人关注, html) if follow_match: item[follow_count] int(follow_match.group(1)) # 基本信息区用 label 做键值匹配页面结构变化时只丢单字段 for li in soup.select(.baseattribute li): label li.select_one(.label) content li.select_one(.content) if not label or not content: continue key label.get_text(stripTrue).rstrip(:) value content.get_text(stripTrue) if key 挂牌时间: item[listing_date] value elif key 建筑年代: item[build_year] int(value) if value.isdigit() else None return itemfollow_count用正则直接从HTML源码里抓“数字人关注”比遍历DOM节点更快。挂牌时间和建筑年代则遍历.baseattribute li按label的文本匹配。这里不建议用固定索引因为详情页的明细行顺序偶尔会有差异。详情页请求频率要比列表页更保守一个li配一个详情页会使请求量放大三十倍我在课程设计里的做法是只采集前500条房源的详情页其余字段留空论文里说明采样范围即可。3.3 find与select的取舍页面结构变化时的兜底方案select基于CSS选择器写起来简洁但链家调整HTML层级时比如.totalPrice span改成.totalPrice span选择器会静默返回空。find和find_all基于标签和属性名匹配灵活性低但对层级关系相对宽容。我的兜底写法是解析主流程用select关键字段出现缺失时立刻切find验证if not items: # 反爬页通常没有游? .sellListContent判定为风控后直接返回 return [] soup_check BeautifulSoup(html, lxml) if not soup_check.find(ul, class_sellListContent): return []列表页解析出全部空字段或者items长度明显小于每页30条说明DOM选择器失效审计日志就要记录“parse_mismatch”。另外每次运行前先手工看一眼最新版网页源码里的class名比长期信任一段固定选择器可靠得多。4. 数据库存储设计MySQL表结构、批量写入与唯一键去重4.1 按课程设计要求建表字段、索引、唯一约束MySQL建表是数据库课程设计的核心评审项字段类型、主键、唯一键、索引都要能在答辩时说清楚。链家二手房抓取结果的表结构按下面这种形态设计CREATE DATABASE IF NOT EXISTS lianjia_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE lianjia_db; CREATE TABLE IF NOT EXISTS ershoufang ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 自增主键, house_id VARCHAR(20) NOT NULL COMMENT 链家房源编号抓取去重的唯一键, title VARCHAR(120) DEFAULT NULL COMMENT 挂牌标题, district VARCHAR(50) DEFAULT NULL COMMENT 行政区, bizcircle VARCHAR(50) DEFAULT NULL COMMENT 商圈, community VARCHAR(80) DEFAULT NULL COMMENT 小区名称, layout VARCHAR(50) DEFAULT NULL COMMENT 户型如2室1厅1卫, area DECIMAL(6,2) DEFAULT NULL COMMENT 建筑面积单位平米, orientation VARCHAR(20) DEFAULT NULL COMMENT 朝向, decoration VARCHAR(20) DEFAULT NULL COMMENT 装修情况, floor_info VARCHAR(60) DEFAULT NULL COMMENT 楼层描述, build_year SMALLINT UNSIGNED DEFAULT NULL COMMENT 建筑年代, total_price DECIMAL(9,2) DEFAULT NULL COMMENT 总价单位万元, unit_price DECIMAL(7,2) DEFAULT NULL COMMENT 单价单位元/平米, listing_date DATE DEFAULT NULL COMMENT 挂牌时间, follow_count INT UNSIGNED DEFAULT NULL COMMENT 关注人数, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 最近更新时间, PRIMARY KEY (id), UNIQUE KEY uk_house_id (house_id), KEY idx_district (district), KEY idx_total_price (total_price) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT链家二手房抓取结果表;字段类型约束说明house_idVARCHAR(20)房源唯一编号自动去重的核心areaDECIMAL(6,2)面积保留两位小数避免浮点误差total_priceDECIMAL(9,2)大户型总价会到千万9位够了create_timeDATETIME入库时间自动填充update_timeDATETIME更新时时间配合价格变更追踪house_id设UNIQUE KEY是最关键的设计决策。每次抓取结果里同一套房源重复出现时可以直接交给MySQL判断重复不需要在Python里维护一个已见集合。created_time设为DEFAULT CURRENT_TIMESTAMP每条数据落库时自动写时间少写一行插入语句。为什么不用url做主键链家房源短链有时带utm参数不同tracking参数导致URL不同但房源是同一套只有去掉参数的house_id才能稳定去重。4.2 pymysql参数化执行与批量写入代码pymysql是纯Python实现的MySQL驱动课程设计环境里比mysql-connector更轻量。连接数据库时charset这一项必须传utf8mb4不传的话中文会以latin1编码写入导致乱码。import pymysql def save_items(items: list[dict], batch_size: int 50) - int: conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databaselianjia_db, charsetutf8mb4, autocommitFalse, ) # 只保留有 house_id 的数据防止脏行入库 rows [ ( it[house_id], it[title], it[district], it[bizcircle], it[community], it[layout], it[area], it[orientation], it[decoration], it[floor_info], it[build_year], it[total_price], ) for it in items if it.get(house_id) ] sql INSERT INTO ershoufang (house_id, title, district, bizcircle, community, layout, area, orientation, decoration, floor_info, build_year, total_price) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE title VALUES(title), total_price VALUES(total_price), update_time CURRENT_TIMESTAMP affected 0 with conn.cursor() as cursor: for i in range(0, len(rows), batch_size): # 分批执行避免一次提交太多参数导致SQL包过大 affected cursor.executemany(sql, rows[i:i batch_size]) conn.commit() conn.close() return affected参数化这里的意义是防止SQL注入课程设计答辩被问“为什么不用字符串拼接”时直接回答参数化能避免字段里的单引号破坏SQL结构。executemany内部会把数据分批绑定到预编译语句上网络交互比循环execute少很多。50条一批是性能和内存的平衡点一次几百条也可以但超过上千条时SQL包变大反而容易触发MySQL的max_allowed_packet限制。这里补充一个MySQL版本相关的细节8.0.20起VALUES()被标记为deprecated虽然目前还能用但答辩时如果用的是MySQL 8.0.20以上版本建议写成INSERT ... AS new ON DUPLICATE KEY UPDATE title new.title的新语法。MySQL 5.7则完全兼容VALUES()写法大多数课程设计环境都没问题。4.3 重复抓取的处理跟价更新而不是插入新行ON DUPLICATE KEY UPDATE在这里承担了“跟价”功能。同一套房源再次抓到house_id已存在MySQL执行UPDATE而不是INSERT。上面SQL里update_time CURRENT_TIMESTAMP的写法配合表的ON UPDATE CURRENT_TIMESTAMP效果一样写在SQL里更明确查询这条记录有没有在我们这次抓取中更新直接看update_time即可。如果课程设计要求的只是“不重复入库”不需要跟踪价格变化可以把ON DUPLICATE KEY UPDATE整段换成INSERT IGNORE。两者的差异是INSERT IGNORE遇到重复键直接丢弃新数据更新时间不变ON DUPLICATE KEY UPDATE会覆盖旧值。做爬虫库的增量更新一般倾向后者。pymysql事务的细节也值得注意connect时设置autocommitFalse一批抓取全部执行后再conn.commit()中途失败时数据不会半截入库。执行完成后affected返回值是插入和更新行数的总和抽样核对数据量时可以直接打印这个值。5. 链家二手房入库后的正确性校验三条最容易被忽略的检查点5.1 先看总数是否一致再看峰值归属抓完一批数据第一步不是急着写报告而是核对总数mysql -h localhost -u root -p lianjia_db -e \ SELECT COUNT(*) AS total, COUNT(DISTINCT house_id) AS unique_count FROM ershoufang;total和unique_count不一致说明去重逻辑失效通常是house_id没解析出来去检查列表页链接格式是否变化。链家一个城市最多100页约3000条房源如果库里的总数远小于这个值说明中途踩了风控导致提前退出不是正常结束。5.2 按行政区聚合验证字段有没有串位SELECT district, COUNT(*) AS cnt, ROUND(AVG(total_price), 2) AS avg_price, ROUND(AVG(area), 2) AS avg_area FROM ershoufang GROUP BY district ORDER BY cnt DESC;行政区和板块的平均价如果出现明显的数量级异常比如某个商圈均价显示成几万元多半是解析阶段字段串位把总价和单价串了。链家列表页同时有总价和每平米单价我见过不少人解析时把.unitPrice的文本存进total_price导致均价和真实房价对不上。5.3 抽样比对详情页确认两个被忽略字段抽查三到五套房源连着点开线上详情页手工比对朝向、挂牌时间、跟进人数。这三个字段最容易出现解析偏差朝向可能被拆成南北两个挂牌时间的格式在链家内部也有“2025-01-05”和“2025.01.05”两种入库前统一转成%Y-%m-%d格式用DATE类型存后面做按月份分组统计时才不会出错。比对完成后在日志文件里随机打印几条完整记录留下审计痕迹。链家二手房这条链路的完整度其实比大多数课程设计要求高出一截从requests请求到BeautifulSoup解析再到pymysql入库每个环节都有独立的错误处理答辩时可以按“请求层—解析层—存储层”逐层展开遇到评审问哪个字段丢了直接说这轮以列表页为主详情页只补了挂牌时间和关注人数能自圆其说就算合格。# 校验脚本最后一段打印每条抽样房源的完整信息 import json with open(sample_check.json, r, encodingutf-8) as f: sample json.load(f) for r in sample: print(r[house_id], r[district], r[community], r[layout], r[area], r[total_price])本文还有配套的精品资源点击获取
返回列表