Scrapy爬取TripAdvisor餐厅评论出现字段混淆问题求助
嘿,这个问题我之前帮朋友排查过几乎一模一样的情况——核心问题基本都出在异步请求下的数据传递不严谨上,跟MAX_CONCURRENT_REQUESTS关系不大,调那个只是改并发数,本质的共享状态冲突问题没解决。下面给你拆解具体原因和解决方案:
1. 别用全局变量存储当前餐厅信息
这是最常见的坑:如果你在爬虫里定义了类似current_restaurant = {}的全局变量,用来临时存当前爬取的餐厅名称/ID,那Scrapy的异步并发会让多个请求同时修改这个变量,直接导致评论和所属餐厅串线。
正确做法:用meta传递餐厅标识
把餐厅的唯一标识(名称、ID、详情页URL都行)通过Request的meta参数,传递给每一个分页评论请求,让每个评论Item生成时都从response.meta里取对应的归属信息:
def parse_restaurant_detail(self, response): # 从餐厅详情页提取基础信息 restaurant_info = { 'name': response.css('h1[data-test-target="top-info-header"]::text').get().strip(), 'restaurant_id': response.url.split('-')[-1].split('.')[0] } # 发起第一页评论请求,把餐厅信息塞进meta first_review_url = response.url + '#REVIEWS' yield scrapy.Request( url=first_review_url, callback=self.parse_reviews, meta={'restaurant': restaurant_info} # 传递完整餐厅信息 ) # 处理分页链接,每个分页请求都复用meta next_page = response.css('a.nav.next.ui_button.primary::attr(href)').get() if next_page: yield scrapy.Request( url=response.urljoin(next_page), callback=self.parse_reviews, meta=response.meta # 直接复用当前meta,避免手动重复写 ) def parse_reviews(self, response): # 从meta里取出当前所属餐厅的信息 current_restaurant = response.meta['restaurant'] # 遍历解析评论 for review_card in response.css('div.review-container'): yield { 'restaurant_name': current_restaurant['name'], 'restaurant_id': current_restaurant['restaurant_id'], 'review_text': review_card.css('p.partial_entry::text').get(default='').strip(), 'rating': review_card.css('span.ui_bubble_rating::attr(class)').re(r'ui_bubble_rating bubble_(\d+)')[0], # 其他评论字段... } # 处理下一页,继续传递meta next_page = response.css('a.nav.next.ui_button.primary::attr(href)').get() if next_page: yield scrapy.Request( url=response.urljoin(next_page), callback=self.parse_reviews, meta=response.meta )
2. 检查Item Pipeline的共享状态问题
如果你的Pipeline里用了类属性(比如self.temp_reviews = [])来临时按餐厅分组存储数据,异步并发下多个Item同时写入也会导致数据混淆。
正确做法:让Item自带归属标识,Pipeline直接独立处理
不需要在Pipeline里做临时分组,每个Item已经通过meta带上了餐厅ID/名称,存储到JSON或MongoDB时,直接按这个标识写入即可:
class TripAdvisorPipeline: def process_item(self, item, spider): # 直接写入MongoDB,或者追加到JSON文件 # 比如MongoDB: spider.collection.insert_one(dict(item)) return item
如果必须在Pipeline里做分组,一定要用线程安全的锁来保护临界区:
import threading class TripAdvisorPipeline: def __init__(self): self.lock = threading.Lock() self.grouped_reviews = {} def process_item(self, item, spider): with self.lock: res_id = item['restaurant_id'] if res_id not in self.grouped_reviews: self.grouped_reviews[res_id] = [] self.grouped_reviews[res_id].append(item) return item
3. 验证分页链接的正确性
有时候TripAdvisor的分页链接是动态生成的相对路径,如果手动拼接URL出错,会导致某个餐厅的分页请求跳转到另一个餐厅的页面。
排查方式:
在parse_reviews函数开头加日志,打印当前请求的URL和所属餐厅名称:
def parse_reviews(self, response): current_restaurant = response.meta['restaurant'] spider.logger.info(f"正在爬取餐厅:{current_restaurant['name']},URL:{response.url}") # 后续解析逻辑...
如果发现日志里URL对应的餐厅和current_restaurant不一致,说明分页链接拼接错误,一定要用response.urljoin(next_page)生成绝对URL,不要手动拼接字符串。
4. 谨慎使用dont_filter=True
如果为了绕反爬给分页请求加了dont_filter=True,可能会导致重复请求或错误的请求被处理,进而混入其他餐厅的数据。非必要情况下不要开启这个参数,真要绕反爬可以用RotatingProxyMiddleware或者调整DOWNLOAD_DELAY。
内容的提问来源于stack exchange,提问作者Clement Lombard

