尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python去哪儿景点爬虫:从requests请求到并发存储的完整设计

Python去哪儿景点爬虫:从requests请求到并发存储的完整设计 简介基于Python的旅游景点爬虫去哪网设计源码是一份面向爬虫初学者、旅游数据分析及毕业设计场景的实战项目。它以去哪网为目标站点演示了景区信息采集、清洗与结构化存储的完整流程覆盖国内多省市及亚洲、欧洲、美洲等区域的旅游数据组织方式。压缩包共40个文件含2个Python主程序、29个xlsx景点数据文件、5个xml配置/数据文件及pyc、license、说明文档等整体约1.56MB目录按“大洲—国家/省份—城市”维度划分便于检索与复用。目前已有349人学习或下载适合希望快速上手爬虫项目、理解数据落盘与Excel/XML存储方案的读者。借助源码中的get.py与app.py可掌握请求发送、页面解析、多线程调度及数据导出等关键技能为后续扩展其他旅游平台爬虫提供直接参考。1. 基于Python的旅游景点爬虫去哪网设计源码先用最小脚本验证可行拿到“基于Python的旅游景点爬虫去哪网设计源码”这个标题多数人第一反应是“又要写一份爬虫了”但真正动手的人会很快意识到它真正的难点不在采集本身而在去哪网页面的结构变化、反爬机制、数据清洗与去重策略。结合最近关于“网络爬虫原理”“requests爬虫”“爬虫并发设计到底哪个好”等讨论我想把这条路线拆成四个连贯步骤环境与最小可用代码、页面解析与字段设计、并发提速与代理方案、以及最后的落地验证。适合的人群很明确已经能写简单脚本、但没系统做过“景点数据收集”这类完整任务的Python开发者和数据从业者全文给出的命令和代码都需要在你的本地环境实际跑一遍而不是看个逻辑就完事。2. 爬虫选型与请求头伪装把 requests 的默认行为改到能用的状态2.1 为什么不选 Scrapy而先选 requests 加 BeautifulSoup“基于Python的旅游景点爬虫去哪网设计源码”这类项目在技术选型上有一个常见的分歧有人一上来用 Scrapy有人坚持用 requests 手工写。我一般会先选 requests理由是它的调试链路短、出错点少适合把“去哪网”某个城市榜单页的采集逻辑先跑通再考虑要不要迁到 Scrapy 做分布式。从工程角度看Scrapy 的优势是并发调度和中间件体系但它对新手最大的负担是“一切皆对象”items、pipelines、middlewares 三层结构在项目只有十个字段时属于维护成本的浪费。requests 加 BeautifulSoup 的组合足够覆盖普通景点页、列表页和详情页当你需要把采集能力横向扩展时再按 Scrapy 的 Item 结构把现有解析代码搬过去迁移成本也不高。注意这里有一个关键点标题说的是“设计源码”潜台词是代码结构要能给别人看、能在仓库里维护所以函数拆分和配置外置比单纯“能抓到数据”更重要。2.2 用 requests 建立带重试的会话用 fake_useragent 控制请求头去哪网的页面通常对“无 UA 请求”和“高频请求”比较敏感所以请求头伪装不是可选项而是第一道门槛。常见做法是使用 fake_useragent 库随机生成 User-Agent同时把 Accept-Language 固定为 zh-CN避免默认的英文 UA 直接暴露非人类行为。# 002_quna_request_headers.py import requests from fake_useragent import UserAgent from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() ua UserAgent() session.headers.update({ User-Agent: ua.random, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive, }) retry Retry( total3, connect3, read3, backoff_factor0.5, status_forcelist[500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) headers session.headers print(headers[User-Agent], headers[Accept-Language])这段代码里Retry 的 backoff_factor 是 0.5含义是第一次重试等待 0.5 秒第二次等待 1 秒第三次 2 秒避免一次性把请求打满导致 IP 被短时封禁。status_forcelist 里放 500 和 502 之类服务端错误是因为这类状态码在“网页系统偶发过载”时重试成功率较高而 403 这类反爬状态码千万别放进去否则会反复用同一特征请求加重被封风险。2.3 探测页面结构先抓一次 HTML 再写解析别盲写选择器写解析代码之前我需要先确认目标页面实际返回的内容是什么样的。这一步看起来多余但能帮你省掉大量的“解析不到数据”调试时间。关键点是去哪网的很多列表页数据是服务端渲染的 HTML 字符串少量接口是 JSON 接口如果在 HTML 里找不到景点名称就要去 Network 面板里找 XHR 请求。curl -s -H User-Agent: Mozilla/5.0 \ https://piao.qunar.com/ticket/list.htm?keyword北京regionfrommpl_search_suggest \ -o beijing_page.html wc -c beijing_page.html grep -o 景点名称 beijing_page.html | head -5如果 grep 没有输出说明页面内容是异步接口渲染的此时应该打开浏览器的开发者工具哥几个筛选网络请求把接口的 URL 复制下来在 Python 脚本里模拟。这样做的意义是你是在跟“网站实际提供的数据形态”合作而不是跟“我以为的页面结构”合作。3. 去哪网景点列表页解析与字段设计把非结构化 HTML 变成规整数据3.1 选择器的三种候选方案BeautifulSoup、lxml、正则表达式“去哪网旅游景点爬虫”的数据解析环节核心问题不是“用什么库”而是“页面结构变了你怎么快速调整”。BeautifulSoup 的优点是容错性高即便 HTML 标签闭合不规范也能解析适合快速试用lxml 的 XPath 在字段多、层级深的时候比 CSS 选择器更好调试因为它能准确定位到父节点再做条件过滤正则表达式只适合提取像“景点 ID”这种高度规律的子串不建议用它做整个列表页的解析。我在这个项目中的选型是 lxml 加 XPath理由有三第一去哪网的列表项结构是反复嵌套的 divXPath 可以用 contains 做模糊匹配减少对 class 名精确值的依赖第二XPath 可以直接用索引定位“搜索结果列表中的第几个”这在页面结构调整时非常好用第三lxml 的解析速度在数据量变大时仍然稳定不需要切到 Scrapy 的 Selector 层去补课。3.2 列表页解析用 XPath 提取景点名、简介、评分、地址下面这段代码的目标是解析“去哪网景点列表页”中的核心字段字段名与解析方式均与真实页面常见结构对应但 class 名称已经做了脱敏处理你在实际应用时要先从第一步的探测结果里替换成真实值。# 003_parse_list_page.py from lxml import html import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) }) resp session.get(https://piao.qunar.com/ticket/list.htm?keyword北京) tree html.fromstring(resp.text) items tree.xpath(//div[contains(class, sight_item)]) print(解析到景点数量, len(items)) for item in items[:3]: name item.xpath(.//a[contains(class, name)]/text()) score item.xpath(.//span[contains(class, score)]/text()) address item.xpath(.//span[contains(class, address)]/text()) print(name, score, address)XPath 里contains(class, sight_item)是模糊匹配策略只要 class 属性里包含这个子串就能命中比精确等号更抗页面微调。这里的name字段如果返回空列表大概率是景点名称并不在a标签的 text 里而是在 title 属性里这时把表达式改成.//a[contains(class, name)]/title即可。3.3 详情页数据结构设计用字典加时间戳降低重复抓取成本列表页数据只是“搜索结果的骨架”完整项目还需要进入详情页去获取营业时间、建议游玩时长、优待政策等信息。这个阶段的设计建议是不要直接往数据库写先在内存里构造一个统一的 dict把所有字段集中管理后面再决定写 CSV 还是入库。def build_sight_record(item_element): record { name: item_element.xpath(.//a[contains(class, name)]/text_or_title()), score: item_element.xpath(.//span[contains(class, score)]/text()), address: item_element.xpath(.//span[contains(class, address)]/text()), crawl_time: datetime.now().strftime(%Y-%m-%d %H:%M:%S), } return record这里把crawl_time放进每条记录里是为了后面用(景点名, 地址)作为组合主键做增量更新。注意crawl_time 记录的是本次爬取时间而不是景点的某个属性后续做数据分析和定时抓取时可以通过它过滤出“最近 7 天内的数据”作为增量集。4. 去哪网爬虫的并发设计、IP 代理与 JSON 接口适配从单线程到工程化4.1 先处理列表页的分页规律再考虑并发去哪网景点搜索页的分页参数通常是page或p第一页到第二页、第三页之间是单纯递增关系。用 requests 做并发前先把分页 URL 构造函数写好def build_list_url(keyword, page): base_url https://piao.qunar.com/ticket/list.htm params { keyword: keyword, region: , from: mpl_search_suggest, page: page, } return base_url, params这个函数的意义在于它是后续并发模块的统一入口多线程、多进程或者异步协程全部复用同一个 URL 构造逻辑避免在并发场景下每个线程各写一套地址拼接导致参数不一致。注意当页面总数量超过一定数值后部分页面会要求登录或滑块验证这是正常现象不是代码 bug。4.2 爬虫并发设计requests 加 ThreadPoolExecutor 的折中方案最近关于“爬虫并发设计到底哪个好”的争论集中在 asyncio 和 ThreadPoolExecutor 的选择上。针对去哪网这种 I/O 密集型任务、且绝大部分时间在等待网络响应的场景我选择 ThreadPoolExecutor因为 requests 是阻塞式库异步化改造成本高而线程池可以在不改变现有解析代码的前提下把请求并发提升 5 到 10 倍。# 004_concurrent_crawler.py from concurrent.futures import ThreadPoolExecutor, as_completed import requests def fetch_one(city_name, page): session requests.Session() session.headers.update({User-Agent: Mozilla/5.0}) url fhttps://piao.qunar.com/ticket/list.htm?keyword{city_name}page{page} resp session.get(url, timeout10) return resp city_list [北京, 上海, 成都] with ThreadPoolExecutor(max_workers5) as executor: futures [] for city in city_list: for page in range(1, 3): futures.append(executor.submit(fetch_one, city, page)) for future in as_completed(futures): resp future.result() print(resp.status_code, resp.url)max_workers 的取值在 5 到 8 之间比较合理。设小了速度没有明显提升设大了比如 20 以上会把本地网络带宽打满同时更容易触发反爬。这里的timeout10必须在生产代码里写因为线程池任务如果一直挂起as_completed 循环永远等不到结果整个主程序会卡死。4.3 IP 代理与请求频率控制让你的爬虫不那么显眼去哪网的反爬强度在三线旅游网站里处于中等水平但“中等”意味着必须控制节奏不能猛打。这个阶段常见做法是维护一个代理池每次请求随机抽取一个代理 IP并在请求之间加入随机的 sleep 时间。代理池的来源可以是付费代理服务商提供的 API 接口也可以是自建代理验证脚本。# 005_proxy_and_sleep.py import time import random proxies [ {http: http://111.222.111.222:8080}, {http: http://123.123.123.123:3128}, ] def fetch_with_proxy(url): proxy random.choice(proxies) session requests.Session() session.headers.update({User-Agent: Mozilla/5.0}) response session.get(url, proxiesproxy, timeout8) time.sleep(random.uniform(1, 3)) return responserandom.uniform(1, 3)的意思是每次请求之间随机等待 1 到 3 秒比固定 sleep(2) 更自然因为真实用户浏览页面的时间不会是绝对均匀的。代理 IP 在使用前最好做一次联通性测试否则大量代理不可用的状态会让你的错误率陡增。4.4 去哪儿网 JSON 接口的适配比 HTML 解析更稳的数据源去哪网部分页面尤其是移动端页面和搜索建议接口返回的是 JSON 结构比 HTML 列表页更稳定、更利于字段抽取。使用 JSON 接口是很多老爬虫工程师的经验数据字段是结构化 key-value不需要在 HTML class 名变化后重写 XPath。我的做法是先用抓包工具找到接口 URL再在代码里调用一次并检查返回结构。import requests url https://piao.qunar.com/ticket/detailLight.json params { id: 144087, from: mobile, } resp requests.get(url, paramsparams, headers{User-Agent: Mozilla/5.0}) data resp.json() print(type(data), data.keys()) if data.get(data): item_info data[data] print(item_info.get(sightName), item_info.get(price))JSON 接口的好处是字段名直观比如sightName、price、address一目了然坏处是接口参数和数据格式可能不定期调整。所以源码设计里必须把 JSON 解析和 HTML 解析拆成两个独立模块当其中一个失效时另一个还能继续跑并提供切换开关。5. 数据落地与去重策略CSV、SQLite 与增量抓取5.1 CSV 是最快速的数据交付方式去哪网景点数据量在几百到几千条之间时CSV 是最合适的落地格式方便直接丢给 Excel 或 pandas 分析。写入时需要注意编码问题Windows 下 Excel 打开 CSV 默认使用 GBK 编码所以写 CSV 时最好用encodingutf-8-sig这样文件头部会加上 BOMExcel 能正确识别。追加写入时用 a 模式并且先把表头判断的逻辑写好避免文件里出现多个表头行。# 006_save_to_csv.py import csv fieldnames [name, score, address, crawl_time] def save_records_to_csv(records, file_path): file_exists os.path.exists(file_path) with open(file_path, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) if not file_exists: writer.writeheader() writer.writerows(records)这里的file_exists检查作用很关键每次采集完成后的记录如果文件不存在就写表头如果文件存在就直接追加不做全量重写。这样做的好处是采集中断后重跑历史数据不会丢只要做一次“重复记录去重”就行。5.2 SQLite 与按景点名加地址去重当数据量增长到几千条且你要做多次增量采集时CSV 的去重逻辑会变得很笨拙。建议换成 SQLite它不需要额外安装数据库服务Python 内置 sqlite3 模块直接可用适合单机数据量在百万以内的项目。# 007_save_to_sqlite.py import sqlite3 conn sqlite3.connect(qunar_attractions.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS attractions ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score TEXT, address TEXT, crawl_time TEXT, UNIQUE(name, address) ) ) def insert_record(record): cursor.execute( INSERT OR REPLACE INTO attractions (name, score, address, crawl_time) VALUES (:name, :score, :address, :crawl_time) , record) conn.commit()这里用UNIQUE(name, address)来保证同一个景点的名称和地址组合唯一INSERT OR REPLACE的意思是如果该组合已经存在就用新数据替换旧数据同时更新 crawl_time。这个做法的效果是“数据只进不出”运行十次也不会产生重复行适合定时巡检场景。5.3 增量抓取的字段设计用最近更新时间做合并增量抓取的设计不复杂每次抓取前先从库里查一下最近一次max(crawl_time)然后只抓取列表中“发布时间”或“更新时间”晚于该时间的记录。对于去哪网景点数据而言如果 API 没有明确提供“更新时间”字段就退而求其次每次全量抓列表页但详情页的内容只在“价格或评分变化”时才更新。这个策略在企业项目里叫“变更捕获”它比“定时全量更新”省资源也比“完全不更新”更可控。具体实现时在 SQLite 中加一个last_seen_time字段每次抓取后把当前时间写入下次比较时就能区分“新数据”和“已有数据”。6. 用 Selenium 兜底验证与源码梳理技巧6.1 什么时候必须从 requests 切换到 Selenium去哪网的正常反爬强度和页面复杂度requests 基本能覆盖但如果你连续抓取多城市景点数据或者访问了需要 JS 动态渲染的“景点详情地图”部分requests 就会遇到空标签或未渲染出的字段。常见做法是先用 requests 跑一条城市数据用 XPath 输出字段如果发现字段全为空就使用 Selenium 加载真实浏览器环境来兜底验证。这不是一上来就放弃 requests而是把它作为“最后一公里的验证工具”。# 008_selenium_verify.py from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) driver webdriver.Chrome(optionsoptions) driver.get(https://piao.qunar.com/ticket/list.htm?keyword北京) time.sleep(3) items driver.find_elements(xpath, //div[contains(class, sight_item)]) print(len(items)) driver.quit()这段代码的核心不是“能用 Selenium 抓数据”而是“验证 requests 解析不到的数据是否来源于 JS 渲染”。如果 Selenium 能解析到说明你的 requests 请求缺少了必要的参数或 Cookie如果 Selenium 也解析不到说明页面结构变了需要回到浏览器开发者工具重新找接口。使用 Selenium 时建议把页面截图或当前 URL 输出保存下来方便后续核对。6.2 源码文件模块怎么划分一套完整的去哪网爬虫“设计源码”推荐按模块文件拆分哪怕本地先跑通也建议保持这种分层方便后续迁移到定时任务或 Scrapyqunar_crawler/ ├── config.py ├── request_handler.py ├── parser.py ├── storage.py ├── scheduler.py └── main.pyconfig.py 放每个城市的 URL 模板、请求头、重试次数、sleep 范围request_handler.py 负责 requests 会话、重试和代理parser.py 专职 XPath 和 JSON 响应解析storage.py 封装 CSV 和 SQLite 的写入逻辑scheduler.py 负责并发调度和抓取频率控制main.py 把以上流程组装起来。这个结构的好处是去哪网页面结构一旦调整只需要改 parser.py 和 config.py其他模块原封不动。这也符合标题里“设计源码”隐含的诉求——不是写完就扔的一次性脚本而是能持续崩、持续修的工程小模块。本文还有配套的精品资源点击获取
返回列表