From 5e3a1984f5c24d612635b724ff826de9da296901 Mon Sep 17 00:00:00 2001 From: Hynek Kydlicek Date: Thu, 11 May 2023 12:35:36 +0200 Subject: [PATCH 1/3] corrected types + added additional_info --- cmoncrawl/processor/pipeline/pipeline.py | 18 ++++++++---------- cmoncrawl/processor/pipeline/router.py | 4 +++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmoncrawl/processor/pipeline/pipeline.py b/cmoncrawl/processor/pipeline/pipeline.py index c386ecdd..5c2b320d 100644 --- a/cmoncrawl/processor/pipeline/pipeline.py +++ b/cmoncrawl/processor/pipeline/pipeline.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List +from typing import Any, Dict, List from cmoncrawl.processor.pipeline.downloader import IDownloader from cmoncrawl.processor.pipeline.streamer import IStreamer from cmoncrawl.processor.pipeline.router import IRouter @@ -16,7 +16,9 @@ def __init__( self.downloader = downloader self.oustreamer = outstreamer - async def process_domain_record(self, domain_record: DomainRecord): + async def process_domain_record( + self, domain_record: DomainRecord, additional_info: Dict[str, Any] = {} + ): paths: List[Path] = [] downloaded_articles = [] try: @@ -33,16 +35,12 @@ async def process_domain_record(self, domain_record: DomainRecord): ) output = extractor.extract(downloaded_article, metadata) if output is None: - metadata_logger.debug( - f"No output from {extractor.__class__}", - extra={"domain_record": metadata.domain_record}, - ) continue + + if "additional_info" not in output: + output["additional_info"] = additional_info + paths.append(await self.oustreamer.stream(output, metadata)) - metadata_logger.info( - "Successfully processed", - extra={"domain_record": metadata.domain_record}, - ) except ValueError as e: metadata_logger.error( str(e), diff --git a/cmoncrawl/processor/pipeline/router.py b/cmoncrawl/processor/pipeline/router.py index e2af382c..0fe21fb3 100644 --- a/cmoncrawl/processor/pipeline/router.py +++ b/cmoncrawl/processor/pipeline/router.py @@ -30,7 +30,9 @@ class Route: class IRouter(ABC): @abstractmethod - def route(self, url: str, time: datetime, metadata: PipeMetadata) -> IExtractor: + def route( + self, url: str | None, time: datetime | None, metadata: PipeMetadata + ) -> IExtractor: raise NotImplementedError() From 1f809e04c07b0203a93908b03cccf7b0f9dfade3 Mon Sep 17 00:00:00 2001 From: Hynek Kydlicek Date: Thu, 11 May 2023 13:08:34 +0200 Subject: [PATCH 2/3] router loads init --- cmoncrawl/processor/pipeline/extractor.py | 12 ++++++++++-- cmoncrawl/processor/pipeline/router.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cmoncrawl/processor/pipeline/extractor.py b/cmoncrawl/processor/pipeline/extractor.py index 77a20eb4..c2b94fbd 100644 --- a/cmoncrawl/processor/pipeline/extractor.py +++ b/cmoncrawl/processor/pipeline/extractor.py @@ -92,7 +92,11 @@ def __init__(self, filter_non_ok: bool = True): self.filter_non_ok = filter_non_ok def extract_soup(self, soup: BeautifulSoup, metadata: PipeMetadata): - metadata.name = metadata.domain_record.url.replace("/", "_")[:100] + metadata.name = ( + metadata.domain_record.url.replace("/", "_")[:100] + if metadata.domain_record.url is not None + else "unknown" + ) result_dict: Dict[str, Any] = {"html": str(soup)} return result_dict @@ -120,7 +124,11 @@ def __init__(self, filter_non_ok: bool = True): self.filter_non_ok = filter_non_ok def extract_soup(self, soup: BeautifulSoup, metadata: PipeMetadata): - metadata.name = metadata.domain_record.url.replace("/", "_")[:100] + metadata.name = ( + metadata.domain_record.url.replace("/", "_")[:100] + if metadata.domain_record.url is not None + else "unknown" + ) result_dict: Dict[str, Any] = { "domain_record": metadata.domain_record.to_dict() } diff --git a/cmoncrawl/processor/pipeline/router.py b/cmoncrawl/processor/pipeline/router.py index 0fe21fb3..7229c9f6 100644 --- a/cmoncrawl/processor/pipeline/router.py +++ b/cmoncrawl/processor/pipeline/router.py @@ -41,7 +41,7 @@ def __init__(self): self.registered_routes: List[Route] = [] self.modules: Dict[str, IExtractor] = {} - def load_module(self, module_path: Path) -> IExtractor: + def load_module(self, module_path: Path): module_name = os.path.splitext(os.path.basename(module_path))[0] spec = importlib.util.spec_from_file_location(module_name, module_path) if spec is None: @@ -53,7 +53,10 @@ def load_module(self, module_path: Path) -> IExtractor: raise Exception("Failed to load module: " + module_name) spec.loader.exec_module(module) + return module, module_name + def load_module_as_extractor(self, module_path: Path): + module, module_name = self.load_module(module_path) name: str = getattr(module, "NAME", module_name) extractor: IExtractor | None = getattr(module, "extractor", None) if extractor is None: @@ -69,7 +72,10 @@ def load_modules(self, folder: Path): if not file.endswith(".py"): continue - extractors.append(self.load_module(Path(root) / file)) + if file == "__init__.py": + self.load_module(Path(root) / file) + + extractors.append(self.load_module_as_extractor(Path(root) / file)) all_purpose_logger.info(f"Loaded {len(extractors)} extractors") def load_extractor(self, name: str, extractor: IExtractor): From 47e7a22743a37ba7474883168bfff74f46101fca Mon Sep 17 00:00:00 2001 From: Hynek Kydlicek Date: Thu, 11 May 2023 17:47:27 +0200 Subject: [PATCH 3/3] add tqdml for extraction better error handling --- cmoncrawl/integrations/download.py | 2 +- cmoncrawl/integrations/extract.py | 4 +- cmoncrawl/middleware/synchronized.py | 67 ++++++++++++++++++------ cmoncrawl/processor/pipeline/pipeline.py | 30 +++++------ cmoncrawl/processor/pipeline/streamer.py | 3 +- pyproject.toml | 36 +++---------- requirements.txt | 1 + tests/end_to_end_tests.py | 6 +-- tests/test_extract/files/file.json | 5 -- 9 files changed, 81 insertions(+), 73 deletions(-) delete mode 100644 tests/test_extract/files/file.json diff --git a/cmoncrawl/integrations/download.py b/cmoncrawl/integrations/download.py index 974d323b..ab6b45f9 100644 --- a/cmoncrawl/integrations/download.py +++ b/cmoncrawl/integrations/download.py @@ -24,7 +24,7 @@ class DownloadOutputFormat(Enum): def add_mode_args(subparser: Any): record_parser = subparser.add_parser(DownloadOutputFormat.RECORD.value) - record_parser.add_argument("--max_crawls_per_file", type=int, default=100_000) + record_parser.add_argument("--max_crawls_per_file", type=int, default=500_000) subparser.add_parser(DownloadOutputFormat.HTML.value) return subparser diff --git a/cmoncrawl/integrations/extract.py b/cmoncrawl/integrations/extract.py index df4759b9..ba1ce83f 100644 --- a/cmoncrawl/integrations/extract.py +++ b/cmoncrawl/integrations/extract.py @@ -9,7 +9,7 @@ from cmoncrawl.processor.pipeline.pipeline import ProcessorPipeline from cmoncrawl.middleware.synchronized import extract import argparse -from typing import Any, Dict, List +from typing import Any, List import asyncio from cmoncrawl.processor.pipeline.streamer import ( StreamerFileJSON, @@ -45,7 +45,7 @@ def add_args(subparser: Any): ) parser.add_argument("output_path", type=Path) parser.add_argument("files", nargs="+", type=Path) - parser.add_argument("--max_crawls_per_file", type=int, default=100_000) + parser.add_argument("--max_crawls_per_file", type=int, default=500_000) parser.add_argument("--max_directory_size", type=int, default=1000) parser.add_argument("--n_proc", type=int, default=1) mode_subparser = parser.add_subparsers(dest="mode", required=True) diff --git a/cmoncrawl/middleware/synchronized.py b/cmoncrawl/middleware/synchronized.py index a3656d2d..2850048d 100644 --- a/cmoncrawl/middleware/synchronized.py +++ b/cmoncrawl/middleware/synchronized.py @@ -3,8 +3,9 @@ from cmoncrawl.aggregator.index_query import IndexAggregator from cmoncrawl.processor.pipeline.pipeline import ProcessorPipeline from cmoncrawl.common.types import DomainRecord -from cmoncrawl.common.loggers import all_purpose_logger +from cmoncrawl.common.loggers import all_purpose_logger, metadata_logger from cmoncrawl.aggregator.utils.helpers import unify_url_id +from tqdm import tqdm import asyncio @@ -20,15 +21,11 @@ async def index_and_extract( try: async with index_agg: async for domain_record in index_agg: - if ( - filter_non_unique_url - and unify_url_id(domain_record.url) in processed_urls - ): + url = domain_record.url or "" + if filter_non_unique_url and unify_url_id(url) in processed_urls: continue try: - paths: List[Path] = await pipeline.process_domain_record( - domain_record - ) + await pipeline.process_domain_record(domain_record) except KeyboardInterrupt as e: break @@ -37,26 +34,66 @@ async def index_and_extract( f"Failed to process {domain_record.url} with {e}" ) continue - processed_urls.add(unify_url_id(domain_record.url)) + processed_urls.add(unify_url_id(url)) finally: if hasattr(pipeline.downloader, "__aexit__"): await pipeline.downloader.__aexit__(None, None, None) +async def _extract_task(domain_record: DomainRecord, pipeline: ProcessorPipeline): + result = [] + try: + result = await pipeline.process_domain_record(domain_record) + except KeyboardInterrupt as e: + raise e + except Exception as e: + metadata_logger.error( + f"Failed to process {domain_record.url} with {e}", + extra={"domain_record": domain_record}, + ) + return result + + async def extract( domain_records: List[DomainRecord], pipeline: ProcessorPipeline, + concurrent_length: int = 20, + timeout: int = 5, ): + domain_records_iterator = iter(tqdm(domain_records)) + domains_exausted = False if hasattr(pipeline.downloader, "__aenter__"): await pipeline.downloader.__aenter__() try: - await asyncio.gather( - *[ - pipeline.process_domain_record(domain_record) - for domain_record in domain_records - ] - ) + queue: Set[asyncio.Task[List[Path]]] = set() + while not domains_exausted or len(queue) > 0: + # Put into queue till possible + while len(queue) < concurrent_length and not domains_exausted: + next_domain_record = next(domain_records_iterator, None) + if next_domain_record is None: + domains_exausted = True + break + + queue.add( + asyncio.create_task(_extract_task(next_domain_record, pipeline)) + ) + + done, queue = await asyncio.wait( + queue, timeout=timeout, return_when=asyncio.FIRST_COMPLETED + ) + for task in done: + try: + await task + except KeyboardInterrupt as e: + break + + except Exception as _: + all_purpose_logger.error(f"Failed to process {task}") + pass + except Exception as e: + all_purpose_logger.error(e, exc_info=True) + finally: if hasattr(pipeline.downloader, "__aexit__"): await pipeline.downloader.__aexit__(None, None, None) diff --git a/cmoncrawl/processor/pipeline/pipeline.py b/cmoncrawl/processor/pipeline/pipeline.py index 5c2b320d..bbb1dfaf 100644 --- a/cmoncrawl/processor/pipeline/pipeline.py +++ b/cmoncrawl/processor/pipeline/pipeline.py @@ -27,24 +27,20 @@ async def process_domain_record( metadata_logger.error(f"{e}", extra={"domain_record": domain_record}) for (downloaded_article, metadata) in downloaded_articles: - try: - extractor = self.router.route( - metadata.domain_record.url, - metadata.domain_record.timestamp, - metadata, + extractor = self.router.route( + metadata.domain_record.url, + metadata.domain_record.timestamp, + metadata, + ) + output = extractor.extract(downloaded_article, metadata) + if output is None: + metadata_logger.warn( + f"Extractor {extractor.__class__.__name__} returned None for {metadata.domain_record.url}" ) - output = extractor.extract(downloaded_article, metadata) - if output is None: - continue + continue - if "additional_info" not in output: - output["additional_info"] = additional_info + if "additional_info" not in output: + output["additional_info"] = additional_info - paths.append(await self.oustreamer.stream(output, metadata)) - except ValueError as e: - metadata_logger.error( - str(e), - extra={"domain_record": domain_record}, - ) - # Not catching IOError because some other processor could process it -> nack + paths.append(await self.oustreamer.stream(output, metadata)) return paths diff --git a/cmoncrawl/processor/pipeline/streamer.py b/cmoncrawl/processor/pipeline/streamer.py index c00b2849..3692e4ca 100644 --- a/cmoncrawl/processor/pipeline/streamer.py +++ b/cmoncrawl/processor/pipeline/streamer.py @@ -1,7 +1,6 @@ from abc import ABC, abstractmethod import asyncio import json -from math import log from pathlib import Path import random from typing import Any, Dict, List @@ -183,7 +182,7 @@ def __init__( max_file_size: int, pretty: Boolean = False, ): - super().__init__(root, max_directory_size, max_file_size, extension=".json") + super().__init__(root, max_directory_size, max_file_size, extension=".jsonl") self.pretty = pretty def metadata_to_string(self, extracted_data: Dict[Any, Any]) -> str: diff --git a/pyproject.toml b/pyproject.toml index e3b3e322..292dc63f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,33 +5,8 @@ build-backend = "setuptools.build_meta" [project] name = "CmonCrawl" -version = "0.9.0" -dependencies = [ -"aiofiles==0.8.0", -"aiohttp==3.8.1", -"aiosignal==1.2.0", -"async-timeout==4.0.2", -"attrs==21.4.0", -"beautifulsoup4==4.11.1", -"bs4==0.0.1", -"charset-normalizer==2.1.0", -"dataclasses-json==0.5.7", -"docopt==0.6.2", -"frozenlist==1.3.0", -"idna==3.3", -"marshmallow==3.19.0", -"marshmallow-enum==1.5.1", -"multidict==6.0.2", -"mypy-extensions==1.0.0", -"packaging==23.1", -"six==1.16.0", -"soupsieve==2.3.2.post1", -"stomp.py==8.0.1", -"typing-inspect==0.8.0", -"typing_extensions==4.5.0", -"warcio==1.7.4", -"yarl==1.7.2" -] + +dynamic = ["version"] keywords = [ "Common Crawl", @@ -44,7 +19,7 @@ keywords = [ ] readme = "README.md" -license = {file = "MIT"} +license = {file = "LICENSE"} classifiers = [ "Development Status :: 3 - Alpha", @@ -52,6 +27,11 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", ] +[tool.setuptools_scm] + + +[tool.setuptools.dynamic] +dependencies = {file = "requirements.txt"} [tool.setuptools.packages.find] include = ["cmoncrawl*"] diff --git a/requirements.txt b/requirements.txt index 09c57e67..03ca31fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ packaging==23.1 six==1.16.0 soupsieve==2.3.2.post1 stomp.py==8.0.1 +tqdm==4.65.0 typing-inspect==0.8.0 typing_extensions==4.5.0 warcio==1.7.4 diff --git a/tests/end_to_end_tests.py b/tests/end_to_end_tests.py index 0447b450..0ad5cb05 100644 --- a/tests/end_to_end_tests.py +++ b/tests/end_to_end_tests.py @@ -32,7 +32,7 @@ async def test_extract_from_records(self): cfg: ExtractConfig = ExtractConfig.schema(many=False).load(js) results = await extract_from_files( config=cfg, - files=[self.base_folder / "files" / "file.json"], + files=[self.base_folder / "files" / "file.jsonl"], output_path=self.base_folder / "output", mode=ExtractMode.RECORD, date=datetime(2021, 1, 1), @@ -42,7 +42,7 @@ async def test_extract_from_records(self): max_retry=1, sleep_step=1, ) - with open(self.output_folder / "directory_0" / "0_file.json") as f: + with open(self.output_folder / "directory_0" / "0_file.jsonl") as f: lines = f.readlines() self.assertEqual(len(lines), 5) self.assertEqual( @@ -67,7 +67,7 @@ async def test_extract_from_html(self): max_retry=1, sleep_step=1, ) - with open(self.output_folder / "directory_0" / "0_file.json") as f: + with open(self.output_folder / "directory_0" / "0_file.jsonl") as f: lines = f.readlines() self.assertEqual(len(lines), 1) self.assertEqual( diff --git a/tests/test_extract/files/file.json b/tests/test_extract/files/file.json deleted file mode 100644 index 96e459a6..00000000 --- a/tests/test_extract/files/file.json +++ /dev/null @@ -1,5 +0,0 @@ -{"domain_record":{"digest":"3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ","encoding":null,"filename":"crawl-data/CC-MAIN-2023-14/segments/1679296943746.73/crawldiagnostics/CC-MAIN-20230321193811-20230321223811-00036.warc.gz","length":477,"offset":2592066,"timestamp":"2023-03-21 21:19:02","url":"http://www.seznam.cz/"},"html":""} -{"domain_record":{"digest":"7LSVB2HY2UVZDLCPAOIV4V5T25US46JB","encoding":"UTF-8","filename":"crawl-data/CC-MAIN-2023-14/segments/1679296943746.73/warc/CC-MAIN-20230321193811-20230321223811-00739.warc.gz","length":116849,"offset":1115229508,"timestamp":"2023-03-21 21:19:03","url":"https://www.seznam.cz/"},"html":"\nSeznam – najdu tam, co neznám

\"\"
Hlavní obsah

\"\"Seznam Zprávy • úterý 21. března. Svátek má Radek.

\"Máš

Máš to v topu, ukaž to! Tereza Ramba zavzpomínala, jak jí manžel povolil vytasit poprsí na kameru  

Tereza Ramba (33) patří k našim nejobsazovanějším herečkám. Co se nahoty týče, vždy se držela zpátky a zastávala názor, že vysvléct se před kamerou není potřeba. Velká změna přišla s filmem Betlémské světlo.
\"HOKEJ

HOKEJ ONLINE: Přestřelka pokračuje, drama vrcholí! Vítkovice skolily Kometu  

Hokejisté Pardubic budou v dnešním pokračování čtvrtfinále play off extraligy usilovat na ledě Olomouce po dvou domácích výhrách o postupový mečbol (průběžný stav je 4:4, prodlužuje se). V souběžně hrané sérii mezi Brnem a Vítkovicemi se sice Brňané dostali záhy do vedení, ale nakonec zvítězili hosté 4:1 a ujali se vedení 2:1 na zápasy. Utkání bylo kvůli nezpůsobilé hrací ploše v 15. minutě úvodní třetiny přerušeno a zhruba hodinu se nehrálo. Oba duely můžete sledovat v podrobných online reportážích na Sport.cz.
\"Česká

Česká kosmetika, na kterou nedají dopustit známé tváře: Co používá Hana Vagnerová či Adéla Elbel  

Říkáte si, jak dobře některé slavné ženy vypadají a jaké tajemství je za jejich půvabem… Jsou to dobré geny, nebo kosmetika? To asi nerozsoudíme. V každém případě musejí o pleť pečovat pravidelně – v jejich povolání je požadován téměř dokonalý vzhled. Mrkněte, jaké přípravky používají oblíbené české herečky.
\"Jízdní

Jízdní řád Škody: Na podzim nový Superb a Kodiaq, v lednu 2024 faceliftovaná Octavia  

Škoda Auto v rámci výroční tiskové konference neřešila jenom ekonomické výsledky, ale upřesnila i své plány stran nových modelů. Stěžejní jsou letos nové generace těch největších - Superbu i Kodiaqu. Pracuje se i na omlazené Octavii. Ta se představí začátkem příštího roku.
Automaticky nekonečně načítané články
Následují automaticky nekonečně načítané články. Zde je můžete přeskočit.
Skok před automaticky načítané články.
\"Prasátko

Prasátko Peppa jen po 22. hodině. Proč cenzurovat dětský seriál?

V jednom díle oblíbeného dětského kresleného seriál Prasátko Peppa je jen okrajově zmíněno, že Peppina kamarádka má dvě mámy – lední medvědice. A v Maďarsku přišel zákaz vysílání během dne.
\"Beran\"
Beran
Nyní máte přízeň planet pro pracovní sféru, takže se snažte využít každé šance, která se vám nabídne, abyste se posunuli opět o nějaký ten krok dál. Bude se vám líbit, že můžete uplatnit své nabyté zkušenosti a ukázat ostatním, jak moc jste dobří.
Celý horoskop »
od 22:10do 23:10Moje místa (200)\"Televize
od 21:35do 23:09Vlastníci\"ČT
od 22:05do 00:35Vůně ženy\"ČT
od 21:30do 23:10Mise Nový domov III\"Nova\"
od 21:35do 22:457 pádů Honzy Dědka\"Prima\"
od 21:55do 23:10Slunečná (51)\"Prima
od 21:45do 22:20Taková byla 90. léta\"Barrandov
"} -{"domain_record":{"digest":"3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ","encoding":null,"filename":"crawl-data/CC-MAIN-2023-14/segments/1679296943750.71/robotstxt/CC-MAIN-20230322051607-20230322081607-00067.warc.gz","length":513,"offset":1222873,"timestamp":"2023-03-22 06:04:17","url":"https://seznam.cz"},"html":""} -{"domain_record":{"digest":"3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ","encoding":null,"filename":"crawl-data/CC-MAIN-2023-14/segments/1679296943750.71/crawldiagnostics/CC-MAIN-20230322051607-20230322081607-00013.warc.gz","length":520,"offset":2099791,"timestamp":"2023-03-22 06:35:11","url":"http://www.kura70@seznam.cz"},"html":""} -{"domain_record":{"digest":"ECDH47JDC7BEPRGXMRTPNEDEMNSLKYJB","encoding":"UTF-8","filename":"crawl-data/CC-MAIN-2023-14/segments/1679296943750.71/warc/CC-MAIN-20230322051607-20230322081607-00739.warc.gz","length":116018,"offset":1090287326,"timestamp":"2023-03-22 06:35:12","url":"https://www.seznam.cz/"},"html":"\nSeznam – najdu tam, co neznám

\"\"
Hlavní obsah

\"\"Seznam Zprávy • středa 22. března. Svátek má Leona.

\"Blíží

Blíží se doba bezmasá? Místo hovězího mají lidé jíst zeleninu, plánuje EU

ANALÝZA. Daně na maso, omezování chovů skotu i zákaz reklamy. Evropská strategie „od zemědělce ke spotřebiteli“ dává vyniknout nápadům, jak maso na talíři částečně nahradit rostlinnou stravou. Zvířata ale potřebujeme.
\"Máš

Máš to v topu, ukaž to! Tereza Ramba zavzpomínala, jak jí manžel povolil vytasit poprsí na kameru  

Tereza Ramba (33) patří k našim nejobsazovanějším herečkám. Co se nahoty týče, vždy se držela zpátky a zastávala názor, že vysvléct se před kamerou není potřeba. Velká změna přišla s filmem Betlémské světlo.
\"Krejčí

Krejčí nastartoval bostonskou mašinu, rekord je na dohled  

Hokejisté Bostonu se po drobném zaškobrtnutí v zámořské NHL opět válí a po vítězství 2:1 nad Ottawou slaví čtvrtou výhru v řadě. Úterní souboj sice začal lépe kanadský celek, přesilovkový gól Davida Krejčího ale nastartoval obrat domácích, který ještě v první třetině dokonal Jake DeBrusk. David Pastrňák, jenž vyhlíží magickou hranici 50 gólů a 100 kanadských bodů za sezonu tentokrát vyšel naprázdno.
\"Co

Co možná nevíte o vaření těstovin: Vodu solte, až když je vařící, a porci odměřte pomocí naběračky  

Balíček těstovin se najde snad v každé domácnosti. Poslouží v časové tísni, ve chvílích, kdy se vám nechce moc vařit, a i tehdy, když máte chuť na něco opravdu dobrého. Je to jedno z prvních jídel, které jste uměli připravit, a o tom, jak je uvařit, víte téměř vše. Opravdu?
\"Jízdní

Jízdní řád Škody: Na podzim nový Superb a Kodiaq, v lednu 2024 faceliftovaná Octavia  

Škoda Auto v rámci výroční tiskové konference neřešila jenom ekonomické výsledky, ale upřesnila i své plány stran nových modelů. Stěžejní jsou letos nové generace těch největších - Superbu i Kodiaqu. Pracuje se i na omlazené Octavii. Ta se představí začátkem příštího roku.
Automaticky nekonečně načítané články
Následují automaticky nekonečně načítané články. Zde je můžete přeskočit.
Skok před automaticky načítané články.
\"Prasátko

Prasátko Peppa jen po 22. hodině. Proč cenzurovat dětský seriál?

V jednom díle oblíbeného dětského kresleného seriál Prasátko Peppa je jen okrajově zmíněno, že Peppina kamarádka má dvě mámy – lední medvědice. A v Maďarsku přišel zákaz vysílání během dne.
\"Panna\"
Panna
Nejste tolik pohotoví, jak jste zvyklí, a to vás může naprosto vykolejit. Ty tam jsou vaše nápady, ta tam je vaše motivace a trpělivost. Místo toho můžete být navztekaní sice sami na sebe, ale odnesou to ostatní kolem vás. Zavání to velkým rizikem.
Celý horoskop »
od 07:25do 08:15Agentura Jasno V (16)\"Televize
od 05:59do 09:00Studio 6\"ČT
od 05:59do 08:30Dobré ráno\"ČT
od 05:55do 08:25Snídaně\"Nova\"
od 07:00do 08:10Nový den\"Prima\"
od 07:35do 08:10Těžká dřina\"Prima
od 06:45do 07:55Divoký anděl (259)\"Prima
od 06:30do 08:55Jak to dopadlo!?\"Barrandov
"}