url.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. # coding=utf-8
  2. """
  3. URL 处理工具模块
  4. 提供 URL 标准化功能,用于去重时消除动态参数的影响:
  5. - normalize_url: 标准化 URL,去除动态参数
  6. """
  7. from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
  8. from typing import Dict, Set
  9. # 各平台需要移除的特定参数
  10. # - weibo: 有 band_rank(排名)和 Refer(来源)动态参数
  11. # - 其他平台: URL 为路径格式或简单关键词查询,无需处理
  12. PLATFORM_PARAMS_TO_REMOVE: Dict[str, Set[str]] = {
  13. # 微博:band_rank 是动态排名参数,Refer 是来源参数,t 是时间范围参数
  14. # 示例:https://s.weibo.com/weibo?q=xxx&t=31&band_rank=1&Refer=top
  15. # 保留:q(关键词)
  16. # 移除:band_rank, Refer, t
  17. "weibo": {"band_rank", "Refer", "t"},
  18. }
  19. # 通用追踪参数(适用于所有平台)
  20. # 这些参数通常由分享链接或广告追踪添加,不影响内容识别
  21. COMMON_TRACKING_PARAMS: Set[str] = {
  22. # UTM 追踪参数
  23. "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
  24. # 常见追踪参数
  25. "ref", "referrer", "source", "channel",
  26. # 时间戳和随机参数
  27. "_t", "timestamp", "_", "random",
  28. # 分享相关
  29. "share_token", "share_id", "share_from",
  30. }
  31. def normalize_url(url: str, platform_id: str = "") -> str:
  32. """
  33. 标准化 URL,去除动态参数
  34. 用于数据库去重,确保同一条新闻的不同 URL 变体能被正确识别为同一条。
  35. 处理规则:
  36. 1. 去除平台特定的动态参数(如微博的 band_rank)
  37. 2. 去除通用追踪参数(如 utm_*)
  38. 3. 保留核心查询参数(如搜索关键词 q=, wd=, keyword=)
  39. 4. 对查询参数按字母序排序(确保一致性)
  40. Args:
  41. url: 原始 URL
  42. platform_id: 平台 ID,用于应用平台特定规则
  43. Returns:
  44. 标准化后的 URL
  45. Examples:
  46. >>> normalize_url("https://s.weibo.com/weibo?q=test&band_rank=6&Refer=top", "weibo")
  47. 'https://s.weibo.com/weibo?q=test'
  48. >>> normalize_url("https://example.com/page?id=1&utm_source=twitter", "")
  49. 'https://example.com/page?id=1'
  50. """
  51. if not url:
  52. return url
  53. try:
  54. # 解析 URL
  55. parsed = urlparse(url)
  56. # 如果没有查询参数,直接返回
  57. if not parsed.query:
  58. return url
  59. # 解析查询参数
  60. params = parse_qs(parsed.query, keep_blank_values=True)
  61. # 收集需要移除的参数(使用小写进行比较)
  62. params_to_remove: Set[str] = set()
  63. # 添加通用追踪参数
  64. params_to_remove.update(COMMON_TRACKING_PARAMS)
  65. # 添加平台特定参数
  66. if platform_id and platform_id in PLATFORM_PARAMS_TO_REMOVE:
  67. params_to_remove.update(PLATFORM_PARAMS_TO_REMOVE[platform_id])
  68. # 过滤参数(参数名转小写进行比较)
  69. filtered_params = {
  70. key: values
  71. for key, values in params.items()
  72. if key.lower() not in {p.lower() for p in params_to_remove}
  73. }
  74. # 如果过滤后没有参数了,返回不带查询字符串的 URL
  75. if not filtered_params:
  76. return urlunparse((
  77. parsed.scheme,
  78. parsed.netloc,
  79. parsed.path,
  80. parsed.params,
  81. "", # 空查询字符串
  82. "" # 移除 fragment
  83. ))
  84. # 重建查询字符串(按字母序排序以确保一致性)
  85. sorted_params = []
  86. for key in sorted(filtered_params.keys()):
  87. for value in filtered_params[key]:
  88. sorted_params.append((key, value))
  89. new_query = urlencode(sorted_params)
  90. # 重建 URL(移除 fragment)
  91. normalized = urlunparse((
  92. parsed.scheme,
  93. parsed.netloc,
  94. parsed.path,
  95. parsed.params,
  96. new_query,
  97. "" # 移除 fragment
  98. ))
  99. return normalized
  100. except Exception:
  101. # 解析失败时返回原始 URL
  102. return url