You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 23:42:09