2015-10-03 12:53:45 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2022-02-11 00:42:49 +01:00
|
|
|
# Copyright 2015-2022 Mike Fährmann
|
2015-10-03 12:53:45 +02:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License version 2 as
|
|
|
|
# published by the Free Software Foundation.
|
|
|
|
|
2019-01-29 13:14:30 +01:00
|
|
|
"""Collection of functions that work on strings/text"""
|
2015-10-03 12:53:45 +02:00
|
|
|
|
|
|
|
import re
|
2024-09-19 20:09:03 +02:00
|
|
|
import sys
|
2016-02-18 15:54:58 +01:00
|
|
|
import html
|
2024-09-19 20:09:03 +02:00
|
|
|
import time
|
2019-04-21 15:28:27 +02:00
|
|
|
import datetime
|
2015-10-03 12:53:45 +02:00
|
|
|
import urllib.parse
|
|
|
|
|
2020-03-13 23:30:16 +01:00
|
|
|
HTML_RE = re.compile("<[^>]+>")
|
|
|
|
|
2017-03-14 09:09:04 +01:00
|
|
|
|
2019-07-17 14:48:24 +02:00
|
|
|
def remove_html(txt, repl=" ", sep=" "):
|
2015-10-03 12:53:45 +02:00
|
|
|
"""Remove html-tags from a string"""
|
2018-04-14 22:09:42 +02:00
|
|
|
try:
|
2020-03-13 23:30:16 +01:00
|
|
|
txt = HTML_RE.sub(repl, txt)
|
2018-04-14 22:09:42 +02:00
|
|
|
except TypeError:
|
|
|
|
return ""
|
2019-07-17 14:48:24 +02:00
|
|
|
if sep:
|
|
|
|
return sep.join(txt.split())
|
|
|
|
return txt.strip()
|
2015-10-03 12:53:45 +02:00
|
|
|
|
2017-01-30 19:40:15 +01:00
|
|
|
|
2021-03-29 02:12:29 +02:00
|
|
|
def split_html(txt):
|
|
|
|
"""Split input string by HTML tags"""
|
2018-05-27 15:00:41 +02:00
|
|
|
try:
|
|
|
|
return [
|
2021-03-29 02:12:29 +02:00
|
|
|
unescape(x).strip()
|
|
|
|
for x in HTML_RE.split(txt)
|
2018-05-27 15:00:41 +02:00
|
|
|
if x and not x.isspace()
|
|
|
|
]
|
|
|
|
except TypeError:
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
2022-08-23 21:14:55 +02:00
|
|
|
def slugify(value):
|
|
|
|
"""Convert a string to a URL slug
|
|
|
|
|
|
|
|
Adapted from:
|
|
|
|
https://github.com/django/django/blob/master/django/utils/text.py
|
|
|
|
"""
|
|
|
|
value = re.sub(r"[^\w\s-]", "", str(value).lower())
|
|
|
|
return re.sub(r"[-\s]+", "-", value).strip("-_")
|
|
|
|
|
|
|
|
|
2020-05-19 21:25:07 +02:00
|
|
|
def ensure_http_scheme(url, scheme="https://"):
|
|
|
|
"""Prepend 'scheme' to 'url' if it doesn't have one"""
|
|
|
|
if url and not url.startswith(("https://", "http://")):
|
|
|
|
return scheme + url.lstrip("/:")
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
2022-03-01 02:56:51 +01:00
|
|
|
def root_from_url(url, scheme="https://"):
|
|
|
|
"""Extract scheme and domain from a URL"""
|
|
|
|
if not url.startswith(("https://", "http://")):
|
2024-02-29 15:05:46 +01:00
|
|
|
try:
|
|
|
|
return scheme + url[:url.index("/")]
|
|
|
|
except ValueError:
|
|
|
|
return scheme + url
|
|
|
|
try:
|
|
|
|
return url[:url.index("/", 8)]
|
|
|
|
except ValueError:
|
|
|
|
return url
|
2022-03-01 02:56:51 +01:00
|
|
|
|
|
|
|
|
2015-10-03 12:53:45 +02:00
|
|
|
def filename_from_url(url):
|
2019-01-31 12:23:25 +01:00
|
|
|
"""Extract the last part of an URL to use as a filename"""
|
2015-10-03 12:53:45 +02:00
|
|
|
try:
|
2020-10-23 00:04:43 +02:00
|
|
|
return url.partition("?")[0].rpartition("/")[2]
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2018-04-14 22:09:42 +02:00
|
|
|
return ""
|
2015-10-03 12:53:45 +02:00
|
|
|
|
2017-01-30 19:40:15 +01:00
|
|
|
|
2019-01-31 12:23:25 +01:00
|
|
|
def ext_from_url(url):
|
|
|
|
"""Extract the filename extension of an URL"""
|
2021-03-28 03:54:12 +02:00
|
|
|
name, _, ext = filename_from_url(url).rpartition(".")
|
|
|
|
return ext.lower() if name else ""
|
2019-01-31 12:23:25 +01:00
|
|
|
|
|
|
|
|
2015-11-16 02:20:22 +01:00
|
|
|
def nameext_from_url(url, data=None):
|
2019-01-31 12:23:25 +01:00
|
|
|
"""Extract the last part of an URL and fill 'data' accordingly"""
|
2015-11-16 02:20:22 +01:00
|
|
|
if data is None:
|
|
|
|
data = {}
|
2021-03-28 03:54:12 +02:00
|
|
|
|
|
|
|
filename = unquote(filename_from_url(url))
|
|
|
|
name, _, ext = filename.rpartition(".")
|
2021-05-02 21:15:50 +02:00
|
|
|
if name and len(ext) <= 16:
|
2021-03-28 03:54:12 +02:00
|
|
|
data["filename"], data["extension"] = name, ext.lower()
|
|
|
|
else:
|
|
|
|
data["filename"], data["extension"] = filename, ""
|
|
|
|
|
2015-11-16 02:20:22 +01:00
|
|
|
return data
|
|
|
|
|
2017-01-30 19:40:15 +01:00
|
|
|
|
2015-10-03 12:53:45 +02:00
|
|
|
def extract(txt, begin, end, pos=0):
|
2015-11-02 15:52:26 +01:00
|
|
|
"""Extract the text between 'begin' and 'end' from 'txt'
|
|
|
|
|
|
|
|
Args:
|
|
|
|
txt: String to search in
|
|
|
|
begin: First string to be searched for
|
|
|
|
end: Second string to be searched for after 'begin'
|
|
|
|
pos: Starting position for searches in 'txt'
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
The string between the two search-strings 'begin' and 'end' beginning
|
|
|
|
with position 'pos' in 'txt' as well as the position after 'end'.
|
|
|
|
|
|
|
|
If at least one of 'begin' or 'end' is not found, None and the original
|
|
|
|
value of 'pos' is returned
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
extract("abcde", "b", "d") -> "c" , 4
|
|
|
|
extract("abcde", "b", "d", 3) -> None, 3
|
|
|
|
"""
|
2015-10-03 12:53:45 +02:00
|
|
|
try:
|
|
|
|
first = txt.index(begin, pos) + len(begin)
|
|
|
|
last = txt.index(end, first)
|
|
|
|
return txt[first:last], last+len(end)
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2015-10-03 12:53:45 +02:00
|
|
|
return None, pos
|
|
|
|
|
2017-01-30 19:40:15 +01:00
|
|
|
|
2022-11-09 11:00:32 +01:00
|
|
|
def extr(txt, begin, end, default=""):
|
2022-11-04 21:37:36 +01:00
|
|
|
"""Stripped-down version of 'extract()'"""
|
|
|
|
try:
|
|
|
|
first = txt.index(begin) + len(begin)
|
|
|
|
return txt[first:txt.index(end, first)]
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2022-11-09 11:00:32 +01:00
|
|
|
return default
|
2022-11-04 21:37:36 +01:00
|
|
|
|
|
|
|
|
2019-05-28 21:03:41 +02:00
|
|
|
def rextract(txt, begin, end, pos=-1):
|
|
|
|
try:
|
|
|
|
lbeg = len(begin)
|
|
|
|
first = txt.rindex(begin, 0, pos)
|
|
|
|
last = txt.index(end, first + lbeg)
|
|
|
|
return txt[first + lbeg:last], first
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2019-05-28 21:03:41 +02:00
|
|
|
return None, pos
|
|
|
|
|
|
|
|
|
2015-11-03 00:05:18 +01:00
|
|
|
def extract_all(txt, rules, pos=0, values=None):
|
2015-11-02 15:51:32 +01:00
|
|
|
"""Calls extract for each rule and returns the result in a dict"""
|
2015-11-03 00:05:18 +01:00
|
|
|
if values is None:
|
|
|
|
values = {}
|
2015-11-02 15:51:32 +01:00
|
|
|
for key, begin, end in rules:
|
|
|
|
result, pos = extract(txt, begin, end, pos)
|
|
|
|
if key:
|
|
|
|
values[key] = result
|
|
|
|
return values, pos
|
2015-10-03 12:53:45 +02:00
|
|
|
|
2017-01-30 19:40:15 +01:00
|
|
|
|
2015-11-28 01:46:34 +01:00
|
|
|
def extract_iter(txt, begin, end, pos=0):
|
2019-04-18 23:37:17 +02:00
|
|
|
"""Yield values that would be returned by repeated calls of extract()"""
|
|
|
|
index = txt.index
|
|
|
|
lbeg = len(begin)
|
|
|
|
lend = len(end)
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
first = index(begin, pos) + lbeg
|
|
|
|
last = index(end, first)
|
|
|
|
pos = last + lend
|
|
|
|
yield txt[first:last]
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2019-04-18 23:37:17 +02:00
|
|
|
return
|
2015-11-28 01:46:34 +01:00
|
|
|
|
2017-01-30 19:40:15 +01:00
|
|
|
|
2019-04-19 22:30:11 +02:00
|
|
|
def extract_from(txt, pos=0, default=""):
|
|
|
|
"""Returns a function object that extracts from 'txt'"""
|
|
|
|
def extr(begin, end, index=txt.index, txt=txt):
|
|
|
|
nonlocal pos
|
|
|
|
try:
|
|
|
|
first = index(begin, pos) + len(begin)
|
|
|
|
last = index(end, first)
|
|
|
|
pos = last + len(end)
|
|
|
|
return txt[first:last]
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2019-04-19 22:30:11 +02:00
|
|
|
return default
|
|
|
|
return extr
|
|
|
|
|
|
|
|
|
2019-06-16 21:46:26 +02:00
|
|
|
def parse_unicode_escapes(txt):
|
|
|
|
"""Convert JSON Unicode escapes in 'txt' into actual characters"""
|
|
|
|
if "\\u" in txt:
|
|
|
|
return re.sub(r"\\u([0-9a-fA-F]{4})", _hex_to_char, txt)
|
|
|
|
return txt
|
|
|
|
|
|
|
|
|
|
|
|
def _hex_to_char(match):
|
|
|
|
return chr(int(match.group(1), 16))
|
|
|
|
|
|
|
|
|
2018-04-20 14:53:21 +02:00
|
|
|
def parse_bytes(value, default=0, suffixes="bkmgtp"):
|
|
|
|
"""Convert a bytes-amount ("500k", "2.5M", ...) to int"""
|
|
|
|
try:
|
|
|
|
last = value[-1].lower()
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2018-04-20 14:53:21 +02:00
|
|
|
return default
|
|
|
|
|
|
|
|
if last in suffixes:
|
|
|
|
mul = 1024 ** suffixes.index(last)
|
|
|
|
value = value[:-1]
|
|
|
|
else:
|
|
|
|
mul = 1
|
|
|
|
|
|
|
|
try:
|
|
|
|
return round(float(value) * mul)
|
|
|
|
except ValueError:
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
def parse_int(value, default=0):
|
|
|
|
"""Convert 'value' to int"""
|
|
|
|
if not value:
|
|
|
|
return default
|
|
|
|
try:
|
|
|
|
return int(value)
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2018-04-20 14:53:21 +02:00
|
|
|
return default
|
|
|
|
|
|
|
|
|
2019-01-29 13:14:30 +01:00
|
|
|
def parse_float(value, default=0.0):
|
|
|
|
"""Convert 'value' to float"""
|
|
|
|
if not value:
|
|
|
|
return default
|
|
|
|
try:
|
|
|
|
return float(value)
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2019-01-29 13:14:30 +01:00
|
|
|
return default
|
|
|
|
|
|
|
|
|
2017-08-24 20:55:58 +02:00
|
|
|
def parse_query(qs):
|
|
|
|
"""Parse a query string into key-value pairs"""
|
2024-10-01 20:04:43 +02:00
|
|
|
if not qs:
|
|
|
|
return {}
|
|
|
|
|
2018-04-13 19:21:32 +02:00
|
|
|
result = {}
|
2018-04-14 22:09:42 +02:00
|
|
|
try:
|
2024-10-01 20:04:43 +02:00
|
|
|
for name_value in qs.split("&"):
|
|
|
|
name, eq, value = name_value.partition("=")
|
|
|
|
if eq:
|
|
|
|
name = unquote(name.replace("+", " "))
|
|
|
|
if name not in result:
|
|
|
|
result[name] = unquote(value.replace("+", " "))
|
2024-04-13 18:51:40 +02:00
|
|
|
except Exception:
|
2018-04-14 22:09:42 +02:00
|
|
|
pass
|
2018-04-13 19:21:32 +02:00
|
|
|
return result
|
2017-08-24 20:55:58 +02:00
|
|
|
|
|
|
|
|
2024-09-19 20:09:03 +02:00
|
|
|
if sys.hexversion < 0x30c0000:
|
|
|
|
# Python <= 3.11
|
|
|
|
def parse_timestamp(ts, default=None):
|
|
|
|
"""Create a datetime object from a Unix timestamp"""
|
|
|
|
try:
|
|
|
|
return datetime.datetime.utcfromtimestamp(int(ts))
|
|
|
|
except Exception:
|
|
|
|
return default
|
|
|
|
else:
|
|
|
|
# Python >= 3.12
|
|
|
|
def parse_timestamp(ts, default=None):
|
|
|
|
"""Create a datetime object from a Unix timestamp"""
|
|
|
|
try:
|
|
|
|
Y, m, d, H, M, S, _, _, _ = time.gmtime(int(ts))
|
|
|
|
return datetime.datetime(Y, m, d, H, M, S)
|
|
|
|
except Exception:
|
|
|
|
return default
|
2019-04-21 15:28:27 +02:00
|
|
|
|
|
|
|
|
2020-04-11 02:05:00 +02:00
|
|
|
def parse_datetime(date_string, format="%Y-%m-%dT%H:%M:%S%z", utcoffset=0):
|
2019-05-08 00:00:00 +02:00
|
|
|
"""Create a datetime object by parsing 'date_string'"""
|
|
|
|
try:
|
2019-05-09 21:53:17 +02:00
|
|
|
if format.endswith("%z") and date_string[-3] == ":":
|
|
|
|
# workaround for Python < 3.7: +00:00 -> +0000
|
2019-05-25 23:22:26 +02:00
|
|
|
ds = date_string[:-3] + date_string[-2:]
|
|
|
|
else:
|
|
|
|
ds = date_string
|
|
|
|
d = datetime.datetime.strptime(ds, format)
|
2019-05-08 00:00:00 +02:00
|
|
|
o = d.utcoffset()
|
|
|
|
if o is not None:
|
2020-04-11 02:05:00 +02:00
|
|
|
# convert to naive UTC
|
2020-06-17 21:40:16 +02:00
|
|
|
d = d.replace(tzinfo=None, microsecond=0) - o
|
|
|
|
else:
|
|
|
|
if d.microsecond:
|
|
|
|
d = d.replace(microsecond=0)
|
|
|
|
if utcoffset:
|
|
|
|
# apply manual UTC offset
|
|
|
|
d += datetime.timedelta(0, utcoffset * -3600)
|
2019-05-08 00:00:00 +02:00
|
|
|
return d
|
2019-05-09 21:53:17 +02:00
|
|
|
except (TypeError, IndexError, KeyError):
|
2019-05-08 00:00:00 +02:00
|
|
|
return None
|
|
|
|
except (ValueError, OverflowError):
|
|
|
|
return date_string
|
|
|
|
|
|
|
|
|
2018-04-20 14:53:21 +02:00
|
|
|
urljoin = urllib.parse.urljoin
|
2019-01-29 13:14:30 +01:00
|
|
|
|
|
|
|
quote = urllib.parse.quote
|
2015-10-03 12:53:45 +02:00
|
|
|
unquote = urllib.parse.unquote
|
|
|
|
|
2019-01-29 13:14:30 +01:00
|
|
|
escape = html.escape
|
|
|
|
unescape = html.unescape
|