【问题标题】:Unable to scrape content from next pages with complex layout无法从具有复杂布局的下一页中抓取内容
【发布时间】:2022-01-23 19:32:24
【问题描述】:

我一直在尝试使用 requests 模块从 webpage 中抓取表格内容。该表的数据分布在多个页面上。我希望继续解析,直到所有下一页都用完为止。

要填充该网页上的表格,只需选择Baltimore 作为县,日期范围从08-08-202001-01-2021,然后按搜索按钮,保留所有内容。

以下脚本可以解析几页(12 或更多)的数据,然后中断。那里没有可用的下一页按钮,所以我不明白如何修改脚本中的逻辑以获取所有下一页。要进入下一页,需要使用__EVENTTARGET 值内的页码。

这是我一直在尝试的:

import requests
from bs4 import BeautifulSoup

coreid = 'dgSearchResults$ctl24$ctl{}'
link = 'https://registers.maryland.gov/RowNetWeb/Estates/frmEstateSearch2.aspx'

p = 1

with requests.Session() as s:
    s.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
    r = s.get(link)
    soup = BeautifulSoup(r.text,"lxml")
    payload = {i['name']:i.get('value','') for i in soup.select('input[name]')}
    payload.pop('cmdClear')
    payload['cboCountyId'] = '3' #Baltimore (next to county)
    payload['DateOfFilingFrom'] = '08-08-2020' #date from (under range)
    payload['DateOfFilingTo'] = '01-01-2021'   #date to (under range)
    payload['cboPartyType'] = 'Decedent'

    while True:
        res = s.post(link,data=payload)
        soup = BeautifulSoup(res.text,"lxml")
        
        if not soup.select_one("table#dgSearchResults tr:has(> td > a[href^='frmDocketImages.aspx'])"): break
        
        for item in soup.select("table#dgSearchResults tr:has(> td > a[href^='frmDocketImages.aspx'])"):
            data = [i.text for i in item.select("td")]
            print(data)

        payload = {i['name']:i.get('value','') for i in soup.select('input[name]')}
        payload['__EVENTTARGET'] = coreid.format(str(p).zfill(2))
        try:
            payload.pop('cmdSearchNew')
        except KeyError: pass
        print('-------------->',payload['__EVENTTARGET'])
        p+=1

如何使用 requests 模块从所有下一页中抓取数据?

【问题讨论】:

  • 我不确定我是否理解您的问题,但它会重复最后一页,因为您从未退出 while 循环。我认为罪魁祸首是你的尝试块,如果cmdSearchNew 告诉是否有新页面,那么except KeyError: break 应该可以解决问题
  • 重复第一页的最后一个数字,如@diggusbickus 中的 10。脚本无法进入12、13、14等页面。
  • while循环下面的第三行是用来打破循环的。
  • 我运行了脚本。它停在dgSearchResults$ctl24$ctl12,没有重复。如果我绕过你的休息,数据会重复是的。您认为有更多数据是因为该网站告诉您有数百个页面还是您知道有更多数据?
  • 是的,我确定。我手动遍历了至少 350 页来检查它。

标签: python python-3.x web-scraping python-requests


【解决方案1】:

您正在抓取的网站是由 asp.net WebForms 生成的。用户可以与之交互的所有内容都被视为“控件”,并且将具有动态生成的标识符,这些标识符取决于它们在容器中的顺序。这些标识符并不总是反映在元素的 id 属性中。当用户与页面交互时,会触发一个名为__doPostBack(eventTarget, eventArgs) 的函数,它会填写一些标准的隐藏表单字段,然后发布到服务器以获取更新的页面。 eventTarget 是被点击元素的 asp.net 标识符。

在这种情况下,您的容器是 ID 为 dgSearchResults 的表。

此站点上的分页导航是容器表的子项,因此导航将以相同的方式工作,使用__EVENTTARGET 进行回发。为了让你使用请求单步浏览网站,你需要知道你当前页面的索引,你需要找到到下一页的链接,你必须将你的帖子中的__EVENTTARGET设置为标识符那个链接。

首先,我们要使用几次正则表达式,所以让我们导入它:

import re

接下来,为了避免猛击网站,我们将在发布请求之间进行短暂的随机等待。这不是您问题的一部分,但这是一个重要的考虑因素。为此,我们将使用random.uniform()time.sleep()

from random import uniform 
from time import sleep

您已经声明 p = 1 来跟踪页码。为了使代码更清晰一点,我会将p 更改为page_num,将其初始化为0,然后在页面循环的每次迭代中的发布请求之后递增它。此外,在每个发布请求之前,我们将等待 500 到 1000 毫秒之间的随机时间(您可以根据需要调整这些时间)。这种等待将避免猛击服务器,并且随机性有助于防止您的代码在服务器日志中看起来像一个机器人:

page_num = 0

