
简介基于landchina的爬虫项目聚焦土地市场结果公告及市场交易数据的自动采集适合爬虫入门与Java综合项目实践尤其适用于毕业设计、课程设计场景。资源共178个文件压缩包约2.13MB包含45个Python脚本、36个编译后的pyc文件、33个XML配置、11个XLS表格及8个CSV数据文件等脚本负责抓取解析表格用于存储结果配置与文档说明完整。项目代码经过测试运行成功并附有详细文档方便对照理解抓取流程、字段结构及异常处理思路已有72人学习下载适合作为Java方向爬虫项目的参考模板也可在现有基础上二次开发。对于希望快速搭建土地数据采集演示或储备项目经验的学习者项目能节省大量前期调研与排错时间。1. landchina 爬虫要抓的「结果公告」与「市场交易」是两个什么入口做城市土地出让分析时最耗时的不是建模而是把 landchina 上的公告一条条攒下来。手动翻列表页不仅慢结果公告和市场交易两个入口的查询条件还不一样截图存 Excel 的方式撑不过一千条就乱套。“选项资料齐全”指的是把两个入口的下拉条件、隐藏表单字段、分页参数一次性配齐让爬虫在任意行政区、任意时间范围、任意交易方式下都能稳定翻页“文档详细”则要求交付物不止有代码还要有参数清单、数据字典和报错对照三个月后接手的人依然改得动。这套爬虫适合三类人地产投资岗想批量拿地数据做比价数据分析师需要把公告解析成结构化字段以及接单写 python 爬虫的工程师——后者尤其需要“选项资料齐全文档详细”这种能直接交给甲方的形态。下面按我平时做这类 requests 爬虫的顺序把表单参数、请求差异、详情解析、容错与交付逐层讲清楚每一步都给出可以直接抄的代码和参数说明。2. 爬 landchina 前先补齐表单参数与选项下拉的依赖2.1 为什么选项资料是 landchina 爬虫的第一道坎landchina 是 ASP.NET 系站点查询页是一个服务端表单翻页和筛选本质都是 POST 回传。这类页面的特点是除了真正关心的查询条件行政区、用途、公告类型、时间范围表单里还藏着一批动态字段常见的是__VIEWSTATE、__VIEWSTATEGENERATOR、__EVENTVALIDATION由服务端在下一次 POST 前生成并校验。如果只带日期和关键字就提交最常见的现象是返回的 HTML 仍是默认查询结果或者直接提示“查询条件错误”。不少新手在这里误判成被限流其实是没有把隐藏字段原样回传。所谓“选项资料齐全”第一步就是把查询页里的 hidden input 全部抓下来连同选中的下拉选项一起 POST 回去缺一个都可能翻不了页。2.1.1 用 requests 抓取查询页里的全部 hidden 字段常见做法是先 GET 查询页解析所有input typehidden提交时原样带上import requests from lxml import etree def get_hidden_fields(session: requests.Session, page_url: str) - dict: GET 查询页提取表单中的 hidden 字段返回参数字典 resp session.get(page_url, timeout15) resp.raise_for_status() html etree.HTML(resp.text) fields {} # 抓页面里所有隐藏字段包括 __VIEWSTATE、__EVENTVALIDATION 等 for inp in html.xpath(//input[typehidden]): name inp.get(name) value inp.get(value, ) if name: fields[name] value return fields这段代码的逻辑是先用同一个session访问查询页拿到最新的隐藏字段再在提交时把fields合并进 POST 数据。注意不要从浏览器里复制value写死到代码里__VIEWSTATE每次会话都可能变化写死的脚本第二天就会失效。2.2 把下拉选项做成参数表而不是堆在 if 里“选项资料齐全”的第二层含义是下拉选项本身。结果公告页的公告类型、行政区、用途市场交易页的交易方式、资源类型每个下拉框的name和可选值都可以人工切一遍从浏览器 DevTools 的 Network 面板里记录真实的 POST 体。我更推荐在项目里维护一张参数表把每个入口的选项名和取值固化下来参数角色入口取值特征失败时的表现__VIEWSTATE等隐藏字段两个入口共用每次 GET 动态生成漏带时返回默认列表或参数错误入口标识TAB两个入口共用固定字符串缺了会串到另一个栏目查询条件condition两个入口共用JSON 串或拼接串格式不正确时列表为空用途/行政区编码两个入口共用下拉选项值编码错误时无数据或报错交易方式市场交易下拉选项值缺省时包含全部方式结果膨胀页码参数两个入口共用自增数字从 1 开始超出范围返回空页上面这组参数名以页面当前实际字段为准不必逐字对应但结构是稳定的隐藏字段做底座固定值区分入口下拉选项决定过滤范围页码控制翻页。把它们提取成配置后新增“按工业用地抓市场交易”的需求只需加一组参数值解析和入库逻辑完全不用动。2.3 用 Session 保持会话避免换页就丢选项手动浏览时登录态和会话由 Cookie 维持爬虫里对应的是requests.Session()。所有 GET 和 POST 都用同一个 Session 对象服务端才会认为你是在连续操作同一个浏览器会话。session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.landchina.com/, }) # 先拿隐藏字段再带字段去翻页 hidden get_hidden_fields(session, QUERY_PAGE_URL) data {**hidden, TAB: result_announcement, pageNo: 1} resp session.post(QUERY_PAGE_URL, datadata, timeout15)这里User-Agent和Referer是常规请求头属于模拟正常浏览器访问公开数据的标准做法。真正容易踩的坑是每次翻页都重新 GET 一遍查询页——这虽然能拿到新的隐藏字段却也清空了当前查询条件导致第二页起结果和第一页对不上。正确做法是首次请求拿 hidden 字段后续翻页只更新pageNo其他参数保持原样直到列表抓完再重新触发新一轮查询。3. 切换 landchina 结果公告与市场交易请求差异与详情解析3.1 先分清两个入口在业务语义上的差别结果公告对应已成交的出让结果核心信息是竞得人、成交价格、成交时间市场交易对应正在公告期或交易过程中的地块核心信息是起始价、保证金、报名截止时间。两者虽然都叫“地块列表”但详情页字段差别很大解析规则必须分开写。如果共用同一套解析很容易出现结果公告抓下来的成交价格正常切到市场交易后该字段变成空值或默认值。业务语义决定解析分支这是“选项资料齐全”之外容易被忽略的一环。3.2 两个入口的 POST 差异对照在统一封装请求之前先把差异做成对照表实现时按入口选择参数即可维度结果公告市场交易列表语义已成交的出让结果公告中/交易中的地块关键过滤条件公告类型、成交方式交易方式、资源类型、公告期详情页必采字段竞得人、成交价格、成交日期起始价、保证金、报名截止时间时间字段含义成交时间公告/挂牌开始时间翻页参数结构共用同一套分页参数与左侧结构一致抓取时我一般把两个入口配置成两个数据源对象每个对象维护自己的 POST 参数模板和解析函数但共用同一个请求函数。这样代码不重复业务差异又各自隔离。3.3 统一封装列表页请求函数def post_list_page(session, base_params: dict, page_no: int, entry: str): 按入口和页码拼接 POST 数据并请求列表页 if entry result: # 结果公告 params {**base_params, TAB: result, pageNo: str(page_no)} elif entry market: # 市场交易 params {**base_params, TAB: market, pageNo: str(page_no)} else: raise ValueError(funknown entry: {entry}) resp session.post(QUERY_PAGE_URL, dataparams, timeout20) resp.raise_for_status() return etree.HTML(resp.text)这里的base_params是最初从查询页抓到的 hidden 字段加上人工确认的下拉选项entry决定TAB参数。注意pageNo每次都要重新构造字符串部分 ASP.NET 页面把页码当字符串比较传 int 可能触发类型校验失败。3.4 详情页解析与入库结构列表页解析出的是详情链接二次请求详情页后按下述分支提取。以结果公告为例常见做法是取出地块编号、位置、面积、用途、出让年限、竞得人、成交价格、公告日期再把列表页的行政区合并进记录def parse_detail(html): 从详情页提取字段字段名以页面实际布局为准 item {} # 每一行形如“宗地编号xxxx”或“位置xxx” for row in html.xpath(//div[contains(class,detail)]//tr): tds row.xpath(./td/text()) if len(tds) 2: key tds[0].strip().replace(, ) value tds[1].strip() if key and value: item[key] value return item入库我倾向用 SQLite零部署且便于交付。表结构至少要包含业务唯一键和抓取状态字段CREATE TABLE IF NOT EXISTS land_detail ( id INTEGER PRIMARY KEY AUTOINCREMENT, entry TEXT NOT NULL, -- result / market biz_no TEXT NOT NULL, -- 宗地编号或公告编号 title TEXT, region TEXT, -- 行政区 price TEXT, -- 成交价/起始价按入口解释 detail_json TEXT, -- 完整详情字段冗余存储 crawled_at TEXT DEFAULT (datetime(now, localtime)), UNIQUE(entry, biz_no) -- 去重键 );detail_json的作用是防止解析规则升级后历史数据丢失原始详情先冗余一份后续要改字段再重解析 JSON而不是重新抓全站。这个设计在接单交付场景里尤其重要甲方后续要新增字段不需要重新跑一遍完整爬虫。4. landchina 爬虫的容错、限速与断点续爬4.1 先分清是参数错误还是被限流爬虫工程师最容易犯的错是看到状态码异常就怀疑被封实际上 landchina 这类站点大多数异常来自参数或页面结构变化。状态码 400 通常意味着 POST 参数缺失可能是隐藏字段过期503 可能是网关瞬时错误或请求过密200 但列表为空则大概率是查询条件没生效。处理顺序应该是先验证单条请求是否正常再谈加代理和延时。验证方法很直接把当前 POST 参数原样复制到浏览器的 Network 面板里重放一次如果浏览器也返回空说明参数模板本身需要重新抓取。4.2 请求封装随机 UA、指数退避、低频限速import random import time def request_with_retry(session, url, dataNone, max_retry3): 带指数退避的 POST 封装区分可重试与不可重试的状态码 for attempt in range(max_retry): try: resp session.post(url, datadata, timeout20) if resp.status_code in (429, 503): # 服务端繁忙可重试 time.sleep(2 ** attempt random.random()) continue resp.raise_for_status() # 其他错误直接抛出 return resp except requests.ConnectionError: time.sleep(2 ** attempt random.random()) raise RuntimeError(fretry failed: {url})状态码和处置策略建议固定成一张表写入项目文档状态码含义处置200正常解析400参数错误不重试重新抓 hidden 字段404详情页已下架记录日志跳过429请求过密指数退避 2s/4s/8s503网关繁忙指数退避 2s/4s/8s5xx 其他服务端异常重试 1 次后失败并告警限速不是可选项。公开数据的抓取要控制频率我一般把相邻请求间隔设成 2 到 5 秒的随机值既能稳定翻完整个列表也不会给源站造成压力。对接到一个晚上能跑完的体量来说慢比快可靠。4.3 断点续爬与去重列表页爬到一半断网是常态。做法是维护一张批次表记录每个入口、起始日期和已完成的页码CREATE TABLE IF NOT EXISTS crawl_state ( entry TEXT NOT NULL, start_date TEXT NOT NULL, page_no INTEGER NOT NULL, updated_at TEXT DEFAULT (datetime(now, localtime)), PRIMARY KEY (entry, start_date) );恢复时先查crawl_state从上次完成的page_no 1继续详情入库时依赖UNIQUE(entry, biz_no)去重重复解析直接忽略。两个机制叠加后即使列表页重复抓取详情数据也不会产生重复记录这就是断点续爬的完整闭环。5. 让 landchina 的选项资料齐全变成配置化与文档化5.1 用一份配置文件管理所有查询选项把第 2 章列出的参数表落成 YAML 配置爬虫启动时读取并组装 POST 数据entries: result: tab: result filters: announcement_type: 招拍挂出让结果公告 region: 110000 parse: result_parser market: tab: market filters: transaction_type: 挂牌出让 resource_type: 国有建设用地使用权 parse: market_parser crawl: start_date: 2024-01-01 interval: [2, 5] retry: 3配置化带来的直接好处是甲方要换行政区、换交易方式只改 YAML 不用碰代码。“选项资料齐全”从代码约束变成了配置约束回归风险大幅下降。5.2 文档交付的清单与模板骨架接单场景里文档详细比代码技巧更重要。一份合格的交付文档至少包含运行环境与依赖安装、配置文件字段说明、参数表与实际页面的对照、SQLite 表结构说明、常见报错对照表。README 里把目录和命令写清楚对方照着跑就能出数据。# landchina 爬虫交付文档 ## 1. 环境要求Python 3.10、依赖清单 ## 2. 安装与启动命令 ## 3. config.yaml 字段说明 ## 4. 两个入口的请求参数差异 ## 5. 输出表结构与数据字典 ## 6. 常见报错与处理办法5.3 用校验任务锁住“选项资料齐全”最后一层保障是数据校验抓完一个批次后统计总页数与入库条数对比源站显示的结果数量是否一致再抽检详情页关键字段的非空率。把校验脚本放进 cron每周跑一次任何参数过期或页面改版都会在字段级暴露出来而不是等甲方发现数据缺了一大块才回来排查。本文还有配套的精品资源点击获取