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,这是一个高效的驱动。考虑到评论数据可能是半结构化的JSON,MongoDB是个不错的选择。当然,使用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, callback=self.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}?tab=review` # 需要实际分析。这里假设为 `{app_url}?tab=review&page=1` comment_start_url = f"{item['app_url']}?tab=review&page=1" yield scrapy.Request(comment_start_url, callback=self.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, callback=self.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?tab=review)。 - 按F12打开开发者工具,切换到Network(网络)选项卡。
- 刷新页面,并滚动评论列表触发加载更多。
- 在请求列表中,过滤
XHR或Fetch类型的请求,寻找返回评论数据的请求。通常,其响应体是JSON格式。
- 打开华为应用市场某个应用的评论页面(如
模拟API请求: 假设我们找到了一个类似
https://web-drcn.hispace.dbankcloud.cn/uowap/index?method=internal.getTabDetail&uri=app|C10123456&tabKey=review&page=1的接口,它返回了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?method=internal.getTabDetail&uri=app|{app_id}&tabKey=review&page={page}” first_comment_page_url = comment_api_template.format(app_id=item[‘app_id’], page=1) yield scrapy.Request(first_comment_page_url, callback=self.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, callback=self.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(f'Using 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, */*; q=0.01’, ‘Accept-Language’: ‘zh-CN,zh;q=0.9,en;q=0.8’, ‘Referer’: ‘https://appgallery.huawei.com/’, # 设置合适的来源页 ‘X-Requested-With’: ‘XMLHttpRequest’, # 如果是Ajax请求 } yield scrapy.Request(url, callback=self.parse_comment_api, headers=headers, meta=meta)对于需要登录后才能查看的评论(虽然华为应用市场评论通常公开),可能需要处理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_uri=crawler.settings.get('MONGO_URI', 'mongodb://localhost:27017'), mongo_db=crawler.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', encoding='utf-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_ascii=False) + "\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)}, upsert=True # 如果不存在则插入 ) return item在settings.py中启用这个Pipeline并配置MongoDB连接字符串。
ITEM_PIPELINES = { 'huawei_appmarket.pipelines.HuaweiAppmarketPipeline': 300, } MONGO_URI = 'mongodb://localhost:27017' MONGO_DATABASE = 'huawei_appmarket'5.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_FILE=spider.log为了长时间稳定运行,并记录日志,建议使用nohup或screen等工具在后台运行。
nohup scrapy crawl main_spider > crawl.log 2>&1 &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_ITEMS(settings中设置),减少同时处理的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或jieba+sklearn进行情感分析(正面/负面/中性),或提取高频关键词,了解用户对应用的关注点和不满之处。 - 构建监控告警系统:编写一个简单的脚本,定期运行爬虫的核心测试部分(如访问一个固定应用的评论页),检查是否能正常获取数据。如果连续失败,则通过邮件、钉钉机器人等方式发送告警。
这个项目从表面看是一个标准的Scrapy爬虫应用,但深入其中,你会涉及到HTTP协议、前端逆向、反爬对抗、数据清洗、异步编程、数据库存储乃至简单的系统设计等多个方面的知识。每一个环节的深入,都能带来技术上的切实提升。我最初版本爬取10万个评论花了近一天,经过代理池、分布式和请求参数优化后,时间缩短到了几个小时。这个过程里最大的体会就是:耐心分析网络请求,谨慎模拟浏览器行为,尊重目标网站的服务器压力,是爬虫项目能够长期稳定运行的不二法门。希望这份详细的实战记录,能帮你少走弯路,顺利拿到你需要的数据。如果在实际操作中遇到新的具体问题,比如某个特定的API参数怎么构造,欢迎随时交流讨论。