为什么BeautifulSoup没有找到特定的表类?

时间:2014-02-09 03:42:03

标签: python web-scraping beautifulsoup

我正在使用Beautiful Soup尝试从Oil-Price.net上刮下商品表。我可以找到第一个div,table,table body和table body的行。但是其中一行中有一列我用美丽的汤找不到。当我告诉python打印该特定行中的所有表时,它没有显示我想要的那个。这是我的代码:

from urllib2 import urlopen
from bs4 import BeautifulSoup

html = urlopen('http://oil-price.net').read()
soup = BeautifulSoup(html)

div = soup.find("div",{"id":"cntPos"})
table1 = div.find("table",{"class":"cntTb"})
tb1_body = table1.find("tbody")
tb1_rows = tb1_body.find_all("tr")
tb1_row = tb1_rows[1]
td = tb1_row.find("td",{"class":"cntBoxGreyLnk"})
print td

所有打印都是无。我甚至尝试打印每一行,看看我是否可以手动找到列而不是任何内容。 “它会向别人展示。但不是我想要的那个。

2 个答案:

答案 0 :(得分:5)

该页面使用损坏的HTML,不同的解析器将尝试以不同方式修复它。安装lxml解析器,它会更好地解析该页面:

>>> BeautifulSoup(html, 'html.parser').find("div",{"id":"cntPos"}).find("table",{"class":"cntTb"}).tbody.find_all("tr")[1].find("td",{"class":"cntBoxGreyLnk"}) is None
True
>>> BeautifulSoup(html, 'lxml').find("div",{"id":"cntPos"}).find("table",{"class":"cntTb"}).tbody.find_all("tr")[1].find("td",{"class":"cntBoxGreyLnk"}) is None
False

这并不意味着lxml将比其他解析器选项更好地处理所有损坏的HTML。另请参阅html5lib,这是WHATWG HTML spec的纯Python实现,因此更紧密地遵循当前浏览器实现如何处理损坏的HTML。

答案 1 :(得分:5)

查看页面来源:

<td class="cntBoxGreyLnk" rowspan="2" valign="top">
    <script type="text/javascript" src="http://www.oil-price.net/COMMODITIES/gen.php?lang=en"></script>
    <noscript> To get live <a href="http://www.oil-price.net/dashboard.php?lang=en#COMMODITIES">gold, oil and commodity price</a>, please enable Javascript.</noscript>

您想要的数据会动态加载到页面中;您无法使用BeautifulSoup获取它,因为HTML中不存在

如果您在http://www.oil-price.net/COMMODITIES/gen.php?lang=en查看链接的脚本网址 你看到一堆像

这样的javascript
document.writeln('<table summary=\"Crude oil and commodity prices (c) http://oil-price.net\" style=\"font-family: Lucida Sans Unicode, Lucida Grande, Sans-Serif; font-size: 12px; background: #fff; border-collapse: collapse; text-align: left; border-color: #6678b1; border-width: 1px 1px 1px 1px; border-style: solid;\">');
document.writeln('<thead>');
/* ... */
document.writeln('<tr>');
document.writeln('<td style=\"font-size: 12px; font-weight: bold; border-bottom: 1px solid #ccc; color: #1869bd; padding: 2px 6px; white-space: nowrap;\">');
document.writeln('<a href=\"http://oil-price.net/dashboard.php?lang=en#COMMODITIES\"  style=\"color: #1869bd; text-decoration:none\">Heating Oil<\/a>');
document.writeln('<\/td>');
document.writeln('<td style=\"font-size: 12px; font-weight: normal; border-bottom: 1px solid #ccc; color: #000000; padding: 2px 6px; white-space: nowrap;\">');
document.writeln('3.05');
document.writeln('<\/td>');
document.writeln('<td style=\"font-size: 12px; font-weight: normal; border-bottom: 1px solid #ccc; color: green;    padding: 2px 6px; white-space: nowrap;\">');
document.writeln('+1.81%');
document.writeln('<\/td><\/tr>');

加载页面时,会运行此javascript并动态写入您要查找的值。 (顺便说一句:这是一种完全陈旧,诋毁,通常可怕的做事方式;我只能假设某人认为它是一个额外的安全层。他们应该因为他们的冒失而受到惩罚!)。

