diff --git a/.gitignore b/.gitignore
index 18ce86f..f99b29d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,7 @@ wandb
**/*.parquet
**/*.tar.gz
.mypy_cache
-.pytest_cache
\ No newline at end of file
+.pytest_cache
+assets/*.bin
+build/*
+assets/*.out
\ No newline at end of file
diff --git a/assets/spark_slurm.sh b/assets/spark_slurm.sh
new file mode 100644
index 0000000..ec15aac
--- /dev/null
+++ b/assets/spark_slurm.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+#SBATCH --partition=spark
+#SBATCH --job-name=bild-cc-pile
+#SBATCH --nodes 5
+#SBATCH --ntasks-per-node 1
+#SBATCH --cpus-per-task=48
+#SBATCH --mem=0 # 0 means use all available memory (in MB)
+#SBATCH --output=%x_%j.out
+#SBATCH --comment laion
+#SBATCH --exclusive
+
+# wget https://dlcdn.apache.org/spark/spark-3.3.1/spark-3.3.1-bin-hadoop3.tgz && tar xf spark-3.3.1-bin-hadoop3.tgz
+
+# step 1: get environment variables
+# step 2: setup rank 0 to be the master and start the indexing python file (the last two operations happen in parallel)
+# step 3: start workers on all nodes
+
+srun --comment laion bash worker_spark_on_slurm.sh
diff --git a/assets/worker_spark_on_slurm.sh b/assets/worker_spark_on_slurm.sh
new file mode 100644
index 0000000..0dc4681
--- /dev/null
+++ b/assets/worker_spark_on_slurm.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# get environment variables
+GLOBAL_RANK=$SLURM_PROCID
+CPUS=$SLURM_CPUS_PER_TASK
+MEM=$SLURM_MEM_PER_NODE # seems to be in MB
+MASTER_ADDR=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)
+LOCAL_IP=$(hostname -I | awk '{print $1}')
+
+# set some environment variables for the indexing script
+export MASTER_ADDR=$MASTER_ADDR
+export MEMORY=$MEM
+export CPUS=$CPUS
+export SPARK_LOCAL_IP=$LOCAL_IP
+
+# setup the master node
+if [ $GLOBAL_RANK == 0 ]
+then
+ # print out some info
+ echo -e "MASTER ADDR: $MASTER_ADDR\tGLOBAL RANK: $GLOBAL_RANK\tCPUS PER TASK: $CPUS\tMEM PER NODE: $MEM"
+
+ # then start the spark master node in the background
+ ./spark-3.3.1-bin-hadoop3/sbin/start-master.sh -p 7079 -h $LOCAL_IP
+
+fi
+
+sleep 10
+
+# then start the spark worker node in the background
+MEM_IN_GB=$(($MEM / 1000))
+# concat a "G" to the end of the memory string
+MEM_IN_GB="$MEM_IN_GB"G
+echo "MEM IN GB: $MEM_IN_GB"
+
+export SPARK_LOCAL_DIRS=/scratch/local
+export SPARK_WORKER_DIR=/scratch/work
+
+./spark-3.3.1-bin-hadoop3/sbin/start-worker.sh -c $CPUS -m $MEM_IN_GB "spark://$MASTER_ADDR:7079"
+echo "Hello from worker $GLOBAL_RANK"
+
+
+sleep 10
+
+if [ $GLOBAL_RANK == 0 ]
+then
+ # then start some script
+ echo "hi"
+fi
+
+sleep 10000
diff --git a/cc2dataset/__init__.py b/cc2dataset/__init__.py
index 9c1bcc8..6b238a1 100644
--- a/cc2dataset/__init__.py
+++ b/cc2dataset/__init__.py
@@ -1,3 +1,6 @@
"""cc2dataset"""
-from cc2dataset.main import process_wat, cc2dataset
+from cc2dataset.main import cc2dataset
+from cc2dataset.warc_utils import process_warc, extract_documents_from_warc
+from cc2dataset.wat_utils import process_wat
+
diff --git a/cc2dataset/index_utils.py b/cc2dataset/index_utils.py
new file mode 100644
index 0000000..4359ed3
--- /dev/null
+++ b/cc2dataset/index_utils.py
@@ -0,0 +1,61 @@
+import fsspec
+from multiprocessing.pool import ThreadPool
+import random
+
+def get_cc_links(source_cc_protocol,ccfile):
+ """Get cc wat links"""
+ if source_cc_protocol == "s3":
+ fs, p = fsspec.core.url_to_fs("s3://commoncrawl/crawl-data/")
+ if ccfile == "warc":
+ links = ["s3://" + e for e in fs.glob(p + "/*/warc.paths.gz")]
+ elif ccfile == "wat":
+ links = ["s3://" + e for e in fs.glob(p + "/*/wat.paths.gz")]
+ elif ccfile == "wet":
+ links = ["s3://" + e for e in fs.glob(p + "/*/wet.paths.gz")]
+ else:
+ raise ValueError(f"Unknown ccfile: {ccfile}")
+ return links
+ elif source_cc_protocol == "http":
+ fs, p = fsspec.core.url_to_fs("https://commoncrawl.org/the-data/get-started/")
+ a = fs.open(p).read()
+ l = a.splitlines()
+ l = [e.decode("utf8").replace("[WARC] ", "") for e in l]
+ l = [e for e in l if "
s3://commoncrawl/crawl-data/" in e]
+ l = [
+ e.split(" ")[0].replace("s3://commoncrawl/", "https://data.commoncrawl.org/").replace("", "")
+ for e in l
+ ]
+ if ccfile == "warc":
+ l = [(e + "/warc.paths.gz").replace("//warc", "/warc") for e in l]
+ elif ccfile == "wat":
+ l = [(e + "/wat.paths.gz").replace("//wat", "/wat") for e in l]
+ elif ccfile == "wet":
+ l = [(e + "/wet.paths.gz").replace("//wet", "/wet") for e in l]
+ return l
+ else:
+ raise ValueError(f"Unknown protocol {source_cc_protocol}")
+
+
+def read_index_file(file_index):
+ with fsspec.open(file_index, "rb", compression="gzip") as f:
+ crawlfiles = [a.decode("utf8").strip() for a in f.readlines()]
+ return crawlfiles
+
+
+def read_index_files(crawl_count, file_count, source_cc_protocol, ccfile, crawl_index_list, shuffle):
+ """Read all wat index files"""
+ cc_links = get_cc_links(source_cc_protocol,ccfile)
+ if crawl_index_list is not None:
+ cc_links = [cc_links[i] for i in crawl_index_list]
+ if crawl_count is not None:
+ cc_links = cc_links[-crawl_count:] # pylint: disable=invalid-unary-operand-type
+ all_files = []
+ with ThreadPool(16) as pool:
+ for wats in pool.imap_unordered(read_index_file, cc_links):
+ all_files.extend(wats)
+ if file_count is not None:
+ all_files = random.choices(all_files, k=file_count)
+ if shuffle:
+ # shuffle to increase duplication over each part hence reduce size of each part after duplication
+ random.shuffle(all_files)
+ return all_files
\ No newline at end of file
diff --git a/cc2dataset/lang_utils.py b/cc2dataset/lang_utils.py
new file mode 100644
index 0000000..be1bc93
--- /dev/null
+++ b/cc2dataset/lang_utils.py
@@ -0,0 +1,86 @@
+import fasttext
+from pathlib import Path
+
+def load_fasttext_model(path_fasttext_model):
+ print("Loading fasttext model from ", path_fasttext_model)
+ return fasttext.load_model(path_fasttext_model)
+
+def get_fasttext_info(line, model_lang_id):
+ """The line should be in lower case and without \n in it."""
+ pred = model_lang_id.predict(line)
+ lang_pred_fasttext_id = pred[0][0].replace("__label__", "")
+ score_pred = pred[1][0]
+ return lang_pred_fasttext_id, score_pred
+
+
+
+class LangDetection:
+ #adapted from https://github.com/bigcode-project/bigcode-analysis/blob/main/data_analysis/python_data_analysis/nl_language_identification/language_identifier.py
+ def __init__(self,model_dump_path:str) -> None:
+ self.lang_model_path : str = model_dump_path
+ self.model = load_fasttext_model(self.lang_model_path)
+
+
+ def detect(self, text: str) -> str:
+ """
+ Detects the language of the text
+ args:
+ text (str) : Text to detect the language
+
+ returns:
+ language (str) : Predicted Language of the text
+ score_pred (str) : confidence of the prediction
+
+ """
+ text = text.lower()
+
+ fasttext_pred = get_fasttext_info(
+ text, self.model
+ )
+ return fasttext_pred[0], fasttext_pred[1]
+
+
+import re
+
+
+class LicensePattern:
+ cc_pattern = re.compile("http[s]?://creativecommons\\.org/licenses/(by|by-sa|by-nd|by-nc|by-nc-sa|by-nc-nd|publicdomain)[\"/ >]")
+
+def detect_licence(html:str):
+ """
+ Given a HTML string, this function detects the licence of the page.
+ It returns a string with the licence name, or NO-LICENCE-FOUND if no licence is found.
+ """
+ license_attribute_pattern = re.compile(LicensePattern.cc_pattern)
+
+ # storing counts of all difference occurrences of link to CC
+ multiple_occurrences_map = {}
+
+ # add all of them to the list
+ for match in license_attribute_pattern.finditer(html):
+ licence = match.group(1)
+
+ # add entry
+ if licence not in multiple_occurrences_map:
+ multiple_occurrences_map[licence] = 0
+
+ # and increase count
+ multiple_occurrences_map[licence] += 1
+
+ # no licence found
+ if not multiple_occurrences_map:
+ return "no-licence-found"
+
+ # only one link found or if multiple links found but the same type
+ if len(multiple_occurrences_map) == 1:
+ return list(multiple_occurrences_map.keys())[0]
+
+ # if multiple different links found, we return a general CC-UNSPECIFIED
+ return "cc-unspecified"
+
+
+if __name__ == "__main__":
+ lang_detector = LangDetection("lid_model_dump/lid.176.bin")
+ print(lang_detector.detect("Das ist ein Test."))
+
+#output : ('de', 1.000038981437683)
\ No newline at end of file
diff --git a/cc2dataset/main.py b/cc2dataset/main.py
index efbd6bb..5821384 100644
--- a/cc2dataset/main.py
+++ b/cc2dataset/main.py
@@ -8,7 +8,7 @@
from loguru import logger
import hashlib
import datetime
-from multiprocessing.pool import ThreadPool
+
from pyspark import SparkContext
from pyspark.sql.functions import rand
from pyspark.sql import SparkSession
@@ -16,200 +16,11 @@
import math
import time
from .spark_session_builder import build_spark_session
-from io import BytesIO
-
-
-def valid_video_link(link):
- valid_http = link.get("url", "").startswith("http")
- valid_video = any(
- link.get("url", "").endswith(ext) for ext in [".avi", ".mp4", ".mkv", ".webm", ".mov", ".mpg", ".mpeg", ".m4v"]
- )
- return valid_http and valid_video
-
-
-def extract_video_from_links(links):
- filtered_links = [{"url": link["url"], "alt": link.get("text", "")} for link in links if valid_video_link(link)]
- return filtered_links
-
-
-text_extensions = set(
- [
- "pdf",
- "epub",
- "djvu",
- "mobi",
- "doc",
- "docx",
- "rtf",
- "txt",
- "odt",
- "ppt",
- "pptx",
- "pages",
- "keynote",
- "wps",
- "md",
- ]
-)
-
-
-def valid_text_link(link):
- if not link.get("url", "").startswith("http"):
- return False
- splits = link.get("url", "").split(".")
- if len(splits) < 2:
- return False
- if splits[-1] not in text_extensions:
- return False
- return True
-
-
-def extract_text_from_links(links):
- filtered_links = [{"url": link["url"], "alt": link.get("text", "")} for link in links if valid_text_link(link)]
- return filtered_links
-
-
-def valid_audio_link(link):
- valid_http = link.get("url", "").startswith("http")
- valid_audio = any(link.get("url", "").endswith(ext) for ext in [".ogg", ".wav", ".mp3", ".flac", ".m4a"])
- return valid_http and valid_audio
-
-
-def extract_audio_from_links(links):
- """Extract image from links"""
- filtered_links = [{"url": link["url"], "alt": link.get("text", "")} for link in links if valid_audio_link(link)]
- return filtered_links
-
-
-def valid_image_link(link):
- valid_path = link.get("path", "") == "IMG@/src"
- valid_alt = len(link.get("alt", "")) > 0
- valid_http = link.get("url", "").startswith("http")
- return valid_path and valid_http and valid_alt
-
-
-def extract_image_from_links(links):
- """Extract image from links"""
- filtered_links = [{"url": link["url"], "alt": link["alt"]} for link in links if valid_image_link(link)]
- return filtered_links
-
-
-def extract_documents_from_links(links, document_type):
- """Extract documents from links ; this function returns a list of dict {"alt": ..., "url": ...}"""
-
- if document_type == "image":
- return extract_image_from_links(links)
- elif document_type == "audio":
- return extract_audio_from_links(links)
- elif document_type == "text":
- return extract_text_from_links(links)
- elif document_type == "video":
- return extract_video_from_links(links)
- else:
- raise ValueError(f"Unknown document type {document_type}")
-
-
-def extract_documents_from_wat(stream, document_type):
- """Extract document from stream"""
- all_links = []
- try:
- for record in ArchiveIterator(stream, record_types=WarcRecordType.metadata, parse_http=False):
- try:
- record_data = simdjson.load(record.reader) # type: ignore
- except: # pylint: disable=bare-except
- logger.info("A shard record failed")
- continue
- envelope = record_data["Envelope"]
- payload = envelope["Payload-Metadata"]
- if "HTTP-Response-Metadata" not in payload:
- continue
- http_resp = payload["HTTP-Response-Metadata"]
- if "HTML-Metadata" not in http_resp:
- continue
- metadata = http_resp["HTML-Metadata"]
- if "Links" not in metadata:
- continue
-
- links = metadata["Links"]
-
- filtered_links = extract_documents_from_links(links, document_type)
- for link in filtered_links:
- link["uid"] = str(hashlib.md5((link["alt"] + link["url"]).encode()).hexdigest())
- all_links.extend(filtered_links)
- except Exception as e: # pylint: disable=broad-except
- logger.info(e)
- logger.info("A shard failed to parse")
- return []
-
- return all_links
-
-
-def process_wat(path, document_type):
- """Process a single wat file"""
- begin_read = timer()
- with fsspec.open(path, "rb") as f:
- for i in range(10):
- try:
- tf = BytesIO(f.read())
- break
- except Exception as ex: # pylint: disable=broad-except
- if i == 9:
- logger.info("failed 10 times, skipping ", path)
- return
- logger.info(ex)
- logger.info(f"retrying reading {i}/10")
- time.sleep(1)
-
- for e in extract_documents_from_wat(tf, document_type):
- yield (e["uid"], e["url"], e["alt"])
- end_read = timer()
- tot_read_time = end_read - begin_read
- logger.info(f"Took {tot_read_time} to parse")
-
-
-def get_cc_wat_links(source_cc_protocol):
- """Get cc wat links"""
- if source_cc_protocol == "s3":
- fs, p = fsspec.core.url_to_fs("s3://commoncrawl/crawl-data/")
- links = ["s3://" + e for e in fs.glob(p + "/*/wat.paths.gz")]
- return links
- elif source_cc_protocol == "http":
- fs, p = fsspec.core.url_to_fs("https://commoncrawl.org/the-data/get-started/")
- a = fs.open(p).read()
- l = a.splitlines()
- l = [e.decode("utf8").replace("[WARC] ", "") for e in l]
- l = [e for e in l if "s3://commoncrawl/crawl-data/" in e]
- l = [
- e.split(" ")[0].replace("s3://commoncrawl/", "https://data.commoncrawl.org/").replace("", "")
- for e in l
- ]
- l = [(e + "/wat.paths.gz").replace("//wat", "/wat") for e in l]
- return l
- else:
- raise ValueError(f"Unknown protocol {source_cc_protocol}")
-
-
-def read_wat_index_file(wat_index):
- with fsspec.open(wat_index, "rb", compression="gzip") as f:
- wats = [a.decode("utf8").strip() for a in f.readlines()]
- return wats
-
-
-def read_wat_index_files(shard_count, wat_count, source_cc_protocol):
- """Read all wat index files"""
- cc_wat_links = get_cc_wat_links(source_cc_protocol)
- if shard_count is not None:
- cc_wat_links = cc_wat_links[-shard_count:] # pylint: disable=invalid-unary-operand-type
- all_wats = []
- with ThreadPool(16) as pool:
- for wats in pool.imap_unordered(read_wat_index_file, cc_wat_links):
- all_wats.extend(wats)
- if wat_count is not None:
- all_wats = random.choices(all_wats, k=wat_count)
- else:
- # shuffle to increase duplication over each part hence reduce size of each part after duplication
- random.shuffle(all_wats)
- return all_wats
+from .wat_utils import process_wat
+from .warc_utils import process_warc
+from .index_utils import read_index_files
+
+
def deduplicate_repartition_count(df, output_path, wat_count, spark, shuffle=False):
@@ -227,25 +38,41 @@ def deduplicate_repartition_count(df, output_path, wat_count, spark, shuffle=Fal
logger.info(f"Size: {df.count()}")
-def process_one_part(output_path, wat_index_files, build_spark, shuffle, document_type, source_cc_protocol):
+def process_one_part(output_path, cc_index_files, build_spark, shuffle, document_type, source_cc_protocol,ccfile):
"""Process one part"""
spark = build_spark()
sc = SparkContext.getOrCreate()
- wat_count = len(wat_index_files)
- wat_rdd = sc.parallelize(wat_index_files, wat_count)
+ ccfile_count = len(cc_index_files)
+ wat_rdd = sc.parallelize(cc_index_files, ccfile_count)
+
if source_cc_protocol == "s3":
prefix = "s3://commoncrawl/"
elif source_cc_protocol == "http":
prefix = "https://data.commoncrawl.org/"
- def extract(x):
- x = list(x)
- yield from process_wat(prefix + x[0], document_type)
+ if ccfile == "warc":
+ def extract(x):
+ x = list(x)
+ yield from process_warc(prefix + x[0])
+
+ elif ccfile == "wat":
+ def extract(x):
+ x = list(x)
+ yield from process_wat(prefix + x[0], document_type)
+
+ elif ccfile == "wet":
+ def extract(x):
+ x = list(x)
+ yield from process_wet(prefix + x[0])
+ else:
+ raise ValueError(f"Unknown ccfile: {ccfile}")
+
output = wat_rdd.mapPartitions(extract)
- df = output.toDF(["uid", "url", "alt"])
+ # e["uid"], e["url"], e["text"],e['lang'],e['license'],e['perplexity']
+ df = output.toDF(["uid", "url", "text","lang","license","fwords","cratio","crep","swords","wnum","perplexity"])
- deduplicate_repartition_count(df, output_path, wat_count, spark, shuffle)
+ deduplicate_repartition_count(df, output_path, ccfile_count, spark, shuffle)
def get_last_successful_part(output_path):
@@ -258,7 +85,7 @@ def get_last_successful_part(output_path):
def process_multi_part(
- output_path, wat_index_files, build_spark, multipart, shuffle, resume, document_type, source_cc_protocol
+ output_path, cc_index_files, build_spark, multipart, shuffle, resume, document_type, source_cc_protocol,ccfile
):
"""Process multi part"""
if resume:
@@ -266,16 +93,16 @@ def process_multi_part(
else:
start_part = 0
- wat_count = len(wat_index_files)
- wat_per_part = math.ceil(wat_count / multipart)
+ ccfile_count = len(cc_index_files)
+ ccfile_per_part = math.ceil(ccfile_count / multipart)
part_paths = []
for i in range(start_part, multipart):
- start = i * wat_per_part
- end = (i + 1) * wat_per_part
+ start = i * ccfile_per_part
+ end = (i + 1) * ccfile_per_part
part_path = f"{output_path}/part_{i}"
part_paths.append(part_path)
logger.info(f"Processing part {i} from {start} to {end} into {part_path}")
- process_one_part(part_path, wat_index_files[start:end], build_spark, False, document_type, source_cc_protocol)
+ process_one_part(part_path, cc_index_files[start:end], build_spark, False, document_type, source_cc_protocol,ccfile)
spark = build_spark()
logger.info("Merging parts")
@@ -287,7 +114,7 @@ def process_multi_part(
else:
df = df.union(spark.read.parquet(part_path))
- deduplicate_repartition_count(df, output_path + "/merged", wat_count, spark, shuffle)
+ deduplicate_repartition_count(df, output_path + "/merged", ccfile_count, spark, shuffle)
def get_date_str():
@@ -296,8 +123,8 @@ def get_date_str():
def cc2dataset(
output_path,
- wat_index_count=1,
- wat_count=100,
+ crawl_index_count=1,
+ files_count=100,
master="local",
num_cores=128,
mem_gb=256,
@@ -307,6 +134,8 @@ def cc2dataset(
spark_builder=None,
document_type="image",
source_cc_protocol="s3",
+ ccfile="wat",
+ crawl_index_list=None,
):
"""Convert common crawl to image caption set"""
@@ -332,20 +161,19 @@ def build_spark():
return spark_builder()
if resume is None:
- wat_index_files = read_wat_index_files(wat_index_count, wat_count, source_cc_protocol)
- # write wat index files to disk in output_path with fsspec
- with fsspec.open(f"{output_path}/wat_index_files.txt", "w", encoding="utf8") as f:
- f.write("\n".join(wat_index_files))
+ cc_index_files = read_index_files(crawl_index_count, files_count, source_cc_protocol, ccfile, crawl_index_list, shuffle )
+ # write ccfile index files to disk in output_path with fsspec
+ with fsspec.open(f"{output_path}/crawl_index_files.txt", "w", encoding="utf8") as f:
+ f.write("\n".join(cc_index_files))
else:
- with fsspec.open(f"{output_path}/wat_index_files.txt", "r", encoding="utf8") as f:
- wat_index_files = f.read().splitlines()
+ with fsspec.open(f"{output_path}/crawl_index_files.txt", "r", encoding="utf8") as f:
+ cc_index_files = f.read().splitlines()
if multipart is None:
- process_one_part(output_path, wat_index_files, build_spark, shuffle, document_type, source_cc_protocol)
+ process_one_part(output_path, cc_index_files, build_spark, shuffle, document_type, source_cc_protocol,ccfile)
else:
process_multi_part(
- output_path, wat_index_files, build_spark, multipart, shuffle, resume, document_type, source_cc_protocol
- )
+ output_path, cc_index_files, build_spark, multipart, shuffle, resume, document_type, source_cc_protocol, ccfile)
def main():
diff --git a/cc2dataset/warc_utils.py b/cc2dataset/warc_utils.py
new file mode 100644
index 0000000..5e0240e
--- /dev/null
+++ b/cc2dataset/warc_utils.py
@@ -0,0 +1,113 @@
+from fastwarc import ArchiveIterator
+import hashlib
+import fsspec
+from resiliparse.parse import detect_encoding
+from resiliparse.parse.html import HTMLTree
+from resiliparse.extract.html2text import extract_plain_text
+from io import BytesIO
+from timeit import default_timer as timer
+from .lang_utils import LangDetection,detect_licence
+from loguru import logger
+from squeakily.filter import (
+ check_char_repetition,
+ check_flagged_words,
+ check_stop_word_ratio,
+ check_compression_ratio,
+ check_word_number,
+ check_perplexity,
+)
+
+from squeakily.helpers import KenlmModel
+
+model = KenlmModel.from_pretrained(
+ model_dataset="wikipedia",
+ language="en",
+ lower_case=True,
+ remove_accents=True,
+ normalize_numbers=True,
+ punctuation=1,
+)
+
+#check_perplexity(low_test_str, model=model,dry_run=True)
+
+def extract_documents_from_warc(stream):
+ """Extract document from stream"""
+ all_extend = []
+ # download https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin and store to a path
+ lang_model_path = "/home/harrysaini/cc2dataset/assets/lid.176.bin"
+ detector = LangDetection(lang_model_path)
+ try:
+ for idx, record in enumerate(ArchiveIterator(stream, max_content_length=4 * 1024**2)):
+ try:
+ if record.headers is None:
+ continue
+ if record.http_headers is None:
+ continue
+ if (
+ record.headers["WARC-Type"] == "response"
+ and record.content_length >= 128
+ ):
+ content_type = str(record.http_content_type).lower()
+ if content_type.startswith("text/html"):
+ url = str(record.headers["WARC-Target-URI"])
+ html_bytes = record.reader.read()
+ encoding = detect_encoding(html_bytes)
+ licence = detect_licence(str(html_bytes))
+ tree = HTMLTree.parse_from_bytes(html_bytes, encoding)
+
+ for ele in tree.body.get_elements_by_tag_name("nav"):
+ ele.parent.remove_child(ele)
+
+
+ text = extract_plain_text(tree, preserve_formatting=False,
+ main_content=False, list_bullets=False,
+ alt_texts=True, links=False,
+ form_fields=False, noscript=False)
+ text = text.replace("\n", " ").replace("\t", " ").replace("\r", " ")
+
+ cre=dict()
+ cre["text"] = text
+ cre["url"] = url
+ cre["uid"] = str(hashlib.md5((text+url).encode()).hexdigest())
+ cre["lang"],_ = detector.detect(text)
+ cre['license'] = licence
+ cre['fwords'] = check_flagged_words(text,dry_run=True)
+ cre['cratio'] = check_compression_ratio(text,dry_run=True)
+ cre['crep'] = check_char_repetition(text,dry_run=True)
+ cre['swords'] = check_stop_word_ratio(text,dry_run=True)
+ cre['wnum'] = check_word_number(text,dry_run=True)
+ cre['perplexity'] = check_perplexity(text, model=model,dry_run=True)
+
+ #cre.update(permodel(text, prefix="perplexity/"))
+ all_extend.append(cre)
+ except:
+ continue
+
+ except Exception as e: # pylint: disable=broad-except
+ logger.info(e)
+ logger.info("A shard failed to parse")
+ return []
+
+ return all_extend
+
+def process_warc(path):
+ """Process a single warc file"""
+ begin_read = timer()
+ with fsspec.open(path, "rb") as f:
+ for i in range(10):
+ try:
+ tf = BytesIO(f.read())
+ break
+ except Exception as ex: # pylint: disable=broad-except
+ if i == 9:
+ logger.info("failed 10 times, skipping ", path)
+ return
+ logger.info(ex)
+ logger.info(f"retrying reading {i}/10")
+ time.sleep(1)
+
+ for e in extract_documents_from_warc(tf):
+ yield (e["uid"], e["url"], e["text"], e["lang"], e["license"])
+ end_read = timer()
+ tot_read_time = end_read - begin_read
+ logger.info(f"Took {tot_read_time} to parse")
diff --git a/cc2dataset/wat_utils.py b/cc2dataset/wat_utils.py
new file mode 100644
index 0000000..10dd34d
--- /dev/null
+++ b/cc2dataset/wat_utils.py
@@ -0,0 +1,154 @@
+import hashlib
+from fastwarc.warc import ArchiveIterator, WarcRecordType
+import simdjson
+import fsspec
+from timeit import default_timer as timer
+from loguru import logger
+from io import BytesIO
+
+def valid_video_link(link):
+ valid_http = link.get("url", "").startswith("http")
+ valid_video = any(
+ link.get("url", "").endswith(ext) for ext in [".avi", ".mp4", ".mkv", ".webm", ".mov", ".mpg", ".mpeg", ".m4v"]
+ )
+ return valid_http and valid_video
+
+
+def extract_video_from_links(links):
+ filtered_links = [{"url": link["url"], "alt": link.get("text", "")} for link in links if valid_video_link(link)]
+ return filtered_links
+
+
+text_extensions = set(
+ [
+ "pdf",
+ "epub",
+ "djvu",
+ "mobi",
+ "doc",
+ "docx",
+ "rtf",
+ "txt",
+ "odt",
+ "ppt",
+ "pptx",
+ "pages",
+ "keynote",
+ "wps",
+ "md",
+ ]
+)
+
+
+def valid_text_link(link):
+ if not link.get("url", "").startswith("http"):
+ return False
+ splits = link.get("url", "").split(".")
+ if len(splits) < 2:
+ return False
+ if splits[-1] not in text_extensions:
+ return False
+ return True
+
+
+def extract_text_from_links(links):
+ filtered_links = [{"url": link["url"], "alt": link.get("text", "")} for link in links if valid_text_link(link)]
+ return filtered_links
+
+
+def valid_audio_link(link):
+ valid_http = link.get("url", "").startswith("http")
+ valid_audio = any(link.get("url", "").endswith(ext) for ext in [".ogg", ".wav", ".mp3", ".flac", ".m4a"])
+ return valid_http and valid_audio
+
+
+def extract_audio_from_links(links):
+ """Extract image from links"""
+ filtered_links = [{"url": link["url"], "alt": link.get("text", "")} for link in links if valid_audio_link(link)]
+ return filtered_links
+
+
+def valid_image_link(link):
+ valid_path = link.get("path", "") == "IMG@/src"
+ valid_alt = len(link.get("alt", "")) > 0
+ valid_http = link.get("url", "").startswith("http")
+ return valid_path and valid_http and valid_alt
+
+
+def extract_image_from_links(links):
+ """Extract image from links"""
+ filtered_links = [{"url": link["url"], "alt": link["alt"]} for link in links if valid_image_link(link)]
+ return filtered_links
+
+
+def extract_documents_from_links(links, document_type):
+ """Extract documents from links ; this function returns a list of dict {"alt": ..., "url": ...}"""
+
+ if document_type == "image":
+ return extract_image_from_links(links)
+ elif document_type == "audio":
+ return extract_audio_from_links(links)
+ elif document_type == "text":
+ return extract_text_from_links(links)
+ elif document_type == "video":
+ return extract_video_from_links(links)
+ else:
+ raise ValueError(f"Unknown document type {document_type}")
+
+
+def extract_documents_from_wat(stream, document_type):
+ """Extract document from stream"""
+ all_links = []
+ try:
+ for record in ArchiveIterator(stream, record_types=WarcRecordType.metadata, parse_http=False):
+ try:
+ record_data = simdjson.load(record.reader) # type: ignore
+ except: # pylint: disable=bare-except
+ logger.info("A shard record failed")
+ continue
+ envelope = record_data["Envelope"]
+ payload = envelope["Payload-Metadata"]
+ if "HTTP-Response-Metadata" not in payload:
+ continue
+ http_resp = payload["HTTP-Response-Metadata"]
+ if "HTML-Metadata" not in http_resp:
+ continue
+ metadata = http_resp["HTML-Metadata"]
+ if "Links" not in metadata:
+ continue
+
+ links = metadata["Links"]
+
+ filtered_links = extract_documents_from_links(links, document_type)
+ for link in filtered_links:
+ link["uid"] = str(hashlib.md5((link["alt"] + link["url"]).encode()).hexdigest())
+ all_links.extend(filtered_links)
+ except Exception as e: # pylint: disable=broad-except
+ logger.info(e)
+ logger.info("A shard failed to parse")
+ return []
+
+ return all_links
+
+def process_wat(path, document_type):
+ """Process a single wat file"""
+ begin_read = timer()
+ with fsspec.open(path, "rb") as f:
+ for i in range(10):
+ try:
+ tf = BytesIO(f.read())
+ break
+ except Exception as ex: # pylint: disable=broad-except
+ if i == 9:
+ logger.info("failed 10 times, skipping ", path)
+ return
+ logger.info(ex)
+ logger.info(f"retrying reading {i}/10")
+ time.sleep(1)
+
+ for e in extract_documents_from_wat(tf, document_type):
+ yield (e["uid"], e["url"], e["alt"])
+ end_read = timer()
+ tot_read_time = end_read - begin_read
+ logger.info(f"Took {tot_read_time} to parse")
+
diff --git a/examples/single_warc.py b/examples/single_warc.py
new file mode 100644
index 0000000..5ded9a8
--- /dev/null
+++ b/examples/single_warc.py
@@ -0,0 +1,16 @@
+from cc2dataset import process_warc
+import os
+import pandas as pd
+
+if __name__ == "__main__":
+ from_s3 = True
+ wurl = "crawl-data/CC-MAIN-2022-33/segments/1659882570651.49/warc/CC-MAIN-20220807150925-20220807180925-00000.warc.gz"
+ if from_s3:
+ url = "s3://commoncrawl/" + wurl
+ else:
+ url = "https://data.commoncrawl.org/" + wurl
+
+ results = process_warc(url)
+ df = pd.DataFrame(results, columns=["uid", "url", "text",'lang','license'])
+ df.to_parquet(os.getcwd() + "/output.parquet")
+ print(df)
diff --git a/examples/single_warc_example.py b/examples/single_wat_example.py
similarity index 100%
rename from examples/single_warc_example.py
rename to examples/single_wat_example.py
diff --git a/requirements.txt b/requirements.txt
index 6f00d62..c9dd112 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,4 +7,10 @@ pyarrow
fastwarc
s3fs
fire
-requests
\ No newline at end of file
+requests
+squeakily
+spacy
+spacy_fastlang
+resiliparse
+pip install https://github.com/kpu/kenlm/archive/master.zip sentencepiece
+pip install git+https://github.com/CarperAI/squeakily.git
\ No newline at end of file
diff --git a/tests/test_kenlm.py b/tests/test_kenlm.py
new file mode 100644
index 0000000..6980bb9
--- /dev/null
+++ b/tests/test_kenlm.py
@@ -0,0 +1,17 @@
+
+from os import environ
+import pytest
+
+from cc2dataset import kenlm as mod
+
+DEFAULT_CACHEDIR = '/tmp'
+
+
+def test_perplexity_scorer():
+ cache_dir = environ.get("TMPDIR", DEFAULT_CACHEDIR)
+ model = mod.PerplexityScorer(cache_dir=cache_dir)
+ result = model("this is an example", prefix="perp/")
+ print(result)
+ assert list(result) == ['perp/ccnet/wikipedia', 'perp/ontocord/riverbed_kenlm']
+ assert result['perp/ccnet/wikipedia'] == pytest.approx(5394.3)
+ assert result['perp/ontocord/riverbed_kenlm'] == pytest.approx(30.2)
diff --git a/tests/test_newmain.py b/tests/test_newmain.py
new file mode 100644
index 0000000..c3c73d5
--- /dev/null
+++ b/tests/test_newmain.py
@@ -0,0 +1,26 @@
+import pytest
+from cc2dataset import cc2dataset
+from cc2dataset.index_utils import get_cc_links
+import os
+import pandas as pd
+import tempfile
+from glob import glob
+
+
+def test_main():
+ with tempfile.TemporaryDirectory() as tmpdir:
+ cc2dataset(
+ tmpdir,
+ crawl_index_count=None,
+ files_count=1,
+ master="local",
+ num_cores=1,
+ mem_gb=2,
+ multipart=None,
+ source_cc_protocol="s3",
+ shuffle=False,ccfile='warc',crawl_index_list=[-3]
+ )
+ files = list(glob(os.path.join(tmpdir, "*/*.parquet")))
+ assert len(files) == 256
+ df = pd.read_parquet(files)
+ assert len(df) > 100