Building an AI-Powered Web Scraping & Report Generation System

A comprehensive look at the engineering challenges and solutions behind combining async web crawling, document parsing, and LLM integration in a Streamlit application.


Tech Stack at a Glance

UI Framework

Streamlit 1.51

Web Scraping

Crawl4AI 0.7.6

Video Transcripts

youtube-transcript-api 1.2.3

LLM

Google Gemini 0.8.5

Document Parsing

PyPDF2, python-docx

Browser Automation

Playwright 1.55.0


Challenge #1: Async Operations Inside Streamlit's Sync Context

The Problem

Crawl4AI is built on async/await patterns for efficient I/O operations. However, Streamlit runs in a synchronous context that may already have an event loop running. Calling asyncio.run() inside an existing loop raises:

RuntimeError: This event loop is already running

The Solution: Thread Delegation with Queue-Based Communication

Python
def run(url: str) -> str:
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None

    if loop and loop.is_running():
        # Already in an async context - delegate to a separate thread
        q: queue.Queue[tuple[bool, object]] = queue.Queue()

        def worker():
            new_loop = asyncio.new_event_loop()
            asyncio.set_event_loop(new_loop)
            try:
                res = new_loop.run_until_complete(scrape(url))
                q.put((True, res))
            except Exception as e:
                q.put((False, e))
            finally:
                new_loop.close()

        t = threading.Thread(target=worker, daemon=True)
        t.start()
        ok, val = q.get()  # Block until result ready
        if ok:
            return val
        raise val
    else:
        # No existing loop - safe to use asyncio.run()
        return asyncio.run(scrape(url))
Thread isolation

Each async operation gets its own event loop in a dedicated thread

Queue-based result passing

Thread-safe communication without shared mutable state

Daemon threads

Auto-cleanup on application exit

Tuple unpacking for errors

Preserves exception type and traceback

Windows-Specific Event Loop Policy

Windows requires special handling for async subprocess operations:

asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())

The ProactorEventLoop is required on Windows for proper subprocess and pipe handling that Crawl4AI uses internally.


Challenge #2: Unified Content Extraction from Heterogeneous Sources

The Problem

Different source types require different extraction strategies:

Web URLs

Need headless browser rendering for JavaScript-heavy sites

YouTube URLs

Standard scraping fails—need transcript API

PDFs

Binary format requiring specialized parsing

DOCX

XML-based format with paragraph structure

The Solution: Smart Routing with Fallback Chains

Python - Smart URL Routing
async def scrape(url: str) -> str:
    # Route based on URL pattern
    if _is_youtube_url(url):
        vid = _extract_youtube_video_id(url)
        if vid:
            try:
                return _fetch_youtube_transcript_text(vid)
            except Exception:
                pass  # Fallback to web crawling
    
    # Default: headless browser crawl
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(url)
        return result.markdown or ""

YouTube URL Detection Flow

Input URL
↓
Is YouTube URL?
Yes
↓
Extract Video ID
↓
youtu.be/ID
?v=ID
/shorts/ID
/embed/ID
↓
Fetch Transcript
No
↓
Web Crawler

Challenge #3: LLM API Reliability and Model Fallback

The Problem

LLM APIs can fail for various reasons. A single API call failure shouldn't break the entire workflow.

Rate limitingModel deprecationTemporary outagesQuota exhaustion

The Solution: Cascading Model Fallback

Python - Cascading Fallback
def summarize_with_gemini(text: str, prompt: str | None = None) -> str:
    genai.configure(api_key=api_key)
    
    # Ordered by preference: latest → stable fallbacks
    choices = [
        "gemini-2.5-flash",
        "gemini-2.0-flash",
        "gemini-2.5-flash-lite",
        "gemini-1.5-flash",
        "gemini-1.5-pro",
    ]
    
    last_err: Exception | None = None
    for name in choices:
        try:
            model = genai.GenerativeModel(name)
            resp = model.generate_content(full_prompt)
            
            # Handle different response structures
            if hasattr(resp, "text") and resp.text:
                return resp.text
            ...
        except Exception as e:
            last_err = e
            continue  # Try next model
    
    if last_err:
        raise last_err
    return ""

