mirror of
https://forge.chapril.org/tykayn/orgmode-to-gemini-blog
synced 2025-06-20 09:04:42 +02:00
add quality control for links in a blog source
This commit is contained in:
parent
7b12ef533b
commit
22285e44ae
3 changed files with 424 additions and 8 deletions
32
README.md
32
README.md
|
@ -97,28 +97,43 @@ https://forge.chapril.org/tykayn/org-report-stats
|
|||
|
||||
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
|
||||
- réécriture des liens internes
|
||||
- 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
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
|
||||
- vérifier que les pages non articles sont bien générées
|
||||
- vérifier que les flux Atom sont valides
|
||||
- documenter les scripts
|
||||
- find_correspondances.py
|
||||
- atom_generate.py
|
||||
- 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
|
||||
- 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
|
||||
|
@ -130,5 +145,6 @@ https://forge.chapril.org/tykayn/org-report-stats
|
|||
- génération de fichiers gmi
|
||||
- config par site web de son nom de domaine
|
||||
- navigation sur les pages d'article
|
||||
- gestion des langues dans la source et la destination
|
||||
- gestion multi site et multi langue
|
||||
- gestion des langues dans la source et la destination
|
||||
- gestion multi site et multi langue
|
||||
- - vérifier que les flux Atom sont valides
|
226
check_links.py
Normal file
226
check_links.py
Normal 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
174
scan_links.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue