Free tools Windows power users keep installed
One-click scans. No signup required.
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use Beautiful Soup’s find_all() with a list of tag names when you want every element whose tag is one of several alternatives:
matches = soup.find_all(["a", "b"])
The CSS equivalent is a comma-separated selector list:
matches = soup.select("a, b")
Both forms return all matching elements. Choose find_all() for a simple set of tag names and select() when you need full CSS selectors, classes, IDs or attribute logic.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteChoose the right multiple-tag query
“Multiple tags” can mean two different things. You may want alternatives such as every <a>, <b> and <img> element, or you may want one element that satisfies several conditions. Beautiful Soup expresses those cases differently.
#1 Best Overall
| Goal | Query | Meaning |
|---|---|---|
| Any of several tag names | soup.find_all(["a", "b", "img"]) |
Match an element whose tag name is a, b or img. |
| Any of several CSS selectors | soup.select("a, b, img") |
Match whichever selector alternative applies. |
| All conditions on one element | soup.select("p.strikeout.body") |
Match a p element having both classes. |
| Only the first CSS match | soup.select_one("a, b") |
Return one matching element instead of a list. |
A comma means “or.” It does not require the same element to satisfy every selector. To require multiple classes on one element, join class selectors without a comma.
Use find_all() for alternative tag names
Minimal example
from bs4 import BeautifulSoup
html = """
<main>
<h1>Links and emphasis</h1>
<a href="/docs">Documentation</a>
<b>Important</b>
<p>Read the guide.</p>
<img src="diagram.png" alt="Diagram">
</main>
"""
soup = BeautifulSoup(html, "html.parser")
matches = soup.find_all(["a", "b", "img"])
for tag in matches:
print(tag.name, tag.get_text(" ", strip=True), tag.attrs)
The list passed as the first argument is a set of acceptable tag names. The result is a list of matching Tag objects in document order. In the example, the loop visits the link, bold text and image.
Filter several tag names by an attribute
You can combine the list with keyword arguments interpreted as tag-attribute filters. This finds links or images that carry the same class:
matches = soup.find_all(["a", "img"], class_="item")
The tag must be either a or img, and it must also match the class filter. This is different from a comma-separated CSS selector, which expresses alternatives.
Limit the search to direct children
find_all() searches descendants recursively by default. If you only want tags that are immediate children of the object on which you call it, pass recursive=False:
container = soup.find("main")
direct = container.find_all(["a", "b"], recursive=False)
Nested links or bold elements inside another child are excluded. This is useful when the HTML structure gives direct-child meaning to a row, list or panel.
Retrieve one result instead of a list
find_all() always returns a collection. If you need one result, you can index the list after checking that it is non-empty, or use find() for a first-match query. For CSS syntax, the direct first-match method is select_one().
Rank #2
first = soup.select_one("a, b")
if first is not None:
print(first.name, first.get_text(strip=True))
Use select() for CSS selector alternatives
Comma-separated selectors mean OR
CSS selector lists are often clearer when each alternative has its own class, ID or attribute condition:
matches = soup.select("a.download, button.download, [role='button']")
This returns elements matching any one of the three selectors. The result is still a list, and the elements are returned in document order.
Combine conditions on the same element
To require both classes on a paragraph, write the classes together:
both_classes = soup.select("p.strikeout.body")
Do not write p.strikeout, p.body when you mean “both.” That comma changes the query to “a paragraph with strikeout or a paragraph with body,” so either class is sufficient.
Use attributes, descendants and child relationships
CSS becomes more expressive as the query grows. Examples:
# Either links or images with an alt/title-like attribute
media = soup.select("a[href], img[alt]")
# Links inside navigation, regardless of nesting depth
nav_links = soup.select("nav a")
# Only direct article children that are headings
headings = soup.select("article > h2, article > h3")
Use select_one() with the same selector string when only the first matching element matters:
primary = soup.select_one("h1, h2")
A complete Python workflow
The following script parses a local HTML string, finds alternatives with both APIs, applies an attribute filter, and demonstrates the difference between descendant and direct-child searches.
from bs4 import BeautifulSoup
html = """
<section id="products">
<article class="card featured">
<a class="item" href="/one">One</a>
<div class="details">
<img class="item" src="one.png" alt="One product">
</div>
</article>
<article class="card">
<b class="item">Two</b>
</article>
</section>
"""
soup = BeautifulSoup(html, "html.parser")
# Alternative tag names: a, b or img.
by_tag_name = soup.find_all(["a", "b", "img"])
# Equivalent alternatives expressed as CSS.
by_css = soup.select("a, b, img")
# Alternative tags with an attribute filter.
items = soup.find_all(["a", "b", "img"], class_="item")
# Multiple conditions on the same element.
featured_cards = soup.select("article.card.featured")
# Direct children only; nested img is not included here.
products = soup.find("section", id="products")
direct_articles = products.find_all("article", recursive=False)
print("find_all:", [tag.name for tag in by_tag_name])
print("select:", [tag.name for tag in by_css])
print("filtered:", [(tag.name, tag.get_text(" ", strip=True)) for tag in items])
print("featured:", [tag.get_text(" ", strip=True) for tag in featured_cards])
print("direct articles:", len(direct_articles))
Install Beautiful Soup with the package name beautifulsoup4, then run the script with the Python interpreter used by your project. The html.parser parser is included with Python; you can substitute another parser already configured in your environment when your project requires it.
How matching, ordering and filtering behave
Tag names are alternatives, not a sequence
find_all(["h1", "h2", "h3"]) does not look for an h1 followed by an h2 followed by an h3. It returns every matching heading wherever it occurs in the searched tree.
Results are collections of tags
Each result is a Beautiful Soup tag object. Use tag.name for the element name, tag.attrs for its attributes and tag.get_text(" ", strip=True) for readable text. For an image, text content is normally empty, so inspect tag.get("src") or tag.get("alt").
Apply the narrowest scope first
Searching the whole document is convenient, but selecting a containing element first makes intent clearer and avoids unrelated matches:
main = soup.select_one("main.content")
if main:
links_or_buttons = main.select("a, button")
If the container is optional, test for None before calling methods on it.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Understand class matching
With find_all(), class_="item" filters for an element carrying that class. CSS syntax gives explicit control over one or several classes: .item means the class is present, while .item.featured requires both classes on the same element.
Common mistakes and fixes
Passing one comma-separated string to find_all()
This is a common error:
# Not a CSS selector list for find_all()
wrong = soup.find_all("a, b")
Use a Python list for tag-name alternatives:
right = soup.find_all(["a", "b"])
If you want CSS syntax, use select("a, b").
Using commas when conditions must all hold
Replace a comma with adjacent selectors when the same element needs every class:
# Either class
soup.select("p.strikeout, p.body")
# Both classes
soup.select("p.strikeout.body")
Assuming select_one() returns every match
select_one() returns the first match or None. Use select() for all matches, then iterate or count them.
Searching too deeply
If nested elements unexpectedly appear, select the parent and pass recursive=False to find_all(), or use the CSS child combinator (>) with select().
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Getting an empty result
- Confirm the HTML actually contains the tag names you requested.
- Check spelling and capitalization in your selector and attributes.
- Print or inspect the parsed HTML to verify that the content was present when parsing occurred.
- Make sure you selected the intended container rather than an empty or missing element.
- For CSS queries, verify that the selector expresses alternatives with commas and combined conditions without commas.
CSS support, dependencies and speed considerations
Beautiful Soup’s CSS selection is powered by Soup Sieve. Soup Sieve is installed along with Beautiful Soup when Beautiful Soup is installed through pip, so select() and select_one() are available in a normal Beautiful Soup setup.
If your task needs only CSS selectors, the Beautiful Soup documentation recommends skipping Beautiful Soup and parsing with lxml, describing it qualitatively as “a lot faster.” That guidance does not provide a numeric benchmark, so treat it as a tool-selection recommendation rather than a guaranteed speed ratio. If you need Beautiful Soup’s convenient tree navigation, text helpers or mixed API style, keep using Beautiful Soup and narrow the search scope.
Performance and reliability practices
Reduce the search area
Find a stable parent once, then search inside it. This reduces accidental matches and makes later HTML changes easier to diagnose.
Prefer one expressive query over repeated whole-document scans
When the requirement is naturally a CSS expression, one select() call can be clearer than several independent searches followed by manual merging. For simple tag-name alternatives, one find_all() call is direct and readable.
Keep parsing and extraction separate
Parse the response first, then run selectors and validation. If a page is generated by JavaScript, the HTML received by your HTTP client may not contain the elements visible in a browser; Beautiful Soup can only search the markup it receives.
Best Value
Validate assumptions in production
Check for required containers and expected result counts. Log the URL or document identifier and the selector when a query unexpectedly returns no matches. This turns silent scraper drift into an actionable error.
Or skip the browser setup
If you need a rendered website image rather than parsed HTML, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL in one request and can return PNG, JPEG, WebP or PDF. Before capture, it accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled.
Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and each response reports the result in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
Recommended Free Tools
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo documentation for the full option set, including full-page captures with lazy images loaded, CSS-element captures, dark mode, device presets, custom viewports, retina scale, PDF paper settings, custom CSS and JavaScript, clicks, wait conditions, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture of up to 100 URLs per call, usage data and the OpenAPI specification. Existing parameter names used by other screenshot APIs also work, which can simplify migration.
Every plan includes all features. The Free plan provides 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for the free ScreenshotNeo plan to try it without a card.
Frequently Asked Questions
Can I mix find_all() and select() in one scraper?
Yes. Use each query where it is clearest: find_all() for tag-name lists and select() for CSS relationships or attribute expressions. Both return Beautiful Soup tag objects that can be processed in the same loop.
What should I use when I need a predictable first match?
Use select_one() for a CSS query and check for None. For a tag-name query, use find() or verify that the list from find_all() is non-empty before indexing it.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsDoes a selector retrieve content loaded after parsing?
No. Beautiful Soup searches the tree supplied to it. If markup is inserted later by JavaScript, obtain rendered HTML first, then parse that markup.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

