ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python爬虫实战:中国土地市场网公告数据采集与增量更新实现

2026/9/3 20:49:10 拓冰建站 浏览量
Python爬虫实战:中国土地市场网公告数据采集与增量更新实现 简介资源是一套面向Python爬虫学习者与土地数据研究者的实战项目围绕中国土地市场网土地公告公示信息实现自动化采集、解析与分布式存储。项目针对公告页面动态加载、多字段提取及大量链接去重等难点给出完整代码与配置方案帮助读者快速搭建属于自己的土地数据抓取流程。压缩包共7个文件大小约300KB包含2个Python爬虫脚本、3个XML工程配置、1个XLS格式的结果样例及1个IDE工程描述文件既能查看解析逻辑也能直接查看抓取后的结构化数据。代码中演示了如何利用常用爬虫库模拟浏览器获取页面通过选择器或XPath提取公告标题、时间、位置、面积等关键信息并将待抓取任务写入Redis队列实现多节点分布式协作与去重显著提升采集效率。目前已有2499人学习下载适合网络爬虫初学者进阶、土地研究人员获取数据样本以及想了解分布式爬虫架构的开发者学习参考。 算上列表明细光中国土地市场网一个站点的公告信息就能撑起一个区域的拿地情报分析。这个项目其实不复杂但绝对是个典型的“看着简单、做起来一堆细节”的爬虫工程。我花了一周左右把整套流程跑通从列表页到详情页从单次抓取到增量更新中间踩了不少坑这篇就把整个实现思路和完整代码拆开讲清楚。1.1 这个网站为什么值得爬中国土地市场网landchina.com主要发布国有建设用地使用权的出让公告、成交公示、供地计划等官方信息。对于做土地评估、房地产投资、市场研究的朋友来说这些数据是硬通货——比如一个区域近期有哪些地块挂牌、起拍价多少、最终成交楼面价多少都能直接反映市场热度。人工一条条翻网页不是不行但数据量大、更新频繁而且公告字段多地块编号、坐落位置、出让面积、容积率、起始价、保证金等复制粘贴容易出错一套可复用的爬虫脚本能省下大量重复劳动。1.2 项目范围与预期产出这次的目标很明确把网站上“土地出让公告”和“成交公示”两类核心信息抓下来保存为结构化数据。字段至少包括公告标题、公告类型、行政区、发布时间、公告链接、地块编号、出让面积、起始价、成交价等。产出物是 CSV 文件和 SQLite 数据库方便后续用 Excel 透视表或者直接 SQL 查询做分析。整个过程基于 Python 3.9 requests parsel 实现不需要 Selenium因为目标站点的数据是服务端渲染的直接请求 HTML 就能拿到内容效率更高。2. 技术选型与合规边界2.1 为什么选 requests parsel 而不是 Scrapy很多人一上来就上 Scrapy但我觉得这个项目用 requests parsel 反而更顺手。原因是目标网站结构相对规整不需要分布式爬取单机单线程足够用 Scrapy 的中间件、Pipeline 反而增加学习成本。requests 负责发 HTTP 请求parsel 负责解析 HTML它的 XPath 选择器跟 Scrapy 是同源的以后真要换 Scrapy解析逻辑可以直接迁移。对于这种中小规模采集任务轻量组合更灵活。2.2 合规性提醒别碰红线爬虫合规这块务必要重视。中国土地市场网的数据本身是政府公开公示信息目的在于公众查询和监督爬取公开公告内容用于个人研究、行业分析是没问题的但需要注意三件事请求频率务必克制。建议每次请求间隔 2~3 秒不要短时间高频并发以免对服务器造成压力。不采集非公开数据。登录后才可见的接口、后台管理接口一律不碰。只爬公开页面。数据使用需自律。抓下来的数据用于个人学习和市场分析不要用于商业转售或侵犯网站权益的用途。提示爬虫工具本身是中性的关键看使用方式和目的。遵守 robots.txt、设置合理限速、尊重目标站点服务能力是每个爬虫开发者应有的基本素养。3. 站点结构分析与核心字段提取3.1 分析列表页 URL 规律打开中国土地市场网找到“土地供应计划”或“出让公告”栏目后能看到一个列表页展示公告标题、所在地区、公告类型、发布时间等信息。直接浏览器的开发者工具F12看网络请求会发现列表页的 URL 是带查询参数的核心参数一般包括当前页数page每页条数limit行政区编码region公告类型category这意味着列表页的翻页非常方便——只需要循环修改 page 参数就能拿到所有分页数据。这一点对爬虫来说是最理想的情况不需要模拟点击不需要处理复杂的 JavaScript 渲染。3.2 详情页是数据金矿列表页只有标题和发布时间真正的核心数据在详情页里包括地块编号、土地面积、容积率、建筑密度、起始价、保证金、成交价等关键字段。所以爬虫思路是第一步抓列表页拿到公告链接第二步逐个访问详情页抽取结构化字段第三步汇总存储。这个“两段式”设计几乎是所有公告类网站的通用打法。3.3 字段映射表字段名列表页可取详情页可取说明公告标题是是用于去重和展示公告类型是是出让公告 / 成交公示所在地区是是省市区级联用行政区字段发布时间是是列表页就有的摘要时间公告链接是否作为唯一标识地块编号否是详情页核心字段出让面积否是带单位需清洗起始价否是金额字段需转型容积率否是部分公告里有成交价否是仅成交公示有表中可以看到列表页承担“发现”详情页承担“提取”两者配合才能拿到完整的数据。4. 核心代码实现从列表到详情的完整链路4.1 请求 session 与公共头设置import requests from parsel import Selector import time import pandas as pd import sqlite3 import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.landchina.com/, } session requests.Session() session.headers.update(HEADERS)这里统一用一个 Session 对象有三个好处自动携带 Cookie、保持连接复用TCP 连接复用能减少握手开销、header 统一管理。User-Agent 务必设置成常见浏览器的完整 UA否则部分反爬策略会直接拒绝响应。Referer 也建议设置模拟从首页点进来的正常访问路径。4.2 列表页抓取与翻页控制def fetch_list_page(page): list_url https://www.landchina.com/default.aspx params { tabid: 261, paging: str(page), page: str(page), gonggaoleixing: , province: , city: , district: , } resp session.get(list_url, paramsparams, timeout15) resp.encoding utf-8 if resp.status_code ! 200: print(f第{page}页请求失败状态码: {resp.status_code}) return [] sel Selector(resp.text) items [] # 这里需要根据实际页面结构调整选择器 rows sel.xpath(//table[contains(class, table)]/tr) for row in rows: title row.xpath(.//a/text()).get().strip() link row.xpath(.//a/href).get().strip() publish_time row.xpath(.//td[last()]/text()).get().strip() items.append({ title: title, link: https://www.landchina.com link, publish_time: publish_time, }) return itemsXPath 选择器必须根据目标站点实际 HTML 结构调整。我第一次跑的时候就是直接照搬想象的结构结果解析出来全是空列表。调试技巧很简单先用浏览器把列表页源码复制下来用 parsel 慢慢调 XPath确认能拿到数据再写循环。这里分享一个心得//table[contains(class, table)]/tr这种写法可以避免 class 名称的精确匹配问题因为很多网站的 class 会有动态后缀。翻页控制上建议加一个动态间隔避免固定频率被识别for page in range(1, 21): items fetch_list_page(page) # 处理当前页... time.sleep(random.uniform(2.0, 4.0))不要小看这个随机延时。固定间隔 2 秒的请求模式在服务端日志里看就是一串规律电波很容易被识别为自动化脚本而 2~4 秒的随机抖动更接近人工浏览的节奏。另外如果某一页返回空数据建议直接中断分页循环因为公告类网站的分页一般是有终点的。4.3 详情页解析与字段清洗详情页的解析是整个项目的核心。公告详情页的结构通常是一个信息表格左侧是字段名右侧是字段值。这种结构非常适合用 XPath 的属性配对逻辑来提取。def fetch_detail(url): resp session.get(url, timeout15) resp.encoding utf-8 sel Selector(resp.text) result {url: url} # 公告标题 result[title] sel.xpath(//h1/text() | //div[contains(class, title)]/text()).get().strip() # 以地块编号为例抓取表格中对应行 row sel.xpath(//tr[td//text()[contains(., 地块编号)]]) if row: result[block_no] row.xpath(.//td[2]//text()).get().strip() # 出让面积 row sel.xpath(//tr[td//text()[contains(., 出让面积)]]) if row: result[area] row.xpath(.//td[2]//text()).get().strip() # 起始价 row sel.xpath(//tr[td//text()[contains(., 起始价)]]) if row: result[start_price] row.xpath(.//td[2]//text()).get().strip() # 成交价 row sel.xpath(//tr[td//text()[contains(., 成交价)]]) if row: result[deal_price] row.xpath(.//td[2]//text()).get().strip() return result这个“按字段名定位行取第二列值”的思路能够极大提高字段提取的鲁棒性。即使字段顺序换了只要表格结构是“名称-值”模式就不会出错。但这里有一个隐藏的坑contains(., 地块编号)可能匹配到“地块编号”和“地块编号2”两个节点导致取到的值不对。解决办法是用精确匹配或者检查结果是否包含逗号、单位等特征再进行正则清洗。4.4 字段清洗函数def clean_area(text): 清洗面积字段保留数字 import re match re.search(r(\d\.?\d*), text.replace(,, ).replace(, )) return float(match.group(1)) if match else None def clean_price(text): 清洗价格字段单位可能为万元 match re.search(r(\d\.?\d*), text.replace(,, ).replace(, )) if match: return float(match.group(1)) return None清洗逻辑看起来简单但实际踩坑最多的地方就在这。因为网站上很多数字是全角逗号或中文逗号分隔比如“4,520.00”中间那个逗号是英文的但“4520.00”中间可能是中文逗号、甚至是全角空格。所以清洗的第一步一定是把各种逗号统一替换掉再做正则匹配。价格字段的单位也要注意有的是“万元”有的是“元/平方米”清洗时至少要把单位原样保留一份避免后续分析时搞混量纲。5. 数据存储与增量更新5.1 SQLite 建表与插入数据量不大没必要上 MySQLSQLite 单文件就能解决方便迁移和备份。建表语句如下conn sqlite3.connect(land_data.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS land_announcement ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, block_no TEXT, area REAL, start_price REAL, deal_price REAL, publish_time TEXT, url TEXT UNIQUE, crawl_time TEXT ) ) conn.commit()URL 字段加 UNIQUE 约束是实现增量更新的关键。每次插入前直接用INSERT OR IGNORE如果 URL 已存在就自动跳过这样重复抓取同一页不会产生脏数据。这个方案比“先查再插”的写法规整很多也更快。5.2 增量爬取策略增量更新的核心问题是如何知道哪些公告是新增的哪些已经入库我的做法是维护一个“已抓取链接”集合每次跑之前先查数据库已有的 URL然后只处理新链接。但还有一种更轻量的方式按发布时间过滤。列表页上有公告发布时间只要记录上次爬取的时间点只抓这个时间之后发布的公告即可。def get_existing_urls(cursor): cursor.execute(SELECT url FROM land_announcement) return set(row[0] for row in cursor.fetchall()) existing get_existing_urls(cursor) for item in items: if item[link] not in existing: detail fetch_detail(item[link]) save_to_db(detail)如果列表页有明确的发布时间建议直接按“今天发布”的公告来筛这样每天定时跑一次增量更新效率极高不会爬到重复内容。5.3 定时任务自动化手动跑脚本没意义真正有价值的是每天自动抓取。我用系统的 crontab 实现定时调度每天上午 9 点定时爬取前一天的公告数据0 9 * * * cd /path/to/project /usr/bin/python3 crawler.py crawler.log 21Windows 环境下可以用任务计划程序或者直接用 APScheduler 在 Python 内部实现定时调度。我实际跑的时候用的是 crontab因为服务器是 Linux 云主机日志重定向也方便排查问题。6. 常见问题与排查技巧实录6.1 问题速查表现象可能原因解决方法返回 403 或 302请求头不全被 WAF 拦截补全 UA、Referer检查是否触发频率限制列表页解析为空页面结构调整XPath 失效用真实 HTML 调试选择器不要猜结构详情页某些字段缺失部分公告不包含该字段用.get()兜底加 try/except中文乱码站点更新了编码声明先resp.apparent_encoding检测再强制指定抓了几页后报超时频率过高被限流增加随机 sleep必要时换代理 IP数据重复缺少去重机制给 URL 字段加 UNIQUE 约束6.2 实战踩坑动态加载导致的“假列表”我自己踩过一个大坑以为列表页是纯静态的结果仔细看网络请求才发现翻页用的参数不是普通的page而是 POST 请求体里的一个加密字段。当然这是稍复杂的情况后来我通过在页面底部找到分页参数规律把 POST 改成对应的参数模拟同样能解决。核心经验是不要想当然认为网站结构跟你预期一致一切以浏览器的网络请求面板为准。我第一次写的时候没有看网络请求就直接用 GET 请求访问列表页结果返回的 HTML 里确实有表格但翻页怎么都不生效原因就是实际翻页走的是 POST 接口。后来老老实实打开 F12点翻页按钮看请求方式、请求头和表单数据才找到了正确路径。6.3 如何应对页面结构调整这应该是所有爬虫项目逃不开的宿命网站改版后原来的 XPath 全部失效。应对思路有三个层次选择器等定位逻辑尽量写宽泛一些比如用contains(class, title)而不是精确匹配classtitle这样页面微调时不容易挂。记录抓取日志和失败率。比如每隔一段时间统计解析为空的详情页数量一旦异常增多立刻检查选择器是否需要更新。核心字段提取失败时直接把详情页 HTML 存一份快照方便事后离线调试不至于重跑整个流程。我专门写了一个 debug 函数当某个字段解析不到时把整个 HTML 落盘到debug/目录文件名带上时间戳。这样即使第二天醒来看代码也能快速定位问题。跑完整个流程后我自己留了一套可以复用的爬虫模板列表页用统一的翻页循环详情页用字段-值表格的通用解析函数入库统一走INSERT OR IGNORE。这种结构在应对其他政府公开信息网站时也很通用——规划公示、招投标公告、成交信息公开基本都能套用同一套打法。最后提醒一点爬虫代码写好之后不要只顾着跑一定给每个请求设置超时和重试否则任何一次网络抖动都会让整个任务挂掉。多踩几次坑把这些边界情况都补上之后这个爬虫才能真正稳定地每天自动运行变成一个可靠的数据源。本文还有配套的精品资源点击获取