Engineering Considerations

  • Response structure handling: Gemini SDK response objects vary—defensive attribute access required
  • Preserving the last error: If all models fail, raise the most recent exception for debugging
  • Order matters: Prefer faster/cheaper models first, fall back to more capable ones

Challenge #4: Safe Filename Generation from URLs

The Problem

URLs contain characters that are invalid in filenames across different operating systems. Additionally, different URLs might generate the same filename, causing data overwrites.

/\?&=:

The Solution: Slugification with Hash-Based Uniqueness

1
Parse URLhost + path
→
2
SlugifyInvalid → _
→
3
Add HashSHA1[:8]
→
4
TruncateMax 64 chars

Why SHA1?

Fast computation
8-character prefix provides ~4 billion unique values
Full URL is hashed, preserving query string uniqueness

Challenge #5: Markdown to DOCX Conversion with Formatting Preservation

The Problem

Enterprise users need Word documents, but LLM output is Markdown. The conversion must preserve all formatting elements.

H1-H6

Heading hierarchy

BI

Bold & Italic

`code`

Inline code

• —

Lists

```

Code blocks

The Solution: Regex-Based Markdown Parser

Python - Inline Markdown Processing
def _add_inline_markdown(paragraph, text: str):
    """Process inline markdown: **bold**, *italic*, `code`"""
    pattern = re.compile(r"(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)")
    pos = 0
    
    for m in pattern.finditer(text):
        # Add plain text before match
        if m.start() > pos:
            paragraph.add_run(text[pos:m.start()])
        
        token = m.group(0)
        if token.startswith("**"):
            r = paragraph.add_run(token[2:-2])
            r.bold = True
        elif token.startswith("*"):
            r = paragraph.add_run(token[1:-1])
            r.italic = True
        elif token.startswith("`"):
            r = paragraph.add_run(token[1:-1])
            r.font.name = "Courier New"
        
        pos = m.end()
    
    # Add remaining text
    if pos < len(text):
        paragraph.add_run(text[pos:])

Challenge #6: State Management and Data Isolation

The Problem

Multiple users share the same application instance. Each user's data must be isolated, persisted across sessions, and efficiently queryable.

The Solution: TSV-Based Index Files with User ID Column

Index File Structure

URL\tFilename\tTimestamp\tUserID\tUploadFlag\tTitle

System Architecture

STREAMLIT UI (app.py)
Home (Extract)
Extracted Data
Build Report (Synthesis)
↓
CORE ENGINE (main.py)
ASYNC SCRAPING LAYER
Crawl4AI (Browser)
YouTube Transcript API
Thread Delegation
DOCUMENT PARSING LAYER
PyPDF2 (PDF)
python-docx
Plain Text/Markdown
LLM INTEGRATION LAYER
Gemini API → Model Fallback Chain → Response Normalization
PERSISTENCE LAYER
extracted_data/
processed_data/
generated_reports/

Key Takeaways

ChallengeSolutionKey Technique
Async in sync contextThread delegationthreading.Thread + queue.Queue
Heterogeneous sourcesSmart routing with fallbackURL pattern matching + exception handling
LLM reliabilityModel cascadeOrdered fallback with preserved errors
Safe filenamesSlugification + hashingRegex sanitization + SHA1 prefix
Markdown → DOCXLine-by-line parsingRegex pattern matching + python-docx
User isolationIndex file filteringTSV with UserID column

Conclusion

Building a production-ready web scraping and LLM integration system involves solving numerous edge cases and platform-specific issues. The key patterns demonstrated here—thread-based async isolation, cascading fallbacks, and defensive data handling—are applicable to any Python application integrating external APIs with complex I/O patterns.

The combination of Streamlit for rapid prototyping, Crawl4AI for robust web scraping, and Gemini for intelligent summarization creates a powerful foundation that can be extended to various domains beyond financial analysis.

Built by Flairminds — Turning complex data pipelines into simple workflows.

#WebScraping#AI#LLM#Python#Streamlit#Automation