db.py 12 KB

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