scrapyloganalyzer 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scrapyloganalyzer-0.0.1/LICENSE +29 -0
- scrapyloganalyzer-0.0.1/MANIFEST.in +9 -0
- scrapyloganalyzer-0.0.1/PKG-INFO +36 -0
- scrapyloganalyzer-0.0.1/README.rst +14 -0
- scrapyloganalyzer-0.0.1/docs/Makefile +20 -0
- scrapyloganalyzer-0.0.1/docs/api/index.rst +6 -0
- scrapyloganalyzer-0.0.1/docs/changelog.rst +7 -0
- scrapyloganalyzer-0.0.1/docs/conf.py +75 -0
- scrapyloganalyzer-0.0.1/docs/contributing/index.rst +12 -0
- scrapyloganalyzer-0.0.1/docs/index.rst +13 -0
- scrapyloganalyzer-0.0.1/docs/requirements.txt +1 -0
- scrapyloganalyzer-0.0.1/pyproject.toml +62 -0
- scrapyloganalyzer-0.0.1/scrapyloganalyzer/__init__.py +181 -0
- scrapyloganalyzer-0.0.1/scrapyloganalyzer.egg-info/PKG-INFO +36 -0
- scrapyloganalyzer-0.0.1/scrapyloganalyzer.egg-info/SOURCES.txt +29 -0
- scrapyloganalyzer-0.0.1/scrapyloganalyzer.egg-info/dependency_links.txt +1 -0
- scrapyloganalyzer-0.0.1/scrapyloganalyzer.egg-info/requires.txt +5 -0
- scrapyloganalyzer-0.0.1/scrapyloganalyzer.egg-info/top_level.txt +3 -0
- scrapyloganalyzer-0.0.1/setup.cfg +4 -0
- scrapyloganalyzer-0.0.1/tests/__init__.py +5 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log1.log +87 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_crawl_time_crawl_statistic.log +3 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_crawl_time_log_message.log +1 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_crawl_time_spider_argument.log +3 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_error1.log +120 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_error_invalid_json.log +117 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_from_date1.log +161 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_in_progress1.log +68 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_sample1.log +105 -0
- scrapyloganalyzer-0.0.1/tests/fixtures/log_sigint1.log +75 -0
- scrapyloganalyzer-0.0.1/tests/test_scrapy_log_file.py +209 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, Open Contracting Partnership
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
recursive-include docs *.py
|
|
3
|
+
recursive-include docs *.rst
|
|
4
|
+
recursive-include docs *.txt
|
|
5
|
+
recursive-include docs Makefile
|
|
6
|
+
recursive-include tests *.log
|
|
7
|
+
recursive-include tests *.py
|
|
8
|
+
exclude .pre-commit-config.yaml
|
|
9
|
+
exclude .readthedocs.yaml
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: scrapyloganalyzer
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Provides methods to analyze the quality of a Scrapy crawl.
|
|
5
|
+
Author-email: Open Contracting Partnership <data@open-contracting.org>
|
|
6
|
+
License: BSD
|
|
7
|
+
Project-URL: Homepage, https://github.com/open-contracting/scrapy-log-analyzer
|
|
8
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: logparser
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: coverage; extra == "test"
|
|
21
|
+
Requires-Dist: pytest; extra == "test"
|
|
22
|
+
|
|
23
|
+
|PyPI Version| |Build Status| |Coverage Status| |Python Version|
|
|
24
|
+
|
|
25
|
+
Provides methods to analyze the quality of a Scrapy crawl.
|
|
26
|
+
|
|
27
|
+
(If you are viewing this on GitHub, open the `full documentation <https://scrapy-log-analyzer.readthedocs.io/>`__ for additional details.)
|
|
28
|
+
|
|
29
|
+
.. |PyPI Version| image:: https://img.shields.io/pypi/v/scrapyloganalyzer.svg
|
|
30
|
+
:target: https://pypi.org/project/scrapyloganalyzer/
|
|
31
|
+
.. |Build Status| image:: https://github.com/open-contracting/scrapy-log-analyzer/actions/workflows/ci.yml/badge.svg
|
|
32
|
+
:target: https://github.com/open-contracting/scrapy-log-analyzer/actions/workflows/ci.yml
|
|
33
|
+
.. |Coverage Status| image:: https://coveralls.io/repos/github/open-contracting/scrapy-log-analyzer/badge.svg?branch=main
|
|
34
|
+
:target: https://coveralls.io/github/open-contracting/scrapy-log-analyzer?branch=main
|
|
35
|
+
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/scrapyloganalyzer.svg
|
|
36
|
+
:target: https://pypi.org/project/scrapyloganalyzer/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|PyPI Version| |Build Status| |Coverage Status| |Python Version|
|
|
2
|
+
|
|
3
|
+
Provides methods to analyze the quality of a Scrapy crawl.
|
|
4
|
+
|
|
5
|
+
(If you are viewing this on GitHub, open the `full documentation <https://scrapy-log-analyzer.readthedocs.io/>`__ for additional details.)
|
|
6
|
+
|
|
7
|
+
.. |PyPI Version| image:: https://img.shields.io/pypi/v/scrapyloganalyzer.svg
|
|
8
|
+
:target: https://pypi.org/project/scrapyloganalyzer/
|
|
9
|
+
.. |Build Status| image:: https://github.com/open-contracting/scrapy-log-analyzer/actions/workflows/ci.yml/badge.svg
|
|
10
|
+
:target: https://github.com/open-contracting/scrapy-log-analyzer/actions/workflows/ci.yml
|
|
11
|
+
.. |Coverage Status| image:: https://coveralls.io/repos/github/open-contracting/scrapy-log-analyzer/badge.svg?branch=main
|
|
12
|
+
:target: https://coveralls.io/github/open-contracting/scrapy-log-analyzer?branch=main
|
|
13
|
+
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/scrapyloganalyzer.svg
|
|
14
|
+
:target: https://pypi.org/project/scrapyloganalyzer/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Minimal makefile for Sphinx documentation
|
|
2
|
+
#
|
|
3
|
+
|
|
4
|
+
# You can set these variables from the command line, and also
|
|
5
|
+
# from the environment for the first two.
|
|
6
|
+
SPHINXOPTS ?=
|
|
7
|
+
SPHINXBUILD ?= sphinx-build
|
|
8
|
+
SOURCEDIR = .
|
|
9
|
+
BUILDDIR = _build
|
|
10
|
+
|
|
11
|
+
# Put it first so that "make" without argument is like "make help".
|
|
12
|
+
help:
|
|
13
|
+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
14
|
+
|
|
15
|
+
.PHONY: help Makefile
|
|
16
|
+
|
|
17
|
+
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
18
|
+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
19
|
+
%: Makefile
|
|
20
|
+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Configuration file for the Sphinx documentation builder.
|
|
2
|
+
#
|
|
3
|
+
# This file only contains a selection of the most common options. For a full
|
|
4
|
+
# list see the documentation:
|
|
5
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
6
|
+
|
|
7
|
+
# -- Path setup --------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
# If extensions (or modules to document with autodoc) are in another directory,
|
|
10
|
+
# add these directories to sys.path here. If the directory is relative to the
|
|
11
|
+
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, os.path.abspath(".."))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# -- Project information -----------------------------------------------------
|
|
20
|
+
|
|
21
|
+
project = "Scrapy Log Analyzer"
|
|
22
|
+
copyright = "2022, Open Contracting Partnership"
|
|
23
|
+
author = "Open Contracting Partnership"
|
|
24
|
+
|
|
25
|
+
# The short X.Y version
|
|
26
|
+
version = "0.0.1"
|
|
27
|
+
# The full version, including alpha/beta/rc tags
|
|
28
|
+
release = version
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# -- General configuration ---------------------------------------------------
|
|
32
|
+
|
|
33
|
+
# Add any Sphinx extension module names here, as strings. They can be
|
|
34
|
+
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
35
|
+
# ones.
|
|
36
|
+
extensions = [
|
|
37
|
+
"sphinx.ext.autodoc",
|
|
38
|
+
"sphinx.ext.intersphinx",
|
|
39
|
+
"sphinx.ext.viewcode",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Add any paths that contain templates here, relative to this directory.
|
|
43
|
+
templates_path = ["_templates"]
|
|
44
|
+
|
|
45
|
+
# List of patterns, relative to source directory, that match files and
|
|
46
|
+
# directories to ignore when looking for source files.
|
|
47
|
+
# This pattern also affects html_static_path and html_extra_path.
|
|
48
|
+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# -- Options for HTML output -------------------------------------------------
|
|
52
|
+
|
|
53
|
+
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
54
|
+
# a list of builtin themes.
|
|
55
|
+
#
|
|
56
|
+
html_theme = "furo"
|
|
57
|
+
|
|
58
|
+
# Add any paths that contain custom static files (such as style sheets) here,
|
|
59
|
+
# relative to this directory. They are copied after the builtin static files,
|
|
60
|
+
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
61
|
+
html_static_path = []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# -- Extension configuration -------------------------------------------------
|
|
65
|
+
|
|
66
|
+
autodoc_default_options = {
|
|
67
|
+
"members": None,
|
|
68
|
+
"member-order": "bysource",
|
|
69
|
+
}
|
|
70
|
+
autodoc_typehints = "description"
|
|
71
|
+
autodoc_type_aliases = {}
|
|
72
|
+
|
|
73
|
+
intersphinx_mapping = {
|
|
74
|
+
"python": ("https://docs.python.org/3", None),
|
|
75
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Scrapy Log Analyzer |release|
|
|
2
|
+
=============================
|
|
3
|
+
|
|
4
|
+
.. include:: ../README.rst
|
|
5
|
+
|
|
6
|
+
.. toctree::
|
|
7
|
+
:caption: Contents
|
|
8
|
+
|
|
9
|
+
api/index
|
|
10
|
+
contributing/index
|
|
11
|
+
changelog
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2022 Open Contracting Partnership, released under the BSD license
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
furo
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scrapyloganalyzer"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [{name = "Open Contracting Partnership", email = "data@open-contracting.org"}]
|
|
9
|
+
description = "Provides methods to analyze the quality of a Scrapy crawl."
|
|
10
|
+
readme = "README.rst"
|
|
11
|
+
license = {text = "BSD"}
|
|
12
|
+
urls = {Homepage = "https://github.com/open-contracting/scrapy-log-analyzer"}
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: BSD License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
21
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"logparser",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
test = [
|
|
29
|
+
"coverage",
|
|
30
|
+
"pytest",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
exclude = [
|
|
35
|
+
"tests",
|
|
36
|
+
"tests.*",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 119
|
|
41
|
+
target-version = "py39"
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["ALL"]
|
|
45
|
+
ignore = [
|
|
46
|
+
"ANN", "C901", "COM812", "D203", "D212", "D415", "EM", "ISC001", "PERF203", "PLR091", "Q000",
|
|
47
|
+
"D104", "D205",
|
|
48
|
+
"DTZ",
|
|
49
|
+
"PTH",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint.flake8-builtins]
|
|
53
|
+
builtins-ignorelist = ["copyright"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint.flake8-unused-arguments]
|
|
56
|
+
ignore-variadic-names = true
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
"docs/conf.py" = ["D100", "INP001"]
|
|
60
|
+
"tests/*" = [
|
|
61
|
+
"ARG001", "D", "FBT003", "INP001", "PLR2004", "S", "TRY003",
|
|
62
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import datetime
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
from logparser import parse
|
|
8
|
+
from logparser.common import DATETIME_PATTERN, Common
|
|
9
|
+
|
|
10
|
+
# Kingfisher Collect logs an INFO message starting with "Spider arguments:".
|
|
11
|
+
SPIDER_ARGUMENTS_SEARCH_STRING = " INFO: Spider arguments: "
|
|
12
|
+
|
|
13
|
+
MAXIMUM_TIMEDELTA = 3
|
|
14
|
+
|
|
15
|
+
# Hotfix: https://github.com/my8100/logparser/pull/19
|
|
16
|
+
Common.SIGTERM_PATTERN = re.compile(r"^%s[ ].+?:[ ](Received[ ]SIG(?:BREAK|INT|TERM)([ ]twice)?)," % DATETIME_PATTERN) # noqa: UP031
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ScrapyLogFile:
|
|
20
|
+
"""A representation of a Scrapy log file."""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def find(cls, logs_directory, source_id, data_version):
|
|
24
|
+
"""
|
|
25
|
+
Find and return the first matching log file for the given crawl.
|
|
26
|
+
|
|
27
|
+
:param str logs_directory: Kingfisher Collect's project directory within Scrapyd's logs_dir directory
|
|
28
|
+
:param str source_id: the spider's name
|
|
29
|
+
:param datetime.datetime data_version: the crawl directory's name, parsed as a datetime
|
|
30
|
+
"""
|
|
31
|
+
source_directory = os.path.join(logs_directory, source_id)
|
|
32
|
+
if os.path.isdir(source_directory):
|
|
33
|
+
with os.scandir(source_directory) as it:
|
|
34
|
+
for entry in it:
|
|
35
|
+
if entry.name.endswith(".log"):
|
|
36
|
+
scrapy_log_file = ScrapyLogFile(entry.path)
|
|
37
|
+
if scrapy_log_file.match(data_version):
|
|
38
|
+
return scrapy_log_file
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def __init__(self, name) -> None:
|
|
42
|
+
""":param str name: the full path to the log file"""
|
|
43
|
+
self.name = name
|
|
44
|
+
|
|
45
|
+
self._logparser = None
|
|
46
|
+
self._item_counts = None
|
|
47
|
+
self._spider_arguments = None
|
|
48
|
+
|
|
49
|
+
def delete(self):
|
|
50
|
+
"""Delete the log file and any log summary ending in ``.stats``."""
|
|
51
|
+
if os.path.isfile(self.name):
|
|
52
|
+
os.remove(self.name)
|
|
53
|
+
summary = f"{self.name}.stats"
|
|
54
|
+
if os.path.isfile(summary):
|
|
55
|
+
os.remove(summary)
|
|
56
|
+
|
|
57
|
+
# Logparser processing
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def logparser(self) -> dict:
|
|
61
|
+
"""Return the output of `logparser <https://pypi.org/project/logparser/>`__."""
|
|
62
|
+
if self._logparser is None:
|
|
63
|
+
with open(self.name) as f:
|
|
64
|
+
# `taillines=0` sets the 'tail' key to all lines, so we set it to 1.
|
|
65
|
+
self._logparser = parse(f.read(), headlines=0, taillines=1)
|
|
66
|
+
|
|
67
|
+
return self._logparser
|
|
68
|
+
|
|
69
|
+
def match(self, data_version) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Return whether the crawl directory's name, parsed as a datetime, is less than 3 seconds after the log file's
|
|
72
|
+
start time.
|
|
73
|
+
"""
|
|
74
|
+
return 0 <= data_version.timestamp() - self.crawl_time.timestamp() < MAXIMUM_TIMEDELTA
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def crawl_time(self) -> datetime.datetime:
|
|
78
|
+
"""
|
|
79
|
+
Return the ``crawl_time`` spider argument if set, or the ``start_time`` crawl statistic otherwise. If neither
|
|
80
|
+
is logged, return the time of the first log message.
|
|
81
|
+
"""
|
|
82
|
+
crawl_time = self.spider_arguments.get("crawl_time")
|
|
83
|
+
if crawl_time:
|
|
84
|
+
return datetime.datetime.strptime(crawl_time, "%Y-%m-%dT%H:%M:%S")
|
|
85
|
+
if "start_time" in self.logparser["crawler_stats"]:
|
|
86
|
+
return eval(self.logparser["crawler_stats"]["start_time"]).replace(microsecond=0) # noqa: S307
|
|
87
|
+
return datetime.datetime.fromtimestamp(self.logparser["first_log_timestamp"])
|
|
88
|
+
|
|
89
|
+
def is_finished(self) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Return whether the log file contains a "Spider closed (finished)" log message or a ``finish_reason`` crawl
|
|
92
|
+
statistic set to "finished".
|
|
93
|
+
"""
|
|
94
|
+
# See https://kingfisher-collect.readthedocs.io/en/latest/logs.html#check-the-reason-for-closing-the-spider
|
|
95
|
+
# logparser's `finish_reason` is "N/A" for an unclean shutdown, because crawl statistics aren't logged.
|
|
96
|
+
return self.logparser["finish_reason"] == "finished"
|
|
97
|
+
|
|
98
|
+
# Line-by-line processing
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def item_counts(self) -> dict:
|
|
102
|
+
"""Return the number of each type of item, according to the log file."""
|
|
103
|
+
if self._item_counts is None:
|
|
104
|
+
self._process_line_by_line()
|
|
105
|
+
|
|
106
|
+
return self._item_counts
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def spider_arguments(self) -> dict:
|
|
110
|
+
"""Return the spider's arguments."""
|
|
111
|
+
if self._spider_arguments is None:
|
|
112
|
+
self._process_line_by_line()
|
|
113
|
+
|
|
114
|
+
return self._spider_arguments
|
|
115
|
+
|
|
116
|
+
def is_complete(self) -> bool:
|
|
117
|
+
"""Return whether the crawl collected a subset of the dataset, according to the log file."""
|
|
118
|
+
# See https://kingfisher-collect.readthedocs.io/en/latest/spiders.html#spider-arguments
|
|
119
|
+
return not any(
|
|
120
|
+
self.spider_arguments.get(arg)
|
|
121
|
+
for arg in (
|
|
122
|
+
"from_date",
|
|
123
|
+
"until_date",
|
|
124
|
+
"portal",
|
|
125
|
+
"publisher",
|
|
126
|
+
"system",
|
|
127
|
+
"sample",
|
|
128
|
+
"path",
|
|
129
|
+
"qs:",
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _process_line_by_line(self) -> None:
|
|
134
|
+
self._item_counts = defaultdict(int)
|
|
135
|
+
self._spider_arguments = {}
|
|
136
|
+
|
|
137
|
+
buf = []
|
|
138
|
+
with open(self.name) as f:
|
|
139
|
+
for line in f:
|
|
140
|
+
if buf or line.startswith("{"):
|
|
141
|
+
buf.append(line.rstrip())
|
|
142
|
+
if buf and buf[-1].endswith("}"):
|
|
143
|
+
try:
|
|
144
|
+
# Scrapy logs items as dicts. FileError items, representing retrieval errors, are identified by
|
|
145
|
+
# an 'errors' key. FileError items use only simple types, so `ast.literal_eval` can be used.
|
|
146
|
+
item = ast.literal_eval("".join(buf))
|
|
147
|
+
if "errors" in item:
|
|
148
|
+
self._item_counts["FileError"] += 1
|
|
149
|
+
elif "number" in item:
|
|
150
|
+
self._item_counts["FileItem"] += 1
|
|
151
|
+
elif "data_type" in item:
|
|
152
|
+
self._item_counts["File"] += 1
|
|
153
|
+
except ValueError:
|
|
154
|
+
# Scrapy dumps stats as a dict, which uses `datetime.datetime` types that can't be parsed with
|
|
155
|
+
# `ast.literal_eval`.
|
|
156
|
+
pass
|
|
157
|
+
buf = []
|
|
158
|
+
|
|
159
|
+
index = line.find(SPIDER_ARGUMENTS_SEARCH_STRING)
|
|
160
|
+
if index > -1:
|
|
161
|
+
# `eval` is used, because the string can contain `datetime.date` and is written by trusted code in
|
|
162
|
+
# Kingfisher Collect. Otherwise, we can modify the string so that `ast.literal_eval` can be used.
|
|
163
|
+
self._spider_arguments = eval(line[index + len(SPIDER_ARGUMENTS_SEARCH_STRING) :]) # noqa: S307
|
|
164
|
+
|
|
165
|
+
# Mixed processing
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def error_rate(self) -> float:
|
|
169
|
+
"""
|
|
170
|
+
Return an estimated lower bound of the true error rate.
|
|
171
|
+
|
|
172
|
+
Kingfisher Collect is expected to yield at most one FileError item per request leading to a File item, so the
|
|
173
|
+
true error rate can only be less than this estimated lower bound if Kingfisher Collect breaks this expectation.
|
|
174
|
+
On the other hand, the true error rate can easily be higher than the estimated lower bound; for example:
|
|
175
|
+
|
|
176
|
+
- If the spider crawls 10 URLs, each returning 99 URLs, each returning OCDS data, and the requests for 5 of
|
|
177
|
+
the 10 fail, then the estimated lower bound is 5 / 500 (1%), though the true error rate is 50%.
|
|
178
|
+
- Similarly if the spider crawls 10 archive files, each containing 99 OCDS files.
|
|
179
|
+
"""
|
|
180
|
+
error_count = self.item_counts["FileError"] + self.logparser["crawler_stats"].get("invalid_json_count", 0)
|
|
181
|
+
return error_count / (self.item_counts["File"] + error_count)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: scrapyloganalyzer
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Provides methods to analyze the quality of a Scrapy crawl.
|
|
5
|
+
Author-email: Open Contracting Partnership <data@open-contracting.org>
|
|
6
|
+
License: BSD
|
|
7
|
+
Project-URL: Homepage, https://github.com/open-contracting/scrapy-log-analyzer
|
|
8
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: logparser
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: coverage; extra == "test"
|
|
21
|
+
Requires-Dist: pytest; extra == "test"
|
|
22
|
+
|
|
23
|
+
|PyPI Version| |Build Status| |Coverage Status| |Python Version|
|
|
24
|
+
|
|
25
|
+
Provides methods to analyze the quality of a Scrapy crawl.
|
|
26
|
+
|
|
27
|
+
(If you are viewing this on GitHub, open the `full documentation <https://scrapy-log-analyzer.readthedocs.io/>`__ for additional details.)
|
|
28
|
+
|
|
29
|
+
.. |PyPI Version| image:: https://img.shields.io/pypi/v/scrapyloganalyzer.svg
|
|
30
|
+
:target: https://pypi.org/project/scrapyloganalyzer/
|
|
31
|
+
.. |Build Status| image:: https://github.com/open-contracting/scrapy-log-analyzer/actions/workflows/ci.yml/badge.svg
|
|
32
|
+
:target: https://github.com/open-contracting/scrapy-log-analyzer/actions/workflows/ci.yml
|
|
33
|
+
.. |Coverage Status| image:: https://coveralls.io/repos/github/open-contracting/scrapy-log-analyzer/badge.svg?branch=main
|
|
34
|
+
:target: https://coveralls.io/github/open-contracting/scrapy-log-analyzer?branch=main
|
|
35
|
+
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/scrapyloganalyzer.svg
|
|
36
|
+
:target: https://pypi.org/project/scrapyloganalyzer/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.rst
|
|
4
|
+
pyproject.toml
|
|
5
|
+
docs/Makefile
|
|
6
|
+
docs/changelog.rst
|
|
7
|
+
docs/conf.py
|
|
8
|
+
docs/index.rst
|
|
9
|
+
docs/requirements.txt
|
|
10
|
+
docs/api/index.rst
|
|
11
|
+
docs/contributing/index.rst
|
|
12
|
+
scrapyloganalyzer/__init__.py
|
|
13
|
+
scrapyloganalyzer.egg-info/PKG-INFO
|
|
14
|
+
scrapyloganalyzer.egg-info/SOURCES.txt
|
|
15
|
+
scrapyloganalyzer.egg-info/dependency_links.txt
|
|
16
|
+
scrapyloganalyzer.egg-info/requires.txt
|
|
17
|
+
scrapyloganalyzer.egg-info/top_level.txt
|
|
18
|
+
tests/__init__.py
|
|
19
|
+
tests/test_scrapy_log_file.py
|
|
20
|
+
tests/fixtures/log1.log
|
|
21
|
+
tests/fixtures/log_crawl_time_crawl_statistic.log
|
|
22
|
+
tests/fixtures/log_crawl_time_log_message.log
|
|
23
|
+
tests/fixtures/log_crawl_time_spider_argument.log
|
|
24
|
+
tests/fixtures/log_error1.log
|
|
25
|
+
tests/fixtures/log_error_invalid_json.log
|
|
26
|
+
tests/fixtures/log_from_date1.log
|
|
27
|
+
tests/fixtures/log_in_progress1.log
|
|
28
|
+
tests/fixtures/log_sample1.log
|
|
29
|
+
tests/fixtures/log_sigint1.log
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|