add quality control for links in a blog source

This commit is contained in:
Tykayn 2025-03-27 13:37:58 +01:00 committed by tykayn
parent 7b12ef533b
commit 22285e44ae
3 changed files with 424 additions and 8 deletions

View file

@ -97,28 +97,43 @@ https://forge.chapril.org/tykayn/org-report-stats
https://github.com/njamescouk/pandocGmi/tree/master https://github.com/njamescouk/pandocGmi/tree/master
# Contrôle qualité
## Examen des liens morts
Lister tous les liens dans les fichiers org des sources d'un blog:
```shell
py scan_links.py cipherbliss_blog
```
Scanne le dossier et donne un fichier json donnant les noms de domaines trouvés, ainsi que la liste des liens par article dans `links_report_cipherbliss_blog.json` .
Ensuite, on peut tester la viabilité des liens donnés avec check_links.py:
```shell
py check_links.py cipherbliss_blog
```
# Roadmap # Roadmap
- réécriture des liens internes - réécriture des liens internes
- conversion des liens avec nom de domaine si relatifs - conversion des liens avec nom de domaine si relatifs
- détection des ID org-roam pour réécrire les liens html - détection des ID org-roam pour réécrire les liens html lors de la génération de site web et capsule gemini.
- réécriture des url des images vers le dossier courant - réécriture des url des images vers le dossier courant
- gestion des séries d'articles avec un tag orgmode #+serie, ce qui crée des indexes de séries et précise les autres posts de la série en fin d'article - gestion des séries d'articles avec un tag orgmode #+serie, ce qui crée des indexes de séries et précise les autres posts de la série en fin d'article
- page pour un tag listant les articles, trier par date décroissante - page pour un tag listant les articles, trier par date décroissante
- les gains de performance pour ne pas régénérer les pages déjà faites alors qu'elles n'ont pas été modifiée, et un rendu statique un peu plus joli - les gains de performance pour ne pas régénérer les pages déjà faites alors qu'elles n'ont pas été modifiée, et un rendu statique un peu plus joli
- mettre un lien vers le fichier Org d'origine en fin d'article, disponible sur une forge en ligne si on l'a mis en config du site web. - mettre un lien vers le fichier Org d'origine en fin d'article, disponible sur une forge en ligne si on l'a mis en config du site web.
- vérifier que les pages non articles sont bien générées - vérifier que les pages non articles sont bien générées
- vérifier que les flux Atom sont valides
- documenter les scripts - documenter les scripts
- find_correspondances.py - find_correspondances.py
- atom_generate.py - atom_generate.py
- deploy.sh - deploy.sh
- back_files_to_roam.sh - back_files_to_roam.sh, syncroniser les fichiers orgmode sources vers le dossier org-roam. Org-Roam ne permet que de gérer un seul dossier.
# Fait ## Fait
- en fin d'article, mettre le texte incitant au soutien de l'auteur - en fin d'article, mettre le texte incitant au soutien de l'auteur
- faire la conversion en page gemini dans `linking_articles_prev_next.py` - faire la conversion en page gemini dans `linking_articles_prev_next.py`
- liste de N derniers articles développés sur l'accueil, 10 par défaut - liste de N derniers articles développés sur l'accueil, 10 par défaut
@ -130,5 +145,6 @@ https://forge.chapril.org/tykayn/org-report-stats
- génération de fichiers gmi - génération de fichiers gmi
- config par site web de son nom de domaine - config par site web de son nom de domaine
- navigation sur les pages d'article - navigation sur les pages d'article
-gestion des langues dans la source et la destination - gestion des langues dans la source et la destination
- gestion multi site et multi langue - gestion multi site et multi langue
- - vérifier que les flux Atom sont valides

226
check_links.py Normal file
View file

