db.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. """
  2. 数据库模块 — 与 main1.py 分离,独立保存采集数据
  3. 与美团共用同一 MySQL(drug_retrieve 库)和相同的两张表:
  4. - retrieve_scrape_data 商品数据主表(INSERT IGNORE,字段与美团 save_to_database 对齐)
  5. - retrieve_scrape_shop_info 店铺信息表(存在则补全缺失字段,不存在则插入)
  6. 饿了么平台标识:主表 platform_id='12',店铺表 platform=12
  7. (美团平台注释:0=全网 1=淘宝 2=京东 3=拼多多 4=美团 5=药师帮 6=1药城 7=药易购 8=药房网 9=药便宜 10=天猫药药 11=药易购)
  8. """
  9. import time
  10. import pymysql
  11. from area import AreaService
  12. DB_HOST = "120.24.26.108"
  13. DB_PORT = 3307
  14. DB_USER = "collect_user"
  15. DB_PASSWORD = "collect123456"
  16. DB_NAME = "drug_retrieve_test"
  17. PLATFORM_ID = "12" # 主表 platform_id(varchar)
  18. PLATFORM = 12 # 店铺表 platform(int)
  19. # 省市推断服务(与美团同款:area.py + city.json + addr_prefix.json)
  20. _area_service = None
  21. def _get_area_service():
  22. global _area_service
  23. if _area_service is None:
  24. _area_service = AreaService("city.json", "addr_prefix.json")
  25. return _area_service
  26. def _infer_region(address: str):
  27. """从地址推断省市(美团同款逻辑),返回 (province, city, province_id, city_id)"""
  28. if not address:
  29. return "", "", 0, 0
  30. try:
  31. m = _get_area_service().search_area(address)
  32. if m:
  33. return (m.province or ""), (m.city or ""), (m.province_id or 0), (m.city_id or 0)
  34. except Exception as e:
  35. print(f"[db] 省市推断失败: {e}")
  36. return "", "", 0, 0
  37. def get_conn():
  38. return pymysql.connect(
  39. host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD,
  40. database=DB_NAME, charset="utf8mb4", autocommit=False,
  41. )
  42. def _to_price(v) -> float:
  43. """价格字符串 → decimal(10,2) 兼容的浮点,非数字返回 0.0"""
  44. try:
  45. return float(str(v).replace("¥", "").replace("¥", "").strip())
  46. except (ValueError, TypeError):
  47. return 0.0
  48. def _today() -> str:
  49. return time.strftime("%Y-%m-%d")
  50. def _now() -> str:
  51. return time.strftime("%Y-%m-%d %H:%M:%S")
  52. def _license(record: dict) -> dict:
  53. """从采集记录中取营业执照 dict(容错)"""
  54. lic = record.get("license") or {}
  55. return lic if isinstance(lic, dict) else {}
  56. def save_to_database(record: dict) -> bool:
  57. """保存商品数据到主表 retrieve_scrape_data(与美团 save_to_database 完全一致的字段,未采集的留空/默认)"""
  58. shop = str(record.get("shop") or "").strip()
  59. lic = _license(record)
  60. company = str(lic.get("单位名称") or "").strip()
  61. address = str(lic.get("地址") or "").strip()
  62. qualification = str(record.get("license_no") or lic.get("社会信用代码") or "").strip()
  63. price = _to_price(record.get("price"))
  64. province, city, province_id, city_id = _infer_region(address)
  65. sql = """
  66. INSERT IGNORE INTO retrieve_scrape_data (
  67. enterprise_id, platform_id, platform_item_id, province_id, city_id,
  68. province_name, city_name, area_info, product_name, product_specs,
  69. one_box_price, manufacture_date, expiry_date, manufacturer, approval_number,
  70. is_sold_out, online_posting_count, continuous_listing_count, link_url,
  71. store_name, store_url, shipment_province_id, shipment_province_name,
  72. shipment_city_id, shipment_city_name, company_name, qualification_number,
  73. scrape_date, min_price, number, sales, inventory, snapshot_url,
  74. product_brand, search_name, insert_time, update_time, collect_config_info,
  75. collect_equipment_account_id, collect_region_id, collect_round,
  76. shop_id, company_id, task_id
  77. ) VALUES (
  78. %s, %s, %s, %s, %s,
  79. %s, %s, %s, %s, %s,
  80. %s, %s, %s, %s, %s,
  81. %s, %s, %s, %s,
  82. %s, %s, %s, %s,
  83. %s, %s, %s, %s,
  84. %s, %s, %s, %s, %s, %s,
  85. %s, %s, %s, %s, %s, %s, %s,
  86. %s, %s, %s, %s
  87. )
  88. """
  89. params = (
  90. int(record.get("enterprise_id") or 0), # enterprise_id(调度任务传入,手动模式为0)
  91. PLATFORM_ID, # platform_id
  92. # platform_item_id:主表唯一索引 uniq_product_key 含此列,留空会与同采集日其他商品冲突(只存1条),
  93. # 用「店铺名_商品标题」保证唯一,重复采集同商品自动去重
  94. f"{shop}_{record.get('title') or ''}"[:120],
  95. province_id, city_id, # province_id, city_id(执照地址推断)
  96. province, city, # province_name, city_name
  97. address, # area_info(详细地址=执照地址)
  98. str(record.get("title") or "")[:100], # product_name
  99. str(record.get("product_specs") or ""), # product_specs(任务给的规格代码)
  100. price, # one_box_price
  101. "", # manufacture_date(未采集)
  102. str(record.get("validity") or ""), # expiry_date(说明书有效期)
  103. "", # manufacturer(未采集)
  104. str(record.get("approval_no") or ""), # approval_number
  105. 0, # is_sold_out(未采集)
  106. 1, 1, # online_posting_count, continuous_listing_count(未采集)
  107. str(record.get("link") or ""), # link_url
  108. shop, # store_name
  109. "", # store_url(未采集)
  110. 0, "", 0, "", # shipment_*(未采集)
  111. company, # company_name(执照单位名称)
  112. qualification, # qualification_number
  113. _today(), # scrape_date
  114. price, # min_price
  115. 1, # number(未采集)
  116. str(record.get("sales") or ""), # sales(月售)
  117. "", # inventory(未采集)
  118. str(record.get("snapshot_url") or ""), # snapshot_url(网页快照OSS链接)
  119. str(record.get("brand") or ""), # product_brand(任务品牌)
  120. str(record.get("search_name") or ""), # search_name
  121. _now(), _now(), # insert_time, update_time
  122. str(record.get("collect_config_info") or ""), # collect_config_info(调度任务传入)
  123. record.get("collect_equipment_account_id") or 0, # collect_equipment_account_id
  124. record.get("collect_region_id") or 0, # collect_region_id
  125. record.get("collect_round") or "", # collect_round
  126. shop, shop, # shop_id, company_id
  127. record.get("task_id") or 0, # task_id(调度任务传入)
  128. )
  129. return _exec(sql, params, "保存商品数据")
  130. def get_existing_license(shop: str) -> dict:
  131. """
  132. 查询店铺已采集的资质信息(platform+shop 唯一键),已采集过的直接从数据库获取。
  133. 返回 {"license_no": "", "company": "", "address": ""},无记录返回空 dict。
  134. """
  135. shop = str(shop or "").strip()
  136. if not shop:
  137. return {}
  138. conn = get_conn()
  139. try:
  140. cur = conn.cursor()
  141. cur.execute(
  142. "SELECT qualification_number, business_license_company, business_license_address "
  143. "FROM retrieve_scrape_shop_info WHERE platform=%s AND shop_id=%s LIMIT 1",
  144. (PLATFORM, shop),
  145. )
  146. row = cur.fetchone()
  147. if row:
  148. return {
  149. "license_no": str(row[0] or "").strip(),
  150. "company": str(row[1] or "").strip(),
  151. "address": str(row[2] or "").strip(),
  152. }
  153. except Exception as e:
  154. print(f"[db] 查询已有资质失败: {e}")
  155. finally:
  156. try:
  157. conn.close()
  158. except Exception:
  159. pass
  160. return {}
  161. def save_shop_info_to_database(record: dict) -> bool:
  162. """保存店铺信息到 retrieve_scrape_shop_info(存在则补全缺失字段,不存在则插入)"""
  163. shop = str(record.get("shop") or "").strip()
  164. lic = _license(record)
  165. company = str(lic.get("单位名称") or "").strip()
  166. address = str(lic.get("地址") or "").strip()
  167. qualification = str(record.get("license_no") or lic.get("社会信用代码") or "").strip()
  168. province, city, _, _ = _infer_region(address)
  169. if not shop:
  170. return False
  171. conn = get_conn()
  172. try:
  173. cur = conn.cursor()
  174. cur.execute(
  175. "SELECT qualification_number, business_license_company, business_license_address, contact_address, province, city "
  176. "FROM retrieve_scrape_shop_info WHERE platform=%s AND shop_id=%s AND company_id=%s",
  177. (PLATFORM, shop, shop),
  178. )
  179. row = cur.fetchone()
  180. if row:
  181. # 已存在:用新采集到的值补全缺失字段
  182. update_sql = """
  183. UPDATE retrieve_scrape_shop_info SET
  184. qualification_number = %s, business_license_company = %s,
  185. business_license_address = %s, contact_address = %s,
  186. province = %s, city = %s, update_time = %s
  187. WHERE platform=%s AND shop_id=%s AND company_id=%s
  188. """
  189. cur.execute(update_sql, (
  190. qualification or row[0], company or row[1],
  191. address or row[2], address or row[3],
  192. province or row[4], city or row[5], _now(),
  193. PLATFORM, shop, shop,
  194. ))
  195. else:
  196. insert_sql = """
  197. INSERT INTO retrieve_scrape_shop_info (
  198. platform, shop_id, company_id, shop, shop_url, business_license_url,
  199. province, city, contact_address,
  200. qualification_number, business_license_company, business_license_address,
  201. scrape_date, create_time, update_time, ai_generate
  202. ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
  203. """
  204. cur.execute(insert_sql, (
  205. PLATFORM, shop, shop, shop, "", None, # platform, shop_id, company_id, shop, shop_url, business_license_url(未采集)
  206. province, city, address, # province, city(推断), contact_address(执照地址)
  207. qualification, company, address, # 资质编号, 执照公司名, 执照地址
  208. _today(), _now(), _now(), 0, # scrape_date, create_time, update_time, ai_generate(未采集)
  209. ))
  210. conn.commit()
  211. cur.close()
  212. return True
  213. except Exception as e:
  214. print(f"[db] 保存店铺数据失败: {e}")
  215. try:
  216. conn.rollback()
  217. except Exception:
  218. pass
  219. return False
  220. finally:
  221. try:
  222. conn.close()
  223. except Exception:
  224. pass
  225. def save_record(record: dict) -> None:
  226. """保存一条采集记录(商品数据 + 店铺信息),任一步失败不抛异常"""
  227. try:
  228. ok1 = save_to_database(record)
  229. except Exception as e:
  230. print(f"[db] 保存商品数据异常: {e}")
  231. ok1 = False
  232. try:
  233. ok2 = save_shop_info_to_database(record)
  234. except Exception as e:
  235. print(f"[db] 保存店铺信息异常: {e}")
  236. ok2 = False
  237. print(f"[db] 入库完成: 商品{'成功' if ok1 else '失败'} 店铺{'成功' if ok2 else '失败'} <- {record.get('shop', '')}")
  238. def _exec(sql: str, params: tuple, tag: str) -> bool:
  239. conn = get_conn()
  240. try:
  241. cur = conn.cursor()
  242. cur.execute(sql, params)
  243. conn.commit()
  244. cur.close()
  245. return True
  246. except Exception as e:
  247. print(f"[db] {tag}失败: {e}")
  248. try:
  249. conn.rollback()
  250. except Exception:
  251. pass
  252. return False
  253. finally:
  254. try:
  255. conn.close()
  256. except Exception:
  257. pass