Scrapy爬虫:按筛选规则提取网页表格数据构建目标JSON文件
Scrapy爬虫实现指定结构JSON输出的操作步骤
1. 基础信息提取逻辑修正
你已经拿到了赛季、赛事分类的基础文本,先补全剩余基础字段的提取,所有文本提取后统一加.strip()清除首尾空格、换行,避免出现字段值前后带多余空格的问题:
- Jornada(轮次):定位页面上显示轮次的元素(一般class带
print-acta-jornada标识),提取文本后过滤掉非数字字符,转成整数类型 - Estadi(球场)信息:分别定位球场名称、地址对应的文本节点提取即可,选择器如果不对,在Scrapy shell里执行
view(response)打开渲染后的页面,对着元素属性抄class即可。
2. 封装通用提取函数减少重复代码
你需要提取的首发、替补、教练组、裁判、卡牌、进球列表结构高度相似,写通用工具函数复用即可:
- 普通球员列表(首发、替补):遍历表格行,提取球员姓名后用
split(maxsplit=1)按第一个空格拆分,前半段是Nom(名),后半段是Cognom(姓);链接用response.urljoin()把页面里的相对路径转成绝对路径 - 教练组、裁判列表:逻辑和球员列表一致,额外提取对应列的执照号、所属协会字段即可
- 卡牌、进球列表:额外提取类型、分钟数字段,分钟数过滤掉非数字字符后转成整数。
3. 分表格定位对应数据区块
不要全量遍历所有table.acta-table,先在Scrapy shell里执行len(response.css('table.acta-table'))确认返回值为12(和你提到的表格数量一致),再用索引定位每个表格:
- 索引0:主队首发 → TitularsCasa
- 索引1:主队替补 → SuplentsCasa
- 索引2:主队教练组 → CosTecnicCasa
- 索引3:主队卡牌 → TargetesCasa
- 索引4:裁判 → Arbitres
- 索引5:进球 → Gols
- 索引6:客队首发 → TitularsFora
- 索引7:客队替补 → SuplentsFora
- 索引8:客队教练组 → CosTecnicFora
- 索引9:客队卡牌 → TargetesFora
如果索引对应内容不对,逐个打印表格前几行内容确认对应关系,调整索引值即可。
4. 组装结果输出
不要在循环里零散yield字段,先初始化一个空字典,把每个区块提取完的内容赋值到对应key下,最后yield整个字典,Scrapy会自动输出符合结构的JSON文件。
调整后的完整参考代码如下:
import scrapy class ActaSpider(scrapy.Spider): name = 'acta_spider' start_urls = [ 'https://www.fcf.cat/acta/2022/futbol-11/cadet-primera-divisio/grup-2/1c/la-salle-bonanova-ce-a/1c/lhospitalet-centre-esports-b'] def parse(self, response): result = {} # 提取比赛基础信息 temporada = response.css(".print-acta-temp::text").get().strip().replace('TEMPORADA ','') acta_comp = response.css(".print-acta-comp::text").get().strip() acta_comp_llista = acta_comp.split() jornada_text = response.css(".print-acta-jornada::text").get().strip() jornada = int(''.join([c for c in jornada_text if c.isdigit()])) result["DadesPartit"] = { "Temporada": temporada, "Categoria": acta_comp_llista[1], "Divisio": acta_comp_llista[2], "Grup": acta_comp_llista[6], "Jornada": jornada } # 提取球场信息,选择器根据实际页面调整 result["Estadi"] = { "Nom": response.css(".acta-estadi-nom::text").get().strip(), "Direccio": response.css(".acta-estadi-adreca::text").get().strip() } # 通用:提取首发/替补球员列表 def parse_player_list(table): players = [] for row in table.css("tbody tr"): full_name = row.css("a::text").get(default="").strip() if not full_name: continue name_parts = full_name.split(maxsplit=1) nom = name_parts[0] cognom = name_parts[1] if len(name_parts) > 1 else "" link = response.urljoin(row.css("a::attr(href)").get(default="")) players.append({ "Nom": nom, "Cognom": cognom, "Link": link }) return players # 通用:提取教练组列表 def parse_coach_list(table): coaches = [] for row in table.css("tbody tr"): full_name = row.css("td:first-child::text").get(default="").strip() if not full_name: continue name_parts = full_name.split(maxsplit=1) nom = name_parts[0] cognom = name_parts[1] if len(name_parts) > 1 else "" llicencia = row.css("td:last-child::text").get(default="").strip() coaches.append({ "Nom": nom, "Cognom": cognom, "Llicencia": llicencia }) return coaches # 通用:提取卡牌列表 def parse_card_list(table): cards = [] for row in table.css("tbody tr"): full_name = row.css("a::text").get(default="").strip() if not full_name: continue name_parts = full_name.split(maxsplit=1) nom = name_parts[0] cognom = name_parts[1] if len(name_parts) > 1 else "" tipus = row.css("td:nth-child(2)::text").get(default="").strip() minut_text = row.css("td:nth-child(3)::text").get(default="0") minut = int(''.join([c for c in minut_text if c.isdigit()])) cards.append({ "Nom": nom, "Cognom": cognom, "Tipus": tipus, "Minut": minut }) return cards # 通用:提取裁判列表 def parse_referee_list(table): referees = [] for row in table.css("tbody tr"): full_name = row.css("td:first-child::text").get(default="").strip() if not full_name: continue name_parts = full_name.split(maxsplit=1) nom = name_parts[0] cognom = name_parts[1] if len(name_parts) > 1 else "" delegacio = row.css("td:nth-child(2)::text").get(default="").strip() referees.append({ "Nom": nom, "Cognom": cognom, "Delegacio": delegacio }) return referees # 通用:提取进球列表 def parse_goal_list(table): goals = [] for row in table.css("tbody tr"): full_name = row.css("a::text").get(default="").strip() if not full_name: continue name_parts = full_name.split(maxsplit=1) nom = name_parts[0] cognom = name_parts[1] if len(name_parts) > 1 else "" tipus = row.css("td:nth-child(2)::text").get(default="").strip() minut_text = row.css("td:nth-child(3)::text").get(default="0") minut = int(''.join([c for c in minut_text if c.isdigit()])) goals.append({ "Nom": nom, "Cognom": cognom, "Tipus": tipus, "Minut": minut }) return goals # 按索引读取所有表格,赋值到对应字段 tables = response.css('table.acta-table') result["TitularsCasa"] = parse_player_list(tables[0]) result["SuplentsCasa"] = parse_player_list(tables[1]) result["CosTecnicCasa"] = parse_coach_list(tables[2]) result["TargetesCasa"] = parse_card_list(tables[3]) result["Arbitres"] = parse_referee_list(tables[4]) result["Gols"] = parse_goal_list(tables[5]) result["TitularsFora"] = parse_player_list(tables[6]) result["SuplentsFora"] = parse_player_list(tables[7]) result["CosTecnicFora"] = parse_coach_list(tables[8]) result["TargetesFora"] = parse_card_list(tables[9]) yield result
调试提示
- 所有选择器如果返回空值,直接在Scrapy shell里逐行测试,比如执行
tables[0].css("a::text").getall()看第一个表格的球员名提取结果是否符合预期 - 运行爬虫时加
-o result.json参数,即可直接把结果输出到JSON文件中 - 如果表格索引和实际内容不匹配,逐个打印每个表格的第一行文本,确认对应关系后调整索引即可。
内容的提问来源于stack exchange,提问作者futbolbase celh
相关产品推荐
相关产品推荐

