1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
| import re import logging from typing import List, Tuple, Optional from urllib.parse import urljoin
import pandas as pd import requests from bs4 import BeautifulSoup from tqdm import tqdm
logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__)
class Config: """集中管理配置参数""" BASE_DOMAIN = "http://47.117.190.214:32800/" ENDPOINT = "/index.php" REQUEST_TIMEOUT = 15 MAX_PRODUCTS = 500 OUTPUT_FILE = "submit_2.csv" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }
@classmethod def full_url(cls) -> str: """生成完整请求URL""" return urljoin(cls.BASE_DOMAIN, cls.ENDPOINT)
CATEGORY_KEYWORDS = [ (11, ['热水器', '速热', '恒温', '防干烧', '即热式', '储水式', '电热水器', '燃气热水器', '太阳能热水器', '空气能热水器', '壁挂炉', '热水系统', '回水装置', '恒温阀', '镁棒', '防电墙', '速热电热水器', '零冷水', '预即双模']),
(16, ['汽车', '车载', '机油', '轮胎', '变速箱', '刹车片', '雨刷', '火花塞', '滤清器', '蓄电池', '行车记录仪', '车载充电器', '汽车贴膜', '脚垫', '方向盘套', '防冻液', '玻璃水', '悬挂系统', '涡轮增压', '四轮定位', '钣金喷漆', '汽车美容', '车衣']),
(15, ['玩具', '乐高', '积木', '钢琴', '吉他', '拼图', '遥控车', '芭比娃娃', '变形金刚', '拼装模型', '益智玩具', '早教机', '磁力片', '轨道车', '毛绒玩具', '电子琴', '小提琴', '架子鼓', '尤克里里', '口琴', '音乐盒', '画板', '橡皮泥', '沙滩玩具']),
(22, ['医疗', '体温计', '血压仪', '轮椅', '助行器', '血糖仪', '制氧机', '雾化器', '听诊器', '医用口罩', '防护服', '手术刀', '针灸针', '理疗仪', '康复设备', '助听器', '呼吸机', '护理床', '血压计', '血氧仪', '消毒柜', '医用纱布', '棉签', '创可贴']),
(23, ['花卉', '绿植', '多肉', '盆栽', '种子', '花盆', '营养土', '园艺剪', '喷壶', '花架', '观叶植物', '鲜花', '干花', '花肥', '杀虫剂', '生根粉', '育苗盘', '苔藓微景观', '水培植物', '兰花', '仙人掌', '绿萝', '富贵竹', '发财树', '文竹']),
(25, ['园艺工具', '洒水器', '铲子', '花盆', '营养土', '修枝剪', '嫁接刀', '园艺手套', '割草机', '绿篱机', '树枝粉碎机', '园艺锯', '松土器', '育苗箱', '自动喷灌', '园艺支架', '接水盘', '播种器', '园艺地布', '防草布', '智能花盆', '植物补光灯']),
(2, ['母婴', '奶粉', '尿不湿', '婴儿车', '奶瓶', '吸奶器', '孕妇装', '待产包', '婴儿床', '学步车', '儿童餐椅', '隔尿垫', '哺乳枕', '温奶器', '婴儿背带', '防溢乳垫', '婴儿湿巾', '婴儿沐浴露', '护臀膏', '婴儿理发器', '早教玩具', '婴儿监护器']),
(1, ['手机', '5G', '智能机', '曲面屏', '骁龙', '全面屏', '快充', '双卡双待', '游戏手机', '拍照手机', '防水手机', '折叠屏', 'AMOLED', '屏下指纹', '液冷散热', '超级快充', '升降摄像头', 'HiFi音质', 'NFC功能', '无线充电', '移动电源', '手机壳']),
(6, ['厨房', '炒锅', '菜刀', '砧板', '洗碗机', '电饭煲', '微波炉', '压力锅', '榨汁机', '空气炸锅', '料理机', '破壁机', '蒸烤箱', '油烟机', '燃气灶', '消毒柜', '绞肉机', '烘焙模具', '厨房秤', '磨刀器', '沥水篮', '保鲜盒', '厨房置物架']),
(20, ['户外', '帐篷', '登山杖', '睡袋', '冲锋衣', '登山鞋', '户外背包', '野餐垫', '烧烤架', '露营灯', '指南针', '求生哨', '军刀卡', '速干衣', '抓绒衣', '攀岩绳', '冰爪', '滑雪板', '钓鱼竿', '浮潜面镜', '登山扣', '头灯', '户外水壶']),
(10, ['运动', '跑步机', '瑜伽垫', '哑铃', '泳镜', '健身车', '椭圆机', '仰卧板', '运动护具', '拉力器', '跳绳', '羽毛球拍', '乒乓球桌', '足球鞋', '篮球架', '运动手环', '计步器', '筋膜枪', '运动水壶', '运动毛巾', '运动背包']),
(21, ['珠宝', '钻石', '黄金', '翡翠', '铂金', 'K金', '珍珠', '和田玉', '琥珀', '蜜蜡', '珊瑚', '绿松石', '红宝石', '蓝宝石', '祖母绿', '婚戒', '吊坠', '手链', '耳环', '胸针', '转运珠', '生肖挂件', '文玩核桃']),
(14, ['酒水', '红酒', '白酒', '威士忌', '啤酒', '黄酒', '清酒', '伏特加', '朗姆酒', '鸡尾酒', '起泡酒', '香槟', '精酿啤酒', '果酒', '保健酒', '酒具', '醒酒器', '红酒杯', '酒柜', '酒窖', '年份酒', '原浆酒']),
(5, ['蔬菜', '有机', '绿叶菜', '西红柿', '黄瓜', '胡萝卜', '土豆', '辣椒', '茄子', '西兰花', '菠菜', '芹菜', '莲藕', '菌菇', '豆角', '南瓜', '山药', '芦笋', '秋葵', '娃娃菜', '冰草', '羽衣甘蓝', '芽苗菜']),
(8, ['水果', '进口', '榴莲', '车厘子', '蓝莓', '草莓', '芒果', '奇异果', '山竹', '菠萝蜜', '牛油果', '莲雾', '释迦果', '火龙果', '椰子', '百香果', '番石榴', '杨桃', '枇杷', '西梅', '无花果', '人参果']),
(4, ['书籍', '小说', '教材', '儿童读物', '科技', '文学', '历史', '哲学', '心理学', '经济学', '计算机', '编程', '医学', '法律', '建筑', '艺术', '摄影', '旅游指南', '字帖', '绘本', '百科全书', '字典', '考试用书']),
(24, ['游戏', 'PS5', 'Switch', 'Xbox', '手柄', '游戏卡带', '游戏光盘', '电竞椅', '游戏鼠标', '机械键盘', '游戏耳机', 'VR设备', '游戏周边', '手办', '游戏点卡', '游戏加速器', '游戏皮肤', '游戏账号', 'MOD工具', '游戏攻略书']),
(12, ['彩妆', '口红', '粉底', '眼影', '眉笔', '腮红', '遮瑕', '化妆刷', '美妆蛋', '定妆喷雾', '睫毛膏', '眼线笔', '修容棒', '高光', '妆前乳', '卸妆水', '美瞳', '假睫毛', '美甲贴', '化妆镜', '美容工具', '香水']),
(17, ['床上用品', '四件套', '羽绒被', '枕头', '床垫', '蚕丝被', '乳胶枕', '凉席', '床褥', '床笠', '被套', '枕套', '蚊帐', '电热毯', '记忆棉', '榻榻米', '床幔', '靠垫', '抱枕', '床尾凳', '布艺床']),
(18, ['洗护', '洗发水', '沐浴露', '牙膏', '洗面奶', '护发素', '身体乳', '洗手液', '洗衣液', '柔顺剂', '消毒液', '洁厕灵', '厨房清洁剂', '玻璃水', '除霉剂', '空气清新剂', '香薰', '除螨仪', '洗衣凝珠', '衣物除菌液']),
(7, ['办公', '打印机', '投影仪', '复印纸', '文件夹', '订书机', '碎纸机', '考勤机', '白板', '会议电话', '文件柜', '保险箱', '装订机', '标签机', '支票打印机', '扫描仪', '指纹打卡机', '办公沙发', '会议桌', '办公隔断']),
(3, ['家居', '沙发', '茶几', '窗帘', '地毯', '衣柜', '餐桌', '电视柜', '鞋柜', '置物架', '装饰画', '花瓶', '壁纸', '灯具', '智能门锁', '晾衣架', '收纳盒', '墙贴', '门垫', '家居摆件', '香炉']),
(9, ['宠物', '猫粮', '狗粮', '宠物窝', '牵引绳', '猫砂', '宠物玩具', '宠物衣服', '宠物浴液', '驱虫药', '宠物笼', '食盆', '自动喂食器', '猫抓板', '宠物推车', '宠物背包', '宠物美容', '宠物医院', '宠物殡葬', '宠物芯片']),
(13, ['保健', '鱼油', '维生素', '钙片', '蛋白粉', '益生菌', '褪黑素', '护肝片', '胶原蛋白', '葡萄籽', '辅酶Q10', '氨糖', '膳食纤维', '蜂胶', '冬虫夏草', '阿胶', '燕窝', '人参', '灵芝孢子粉', '按摩器', '足浴盆']),
(19, ['五金', '螺丝刀', '电钻', '扳手', '工具箱', '角磨机', '电锤', '切割机', '水平仪', '卷尺', '钳子', '焊枪', '砂轮机', '冲击钻', '电动螺丝刀', '开孔器', '水管配件', '阀门', '密封胶', '膨胀螺栓', '绝缘胶带']), ]
def match_category(product_name: str) -> int: """智能分类匹配算法""" clean_name = product_name.lower() for category_id, keywords in CATEGORY_KEYWORDS: if any(kw in clean_name for kw in keywords): return category_id return 0
def parse_sales(text: str) -> int: """精准解析销量数值""" try: return max(int(text.strip()), 0) except Exception as e: numbers = re.findall(r'\d+', text) return int(numbers[0]) if numbers else 0
def fetch_product_info(session: requests.Session, product_id: int) -> Optional[Tuple]: """获取并解析商品信息(增强描述解析版)""" params = { "controller": "product", "action": "detail", "id": product_id }
try: response = session.get( Config.full_url(), params=params, timeout=Config.REQUEST_TIMEOUT ) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser')
name_tag = soup.find('h1', {'class': 'product-name'}) product_name = name_tag.text.strip() if name_tag else ""
desc_tag = soup.find('div', {'class': 'product-description'}) product_desc = desc_tag.find('p').text.strip() if desc_tag else ""
match_text = f"{product_name} {product_desc}".lower()
sales_tag = soup.find('span', {'id': 'productSales'}) sales_text = sales_tag.text.strip() if sales_tag else "0" sales = parse_sales(sales_text)
reviews_tag = soup.find('div', {'class': 'reviews-container'}) reviews_number = len(reviews_tag.find_all('div', class_='review-item')) if reviews_tag else 0
category_id = match_category(match_text)
return (product_id, sales, category_id, reviews_number)
except requests.exceptions.HTTPError as e: logger.warning(f"商品 {product_id} 请求失败: HTTP {e.response.status_code}") except Exception as e: logger.error(f"处理商品 {product_id} 时发生异常: {str(e)}")
return None
def data_validation(df: pd.DataFrame) -> pd.DataFrame: """数据完整性校验""" df['category_id'] = df['category_id'].replace(0, 25)
df['reviews_number'] = df['reviews_number'].clip(lower=0)
return df.sort_values('product_id')
def main(): """主处理流程""" product_data = []
with requests.Session() as session: session.headers.update(Config.HEADERS)
for product_id in tqdm(range(1, Config.MAX_PRODUCTS + 1), desc="处理商品"): for retry in range(3): if info := fetch_product_info(session, product_id): product_data.append(info) break else: logger.warning(f"商品 {product_id} 第 {retry + 1} 次重试")
if not product_data: logger.error("未获取到有效数据") return
df = pd.DataFrame( product_data, columns=['product_id', 'sales', 'category_id', 'reviews_number'] )
df = data_validation(df)
df.to_csv(Config.OUTPUT_FILE, index=False, encoding='utf-8') logger.info(f"成功生成文件 {Config.OUTPUT_FILE},记录数: {len(df)}")
if __name__ == "__main__": main()
|