@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
Vérifie l'accessibilité des liens trouvés dans le JSON généré par scan_links.py.
Génère un rapport des domaines inaccessibles et des articles comportant ces liens.
"""
import os
import json
import argparse
import requests
import time
from datetime import datetime
from urllib.parse import urlparse
from concurrent.futures import ThreadPoolExecutor
# Limiter les requêtes pour ne pas surcharger les serveurs
REQUEST_DELAY = 0.5 # Délai entre les requêtes vers le même domaine (en secondes)
MAX_WORKERS = 10 # Nombre maximum de threads parallèles
# User agent pour éviter d'être bloqué
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'
}
def check_url(url):
"""Vérifie si une URL est accessible"""
try:
response = requests.head(url, headers=HEADERS, timeout=10, allow_redirects=True)
# Si HEAD échoue, essayer avec GET (certains serveurs n'acceptent pas HEAD)
if response.status_code >= 400:
response = requests.get(url, headers=HEADERS, timeout=10, allow_redirects=True, stream=True)
# Fermer immédiatement la connexion pour éviter de télécharger le contenu entier
response.close()
# Considérer comme réussi les codes 2xx et 3xx
return {
'url': url,
'status': response.status_code,
'accessible': response.status_code < 400,
'error': None
}
except requests.exceptions.RequestException as e:
return {
'url': url,
'status': None,
'accessible': False,
'error': str(e)
}
def main():
parser = argparse.ArgumentParser(description="Vérifier les liens des fichiers Org")
parser.add_argument("json_file", help="Fichier JSON à examiner, généré par scan_links.py")
parser.add_argument("--output_dir", default="link_checker_output", help="Dossier de sortie pour les rapports")
args = parser.parse_args()
json_links_report = 'links_report_'+ args.json_file + '.json'
if not os.path.exists(json_links_report):
print(f"Le fichier {json_links_report} n'existe pas.")
return
# Créer le dossier de sortie
os.makedirs(args.output_dir, exist_ok=True)
# Charger les données JSON
with open(json_links_report, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"Vérification des liens depuis {json_links_report}...")
# Collecter toutes les URLs uniques par domaine
domains_to_urls = {}
url_to_articles = {}
for article, links in data['article_links'].items():
for link in links:
url = link['url']
# S'assurer que l'URL commence par http:// ou https://
if not url.startswith(('http://', 'https://')):
if url.startswith('www.'):
url = 'https://' + url
else:
# Ignorer les liens qui ne sont pas des URLs Web
continue
# Ajouter l'URL au dictionnaire des domaines
domain = urlparse(url).netloc
if domain not in domains_to_urls:
domains_to_urls[domain] = set()
domains_to_urls[domain].add(url)
# Enregistrer les articles qui utilisent cette URL
if url not in url_to_articles:
url_to_articles[url] = []
url_to_articles[url].append(article)
print(f"Vérification de {len(url_to_articles)} URLs uniques sur {len(domains_to_urls)} domaines...")
# Vérifier les URLs avec un ThreadPoolExecutor
urls_to_check = list(url_to_articles.keys())
results = {}
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_url = {executor.submit(check_url, url): url for url in urls_to_check}
for i, future in enumerate(future_to_url):
url = future_to_url[future]
try:
result = future.result()
results[url] = result
# Afficher la progression
if (i + 1) % 10 == 0 or i + 1 == len(urls_to_check):
print(f"Progression: {i + 1}/{len(urls_to_check)} URLs vérifiées")
# Respecter le délai entre les requêtes pour le même domaine
domain = urlparse(url).netloc
time.sleep(REQUEST_DELAY)
except Exception as e:
results[url] = {
'url': url,
'status': None,
'accessible': False,
'error': str(e)
}
# Identifier les URLs inaccessibles
inaccessible_urls = [url for url, result in results.items() if not result['accessible']]
# Collecter les articles avec des liens inaccessibles
articles_with_broken_links = {}
for url in inaccessible_urls:
for article in url_to_articles[url]:
if article not in articles_with_broken_links:
articles_with_broken_links[article] = []
# Trouver le lien original avec sa description
original_links = [link for link in data['article_links'][article] if link['url'] == url]
for link in original_links:
articles_with_broken_links[article].append({
'url': url,
'description': link['description'],
'status': results[url]['status'],
'error': results[url]['error']
})
# Générer les rapports
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# 1. Rapport JSON détaillé
report_data = {
'meta': {
'generated_at': datetime.now().isoformat(),
'source_file': json_links_report,
'total_urls_checked': len(results),
'total_inaccessible_urls': len(inaccessible_urls),
'total_articles_with_broken_links': len(articles_with_broken_links)
},
'inaccessible_domains': {},
'articles_with_broken_links': articles_with_broken_links
}
# Regrouper par domaine
for url in inaccessible_urls:
domain = urlparse(url).netloc
if domain not in report_data['inaccessible_domains']:
report_data['inaccessible_domains'][domain] = []
report_data['inaccessible_domains'][domain].append({
'url': url,
'status': results[url]['status'],
'error': results[url]['error'],
'articles': url_to_articles[url]
})
# Sauvegarder le rapport JSON
json_report_path = os.path.join(args.output_dir, f'broken_links_report_{args.json_file}_{timestamp}.json')
with open(json_report_path, 'w', encoding='utf-8') as f:
json.dump(report_data, f, ensure_ascii=False, indent=2)
# 2. Rapport texte des domaines inaccessibles
domains_report_path = os.path.join(args.output_dir, f'inaccessible_domains_{args.json_file}_{timestamp}.txt')
with open(domains_report_path, 'w', encoding='utf-8') as f:
f.write(f"DOMAINES INACCESSIBLES ({len(report_data['inaccessible_domains'])}):\n")
f.write("=" * 80 + "\n\n")
for domain, urls in report_data['inaccessible_domains'].items():
f.write(f"{domain} ({len(urls)} URLs):\n")
for url_data in urls:
status = f"Status: {url_data['status']}" if url_data['status'] else ""
error = f"Error: {url_data['error']}" if url_data['error'] else ""
f.write(f" - {url_data['url']} {status} {error}\n")
f.write("\n")
# 3. Rapport des liens à changer dans chaque article
articles_report_path = os.path.join(args.output_dir, f'articles_with_broken_links_{timestamp}.txt')
with open(articles_report_path, 'w', encoding='utf-8') as f:
f.write(f"ARTICLES AVEC LIENS CASSÉS ({len(articles_with_broken_links)}):\n")
f.write("=" * 80 + "\n\n")
for article, links in articles_with_broken_links.items():
f.write(f"Fichier: {article}\n")
f.write("-" * 40 + "\n")
for link in links:
description = f" ({link['description']})" if link['description'] else ""
status = f"Status: {link['status']}" if link['status'] else ""
error = f"Error: {link['error']}" if link['error'] else ""
f.write(f" - {link['url']}{description} {status} {error}\n")
f.write("\n")
print("\nRapports générés:")
print(f"- Rapport JSON détaillé: {json_report_path}")
print(f"- Liste des domaines inaccessibles: {domains_report_path}")
print(f"- Liste des articles avec liens cassés: {articles_report_path}")
print(f"\nURLs vérifiées: {len(results)}")
print(f"URLs inaccessibles: {len(inaccessible_urls)}")
print(f"Articles avec liens cassés: {len(articles_with_broken_links)}")
if __name__ == "__main__":
main()

174
scan_links.py Normal file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Scanne les fichiers Org d'un dossier pour répertorier les liens.
Génère un JSON listant pour chaque article ses liens et statistiques par domaine.
"""
import os
import re
import json
import argparse
import urllib.parse
from datetime import datetime
from collections import Counter, defaultdict
def extract_links_from_org(file_path):
"""Extrait tous les liens d'un fichier Org"""
links = []
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Pattern pour trouver les liens Org-mode: [[url][description]] ou [[url]]
pattern = r'\[\[((?:https?:\/\/|www\.)[^\]]+)\](?:\[([^\]]*)\])?'
matches = re.finditer(pattern, content)
for match in matches:
url = match.group(1)
description = match.group(2) if match.group(2) else ""
links.append({"url": url, "description": description})
# Chercher aussi les liens simples http:// ou https:// qui ne sont pas dans la syntaxe [[]]
simple_pattern = r'(?<!\[)(?:https?:\/\/)[^\s\]]+(?!\])'
simple_matches = re.finditer(simple_pattern, content)
for match in simple_matches:
url = match.group(0)
links.append({"url": url, "description": ""})
except Exception as e:
print(f"Erreur lors de la lecture de {file_path}: {e}")
return links
def extract_domain(url):
"""Extrait le nom de domaine d'une URL"""
try:
parsed_url = urllib.parse.urlparse(url)
domain = parsed_url.netloc
# Supprimer www. si présent
if domain.startswith('www.'):
domain = domain[4:]
return domain
except Exception:
return url
def scan_directory(directory):
"""Scanne un répertoire et ses sous-répertoires pour trouver des fichiers Org"""
article_links = {}
domain_counter = Counter()
# Parcourir tous les sous-dossiers
for root, _, files in os.walk(directory):
for file in files:
if file.endswith('.org'):
file_path = os.path.join(root, file)
file_links = extract_links_from_org(file_path)
if file_links:
relative_path = os.path.relpath(file_path, os.path.dirname(directory))
article_links[relative_path] = file_links
# Compter les domaines
for link in file_links:
domain = extract_domain(link["url"])
if domain:
domain_counter[domain] += 1
print(f"Trouvé {len(file_links)} liens dans {relative_path}")
return article_links, domain_counter
def merge_scan_results(results1, results2):
"""Combine les résultats de deux scans"""
merged_article_links = {**results1[0], **results2[0]}
merged_domain_counter = results1[1] + results2[1]
return merged_article_links, merged_domain_counter
def main():
parser = argparse.ArgumentParser(description="Scanner les liens dans les fichiers Org")
parser.add_argument("source_dir", help="Dossier source contenant les fichiers Org")
parser.add_argument("--output", default="links_report.json",
help="Fichier JSON de sortie (défaut: links_report.json)")
args = parser.parse_args()
# Construire le nom du fichier de sortie avec le nom du dossier source
if args.output == "links_report.json":
args.output = f"links_report_{args.source_dir}.json"
folder_fr = f'sources/{args.source_dir}/lang_fr'
folder_en = f'sources/{args.source_dir}/lang_en'
if not os.path.exists(folder_fr) and not os.path.exists(folder_en):
print(f"Les dossiers {folder_fr} et {folder_en} n'existent pas.")
return
# Initialiser des résultats vides
combined_article_links = {}
combined_domain_counter = Counter()
# Scanner le dossier français s'il existe
if os.path.exists(folder_fr):
print(f"Scan des liens dans {folder_fr}...")
fr_results = scan_directory(folder_fr)
combined_article_links, combined_domain_counter = fr_results
print(f"Trouvé {len(combined_article_links)} articles en français avec {sum(len(links) for links in combined_article_links.values())} liens")
# Scanner le dossier anglais s'il existe
if os.path.exists(folder_en):
print(f"Scan des liens dans {folder_en}...")
en_results = scan_directory(folder_en)
# Si nous avons déjà des résultats en français, les combiner
if combined_article_links:
combined_article_links, combined_domain_counter = merge_scan_results(
(combined_article_links, combined_domain_counter),
en_results
)
else:
# Sinon, utiliser directement les résultats anglais
combined_article_links, combined_domain_counter = en_results
print(f"Total cumulé: {len(combined_article_links)} articles avec {sum(len(links) for links in combined_article_links.values())} liens")
# Préparer les données pour le JSON
output_data = {
"meta": {
"generated_at": datetime.now().isoformat(),
"source_directory": args.source_dir,
"total_articles": len(combined_article_links),
"total_links": sum(len(links) for links in combined_article_links.values()),
"total_domains": len(combined_domain_counter)
},
"article_links": combined_article_links,
"domains": {
domain: count for domain, count in sorted(
combined_domain_counter.items(), key=lambda x: x[1], reverse=True
)
}
}
# Créer le dossier de sortie si nécessaire
output_dir = os.path.dirname(args.output)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
# Sauvegarder les données dans un fichier JSON
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(output_data, f, ensure_ascii=False, indent=2)
print(f"Rapport généré avec succès dans {args.output}")
print(f"Total articles: {len(combined_article_links)}")
print(f"Total liens: {sum(len(links) for links in combined_article_links.values())}")
print(f"Total domaines: {len(combined_domain_counter)}")
# Afficher les 10 domaines les plus fréquents
print("\nTop 10 des domaines les plus fréquents:")
for domain, count in list(combined_domain_counter.most_common(10)):
print(f" {domain}: {count} liens")
if __name__ == "__main__":
main()