春江暮客

春江暮客的个人学习分享网站

Build URL Query Parameters in Python: Unicode, Plus Signs, and Repeated Keys

2026-10-23 Technology
Build URL Query Parameters in Python: Unicode, Plus Signs, and Repeated Keys

Hand-built ?q= strings break when a search term contains &, turn C++ into C at the server, or fail on Unicode. This tutorial uses the Python standard library to build query strings, retain repeated parameters and empty values, and check the values received through an actual local HTTP request.

It targets beginner-to-intermediate readers writing Linux automation or small API clients. The builder only prints URLs; the self-test contacts loopback and needs no account or API key.

1. Method: pass raw values and encode once

  1. Store parameters as a list of (key, value) pairs, allowing repeated keys.
  2. Split the base URL, read existing parameters, then append new ones.
  3. Encode the query with urlencode() and reassemble the URL.

The official urlencode() documentation covers query encoding and doseq. Spaces become +; literal plus signs become %2B. Our CLI uses string pairs. A dictionary containing lists needs doseq=True:

from urllib.parse import urlencode

query = urlencode({"tag": ["python", "linux"], "q": "C++"}, doseq=True)
assert query == "tag=python&tag=linux&q=C%2B%2B"

Pass new values unencoded. Pass + for a plus sign; %2B means the literal text %2B and produces %252B. Encoding the whole URL can also encode structural delimiters.

2. Setup

Use Python 3.10+ and curl, with no third-party Python packages. Examples were tested on Linux with Python 3.11.4; commands work in Bash and zsh.

  1. Create a fresh practice directory and download both scripts:
python3 --version
mkdir url-query-demo
cd url-query-demo
curl -fS https://www.bobobk.com/downloads/url-query/build_query.py -o build_query.py
curl -fS https://www.bobobk.com/downloads/url-query/check_query.py -o check_query.py
python3 build_query.py --help
  1. The complete build_query.py implementation follows; you can save it directly. Keep the self-test and URL builder in the same directory.

3. Complete implementation: preserve and append parameters

"""Append raw string query parameters to a trusted HTTP URL. Python 3.10+."""
import argparse
import sys
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit


def parameter(text):
    key, found, value = text.partition("=")
    if not found or not key:
        raise argparse.ArgumentTypeError("use a nonempty KEY=VALUE; VALUE may be empty")
    return key, value


def build_url(base, additions):
    if not base.isascii() or any(ord(c) <= 32 or ord(c) == 127 for c in base):
        raise ValueError("base must be an ASCII URL without whitespace or controls")
    parts = urlsplit(base)
    if parts.scheme not in {"http", "https"} or not parts.hostname:
        raise ValueError("base must have an http(s) scheme and a host")
    # Accessing port also rejects malformed or out-of-range port numbers.
    _ = parts.port
    pairs = parse_qsl(
        parts.query, keep_blank_values=True, encoding="utf-8",
        errors="strict", max_num_fields=1000, separator="&",
    )
    pairs.extend(additions)
    if len(pairs) > 1000:
        raise ValueError("at most 1000 query parameters are allowed")
    query = urlencode(pairs, encoding="utf-8", errors="strict")
    return urlunsplit(parts._replace(query=query))


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("base", help="trusted, already encoded base URL")
    parser.add_argument(
        "--param", action="append", type=parameter, default=[],
        metavar="KEY=VALUE", help="raw string parameter; repeat to append more",
    )
    args = parser.parse_args()
    try:
        result = build_url(args.base, args.param)
    except ValueError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(result)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

parse_qsl() returns pairs; keep_blank_values=True retains empty values. Our policy appends parameters, with a final limit of 1000. partition("=") splits only the first equals sign.

Supply a trusted, correctly encoded ASCII base URL. These are basic checks, not full URL validation or path/domain encoding. Re-encoding existing queries can change %20 into +, so exclude signed URLs requiring original bytes. The generated result retains #results.

4. Run and validate the result

  1. Pass Unicode, plus signs, repeated parameters, and delimiters within values:
python3 build_query.py 'https://example.com/search?page=1&empty=&tag=old#results' \
  --param 'q=C++ & Linux/中文' \
  --param 'tag=python' --param 'tag=linux' \
  --param 'note=a=b#c' > url.txt
cat url.txt

Expected output:

https://example.com/search?page=1&empty=&tag=old&q=C%2B%2B+%26+Linux%2F%E4%B8%AD%E6%96%87&tag=python&tag=linux&note=a%3Db%23c#results

Shell quotes prevent the terminal from splitting arguments; URL encoding prevents the server from misreading values. Both steps matter. This command does not contact example.com.

  1. Decode and assert the values and order:
python3 - <<'PY_CHECK'
from pathlib import Path
from urllib.parse import parse_qsl, urlsplit
parts = urlsplit(Path("url.txt").read_text(encoding="utf-8").strip())
assert parts.fragment == "results"
assert parse_qsl(parts.query, keep_blank_values=True) == [
    ("page", "1"), ("empty", ""), ("tag", "old"),
    ("q", "C++ & Linux/中文"), ("tag", "python"),
    ("tag", "linux"), ("note", "a=b#c"),
]
print("OK: query values and order")
PY_CHECK

Expect OK: query values and order. The original tag=old and empty= remain, with two more tag values appended. To replace a key, filter its old pairs before appending. Converting everything to a dictionary would lose other repeated parameters.

5. Check the actual request with a local HTTP server

Run the downloaded self-test:

python3 check_query.py

Expected output:

OK: HTTP round trip, duplicate/blank values, fragment, and CLI exits

The test binds an ephemeral port on 127.0.0.1, sends a real GET request, and asserts that Unicode, plus signs, repeated keys, blank values, =, #, and literal %2B survive. It also checks that the generated URL retains its fragment while the server request excludes it, then stops the server and closes the port.

The server uses http.server for local practice. Its client uses ProxyHandler({}) to bypass proxies and a three-second blocking-operation timeout. System proxy settings stay untouched.

6. Verify error exits

These commands work interactively and in Shell scripts using set -e:

if python3 build_query.py 'file:///tmp/input'; then
  printf 'UNEXPECTED success\n'
else
  printf 'exit=%s\n' "$?"
fi
if python3 build_query.py 'https://example.com/' --param 'missing-equals'; then
  printf 'UNEXPECTED success\n'
else
  printf 'exit=%s\n' "$?"
fi

The first writes an error to stderr and prints exit=1; the second reports a CLI usage error and prints exit=2. Success returns 0. Failures print no successful URL. --param 'empty=' is valid; --param '=value' returns 2 for an empty key.

7. Troubleshooting

Symptom Concrete fix
The server receives spaces instead of C++ Pass raw C++ through --param rather than inserting it into the query.
Output contains %252B Check for pre-encoding; pass + for a plus sign. This output is correct when the original text is %2B.
Only one repeated value remains Use pairs, or doseq=True for list values in a dictionary.
Empty values disappear Specify keep_blank_values=True when decoding for inspection.
Base URL contains spaces or a Unicode path Prepare a correctly encoded base URL; this tool handles new query parameters.
The test cannot bind loopback Use a local Python environment that permits loopback sockets; keep the test server private.

Continue with:

Summary

Pass raw string parameters, retain repeated keys and empty values, and encode the query in one place. Decode and assert the result, then use local HTTP echo to check received values. When connecting a real API, follow its parameter replacement rules and handle timeouts and request failures.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它