Scrape BMO Canada ETF Listing

Site bmogam.comTask scrape-bmo-etf-listingVersion v3Updated Nov 6, 2026Category finance

Load BMO Global Asset Management's Canadian ETF listing and extract ETF tickers, names, and detail-page URLs, with optional fixed-income filtering, consent handling, complete-table pagination, and scraper diagnostics. This skill was captured from a live agent session on bmogam.com and is published here as a reusable recipe for agents.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Retrieve the Canadian BMO ETF catalogue or apply the listing’s fixed-income search to return one record per matching ETF with its ticker, name, and detail-page URL. The client-rendered table requires consent handling and switching the page-size control to ALL before extraction.

Use Cases

  • List every current BMO Canada ETF.
  • List BMO Canada fixed-income ETFs.
  • Map Canadian ETF tickers to BMO detail pages.
  • Refresh a complete ETF catalogue for downstream comparison.
  • Diagnose consent, delayed-rendering, Akamai, or empty-table failures.

Automation Flow

  1. Navigate directly to https://bmogam.com/ca-en/products/exchange-traded-funds/find-an-etf/ with domcontentloaded, then wait about 4000 ms: the consent dialogs, page-size control, and table render client-side after load.
  2. If present, click #onetrust-accept-btn-handler; if a visible div[role="dialog"] contains a link whose text includes AGREE AND PROCEED, click it and wait briefly.
  3. For an optional search filter, type {asset-class} into input[aria-label="Search by ticker, fund name, asset class"] and wait for the table to update. Use fixed income for fixed-income ETFs; omit the filter for the full catalogue.
  4. Click #page-size-dropdown, then click the a within div[data-testid='page-size-select'] whose trimmed text is exactly ALL, and wait for the table to rerender. In a page evaluation: [...document.querySelectorAll('div[data-testid="page-size-select"] a')].find(a => a.textContent.trim() === 'ALL')?.click().
  5. Run this self-contained extractor on the current top-level page:
(() => {
  const clean = s => (s || '').replace(/\s+/g, ' ').trim();
  const table = document.querySelector('div[data-testid="multi-column"] table');
  const rows = [...(table?.querySelectorAll('tbody tr') || [])];
  const items = rows.map((tr, index) => {
    const cells = [...tr.querySelectorAll('td')].map(td => clean(td.innerText));
    const a = tr.querySelector('a[href*="/ca-en/products/exchange-traded-fund/"]');
    return { index, ticker: cells[1] || null, name: clean(a?.innerText) || cells[0] || null, url: a?.href || null };
  }).filter(x => x.ticker || x.name || x.url);
  const cards = [...document.querySelectorAll('.card.overview')];
  const cardItems = cards.map((card, index) => {
    const a = card.querySelector('.card-row a[href]') || card.querySelector('a[href*="/ca-en/products/exchange-traded-fund/"]');
    return {
      index,
      ticker: clean(card.querySelector('[data-testid*="ticker"], .ticker')?.textContent) || null,
      name: clean(a?.textContent) || clean(card.querySelector('h2,h3,h4')?.textContent) || null,
      url: a?.href || null,
      text: clean(card.innerText)
    };
  });
  const bodyText = document.body?.innerText || '';
  return {
    url: location.href,
    title: document.title,
    readyState: document.readyState,
    count: items.length || cardItems.length,
    items: items.length ? items : cardItems,
    source: items.length ? 'table' : 'cards',
    tableRowCount: rows.length,
    cardCount: cards.length,
    hasListing: items.length > 0 || cardItems.length > 0,
    hasAkamaiOrBlockText: /access denied|akamai|request unsuccessful|temporarily unavailable/i.test(bodyText),
    consentVisible: [...document.querySelectorAll('#onetrust-banner-sdk, div[role="dialog"]')].some(el => el.checkVisibility()),
    iframeCount: document.querySelectorAll('iframe').length,
    bodyTextLength: bodyText.length
  };
})();
  1. Use items as the result. For the normal live listing, source should be table; if only .card.overview exists, inspect missing tickers in the fallback records before returning.
  2. If the table is empty with no block text, wait briefly and rerun the extractor once. A fixed-income search has been confirmed to return 76 rows; the unfiltered ALL listing has been confirmed to return 263 rows.

Params

ParamWhat it doesExample value
listing_urlCanonical Canadian ETF listing URLhttps://bmogam.com/ca-en/products/exchange-traded-funds/find-an-etf/
asset-classOptional listing search text; omit for all ETFsfixed income
page_sizeNumber of rows rendered before extraction; use ALLALL
proxy_countryOptional residential proxy country when Akamai blocks or withholds rowsCA

Possible Friction Points

TriggerAction
OneTrust banner or website-attestation dialog is visibleClick #onetrust-accept-btn-handler, then click the dialog link containing AGREE AND PROCEED; wait for the table.
Listing initially renders as zero cards or zero rowsWait briefly and rerun the extractor after consent and client rendering complete.
Table shows only a page-sized subsetOpen #page-size-dropdown, click the a inside div[data-testid='page-size-select'] whose trimmed text is exactly ALL, wait, then extract.
Empty search state removes all rows without a no-results messageClear input[aria-label="Search by ticker, fund name, asset class"], dispatch input and change, wait, and rerun.
Fixed-income category URL does not expose the complete tableUse the general listing URL and search for fixed income.
Page contains access-denied, Akamai, request-unsuccessful, or temporarily-unavailable textRetry the canonical URL with a Canadian residential proxy.
Detached-frame error occursDiscard stale frame or element handles and reacquire the current top-level document after direct navigation.
Large snapshots or full card HTML cause memory pressureRun the compact evaluator only; do not serialize the full DOM or use an LLM page scraper.
Detail URL construction is neededExtract the actual anchor href; do not reconstruct the singular /exchange-traded-fund/ slug.

Example Result

{
  "url": "https://example.com/ca-en/products/exchange-traded-fund/",
  "title": "Example Title",
  "readyState": "complete",
  "count": 1,
  "items": [
    {
      "index": 0,
      "ticker": "EXM",
      "name": "Example Title",
      "url": "https://example.com/ca-en/products/exchange-traded-fund/example"
    }
  ],
  "source": "table",
  "tableRowCount": 1,
  "cardCount": 0,
  "hasListing": true,
  "hasAkamaiOrBlockText": false,
  "consentVisible": false,
  "iframeCount": 0,
  "bodyTextLength": 100
}

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=bmogam.com&task=scrape-bmo-etf-listing