selenium 可重用的美丽汤解析器/配置?

woobm2wo  于 2022-11-10  发布在  其他
关注(0)|答案(1)|浏览(159)

我有一个基于 selenium 的网络爬虫应用程序,可以监控100多种不同的医学出版物,还有更多的定期添加。这些出版物中的每一种都有不同的站点结构,所以我试图使网络爬虫尽可能通用和可重用(特别是因为这是供其他同事使用的)。对于每个Crawler,用户指定允许Crawler爬行的正则表达式URL模式的列表。从那里,爬虫程序将抓取找到的任何链接以及HTML的指定部分。这在下载大量内容方面非常有用,下载时间只需手动下载的一小部分。
我现在正在尝试找出一种基于特定页面的HTML生成定制报告的方法。例如,在抓取X站点时,导出一个JSON文件,该文件显示页面上的问题数、每个问题的名称、每个问题下的文章数,然后显示每篇文章的标题和作者姓名。我将使用的页面和测试用例是https://www.paediatrieschweiz.ch/zeitschriften/
我已经创建了一个列表理解,它可以产生我想要的输出。

url = "https://www.paediatrieschweiz.ch/zeitschriften/"
html = requests.get(url).content
soup = BeautifulSoup(html, 'html.parser')

report = [{
    'Issue': (unit.find('p', {'class': 'section__spitzmarke'}).text).strip(), 
    'Articles': [{
        'Title': ((article.find('h3', {'class': 'teaser__title'}).text).strip()),
        'Author': ((article.find('p', {'class': 'teaser__authors'}).text).strip())
    } for article in unit.find_all('article')]} 
    for unit in soup.find_all('div', {'class': 'section__inner'})]

JSON输出示例:

