Scrapy实战:从华为应用市场爬取海量应用评论数据

📅 2026/7/30 15:32:49
Scrapy实战:从华为应用市场爬取海量应用评论数据
1. 项目概述与核心价值最近在做一个关于移动应用用户行为分析的项目需要大量真实的应用评论数据作为支撑。市面上公开的数据集要么时效性差要么覆盖的应用不够全面。于是我决定自己动手目标直指华为应用市场爬取其平台上所有应用的评论数据。选择华为应用市场一是因为其作为国内主流的应用分发平台应用数量庞大评论数据丰富且具有代表性二是因为其Web端页面结构相对规整适合作为爬虫实战的典型场景。这个项目听起来简单但实际操作起来从反爬策略应对、数据清洗到大规模异步抓取每一步都藏着不少门道。今天我就把这次从零搭建Scrapy爬虫完整爬取华为应用市场评论数据的实战经验、踩过的坑以及优化技巧毫无保留地分享出来。无论你是刚接触Scrapy的新手想找一个有挑战性的实战项目练手还是已经有一定经验想了解如何应对中等复杂度的商业网站爬取相信这篇内容都能给你带来直接的参考价值。整个项目的核心思路是首先我们需要获取华为应用市场所有应用的唯一标识如包名或ID然后针对每一个应用模拟真实用户访问其评论页面解析并提取结构化的评论数据最后考虑到海量应用和翻页评论必须设计高效的异步抓取与存储方案。在这个过程中我们将重点解决几个关键问题如何高效地发现和遍历所有应用如何稳定地获取并解析评论页面的动态内容如何设计爬虫架构以应对可能的风控和反爬机制下面我们就从环境准备开始一步步拆解实现过程。2. 环境准备与Scrapy项目初始化工欲善其事必先利其器。在开始编写爬虫代码之前我们需要搭建一个稳定、高效的开发环境。我个人的习惯是使用Python 3.8的版本这个版本在稳定性和库兼容性上取得了很好的平衡。2.1 基础环境搭建首先使用虚拟环境隔离项目依赖是一个好习惯可以避免不同项目间的包版本冲突。我通常使用venv来创建。# 创建项目目录并进入 mkdir huawei_appmarket_crawler cd huawei_appmarket_crawler # 创建Python虚拟环境 python3 -m venv venv # 激活虚拟环境 # 在Windows上: venv\Scripts\activate # 在Mac/Linux上: source venv/bin/activate激活虚拟环境后安装核心的Scrapy框架。Scrapy是一个为爬取网站数据、提取结构性数据而编写的强大异步框架。pip install scrapy除了Scrapy我们还需要几个辅助库来应对更复杂的情况scrapy-user-agents: 用于随机轮换User-Agent降低被识别为爬虫的风险。fake-useragent: 方便地生成随机的、真实的浏览器User-Agent字符串。pymongo(可选): 如果你打算将数据存储到MongoDB这是一个高效的驱动。考虑到评论数据可能是半结构化的JSONMongoDB是个不错的选择。当然使用Scrapy自带的JsonItemExporter导出到文件也同样可行。pip install scrapy-user-agents fake-useragent pymongo2.2 Scrapy项目创建与结构解析接下来我们使用Scrapy的命令行工具快速生成项目骨架。scrapy startproject huawei_appmarket cd huawei_appmarket执行后你会看到生成的标准项目结构。理解每个文件的作用对后续开发至关重要huawei_appmarket/ ├── scrapy.cfg # 项目部署配置文件 └── huawei_appmarket/ # 项目Python模块 ├── __init__.py ├── items.py # 定义要爬取的数据结构Item ├── middlewares.py # 自定义中间件如代理、User-Agent处理 ├── pipelines.py # 数据后处理管道清洗、验证、存储 ├── settings.py # 项目全局设置并发、延迟、中间件启用等 └── spiders/ # 爬虫文件存放目录 └── __init__.py关键设置调整 (settings.py):在编写爬虫前我们先对settings.py进行一些关键配置这能事半功倍。# huawei_appmarket/settings.py BOT_NAME huawei_appmarket # 遵守robots协议对于商业网站建议先设置为False以测试但正式运行时应评估风险。 ROBOTSTXT_OBEY False # 配置并发请求数。对于华为应用市场这类网站不宜设置过高避免对服务器造成过大压力或触发风控。 # 我实测下来CONCURRENT_REQUESTS 16 是一个比较稳健的起点。 CONCURRENT_REQUESTS 16 # 下载延迟。添加随机延迟可以模拟人类操作非常重要。 # 使用 RANDOMIZE_DOWNLOAD_DELAY True 并设置一个基础延迟。 DOWNLOAD_DELAY 0.5 RANDOMIZE_DOWNLOAD_DELAY True # 启用我们即将配置的User-Agent中间件 DOWNLOADER_MIDDLEWARES { scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: None, # 禁用默认的 scrapy_user_agents.middlewares.RandomUserAgentMiddleware: 400, # 启用随机的 } # 配置Item Pipeline用于处理爬取到的数据。 ITEM_PIPELINES { huawei_appmarket.pipelines.HuaweiAppmarketPipeline: 300, } # 日志级别开发调试时设为DEBUG生产环境可设为INFO或WARNING。 LOG_LEVEL DEBUG # 设置一个合理的请求超时时间 DOWNLOAD_TIMEOUT 30注意DOWNLOAD_DELAY和CONCURRENT_REQUESTS需要根据目标网站的反爬强度和自身网络状况进行动态调整。一开始可以保守一些观察请求成功率再逐步微调。3. 核心爬虫逻辑设计与实现这是整个项目的核心。我们的爬虫需要完成两个主要任务1. 发现所有应用2. 爬取每个应用的评论。我将采用“广度优先”的策略先由一个“应用列表爬虫”收集所有应用的链接再由“评论详情爬虫”去具体抓取。3.1 定义数据模型 (items.py)首先在items.py中定义我们想要爬取的数据结构。清晰的Item定义能让后续的数据处理和存储更规范。# huawei_appmarket/items.py import scrapy class AppItem(scrapy.Item): # 应用基本信息从列表页或详情页获取 app_id scrapy.Field() # 应用唯一标识通常是包名或数字ID app_name scrapy.Field() # 应用名称 app_category scrapy.Field() # 应用分类 app_url scrapy.Field() # 应用详情页URL # 可能还有其他字段如开发者、评分等根据需求扩展 class CommentItem(scrapy.Item): # 评论数据 app_id scrapy.Field() # 关联的应用ID comment_id scrapy.Field() # 评论唯一ID如果有 user_name scrapy.Field() # 用户名可能匿名化 user_rating scrapy.Field() # 用户评分如5星 comment_text scrapy.Field() # 评论正文 comment_time scrapy.Field() # 评论时间 thumbs_up scrapy.Field() # 点赞数 # 可以添加设备型号、应用版本等字段3.2 应用列表爬虫发现所有应用华为应用市场Web端通常有分类浏览、排行榜、搜索等入口。为了尽可能全地覆盖应用一个有效策略是从“全部分类”页面出发遍历每个分类下的应用列表并处理分页。我们在spiders/目录下创建第一个爬虫文件app_list_spider.py。# huawei_appmarket/spiders/app_list_spider.py import scrapy from urllib.parse import urljoin from huawei_appmarket.items import AppItem class AppListSpider(scrapy.Spider): name app_list allowed_domains [appgallery.huawei.com] # 华为应用市场Web版域名 start_urls [https://appgallery.huawei.com/categoryList] # 假设的分类列表页 def parse(self, response): 解析分类列表页提取所有分类的链接。 # 使用浏览器的开发者工具F12分析页面找到分类链接的CSS选择器或XPath。 # 这里的选择器是示例实际需要根据目标网站HTML结构调整。 category_links response.css(div.category-list a::attr(href)).getall() for cat_link in category_links: full_cat_url urljoin(response.url, cat_link) # 将分类页的请求交给 parse_category 方法处理 yield scrapy.Request(full_cat_url, callbackself.parse_category) def parse_category(self, response): 解析单个分类页面提取该分类下的应用列表和分页。 # 1. 提取当前页的应用信息 app_elements response.css(div.app-item) # 示例选择器 for app in app_elements: item AppItem() # 解析应用名称、ID、详情页链接等 item[app_name] app.css(h4.app-title::text).get() # 详情页链接可能需要拼接 detail_path app.css(a.app-link::attr(href)).get() item[app_url] urljoin(response.url, detail_path) # 从详情页URL或元素中提取app_id # 例如URL可能为 https://.../app/C10123456则app_id为C10123456 item[app_id] item[app_url].split(/)[-1] item[app_category] response.css(h1.category-title::text).get() # 这里可以先yield item也可以先只收集URL在评论爬虫里再抓详情。 # 我选择先yield让Pipeline先存储基础信息。 yield item # 2. 同时为这个应用生成评论页的初始请求交给评论爬虫处理 # 评论页URL通常有规律例如{app_url}/comment 或 {app_url}?tabreview # 需要实际分析。这里假设为 {app_url}?tabreviewpage1 comment_start_url f{item[app_url]}?tabreviewpage1 yield scrapy.Request(comment_start_url, callbackself.parse_comments, meta{app_id: item[app_id]}) # 3. 处理分页查找“下一页”按钮 next_page response.css(a.next-page::attr(href)).get() if next_page: next_page_url urljoin(response.url, next_page) yield scrapy.Request(next_page_url, callbackself.parse_category) def parse_comments(self, response): 解析单个应用的评论第一页。 这个方法的逻辑也可以独立成一个单独的爬虫。 这里为了流程连贯放在一起。实际大型项目建议拆分。 # 评论数据解析逻辑见下一节。 pass实操心得选择器调试使用scrapy shell ‘url’命令在终端快速测试你的CSS选择器或XPath是否正确这是提高开发效率的关键。分页策略对于“加载更多”这种动态分页Ajax需要分析网络请求找到真正的数据接口通常是返回JSON的API而不是解析页面HTML。华为应用市场的评论很可能采用这种方式。去重Scrapy默认根据URL去重。确保应用的详情页URL或评论分页URL能唯一标识该资源避免重复爬取。3.3 评论详情爬虫解析动态内容现代网站大量使用JavaScript动态加载数据评论列表更是如此。直接请求网页URL得到的HTML可能不包含评论数据。我们需要分析浏览器与服务器之间的真实数据交互。使用浏览器开发者工具分析网络请求打开华为应用市场某个应用的评论页面如https://appgallery.huawei.com/app/C10123456?tabreview。按F12打开开发者工具切换到Network网络选项卡。刷新页面并滚动评论列表触发加载更多。在请求列表中过滤XHR或Fetch类型的请求寻找返回评论数据的请求。通常其响应体是JSON格式。模拟API请求 假设我们找到了一个类似https://web-drcn.hispace.dbankcloud.cn/uowap/index?methodinternal.getTabDetailuriapp|C10123456tabKeyreviewpage1的接口它返回了JSON格式的评论数据。那么parse_comments方法就需要重写不再解析HTML而是直接请求这个API接口并处理JSON响应。# 修改或重写 parse_comments 方法 def parse_comments(self, response): app_id response.meta[app_id] # 如果当前响应是HTML页面我们需要从中提取API的URL构造参数。 # 但更常见的做法是直接在 parse_category 中构造API请求。 # 更好的做法在 parse_category 中直接构造API请求 # 在 parse_category 方法里找到应用后 comment_api_template “https://web-drcn.hispace.dbankcloud.cn/uowap/index?methodinternal.getTabDetailuriapp|{app_id}tabKeyreviewpage{page}” first_comment_page_url comment_api_template.format(app_iditem[‘app_id’], page1) yield scrapy.Request(first_comment_page_url, callbackself.parse_comment_api, meta{‘app_id’: item[‘app_id’], ‘page’: 1}) def parse_comment_api(self, response): “””解析评论API返回的JSON数据。””” app_id response.meta[‘app_id’] current_page response.meta[‘page’] try: data response.json() # 解析JSON结构提取评论列表 comment_list data.get(‘list’, []) # 具体字段名需要根据实际API响应确定 if not comment_list: # 如果当前页没有数据说明已爬完 self.logger.info(f‘App {app_id} comments finished at page {current_page}‘) return for comment in comment_list: item CommentItem() item[‘app_id’] app_id item[‘comment_id’] comment.get(‘commentId’) item[‘user_name’] comment.get(‘userName’, ‘匿名用户’) item[‘user_rating’] comment.get(‘score’, 5) # 假设5分制 item[‘comment_text’] comment.get(‘content’, ‘’).strip() item[‘comment_time’] comment.get(‘publishTime’) # 可能是时间戳 item[‘thumbs_up’] comment.get(‘praiseCount’, 0) yield item # 请求下一页 next_page current_page 1 next_page_url response.url.replace(f’page{current_page}‘, f’page{next_page}‘) # 或者根据API返回的totalPage等信息判断是否还有下一页 # if current_page data.get(‘totalPage’, 1): yield scrapy.Request(next_page_url, callbackself.parse_comment_api, meta{‘app_id’: app_id, ‘page’: next_page}) except json.JSONDecodeError as e: self.logger.error(f‘Failed to parse JSON for {response.url}: {e}‘)重要提示上述API URL、参数名method,uri,tabKey以及JSON结构中的字段名list,commentId,score等均为示例并非华为应用市场的真实接口。你必须使用浏览器开发者工具亲自分析目标网站的真实请求找到正确的接口地址和参数格式。这是爬虫开发中最关键的一步。4. 应对反爬策略与提升稳定性商业网站通常没有反爬机制。直接按上述步骤爬取很快可能会遇到请求失败、返回空数据甚至IP被封的情况。4.1 中间件增强User-Agent与代理我们已经配置了随机User-Agent。对于IP封锁可以考虑使用代理IP池。这里以使用中间件集成代理为例。在middlewares.py中自定义一个代理中间件# huawei_appmarket/middlewares.py import random class RandomProxyMiddleware: def __init__(self, proxy_list): self.proxy_list proxy_list classmethod def from_crawler(cls, crawler): # 从settings或外部文件读取代理列表 proxy_list crawler.settings.get(PROXY_LIST, []) # 或者从文件读取proxy_list [line.strip() for line in open(proxies.txt)] return cls(proxy_list) def process_request(self, request, spider): if self.proxy_list and not request.meta.get(proxy): proxy random.choice(self.proxy_list) request.meta[proxy] proxy spider.logger.debug(fUsing proxy: {proxy}) # 在 settings.py 中启用这个中间件并配置代理列表 DOWNLOADER_MIDDLEWARES { scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: None, scrapy_user_agents.middlewares.RandomUserAgentMiddleware: 400, huawei_appmarket.middlewares.RandomProxyMiddleware: 750, # 数字代表优先级 } # PROXY_LIST [http://ip1:port, http://ip2:port, ...]4.2 请求头与Cookie模拟有些API会校验Referer,Origin等请求头。我们需要在请求中模拟浏览器。# 在生成Request时添加headers headers { ‘Accept’: ‘application/json, text/javascript, */*; q0.01’, ‘Accept-Language’: ‘zh-CN,zh;q0.9,en;q0.8’, ‘Referer’: ‘https://appgallery.huawei.com/’, # 设置合适的来源页 ‘X-Requested-With’: ‘XMLHttpRequest’, # 如果是Ajax请求 } yield scrapy.Request(url, callbackself.parse_comment_api, headersheaders, metameta)对于需要登录后才能查看的评论虽然华为应用市场评论通常公开可能需要处理Cookie。可以使用scrapy.Request的cookies参数或者使用start_requests方法先发起一个登录请求获取Cookie。4.3 错误重试与速率控制Scrapy内置了重试中间件和自动限速扩展AutoThrottle合理配置它们能极大提升爬虫的健壮性。# settings.py # 启用并配置重试 RETRY_ENABLED True RETRY_TIMES 3 # 重试次数 RETRY_HTTP_CODES [500, 502, 503, 504, 408, 429] # 需要重试的HTTP状态码 # 启用自动限速扩展它会根据服务器响应和负载自动调整请求延迟 AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 1.0 # 初始延迟 AUTOTHROTTLE_MAX_DELAY 60.0 # 最大延迟 AUTOTHROTTLE_TARGET_CONCURRENCY 4.0 # 目标平均并发数5. 数据存储与后处理管道爬取到的数据需要持久化存储。Scrapy的Pipeline组件非常适合做这件事。5.1 实现数据存储Pipeline我们以存储到MongoDB和JSON文件为例实现一个Pipeline。# huawei_appmarket/pipelines.py import json import pymongo from itemadapter import ItemAdapter from scrapy.exceptions import DropItem class HuaweiAppmarketPipeline: def __init__(self, mongo_uri, mongo_db): self.mongo_uri mongo_uri self.mongo_db mongo_db classmethod def from_crawler(cls, crawler): # 从settings读取MongoDB配置 return cls( mongo_uricrawler.settings.get(MONGO_URI, mongodb://localhost:27017), mongo_dbcrawler.settings.get(MONGO_DATABASE, huawei_appmarket) ) def open_spider(self, spider): # 爬虫启动时连接数据库 self.client pymongo.MongoClient(self.mongo_uri) self.db self.client[self.mongo_db] # 也可以同时打开一个JSON文件 self.json_file open(comments.json, a, encodingutf-8) def close_spider(self, spider): # 爬虫关闭时断开连接关闭文件 self.client.close() self.json_file.close() def process_item(self, item, spider): # 决定存储到哪个集合/表 adapter ItemAdapter(item) if comment_text in adapter: # 判断是否为CommentItem collection_name comments # 存储到MongoDB self.db[collection_name].insert_one(dict(adapter)) # 同时写入JSON文件一行一个JSON对象 line json.dumps(dict(adapter), ensure_asciiFalse) \n self.json_file.write(line) elif app_name in adapter: # 判断是否为AppItem collection_name apps self.db[collection_name].update_one( {app_id: adapter[app_id]}, {$set: dict(adapter)}, upsertTrue # 如果不存在则插入 ) return item在settings.py中启用这个Pipeline并配置MongoDB连接字符串。ITEM_PIPELINES { huawei_appmarket.pipelines.HuaweiAppmarketPipeline: 300, } MONGO_URI mongodb://localhost:27017 MONGO_DATABASE huawei_appmarket5.2 数据清洗与去重在Pipeline中我们还可以加入数据清洗逻辑比如去除空评论、过滤广告、统一时间格式等。def process_item(self, item, spider): adapter ItemAdapter(item) # 清洗评论数据 if comment_text in adapter: text adapter.get(comment_text, ) # 去除空白字符 text text.strip() # 过滤掉过短或无意义的评论如“.”“好” if len(text) 2: raise DropItem(f“Dropped short comment: {text}”) adapter[comment_text] text # 时间格式转换假设原始是时间戳 raw_time adapter.get(comment_time) if raw_time and isinstance(raw_time, (int, float)): # 转换为可读的ISO格式字符串 import datetime adapter[comment_time] datetime.datetime.fromtimestamp(raw_time/1000).isoformat() # ... 后续存储逻辑6. 运行、监控与问题排查6.1 运行爬虫可以使用Scrapy命令行运行特定的爬虫。# 运行应用列表爬虫如果拆分了的话 scrapy crawl app_list -o apps.json # 或者运行一个集成了所有逻辑的主爬虫 scrapy crawl main_spider -s LOG_FILEspider.log为了长时间稳定运行并记录日志建议使用nohup或screen等工具在后台运行。nohup scrapy crawl main_spider crawl.log 21 6.2 常见问题与排查技巧在爬取过程中你几乎一定会遇到下面这些问题。这是我的实战记录问题现象可能原因排查与解决思路返回HTTP 403/429错误IP或请求频率被限制1. 检查DOWNLOAD_DELAY和CONCURRENT_REQUESTS调大延迟降低并发。2. 检查代理IP是否有效、是否被目标网站封禁。3. 检查请求头特别是User-Agent是否模拟到位。API请求返回空数据或错误JSON参数不正确或接口已更新1. 使用scrapy shell ‘api_url’直接测试请求查看原始响应。2. 用浏览器开发者工具对比你的请求和浏览器请求的所有细节包括URL参数、Headers尤其是Cookie和某些特定Token。3. 检查是否需要处理页面上的动态Token如csrf_token可能需要先请求一个页面来获取。爬取速度越来越慢最后停止触发了更严格的风控1. 启用AUTOTHROTTLE扩展让它自动调节速度。2. 模拟更真实的行为随机化请求间隔加入鼠标移动、滚动等行为的模拟可通过Selenium中间件实现但较重。3. 考虑使用更高质量的住宅代理IP。MongoDB连接失败数据库服务未启动或配置错误1. 确认MongoDB服务正在运行 (systemctl status mongod或sudo service mongod status)。2. 检查MONGO_URI是否正确包括IP、端口、认证信息如果有。内存使用持续增长可能发生了内存泄漏或Pipeline处理太慢1. 检查Pipeline中是否有大量数据缓存未释放。2. 适当降低CONCURRENT_ITEMSsettings中设置减少同时处理的Item数量。3. 使用scrapy stats命令查看爬虫运行状态关注item_scraped_count和memusage/startup。一个关键的调试技巧当爬虫行为不符合预期时不要盲目修改代码。首先在parse方法中使用self.logger.debug(f‘Response URL: {response.url}, Status: {response.status}’)打印关键信息。其次将出问题的响应体保存到本地文件方便仔细分析。def parse_comment_api(self, response): with open(‘debug_response.html’, ‘wb’) as f: f.write(response.body) # 然后暂停爬虫用浏览器或文本编辑器打开这个文件分析。7. 项目优化与扩展思路当基础爬虫能稳定运行后可以考虑以下优化和扩展让项目更专业、更强大。分布式爬取使用scrapy-redis组件将爬虫改造成分布式利用多台机器同时爬取速度可成倍提升。这对于“所有应用”这种海量目标非常有效。增量爬取不是每次都全量爬取。在Pipeline中记录每条评论的爬取时间。下次运行时只请求和解析新出现的评论。这需要对API接口支持按时间筛选或者通过对比已存储的最新评论ID来实现。数据丰富化除了评论还可以爬取应用的描述、更新日志、下载量、所属开发者等信息构建更全面的应用画像。情感分析与主题挖掘对爬取到的评论文本进行自然语言处理NLP例如使用snownlp或jiebasklearn进行情感分析正面/负面/中性或提取高频关键词了解用户对应用的关注点和不满之处。构建监控告警系统编写一个简单的脚本定期运行爬虫的核心测试部分如访问一个固定应用的评论页检查是否能正常获取数据。如果连续失败则通过邮件、钉钉机器人等方式发送告警。这个项目从表面看是一个标准的Scrapy爬虫应用但深入其中你会涉及到HTTP协议、前端逆向、反爬对抗、数据清洗、异步编程、数据库存储乃至简单的系统设计等多个方面的知识。每一个环节的深入都能带来技术上的切实提升。我最初版本爬取10万个评论花了近一天经过代理池、分布式和请求参数优化后时间缩短到了几个小时。这个过程里最大的体会就是耐心分析网络请求谨慎模拟浏览器行为尊重目标网站的服务器压力是爬虫项目能够长期稳定运行的不二法门。希望这份详细的实战记录能帮你少走弯路顺利拿到你需要的数据。如果在实际操作中遇到新的具体问题比如某个特定的API参数怎么构造欢迎随时交流讨论。