使用Scrapy的网页爬虫

1 投票
1 回答
539 浏览
提问于 2025-04-18 06:40

我需要从这个链接中提取位置和分数。这个链接上有21个列表(我其实不知道该怎么称呼它们)enter image description here,每个列表里有40个球员enter image description here,除了最后一个。现在我写了一个代码,像这样:

from bs4 import BeautifulSoup
import urllib2

def overall_standing():
    url_list = ["http://www.afl.com.au/afl/stats/player-ratings/overall-standings#", 
                "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/2",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/3",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/4",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/5",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/6",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/7",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/8",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/9",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/10",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/11",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/12",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/13",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/14",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/15",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/16",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/17",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/18",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/19",
#                 "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/20",
                "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/21"]

    gDictPlayerPointsInfo = {}
    for url in url_list:
        print url
        header = {'User-Agent': 'Mozilla/5.0'}
        header = {'User-Agent': 'Mozilla/5.0'}
        req = urllib2.Request(url,headers=header)
        page = urllib2.urlopen(req)
        soup = BeautifulSoup(page)
        table = soup.find("table", { "class" : "ladder zebra player-ratings" })

        lCount = 1
        for row in table.find_all("tr"):
            lPlayerName = ""
            lTeamName = ""
            lPosition = ""
            lPoint = ""
            for cell in row.find_all("td"):
                if lCount == 2:
                    lPlayerName = str(cell.get_text()).strip().upper()
                elif lCount == 3:
                    lTeamName = str(cell.get_text()).strip().split("\n")[-1].strip().upper()
                elif lCount == 4:
                    lPosition = str(cell.get_text().strip())
                elif lCount == 6:
                    lPoint = str(cell.get_text().strip())

                lCount += 1

            if url == "http://www.afl.com.au/afl/stats/player-ratings/overall-standings#page/2":
                print lTeamName, lPlayerName, lPoint
            if lPlayerName <> "" and lTeamName <> "":
                lStr = lPosition + "," + lPoint

#                 if gDictPlayerPointsInfo.has_key(lTeamName):
#                     gDictPlayerPointsInfo[lTeamName].append({lPlayerName:lStr})
#                 else:
                gDictPlayerPointsInfo[lTeamName+","+lPlayerName] = lStr
            lCount = 1


    lfp = open("a.txt","w")
    for key in gDictPlayerPointsInfo:
        if key.find("RICHMOND"):
            lfp.write(str(gDictPlayerPointsInfo[key]))

    lfp.close()
    return gDictPlayerPointsInfo


# overall_standing()

但问题是,它总是只给我第一个列表的分数和位置,忽略了其他20个。我该怎么才能获取所有21个列表的位置信息和分数呢?我听说scrapy可以很简单地做到这一点,但我对scrapy还不太熟悉。除了使用scrapy,还有其他方法吗?

1 个回答

5

这个问题的出现是因为这些链接是由服务器处理的,而链接中以#符号后面的部分被称为片段标识符,通常是由浏览器处理的,指向某个链接或javascript行为,也就是加载不同的结果集。

我建议有两种方法可以尝试,一是找到一种链接,让服务器能够处理,这样你就可以继续使用scrapy;二是使用像selenium这样的网页驱动程序。

Scrapy

第一步是找出javascript加载的调用,通常是ajax,然后用这些链接来获取信息。这些调用是向网站的数据库发出的。你可以打开网页检查工具,观察当你点击下一个搜索结果页面时的网络流量:

Before clicking

然后在点击之后:

after clicking

我们可以看到有一个新的调用到这个网址:

http://www.afl.com.au/api/cfs/afl/playerRatings?roundId=CD_R201401408&pageNum=3&pageSize=40

这个网址返回一个json文件,可以解析,而且看起来你可以更好地控制返回给你的信息。

你可以写一个方法来生成一系列链接:

def gen_url(page_no):
    return "http://www.afl.com.au/api/cfs/afl/playerRatings?roundId=CD_R201401408&pageNum=" + str(page_no) + "&pageSize=40"

然后,比如说,使用scrapy和种子列表:

seed = [gen_url(i) for i in range(20)]

或者你可以尝试调整网址参数,看看能得到什么,也许可以一次获取多个页面:

http://www.afl.com.au/api/cfs/afl/playerRatings?roundId=CD_R201401408&pageNum=1&pageSize=200

我把pageSize参数的值改成了200,因为这似乎直接对应于返回的结果数量。

注意:这种方法可能会失效,因为有些网站会通过检查请求的IP来阻止外部使用他们的数据API。

如果是这样,你应该采用以下方法。

Selenium(或其他网页驱动程序)

使用像selenium这样的网页驱动程序,你可以利用在浏览器中加载的数据来评估在服务器返回网页后加载的数据。

在使用selenium之前需要进行一些初始设置,但一旦设置好,它是一个非常强大的工具。

一个简单的例子是:

from selenium import webdriver

driver = webdriver.Firefox()
driver.get("http://www.afl.com.au/stats/player-ratings/overall-standings")

你会看到一个由Python控制的Firefox浏览器(其他浏览器也可以)在你的屏幕上打开,并加载你提供的网址,然后按照你给出的命令执行,这些命令甚至可以从命令行执行(这对调试很有用),你可以像使用scrapy一样搜索和解析HTML(代码继续自前面的代码部分...)

如果你想执行类似点击下一页按钮的操作:

driver.find_elements_by_xpath("//div[@class='pagination']//li[@class='page']")

这个表达式可能需要一些调整,但它的目的是找到在class='pagination'div中所有li元素,//表示元素之间的简短路径,另一种选择是像/html/body/div/div/.....一直找到目标元素,这就是//div/...的用处和吸引力。

关于定位元素的具体帮助和参考,请查看他们的页面

我通常的方法是通过反复试验来调整表达式,直到找到我想要的目标元素。这时控制台/命令行就派上用场了。在像上面那样设置好driver后,我通常会尝试构建我的表达式:

假设你有一个html结构如下:

<html>
    <head></head>
    <body>
        <div id="container">
             <div id="info-i-want">
                  treasure chest
             </div>
        </div>
    </body>
</html> 

我会从这样的表达式开始:

>>> print driver.get_element_by_xpath("//body")
'<body>
    <div id="container">
         <div id="info-i-want">
              treasure chest 
         </div>
    </div>
</body>'
>>> print driver.get_element_by_xpath("//div[@id='container']")
<div id="container">
     <div id="info-i-want">
          treasure chest 
     </div>
</div>
>>> print driver.get_element_by_xpath("//div[@id='info-i-want']")
<div id="info-i-want">
     treasure chest 
</div>
>>> print driver.get_element_by_xpath("//div[@id='info-i-want']/text()")
treasure chest 
>>> # BOOM TREASURE!

通常会更复杂,但这是一个很好的且常常必要的调试策略。

回到你的情况,你可以将它们保存到一个数组中:

links = driver.find_elements_by_xpath("//div[@class='pagination']//li[@class='page']")

然后逐个点击,抓取新的数据,点击下一个:

import time
from selenium import webdriver

driver = None
try:
    driver = webdriver.Firefox()
    driver.get("http://www.afl.com.au/stats/player-ratings/overall-standings")

    #
    # Scrape the first page
    #

    links = driver.find_elements_by_xpath("//div[@class='pagination']//li[@class='page']")

    for link in links:
        link.click()
        #
        # scrape the next page
        #
        time.sleep(1) # pause for a time period to let the data load
finally:
    if driver:
        driver.close()

最好将所有内容包裹在try...finally类型的块中,以确保你关闭驱动实例。

如果你决定深入了解selenium的方法,可以参考他们的文档,里面有非常优秀和详细的说明以及示例

祝你抓取愉快!

撰写回答