现在,这段代码非常简单;你可以用正则表达式获取html数据。但是(a)有一些逃避代码可能会导致问题,(b)不能保证他们将来不会混淆他们的代码,(c)那里的乐趣在哪里?

PyV8 module提供了一种从Python执行javascript代码的直接方法,甚至允许我们编写javascript可调用的Python代码!我们将利用这一点以不可挽回的方式获取数据:

import PyV8
import requests
from bs4 import BeautifulSoup

SCRIPT = "http://www.oil-price.net/COMMODITIES/gen.php?lang=en"

class Document:
    def __init__(self):
        self.lines = []

    def writeln(self, s):
        self.lines.append(s)

    @property
    def content(self):
        return '\n'.join(self.lines)

class DOM(PyV8.JSClass):
    def __init__(self):
        self.document = Document()

def main():
    # Create a javascript context which contains
    #   a document object having a writeln method.
    # This allows us to capture the calls to document.writeln()
    dom  = DOM()
    ctxt = PyV8.JSContext(dom)
    ctxt.enter()

    # Grab the javascript and execute it
    js = requests.get(SCRIPT).content
    ctxt.eval(js)

    # The result is the HTML code you are looking for
    html = dom.document.content

    # html is now "<table> ... </table>" containing the data you are after;
    # you can go ahead and finish parsing it with BeautifulSoup
    tbl = BeautifulSoup(html)
    for row in tbl.findAll('tr'):
        print(' / '.join(td.text.strip() for td in row.findAll('td')))

if __name__ == "__main__":
    main()

这导致:

Crude Oil / 99.88 / +2.04%
Natural Gas / 4.78 / -3.27%
Gasoline / 2.75 / +2.40%
Heating Oil / 3.05 / +1.81%
Gold / 1263.30 / +0.45%
Silver / 19.92 / +0.06%
Copper / 3.27 / +0.37%

这是您想要的数据。

编辑:我真的不能再愚蠢了;这是完成工作的最低限度代码。但也许我可以更好地解释它是如何工作的(它真的不像看起来那么可怕!):

PyV8模块以一种Python可以与之交互的方式包装Google的V8 javascript解释器。在运行我的代码之前,您需要转到https://code.google.com/p/pyv8/downloads/list下载并安装相应的版本。

javascript语言本身并不知道如何与外界互动;它没有内置的输入或输出方法。这不是非常有用。为了解决这个问题,我们可以传入一个“上下文对象”,其中包含有关外部世界的信息以及如何与之交互。当javascript在Web浏览器中运行时,它会获得一个上下文对象,该对象提供有关浏览器和当前网页的各种信息以及如何与它们进行交互。

来自http://www.oil-price.net/COMMODITIES/gen.php?lang=en的javascript代码假定它将在浏览器中运行,其中上下文具有表示网页的“文档”对象,该对象具有“writeln”方法,该方法将文本附加到当前结尾的网页。在加载页面时,脚本会被加载并运行;它将文本(恰好是有效的HTML)写入页面;这将作为页面的一部分呈现,最终成为您想要的商品表。您无法使用BeautifulSoup获取该表,因为该表在javascript运行之前不存在,并且BeautifulSoup不会加载或运行javascript。

我们想运行javascript;要做到这一点,我们需要一个虚假的浏览器上下文,其中包含一个带有“writeln”方法的“文档”对象。然后我们需要存储传递给“writeln”的信息,我们需要一种方法在脚本完成后将其恢复。我的DOM类是虚假的浏览器上下文;当实例化时(即当我们创建其中一个时),它给自己一个名为document的Document对象,它有一个writeln方法。当调用document.writeln时,它会将文本行追加到document.lines,并且我们可以随时调用document.content来获取到目前为止所写的所有文本。

现在:行动!在main函数中,我们创建一个虚假的浏览器上下文,将其设置为解释器的当前上下文,并启动解释器。我们抓取javascript代码,并告诉解释器评估(即运行)它。 (源代码混淆,可以搞砸静态分析,不会影响我们,因为代码运行时必须产生良好的输出,我们实际上正在运行它!)一旦代码完成,我们从文档中获得最终输出.context;这是你无法获得的表格html。我们将其传递回BeautifulSoup以提取数据,然后打印数据。

希望有所帮助!