【问题标题】:Pulling company name from webpage within <a> tag从 <a> 标签内的网页中提取公司名称
【发布时间】:2020-01-13 16:24:57
【问题描述】:

我正在尝试通过使用 Python 3.7 和 BeautifulSoup 来提取公司名称来简化我的数据收集,如果该公司获得批准或其他,并且如果他们从这个 CT 许可证列表中向住宅和/或企业营销:

http://www.dpuc.state.ct.us/electric.nsf/$FormByElectricApplicantsView?OpenForm&amp;Start=1&amp;Count=1000&amp;ExpandView

如果我能在如何完成代码以至少在 Python 中创建附加公司名称列表方面获得任何帮助,那将是一个很大的帮助。

我正在学习,并且能够连接到该站点并提取源代码。

到目前为止,我知道这部分代码可以工作,但不太确定从哪里开始:

import requests
from bs4 import BeautifulSoup
result = requests.get("http://www.dpuc.state.ct.us/electric.nsf/$FormByElectricApplicantsView?OpenForm&Start=1&Count=1000&ExpandView")
src = result.content
soup = BeautifulSoup(src,'lxml')

我可以在源代码中看到公司名称,但不确定将其和其他名称提取到列表中的最佳方法:

<a href="/electric.nsf/c39dc573ab1299538525743b004d4df6/719f088ca20a6a1f85257dfd00480e13?OpenDocument">3Degrees Group, Inc.</a>

我希望能够将所有这些都提取到 csv 中。在某些时候提交包含公司、许可证状态以及他们的市场对象的文件,但如果有人可以帮助我完成代码以将公司放在 Python 中的列表中,我将不胜感激,并允许我通过示例进行学习。

