Web Crawling Basics β BeautifulSoup
After completing this topic
You will be able to implement the basic flow of fetching web pages with requests and extracting desired data with BeautifulSoup.
What is Web Crawling?
Web crawling is the process of a program visiting web pages and automatically collecting data. It is also known as scraping.
It's essentially performing the same tasks a browser does in code:
- Sending a request to a web server.
- Receiving HTML.
- Extracting the desired data from the HTML.
Step 1: Fetching HTML (requests)
import requests
url = "https://example.com"response = requests.get(url)
print(response.status_code) # 200 = OKprint(response.text[:200]) # The first 200 characters of the HTML textrequests.get(url) sends a request to the server, just like a browser, and receives the HTML sent by the server in response.text.
# Check the status codeif response.status_code == 200: html = response.textelif response.status_code == 404: print("Page not found")elif response.status_code == 403: print("Access denied")Step 2: Parsing HTML (BeautifulSoup)
from bs4 import BeautifulSoup
html = """<html><body> <h1 class="title">News Title</h1> <div class="content"> <p>This is the first paragraph.</p> <p>This is the second paragraph.</p> </div> <ul id="tags"> <li>Python</li> <li>Data</li> <li>Crawling</li> </ul></body></html>"""
soup = BeautifulSoup(html, "html.parser")BeautifulSoup parses the HTML string and creates a tree-structured object. You can then use this object to find the desired parts by tag, class, or ID.
Step 3: Extracting Data
Finding by Tag
# The first h1 tagtitle = soup.find("h1")print(title.text) # "News Title"print(title["class"]) # ["title"]
# All p tagsparagraphs = soup.find_all("p")for p in paragraphs: print(p.text)# "This is the first paragraph."# "This is the second paragraph."Finding by CSS Selector
# select β Uses CSS selectors (equivalent to querySelectorAll)items = soup.select("ul#tags li")for item in items: print(item.text)# "Python"# "Data"# "Crawling"
# Select by classcontent = soup.select_one("div.content")print(content.text.strip())select/select_one is more intuitive for those familiar with CSS than find/find_all.
Practical Example: Extracting Table Data
table_html = """<table> <tr><th>Name</th><th>Score</th></tr> <tr><td>Cheol-soo</td><td>85</td></tr> <tr><td>Young-hee</td><td>92</td></tr> <tr><td>Min-soo</td><td>78</td></tr></table>"""
soup = BeautifulSoup(table_html, "html.parser")rows = soup.select("tr")
data = []for row in rows[1:]: # Exclude header cols = row.find_all("td") data.append({ "Name": cols[0].text, "Score": int(cols[1].text) })
print(data)# [{'Name': 'Cheol-soo', 'Score': 85}, {'Name': 'Young-hee', 'Score': 92}, {'Name': 'Min-soo', 'Score': 78}]# Convert directly to pandas DataFrameimport pandas as pddf = pd.DataFrame(data)print(df)Points to Note
Request Interval
import time
urls = ["https://example.com/page/1", "https://example.com/page/2"]for url in urls: response = requests.get(url) time.sleep(1) # Wait for 1 second - to avoid overloading the serverSending requests too quickly can overload the server or block your IP address.
Setting User-Agent
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0"}response = requests.get(url, headers=headers)Some servers block bots by checking the User-Agent.
robots.txt
https://example.com/robots.txtThis file specifies which paths are allowed and disallowed for crawling. It is polite and legally prudent to first check the site's crawling policy.
Limitations of Crawling
BeautifulSoup can only process static HTML. Content that is dynamically loaded with JavaScript (SPAs, infinite scrolling, etc.) is not included in the HTML and cannot be extracted.
| Situation | Tool |
|---|---|
| Static HTML | requests + BeautifulSoup |
| Dynamic loading with JavaScript | Selenium, Playwright |
| API available | Use requests to call the API directly (most efficient) |
If there is an API, use the API instead of crawling. This allows you to receive structured data accurately and reduces the load on the server.
Summary of the Core Flow
1. requests.get(url) β HTML text
2. BeautifulSoup(html) β Parsed tree
3. soup.select("CSS selector") β Desired element
4. element.text / element["attr"] β Extract dataError Handling
In real-world crawling, various errors can occur:
import requestsfrom bs4 import BeautifulSoup
def safe_fetch(url, retries=3): for attempt in range(retries): try: response = requests.get(url, timeout=10) response.raise_for_status() return response.text except requests.exceptions.Timeout: print(f"Timeout ({attempt + 1}/{retries})") except requests.exceptions.HTTPError as e: print(f"HTTP error: {e}") return None except requests.exceptions.ConnectionError: print(f"Connection failed ({attempt + 1}/{retries})") return NoneIf you do not set a timeout, the program will wait indefinitely for a server that does not respond. raise_for_status() raises an exception for 4xx/5xx responses.
Crawling Multiple Pages
import time
base_url = "https://example.com/articles?page="all_titles = []
for page in range(1, 11): html = safe_fetch(f"{base_url}{page}") if html is None: continue soup = BeautifulSoup(html, "html.parser") titles = soup.select("h2.article-title") all_titles.extend([t.text.strip() for t in titles]) print(f"Page {page}: {len(titles)} items collected") time.sleep(1)
print(f"Total of {len(all_titles)} items collected")Saving Crawled Data
import jsonimport csv
# Save as JSONwith open("articles.json", "w", encoding="utf-8") as f: json.dump(all_titles, f, ensure_ascii=False, indent=2)
# Save as CSVwith open("articles.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["ID", "Title"]) for i, title in enumerate(all_titles, 1): writer.writerow([i, title])Save the collected data in JSON or CSV format so that it can be easily analyzed with pandas.
Extracting Links β href Attribute
# Extract all links on the pagelinks = soup.select("a[href]")for link in links: url = link["href"] text = link.text.strip() print(f"{text}: {url}")
# Only links matching a specific patternarticle_links = [ a["href"] for a in soup.select("a[href]") if "/article/" in a.get("href", "")]Crawling is the first step in "getting data." Data collection is the starting point for everything, from data analysis and machine learning to monitoring.