while True:
    wait_time = uniform(0.5, 1.0)
    sleep(wait_time)

    res = s.post(link,data=payload)
    page_num += 1

    soup = BeautifulSoup(res.text,"lxml")
    if not soup.select_one("table#dgSearchResults tr:has(> td > a[href^='frmDocketImages.aspx'])"): break

包含分页链接的行有类grid-pager,所以让我们抓住它:

nav_row = soup.select_one("table#dgSearchResults tr.grid-pager")
if nav_row is None: break

第一页这一行的html是:

<tr class="grid-pager" align="right">
<td colspan="7">
    <span>1</span>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl01','')">2</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl02','')">3</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl03','')">4</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl04','')">5</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl05','')">6</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl06','')">7</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl07','')">8</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl08','')">9</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl09','')">10</a>&nbsp;
    <a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl10','')">...</a>
</td>

因此,nav_row 是一个表格行元素,其中包含指向其他结果的链接的单个单元格。您当前的页面是&lt;span&gt; 标记的文本,指向下一个结果集的链接始终是下一个&lt;a&gt;

要获取span,请使用正则表达式查找page_num。即使您要查找的 span 是该行中唯一的一个,但找到 page_num 可确保您获得所需的spans,以防后续添加任何其他 spans。

让我们获取该跨度和下一页的链接:

page_span = nav_row.find('span', string=re.compile(rf'\b{page}\b'))
next_page_link = page_span.find_next_sibling('a')
if next_page_link is None: break

如果没有next_page_link,我们就在列表的末尾(或者出现了问题 - 由您来检查),所以我们中断。 next_page_link的html是:

<a href="javascript:__doPostBack('dgSearchResults$ctl24$ctl01','')">2</a>

eventTarget - 这是我们在发布请求中需要 __EVENTTARGET 的值 - 是链接 href__doPostBack() 中的第一个参数:

href = next_page_link.get('href')

接下来我们可以再次使用正则表达式来获取__EVENTTARGET 所需的标识符。 WebForms 的工作方式始终是doPostBack 之后的第一个引用项,并且始终是必需的。

match = re.search(r'doPostBack\([\'\"]([^\'\"]+)', href)
event_target = match[1]

请注意,代码假定总会有匹配项。您可能需要对其进行测试或添加自己的错误处理。

现在我们有了event_target,我们可以构建有效载荷:

payload = {i['name']:i.get('value','') for i in soup.select('input[name]')}
payload['__EVENTTARGET'] = event_target
payload.pop('cmdSearchNew', None)

print('-------------->',payload['__EVENTTARGET'])

就是这样。您的代码将在循环的下一次迭代中导航到下一页。将它们放在一起并使用您指定的输入运行它将浏览返回的所有 96 个页面:

import requests
from bs4 import BeautifulSoup
import re
from random import uniform
from time import sleep

link = 'https://registers.maryland.gov/RowNetWeb/Estates/frmEstateSearch2.aspx'

with requests.Session() as s:
    s.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
    r = s.get(link)
    soup = BeautifulSoup(r.text,"lxml")
    payload = {i['name']:i.get('value','') for i in soup.select('input[name]')}
    payload.pop('cmdClear')
    payload['cboCountyId'] = '3' #Baltimore (next to county)
    payload['DateOfFilingFrom'] = '08-08-2020' #date from (under range)
    payload['DateOfFilingTo'] = '01-01-2021'   #date to (under range)
    payload['cboPartyType'] = 'Decedent'

    page_num = 0

    while True:
        wait_time = uniform(0.5, 1.0)
        sleep(wait_time)

        res = s.post(link,data=payload)
        page_num += 1

        soup = BeautifulSoup(res.text,"lxml")
        if not soup.select_one("table#dgSearchResults tr:has(> td > a[href^='frmDocketImages.aspx'])"): break
        
        for item in soup.select("table#dgSearchResults tr:has(> td > a[href^='frmDocketImages.aspx'])"):
            data = [i.text for i in item.select("td")]
            print(data)

        nav_row = soup.select_one("table#dgSearchResults tr.grid-pager")
        if nav_row is None: break

        page_span = nav_row.find('span', string=re.compile(rf'\b{page_num}\b'))
        next_page_link = page_span.find_next_sibling('a')
        if next_page_link is None: 
            break

        href = next_page_link.get('href')

        match = re.search(r'doPostBack\([\'\"]([^\'\"]+)', href)
        event_target = match[1]

        payload = {i['name']:i.get('value','') for i in soup.select('input[name]')}
        payload['__EVENTTARGET'] = event_target
        payload.pop('cmdSearchNew', None)

        print('-------------->',payload['__EVENTTARGET'])

    print('DONE')

【讨论】:

  • 它似乎以正确的方式工作@Dimitri。在我用几个不同的日期完成测试后,我会通知你。谢谢。
猜你喜欢
  • 1970-01-01
  • 2019-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多