【问题讨论】:

  • 如果你能用某个类唯一地识别所有&lt;a&gt;标签,你就可以做到。 soup.find_all('a', attrs={'class':'xxx'}。 (假设页面中有多个&lt;a&gt;
  • 只是供应商?还是聚合器?
  • 嗨,@QHarr,现在只是供应商。

标签: python html web-scraping beautifulsoup python-requests


【解决方案1】:

在 bs4 4.7.1+ 中,您可以使用 :contains:has 仅过滤与 Supplier 相关的部分。您可以进一步对感兴趣的列的 df 进行子集化。


tl;dr;

向soup对象发出请求并读取响应:

r = requests.get('http://www.dpuc.state.ct.us/electric.nsf/$FormByElectricApplicantsView?OpenForm&Start=1&Count=1000&ExpandView')
soup = bs(r.content, 'lxml')

让我们用一些图片来解释接下来的步骤......

我们有很多嵌套表,这可能会让人感到困惑,但我们可以从考虑感兴趣的“顶级”表开始:

我们可以从中获取(将鼠标悬停在实际 html 中该图像中的不同 trs 并观察页面上突出显示的内容以跟随。您可以在浏览器搜索框中输入 p:nth-child(4) &gt; table 以隔离此表):

确实,实际可见内容是嵌套的,但我们知道所有感兴趣的内容都在trs 内,在给定级别是一系列兄弟trs

这一点

soup.select('tr:has(td:nth-of-type(1) font:contains(Supplier)) ~ tr:not(:has(td:nth-of-type(1) font:contains(Aggregator)), :has(td:nth-of-type(1) font:contains(Aggregator)) ~ tr)')

收集'顶级'级别tr的兄弟trs子节点font包含Suppliertr:has(td:nth-of-type(1) font:contains(Supplier)),然后删除“顶级”级别 tr 包含 Aggregator 及其兄弟姐妹。由于您在 html 中拥有一长串 trs,因此您只想过滤掉感兴趣部分之后的内容。

查看第一部分(我使用select_one,而不是select,来演示匹配的第一个节点,而不是添加general sibling combinator 时匹配的多个兄弟节点 - 更多关于后来):

soup.select_one('tr:has(td:nth-of-type(1) font:contains(Supplier))')

运行上面给出:

将此与页面进行比较:

您会看到我们如何找到一种方法来开始在顶层选择 trs,从包含 Supplier 标签的那个(虽然是嵌套的)开始。然而,我们知道顶层是平坦的,我们需要从(包括)Aggregator 标签中删除所有内容。因此,我们将:not 伪类添加到该通用兄弟组合器中。简而言之,我们说获取所有trs 的兄弟姐妹,除了:not 中的兄弟姐妹

现在我们有了感兴趣的行子集(绿色矩形)。我们for loop 处理这些,每次我们找到与'td:nth-of-type(2) font' 匹配的节点时,我们将status 设置为找到的值,例如Approved, Pending.....

这一行:

if node is not None:

正在检查当前 tr 是否包含 'status' 子节点,例如Approved/Pending(我将其命名为 status 用于输出)并相应地设置此类别的后面行标识符。

如果tr 没有此子节点,那么我们知道它是另一个trs 之一,其中包含tds 以及用于输出的附加信息列,例如:

由于嵌套级别,有一些空的tds 包括我们不想要的。这些我们稍后会使用 pandas 删除:

df.drop([1,2,10], axis=1, inplace=True)

由于我们希望status 标签和所有tds 在一个列表row 中,我使用extend 扩展第一个列表以包含第二个列表。我相信这比insert 更快,但希望能对此表示赞赏。

所以,例如,我们会从:

['Approved'] 

['', '', 'Yes', 'Yes', '3Degrees Group, Inc.', 'http://www.3degreesinc.com', '235 Montgomery Street, Suite 320 San Francisco, CA 94104', '(866) 476-9378', '11-11-07 12/14/2011', ''] 

['Approved', '', '', 'Yes', 'Yes', '3Degrees Group, Inc.', 'http://www.3degreesinc.com', '235 Montgomery Street, Suite 320 San Francisco, CA 94104', '(866) 476-9378', '11-11-07 12/14/2011', ''] 

这些行被添加到名为final 的列表中。所以,你有一个列表列表(每一行)。

您可以将此列表传递给pandas.DataFrame,以使用to_csv 方法生成准备好用于csv 导出的数据框。您使用 columns 参数来指定标头。

生成行时:

tds = [td.text for td in tr.select('td')]

我们偶尔会发现额外的空格和\n(换行符),例如

['', '', '', '', 'WFM Intermediary New England Energy, LLC', '', '125 Cambridgepark Dr, Cambridge, MA 02140', '', '07-10-08  \n11/28/2007\n9/8/2011', ''] 

我实现了一个简单的正则表达式来删除它:

tds = [re.sub('\n+|\s+',' ',td.text) for td in tr.select('td')]

导致(也许不是最好的例子,但只是说明性的):

['', '', '', '', 'WFM Intermediary New England Energy, LLC', '', '125 Cambridgepark Dr, Cambridge, MA 02140', '', '07-10-08 11/28/2007 9/8/2011', '']

正则表达式:

最后(你做到这一点了吗?),我们想为数据框添加一些标题。我们可以使用页面中的那些,并再次通过extend 确保我们包含我们的自定义status 标头。

headers = ['Status']
headers.extend([th.text for th in soup.select('th[align]')])


派:

from bs4 import BeautifulSoup as bs
import requests, re
import pandas as pd

r = requests.get('http://www.dpuc.state.ct.us/electric.nsf/$FormByElectricApplicantsView?OpenForm&Start=1&Count=1000&ExpandView')
soup = bs(r.content, 'lxml')
final = []
headers = ['Status']
headers.extend([th.text for th in soup.select('th[align]')])

for tr in soup.select('tr:has(td:nth-of-type(1) font:contains(Supplier)) ~ tr:not(:has(td:nth-of-type(1) font:contains(Aggregator)), :has(td:nth-of-type(1) font:contains(Aggregator)) ~ tr)'):
    node = tr.select_one('td:nth-of-type(2) font')
    if node is not None:
        status = node.text
    else:
        row = [status]
        tds = [re.sub('\n+|\s+',' ',td.text) for td in tr.select('td')]
        row.extend(tds)
        final.append(row)

df = pd.DataFrame(final)
df.drop([1,2,10], axis=1, inplace=True)
df.columns = headers
df.to_csv(r'C:\Users\User\Desktop\Public Utilities.csv', sep=',', encoding='utf-8-sig',index = False )

输出示例:


补充阅读:

  1. Css selectors

【讨论】:

  • 干得好! website-html-source 对于按类或属性分隔 divshrefs 看起来并不友好。这是一个非常好的简洁的解决方案。
  • @QHarr 非常感谢!这按我想要的方式工作。感谢您抽出时间盯着它看了一段时间,哈哈。非常感激。打算接受这个并尝试从中学习并将其应用于其他要爬行的事物。再次感谢您!
  • 而且后面的块很容易扩展。
  • 嘿@QHarr 我一整天都在研究这个,并且能够在逻辑上将其中的一些拼凑在一起,但不明白它是如何完全完成这项工作的。如果你有时间介意告诉我代码背后的逻辑吗?
  • 绝对会在今天晚些时候更新。有什么特别的吗?
【解决方案2】:
company_list = [];
for company in soup.find_all('a'):
    company_list.append(company.string);

您可以在 BeautifulSoup 文档中找到有关此的更多详细信息。您可能必须修改此代码以通过循环内的条件过滤掉特定标签,但我相信这应该非常简单。

更具体地说,对于您的特定示例,您可以通过正则表达式或任何您喜欢的方式检查 company.get('href') 匹配“.nsf”。

【讨论】:

  • 你可以做company_list = [x.string for x in soup.find_all('a')]。但这假设页面中的所有&lt;a&gt; 标签都是用于名称的(从我的经验来看,大多数情况下并非如此。)
  • 也不要使用.string,而是使用.text 更好。参考:stackoverflow.com/questions/25327693/…
  • 我特别没有使用理解来允许更简单的条件,但是是的,理解可以用于更简单的示例。
  • 这将生成一个交替包含公司名称及其网站的列表。如果 OP 只是寻找其中之一,他将不得不拆分列表。
  • @JackFleeting 阅读我评论的第二部分;如果您通过正则表达式过滤器来匹配 href 标记中的 .nsf,那么它将只包含公司名称。 (或者可能是另一个条件,我没有详细阅读页面源)。
猜你喜欢
  • 2021-04-08
  • 1970-01-01
  • 1970-01-01
  • 2021-05-28
  • 2010-12-22
  • 2021-09-26
  • 2017-06-15
  • 1970-01-01
  • 2020-10-10
相关资源
最近更新 更多