[
    {
        "Issue": "Paediatrica Vol. 33-3/2022",
        "Articles": [
            {
                "Title": "Editorial",
                "Author": "Daniela Kaiser, Florian Schaub"
            },
            {
                "Title": "Interview mit Dr. med. Germann Clenin",
                "Author": "Florian Schaub, Daniela Kaiser"
            },
            {
                "Title": "Ern\u00e4hrung f\u00fcr sportliche Kinder und Jugendliche",
                "Author": "Simone Reber"
            },
            {
                "Title": "Diabetes und Sport",
                "Author": "Paolo Tonella"
            },
            {
                "Title": "Asthma und Belastung",
                "Author": "Isabelle Rochat"
            },
            {
                "Title": "Sport bei Kindern und Jugendlichen mit rheumatischer Erkrankung",
                "Author": "Daniela Kaiser"
            },
            {
                "Title": "H\u00e4mophilie und Sport",
                "Author": "Denise Etzweiler, Manuela Albisetti"
            },
            {
                "Title": "Apophysen - die Achillesferse junger Sportler",
                "Author": "Florian Schaub"
            },
            {
                "Title": "R\u00fcckenschmerzen bei Athleten im Wachstumsalter",
                "Author": "Markus Renggli"
            },
            {
                "Title": "COVID-19 bei jugendlichen AthletenInnen: Diagnose und Return to Sports",
                "Author": "Susi Kriemler, Jannos Siaplaouras, Holger F\u00f6rster, Christine Joisten"
            },
            {
                "Title": "Schutz von Kindern und Jugendlichen im Sport",
                "Author": "Daniela Kaiser"
            }
        ]
    },
    {
        "Issue": "Paediatrica Vol. 33-2/2022",
        "Articles": [
            {
                "Title": "Editorial",
                "Author": "Caroline Schnider, Jean-Cristoph Caubet"
            },
            {
                "Title": "Der prim\u00e4re Immundefekt \u2013 Ein praktischer Leitfaden f\u00fcr den Kinderarzt",
                "Author": "Tiphaine Arlabosse, Katerina Theodoropoulou, Fabio Candotti"
            },
            {
                "Title": "Arzneimittelallergien bei Kindern: was sollten Kinder\u00e4rzte wissen?",
                "Author": "Felicitas Bellutti Enders, Mich\u00e8le Roth, Samuel Roethlisberger"
            },
            {
                "Title": "Orale Immuntherapie bei Nahrungsmittelallergien im Kindesalter",
                "Author": "Yannick Perrin, Caroline Roduit"
            },
            {
                "Title": "Autoimmunit\u00e4t in der P\u00e4diatrie: \u00dcberlegungen der p\u00e4diatrischen Rheumatologie",
                "Author": "Florence A. Aeschlimann, Raffaella Carlomagno"

但是,如果可能的话,我希望避免为每个单独的爬虫程序使用定制的Python语句或函数,因为每个爬虫程序需要不同的代码。每个爬虫都有自己的JSON配置文件,其中指定了起始URL、允许的URL模式、要下载的内容等。
我最初的想法是创建一个JSON配置来指定要抓取并存储在字典中的Beautiful Soup元素--不编写代码的同事将能够设置这些元素。我的想法是这样的.

{
    "name": "Paedriactia",
    "unit": {
        "selector": {
            "name": "div",
            "attrs": {"class": "section__inner"},
            "find_all": true
        }, 
        "items": {
            "issue": {
                "name": "p", "attrs": {"class": "section__spitzmarke"}, "get_text": true
            } 
        }, 
        "subunits": {
            "articles": {
                "selector": {
                    "name": "article",
                    "find_all": true
                },
                "items": {
                    "Title": {
                        "name": "h3",
                        "attrs": {"class": "teaser__title"},
                        "get_text": true
                    }
                }
            }
        }
    }
}

...以及一个能够根据配置解析HTML并生成JSON输出的Python函数。
然而,我完全不知道如何做到这一点,特别是在处理嵌套元素时。每次我试图靠自己来解决这个问题时,我都会把自己搞糊涂,结果又回到了最初的位置。
如果这一切都说得通,有谁会有任何建议或想法来处理这种美丽的汤配置?
我还精通用Beautiful Soup编写代码,因此我愿意为每个爬虫程序编写定制的Beautiful Soup函数和语句(甚至可以培训其他人这样做)。如果我采用这条路线,那么存储所有定制代码并将其导入最佳位置在哪里?我是否可以在每个Crawler文件夹中都有某种“parse.py”文件,并且只在需要时(即,当特定的Crawler运行时)导入它的功能?
如果对您有所帮助,下面是网络爬虫项目当前结构的一个示例:

oalqel3c

oalqel3c1#

在BeautifulSoup中,与使用find_allfind相比,我更喜欢使用selectselect_one来抓取嵌套元素。(如果您不习惯使用css选择器,我发现w3schools reference page对他们来说是一个很好的指南。)
如果您定义了如下函数

def getSoupData(mSoup, dataStruct, maxDepth=None, curDepth=0):
    if type(dataStruct) != dict:
        # so selector/targetAttr can also be sent as a single string 
        if str(dataStruct).startswith('"ta":'):
            dKey = 'targetAttr'
        else:
            dKey = 'cssSelector'
        dataStruct = str(dataStruct).replace('"ta":', '', 1)
        dataStruct = {dKey: dataStruct}

    # default values: isList=False, items={}
    isList = dataStruct['isList'] if 'isList' in dataStruct else False
    if 'items' in dataStruct and type(dataStruct['items']) == dict:
        items = dataStruct['items']
    else: items = {}

    # no selector -> just use the input directly
    if 'cssSelector' not in dataStruct:
        soup = mSoup if type(mSoup) == list else [mSoup]
    else:
        soup = mSoup.select(dataStruct['cssSelector'])
        # so that unneeded parts are not processed:
        if not isList: soup = soup[:1]

    # return empty nothing was selected
    if not soup: return [] if isList else None

    # return text or attribute values - no more recursion
    if items == {}:
        if 'targetAttr' in dataStruct:
            targetAttr = dataStruct['targetAttr']
        else:
            targetAttr = '"text"'  # default

        if targetAttr == '"text"':
            sData = [s.get_text(strip=True) for s in soup]
        # can put in more options with elif
        else:
            sData = [s.get(targetAttr) for s in soup]

        return sData if isList else sData[0]

    # return error - recursion limited
    if maxDepth is not None and curDepth > maxDepth:
        return {
            'errorMsg': f'Maximum [{maxDepth}] exceeded at depth={curDepth}'
        }

    # recursively get items
    sData = [dict([(i, getSoupData(
        s, items[i], maxDepth, curDepth + 1
    )) for i in items]) for s in soup]

    return sData if isList else sData[0]
    # return list only if isList is set

你可以让你的数据结构和你的html结构一样嵌套[因为函数是递归的]……如果你出于某种原因想要这样做;但你也可以设置maxDepth来限制它的嵌套方式--如果你不想设置任何限制,你可以同时去掉maxDepthcurDepth以及涉及它们的任何部分。
然后,您可以使您的配置文件类似于

{
    "name": "Paedriactia",
    "data_structure": {
        "cssSelector": "div.section__inner",
        "items": {
            "Issue": "p.section__spitzmarke",
            "Articles": {
                "cssSelector": "article",
                "items": {
                    "Title": "h3.teaser__title",
                    "Author": "p.teaser__authors" 
                },
                "isList": true
            }
        },
        "isList": true
    }
    "url": "https://www.paediatrieschweiz.ch/zeitschriften/"
}

[这里的"isList": true等同于您的"find_all": true;您的“子单元”在这里也定义为“Items”-该函数可以根据结构/数据类型进行区分。]
现在,您[在问题开头]显示的相同数据可以用


# import json

configC = json.load(open('crawlerConfig_paedriactia.json', 'r'))

url = configC['url']
html = requests.get(url).content
soup = BeautifulSoup(html, 'html.parser')

dStruct = configC['data_structure']
getSoupData(soup, dStruct)

对于本例,您可以通过将{"cssSelector": "a.teaser__inner", "targetAttr": "href"}添加为...Articles.items.Link来添加文章链接。
还要注意,[因为函数开头设置了缺省值],"Title": "h3.teaser__title"

"Title": { "cssSelector": "h3.teaser__title" }

"Link": "\"ta\":href"

会是一样的

"Link": {"targetAttr": "href"}

相关问题