From 201e0665c35244df3fe2e673073233e949bdfcb9 Mon Sep 17 00:00:00 2001
From: johanbeytell1 <107624055+johanbeytell1@users.noreply.github.com>
Date: Sat, 31 Oct 2026 11:57:05 +0000
Subject: [PATCH 1/2] Release 0.1.8: security hardening, bug fixes, offline
tests and CI
Security
- Timeouts on every request (default (10, 30) seconds, overridable).
- Response size caps (max_page_bytes, max_image_bytes), enforced from
Content-Length and while streaming.
- Redirects followed hop by hop, each hop validated; max_redirects bounds
the chain. Only http/https are fetched.
- Child links must stay on the same domain after redirects.
- New block_private_addresses flag (default False) to refuse non-public
targets, applied to pages and images.
- CSV formula injection neutralised.
- Safe, unique image file names; corrupt and decompression-bomb images
are skipped instead of crashing the run.
- requests>=2.32.4 (CVE-2024-47081), pillow>=10.3.0 (CVE-2024-28219).
- CI: actions pinned to commit SHAs, least-privilege permissions, no
persisted checkout credentials, PyPI token only in the publish job.
Bug fixes
- Inline markup no longer glues words; nested containers no longer
repeat text; head/title/script/style/template/comments excluded;
yields line breaks.
- links mode returns img/video/audio/source URLs, absolute, http(s) only.
- unseen mode includes script data.
- 4xx/5xx pages are skipped instead of being scraped.
- Each page and each image is fetched once.
- Images without src are skipped; empty image names no longer crash.
- rate_limit is shared across threads.
- Fragments and host/port variants are de-duplicated.
- Output is deterministic (discovery order); de-duplication runs after the
crawl. Duplicate similarity measured against the larger text, so short
items no longer swallow unrelated paragraphs.
- Failed pages are skipped and logged (logger 'pywebscrapr') instead of
aborting the run and discarding output.
- Links files: UTF-8 (BOM tolerated), blank lines ignored.
- Charset taken from HTTP header or meta tag; honoured.
- max_links_to_follow counts child links only; 0 means no child links.
- Invalid options raise ValueError early.
Packaging, CI and tests
- Version defined once in pywebscrapr/__init__.py (__version__).
- beautifulsoup4 replaces bs4; Python >=3.9.
- Workflow tests on 3.9/3.12/3.13 for pushes and PRs; publishing runs
only for tags that pass the tests and match __version__.
- tests.py rewritten as an offline suite on local fixture servers, plus an
opt-in live test (PYWEBSCRAPR_LIVE_TESTS=1).
- README: consolidated 0.1.8 changelog and usage notes.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
---
.github/workflows/build_and_publish.yml | 84 +-
.gitignore | 4 +-
README.md | 111 ++-
pywebscrapr/__init__.py | 987 +++++++++++++++++-------
requirements.txt | 6 +-
setup.py | 28 +-
tests.py | 510 +++++++++++-
7 files changed, 1385 insertions(+), 345 deletions(-)
diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml
index 4a9ec6b..71d7d52 100644
--- a/.github/workflows/build_and_publish.yml
+++ b/.github/workflows/build_and_publish.yml
@@ -1,34 +1,64 @@
-name: Publish Python 🐍 distributions 📦 to PyPI
+name: Test and publish to PyPI
on:
push:
- tags:
- - '*'
+ branches: [main]
+ tags: ['*']
+ pull_request:
+
+# Least privilege by default. Jobs that need more say so explicitly.
+permissions:
+ contents: read
jobs:
- build-n-publish:
- name: Build and publish Python 🐍 distributions 📦 to PyPI
+ test:
+ name: Test (Python ${{ matrix.python-version }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ['3.9', '3.12', '3.13']
+ steps:
+ - name: Check out code
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: python -m pip install -r requirements.txt
+ - name: Run tests (offline)
+ run: python -m unittest -v tests
+
+ build-and-publish:
+ name: Build and publish to PyPI
+ needs: test
+ # Only tag pushes publish. Pull requests and branch pushes stop after the tests.
+ if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@master
- - name: Set up Python 3.12
- uses: actions/setup-python@v3
- with:
- python-version: '3.12'
- - name: Install pypa/setuptools
- run: >-
- python -m
- pip install setuptools wheel
- - name: Extract tag name
- id: tag
- run: echo ::set-output name=TAG_NAME::$(echo $GITHUB_REF | cut -d / -f 3)
- - name: Update version in setup.py
- run: >-
- sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" setup.py
- - name: Build a binary wheel
- run: >-
- python setup.py sdist bdist_wheel
- - name: Publish distribution 📦 to PyPI
- uses: pypa/gh-action-pypi-publish@master
- with:
- password: ${{ secrets.PYPI_API_TOKEN }}
\ No newline at end of file
+ - name: Check out code
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Set up Python
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
+ with:
+ python-version: '3.12'
+ - name: Install dependencies
+ run: python -m pip install -r requirements.txt
+ - name: Check that the release tag matches the package version
+ env:
+ TAG_NAME: ${{ github.ref_name }}
+ run: |
+ python -c "import os, pywebscrapr; tag = os.environ['TAG_NAME']; version = pywebscrapr.__version__; assert tag == version, 'tag %s does not match __version__ %s' % (tag, version); print('Releasing', version)"
+ - name: Build sdist and wheel
+ run: |
+ python -m pip install build
+ python -m build
+ - name: Publish distribution to PyPI
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
+ with:
+ password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 7391daf..3a70787 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,6 @@ dist/
build/
venv/
json_output.json
-output.txt
\ No newline at end of file
+output.txt
+output/
+*.egg-info/
diff --git a/README.md b/README.md
index 518cc35..ef1fab8 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,71 @@
An open-source Python library for web scraping tasks. Includes support for both text and image scraping.
+## Changes in 0.1.8
+
+0.1.8 is a security, reliability and correctness release. It changes some defaults and behaviour, so read **Breaking changes** before upgrading.
+
+### Breaking changes
+- **Python 3.9 or later is required** (the package previously claimed 3.6+). Pillow 10.3+ and requests 2.32.4+ are the new minimums, and the CI matrix tests 3.9, 3.12 and 3.13.
+- **Failed pages and images are skipped and logged instead of raising.** A bad URL, timeout, DNS failure, HTTP error, oversized response or corrupt image no longer aborts the run or discards the output. Failures are logged as warnings and downloads as info messages on the `pywebscrapr` logger. Previously "Downloaded" and "Ignored" messages were printed; to see them again, use `logging.basicConfig(level=logging.INFO)`.
+- **Text output changes.** Words are no longer glued together, nested containers no longer repeat their text, and `
`, ``, `'
+ 'Visible