สร้าง Parser CIAN ด้วยตัวเอง: คู่มือทีละขั้นตอนสำหรับเก็บข้อมูลประกาศ ราคา และประวัติการเปลี่ยนแปลง
บทความ
- บทนำ: คุณจะได้อะไรจากบทความนี้
- การเตรียมตัวล่วงหน้า: เครื่องมือและสภาพแวดล้อม
- แนวคิดพื้นฐาน: สิ่งที่ต้องเข้าใจก่อนเริ่ม
- ขั้นตอนที่ 1: กำหนดเป้าหมายและโครงสร้างข้อมูล
- ขั้นตอนที่ 2: ศึกษาโครงสร้างของหน้าผลการค้นหา
- ขั้นตอนที่ 3: เขียน parser cian ตัวแรกสำหรับหน้าผลการค้นหา
- ขั้นตอนที่ 4: เก็บข้อมูลจากหน้ารายละเอียดประกาศ
- ขั้นตอนที่ 5: ต่อ mobile proxy และทำให้การเก็บเสถียร
- ขั้นตอนที่ 6: บันทึกข้อมูลและสร้างประวัติราคา
- ขั้นตอนที่ 7: ตั้งเวลารันอัตโนมัติและขยายไปยังเว็บอื่น
- ตรวจสอบผลลัพธ์: เช็กลิสต์ parser ที่พร้อมใช้
- ข้อผิดพลาดทั่วไปและวิธีแก้
- ความสามารถเพิ่มเติมสำหรับผู้สูง
- Faq: คำถามที่พบบ่อยเกี่ยวกับการสร้าง parser อสังหาริมทรัพย์
- บทสรุป
บทนำ: คุณจะได้อะไรจากบทความนี้
ตลาดอสังหาริมทรัพย์ขับเคลื่อนด้วยตัวเลข บางคนหาคอนโดที่ราคาต่ำกว่าตลาด บางคนประเมินคู่แข่งในโครงการใหม่ บางคนทำรายงานให้นักลงทุน สิ่งที่ทุกคนต้องการเหมือนกันคือข้อมูลราคาและประกาศที่เป็นปัจจุบัน ซึ่งการเก็บด้วยมือนั้นแทบเป็นไปไม่ได้ นี่คือจุดที่ parser CIAN ที่สร้างเองมีประโยชน์
ในคู่มือนี้คุณจะสร้างเครื่องมือที่ใช้งานได้จริงจากศูนย์ ซึ่งทำได้สามอย่าง อย่างแรก: ไล่ดูหน้าผลการค้นหาตามตัวกรองที่กำหนด และเก็บรายการประกาศพร้อมราคา ที่อยู่ พื้นที่ และลิงก์ อย่างที่สอง: เข้าไปในหน้ารายละเอียดของแต่ละประกาศและดึงข้อมูลเพิ่มเติมเช่นชั้น ปีที่สร้าง และประเภทอาคาร อย่างที่สาม ซึ่งมีค่าที่สุด: บันทึกข้อมูลลงฐานข้อมูลและสะสมประวัติราคาไปเรื่อย ๆ ทุกวัน เพื่อให้คุณเห็นว่าประกาศไหนลดราคา ประกาศไหนถูกถอด และตลาดในย่านนั้นเคลื่อนไหวอย่างไร
ผลลัพธ์สุดท้ายจะเป็นแบบนี้: คุณมีโฟลเดอร์ที่มีสคริปต์ Python ไฟล์ฐานข้อมูล SQLite และตารางที่เปิดดูใน Excel หรือ Google Sheets ได้ รันสคริปต์ตอนเช้า ก็ได้ข้อมูลสด ๆ ผ่านไปหนึ่งสัปดาห์คุณก็มีแนวโน้มการเปลี่ยนแปลงแล้ว
คู่มือนี้เหมาะกับใคร
- นักการตลาดและนักวิเคราะห์ของบริษัทอสังหาริมทรัพย์ ที่ต้องการติดตามราคาตามย่านโดยไม่ต้องซื้อรายงานแพง ๆ
- นัก arbitrage และเจ้าของธุรกิจที่มองหาช่องทางและอยากเข้าใจอุปสงค์อุปทานเป็นตัวเลข
- นักพัฒนามือใหม่ที่อยากฝึก scraping ด้วยตัวอย่างที่เข้าใจง่ายและใช้งานได้จริง
- นักลงทุนและผู้ซื้อทั่วไปที่อยากจับประกาศลดราคาก่อนคนอื่น
สิ่งที่ควรรู้ล่วงหน้า
ไม่จำเป็นต้องมีประสบการณ์เขียนโปรแกรม เราจะอธิบายทุกบรรทัดของโค้ดและบอกว่าทำไปทำไม แค่ติดตั้งโปรแกรมเป็น เปิด command line ได้ และคัดลอกข้อความเป็น ก็เพียงพอ ถ้าคุณเคยเปิด developer tools ในเบราว์เซอร์อย่างน้อยหนึ่งครั้ง จะยิ่งง่ายมาก ถ้าไม่เคย เราจะบอกว่ามันอยู่ตรงไหน
ข้อกำหนดเดียวคือความละเอียดรอบคอบ การ scrape นั้นอ่อนไหวต่อการพิมพ์ผิดในชื่อคลาสและที่อยู่ ตัวอักษรเกินมาหนึ่งตัวสคริปต์ก็คืนค่ารายการว่าง ไม่ต้องตกใจ เพราะทุกขั้นตอนมีจุดตรวจสอบให้คุณมั่นใจว่าทุกอย่างเป็นไปตามแผน
ใช้เวลานานแค่ไหน
การเตรียมสภาพแวดล้อมใช้เวลาประมาณ 30 นาที parser หน้าผลการค้นหาตัวแรกเขียนเสร็จในหนึ่งชั่วโมง หน้ารายละเอียดของประกาศ พร็อกซี และฐานข้อมูลจะใช้เวลาอีกหนึ่งชั่วโมงครึ่งถึงสองชั่วโมง รวมแล้วประมาณสามถึงสี่ชั่วโมงถ้าทำแบบไม่รีบ ประวัติราคาจะเริ่มสะสมเองตั้งแต่การรันครั้งที่สอง
การเตรียมตัวล่วงหน้า: เครื่องมือและสภาพแวดล้อม
ก่อนเขียนโค้ด มารวบรวมทุกอย่างที่จำเป็นกันก่อน ไม่ควรข้ามส่วนนี้ เพราะปัญหาครึ่งหนึ่งของมือใหม่เกิดจากการติดตั้ง Python ผิดหรือขาดไลบรารี
ข้อกำหนดของระบบ
- คอมพิวเตอร์ที่ใช้ Windows 10 หรือ 11, macOS หรือ Linux แล็ปท็อปที่อายุไม่เกินแปดปีก็เพียงพอ
- หน่วยความจำอย่างน้อย 4 GB ถ้าใช้วิธี automation เบราว์เซอร์ควรมี 8 GB
- พื้นที่ว่างบนดิสก์ประมาณ 2 GB สำหรับ Python ไลบรารี และฐานข้อมูล
- การเชื่อมต่ออินเทอร์เน็ตที่เสถียร
สิ่งที่ต้องติดตั้ง
- Python 3.11 หรือใหม่กว่า ดาวน์โหลดตัวติดตั้งจากเว็บไซต์ทางการ python.org บน Windows อย่าลืมติ๊กถูกที่ Add Python to PATH ด้านล่างของหน้าจอแรก ถ้าไม่ทำ คำสั่ง python จะไม่ทำงานใน terminal บน macOS มักมี Python อยู่แล้ว แต่ควรติดตั้งเวอร์ชันใหม่
- โปรแกรมแก้ไขโค้ด เราแนะนำ Visual Studio Code ฟรี เน้นไวยากรณ์ และแสดงข้อผิดพลาด ติดตั้งส่วนขยาย Python จาก extension marketplace ในตัว (ไอคอนสี่เหลี่ยมสี่อันที่แถบด้านซ้าย)
- เบราว์เซอร์ Chrome หรือ Edge เราจะใช้ developer tools เพื่อดูโครงสร้างหน้าเว็บ
- ไลบรารี Python เราจะติดตั้งผ่าน terminal ด้านล่าง
- การเข้าถึง mobile proxy จำเป็นในขั้นตอนที่ห้า คุณต้องมีที่อยู่เซิร์ฟเวอร์ พอร์ต ชื่อผู้ใช้ รหัสผ่าน และลิงก์สำหรับเปลี่ยน IP ทั้งหมดนี้จะได้รับในหน้าสมาชิกของบริการเมื่อซื้อ ถ้ายังไม่มีพร็อกซี ขั้นตอนแรก ๆ ก็ทำได้โดยไม่ใช้
สร้างโฟลเดอร์ทำงานและ virtual environment
- สร้างโฟลเดอร์ชื่อ cian_parser บนดิสก์ หลีกเลี่ยงตัวอักษรรัสเซียและช่องว่างในพาธ เพราะบางครั้งมันทำให้เครื่องมือพัง
- เปิด terminal บน Windows กด Win+R พิมพ์ cmd แล้วกด Enter บน macOS เปิด Terminal ผ่าน Spotlight
- ไปยังโฟลเดอร์ด้วยคำสั่ง cd และพาธ เช่น
cd C:\projects\cian_parserบน Windows หรือcd ~/projects/cian_parserบน macOS - สร้าง virtual environment ด้วยคำสั่ง
python -m venv venvนี่คือสำเนา Python ที่แยกออกมา เพื่อไม่ให้ไลบรารีของโปรเจกต์ขัดกับของระบบ - เปิดใช้งาน environment บน Windows:
venv\Scripts\activateบน macOS และ Linux:source venv/bin/activateจะมีคำว่า (venv) ปรากฏที่ต้นบรรทัดใน terminal - ติดตั้งไลบรารีด้วยคำสั่งเดียว:
pip install requests beautifulsoup4 lxml pandas openpyxlใช้เวลาหนึ่งถึงสองนาที
ตรวจสอบ: พิมพ์ใน terminal python -c "import requests, bs4, pandas; print('ok')" ถ้าหน้าจอแสดงคำว่า ok โดยไม่มีข้อผิดพลาด แสดงว่าสภาพแวดล้อมพร้อม ถ้าเห็น ModuleNotFoundError แสดงว่า environment ไม่ได้เปิดใช้งานหรือการติดตั้งถูกขัดจังหวะ ให้เปิด venv ใหม่และรัน pip install อีกครั้ง
สำรองข้อมูล
ในโปรเจกต์นี้สิ่งที่มีค่าที่สุดไม่ใช่โค้ด แต่เป็นฐานข้อมูลที่สะสมประวัติราคา มันกู้คืนไม่ได้ เพราะราคาในอดีตจะไม่ปรากฏที่ไหนอีก ดังนั้นตั้งแต่วันแรกให้ตกลงกับตัวเองว่า ไฟล์ฐานข้อมูลจะถูกคัดลอกขึ้นคลาวด์หรือฮาร์ดดิสก์ภายนอกอย่างน้อยสัปดาห์ละครั้ง ต่อไปเราจะเพิ่มการสำรองอัตโนมัติในสคริปต์
แนวคิดพื้นฐาน: สิ่งที่ต้องเข้าใจก่อนเริ่ม
มาดูคำศัพท์ที่จะเจอต่อไป ถ้าคุณคุ้นเคยกับ scraping อยู่แล้ว ข้ามส่วนนี้ได้ แต่ควรอ่านส่วนกฎหมาย
คำศัพท์สำคัญอธิบายง่าย ๆ
- Parsing (scraping): การดึงหน้าเว็บด้วยโปรแกรมโดยอัตโนมัติและแยกข้อมูลที่ต้องการออกมา เหมือนที่คุณทำด้วยตา แต่สคริปต์ทำเร็วกว่าพันเท่า
- HTML: ภาษามาร์กอัปที่ประกอบเป็นหน้าเว็บ ราคาคอนโดบน CIAN อยู่ในแท็ก HTML ที่มีแอตทริบิวต์เฉพาะ หน้าที่เราคือหาแท็กนั้นให้เจอ
- Selector: ที่อยู่ขององค์ประกอบภายใน HTML เช่น span ที่มีแอตทริบิวต์ data-mark เท่ากับ MainPrice ตัว parser ใช้ selector เพื่อรู้ว่าจะดึงราคาจากไหน
- HTTP request: การเรียกไปยังเซิร์ฟเวอร์ของเว็บไซต์ เบราว์เซอร์ทำเมื่อคุณเปิดหน้า ไลบรารี requests ก็ทำแบบเดียวกันจากโค้ด
- Headers: ข้อมูลบริการที่เบราว์เซอร์ส่งไปพร้อมคำขอ เช่น ประเภทเบราว์เซอร์ ภาษา รูปแบบข้อมูล เซิร์ฟเวอร์ใช้ข้อมูลนี้ตัดสินใจว่าจะตอบอะไร
- Proxy: เซิร์ฟเวอร์ตัวกลางที่คำขอของคุณผ่านไป Mobile proxy ใช้อินเทอร์เน็ตจากผู้ให้บริการมือถือและเปลี่ยนที่อยู่ได้ตามคำสั่ง
- Pagination: การแบ่งผลลัพธ์ออกเป็นหน้า ๆ เพื่อเก็บทั้งหมด parser ต้องไล่จากหน้าแรกถึงหน้าสุดท้าย
- SQLite: ฐานข้อมูลเบา ๆ ในไฟล์เดียว ไม่ต้องติดตั้งเซิร์ฟเวอร์ มีอยู่ในตัว Python เหมาะกับประวัติราคามาก
หน้าผลการค้นหาของเว็บอสังหาฯ ทำงานอย่างไร
CIAN, Domclick, Yandex Realty และเว็บอื่น ๆ ทำงานคล้ายกัน มีหน้าค้นหาพร้อมตัวกรอง: เมือง ประเภทดีล จำนวนห้อง ช่วงราคา แต่ละตัวกรองกลายเป็นพารามิเตอร์ในแถบที่อยู่ เช่น พารามิเตอร์ deal_type เท่ากับ sale หมายถึงขาย และ room1 เท่ากับ 1 เพิ่มคอนโดหนึ่งห้องนอน การเข้าใจพารามิเตอร์เหล่านี้ให้เครื่องมือทรงพลัง: แทนที่จะคลิกบนเว็บ คุณแค่สร้างที่อยู่ที่ต้องการ
ภายในผลการค้นหา แต่ละประกาศแสดงเป็น card: หัวข้อ ราคา ที่อยู่ รูปไม่กี่รูป และลิงก์ไปหน้ารายละเอียด หน้ารายละเอียดมีคุณสมบัติเต็มและมักรวมข้อมูลทั้งหมดซ้ำในบล็อก JSON ที่ซ่อนอยู่ ซึ่งเว็บใช้สร้างอินเทอร์เฟซ บล็อกนี้ parse ง่ายกว่า HTML มาก
ขอบเขตทางกฎหมายและจริยธรรม
ข้อควรระวัง: เก็บเฉพาะข้อมูลสาธารณะของอสังหาฯ เช่น ราคา พื้นที่ ที่อยู่ คุณสมบัติอาคาร ห้ามเก็บหรือบันทึกเบอร์โทร ชื่อ และข้อมูลส่วนบุคคลอื่น ๆ ของผู้ขายและนายหน้า เพราะอยู่ภายใต้กฎหมายคุ้มครองข้อมูลส่วนบุคคล และการละเมิดมีโทษจริง อ่านข้อตกลงผู้ใช้ของเว็บก่อนเริ่ม และใช้ข้อมูลเพื่อการวิเคราะห์ของคุณเอง ไม่ใช่เพื่อขายต่อหรือสร้างสำเนาเว็บ รักษาความถี่ของคำขอให้สมเหตุสมผล parser ของคุณไม่ควรสร้างภาระที่รบกวนการทำงานของบริการ
แนวทางนี้ไม่เพียงถูกกฎหมาย แต่ยังใช้ได้จริง parser ที่รอบคอบ มีจังหวะหยุด และหมุนที่อยู่ จะทำงานได้เป็นเดือน ขณะที่ตัวที่ก้าวร้าวจะถูกจำกัดภายในหนึ่งชั่วโมง
ขั้นตอนที่ 1: กำหนดเป้าหมายและโครงสร้างข้อมูล
เป้าหมายของขั้นนี้: อธิบายให้ชัดว่าเราจะเก็บอะไรและเก็บอย่างไร ถ้าไม่มีขั้นนี้ คุณจะเขียน parser ที่ดึงทุกอย่าง แล้วต้องมานั่งงงกับกองข้อมูลเป็นสัปดาห์
- กำหนดคำถามทางธุรกิจ ตัวอย่าง: คอนโดหนึ่งห้องนอนในเซนต์ปีเตอร์สเบิร์กประกาศไหนลดราคาเกิน 5 เปอร์เซ็นต์ในหนึ่งเดือน ราคาต่อตารางเมตรในโครงการใหม่ของย่านหนึ่งเป็นเท่าไร ประกาศที่ถูกกว่าราคาหนึ่งหายไปเร็วแค่ไหน
- กำหนดตัวกรองผลการค้นหา ในคู่มือนี้ยกตัวอย่าง: ขาย มือสอง คอนโดหนึ่งและสองห้องนอน มอสโก ราคาไม่เกิน 15 ล้านรูเบิล คุณจะแทนที่ด้วยพารามิเตอร์ของคุณเอง
- เขียนรายการฟิลด์ สำหรับแต่ละประกาศเราต้องการ: รหัสประกาศเฉพาะ ลิงก์ หัวข้อ ราคา ที่อยู่ พื้นที่รวม ชั้นและจำนวนชั้น ประเภทอาคาร ปีที่สร้าง วันที่พบครั้งแรก วันที่ตรวจล่าสุด สำหรับประวัติราคา: รหัสประกาศ วันที่ ราคา
- เปิดโปรแกรมแก้ไขโค้ดและสร้างไฟล์ config.py ในโฟลเดอร์โปรเจกต์ เขียนพารามิเตอร์ที่เราจะเปลี่ยนบ่อยที่สุด:
BASE_URL = 'https://www.cian.ru/cat.php'
SEARCH_PARAMS = {'deal_type': 'sale', 'engine_version': 2, 'offer_type': 'flat', 'region': 1, 'room1': 1, 'room2': 1, 'maxprice': 15000000}
MAX_PAGES = 5
PAUSE_MIN = 4
PAUSE_MAX = 9
DB_PATH = 'realty.db'สังเกตว่าในตัวอย่างโค้ด การขึ้นบรรทัดใหม่แสดงด้วยสัญลักษณ์ newline ในโปรแกรมแก้ไขให้เขียนแต่ละตัวแปรในบรรทัดใหม่ พารามิเตอร์ region เท่ากับ 1 ตรงกับมอสโก 2 ตรงกับเซนต์ปีเตอร์สเบิร์ก รหัสภูมิภาคอื่น ๆ หาได้โดยใช้ตัวกรองบนเว็บแล้วดูที่แถบที่อยู่
เคล็ดลับ: เริ่มด้วย MAX_PAGES เท่ากับ 2-3 ในแต่ละหน้าผลการค้นหามีประมาณ 28 ประกาศ ซึ่งเพียงพอต่อการดีบัก เริ่มเก็บเต็มรูปแบบเมื่อมั่นใจว่าทุกฟิลด์ถูกดึงอย่างถูกต้อง
ตรวจสอบ: คุณมีไฟล์ config.py และมีรายการ 12 ฟิลด์กับคำถามธุรกิจหนึ่งข้อจดไว้ในโน้ตหรือในหัว ถ้าคำถามคือ อยากได้ข้อมูลทั้งหมดของทั้งรัสเซีย ให้ย้อนกลับและจำกัดให้แคบลง เพราะการเก็บทั่วประเทศคือหลายแสนประกาศและโครงสร้างพื้นฐานที่แตกต่างไปโดยสิ้นเชิง
ปัญหาที่อาจเกิดขึ้น
ไม่เข้าใจว่าพารามิเตอร์ใดควบคุมตัวกรองที่ต้องการ วิธีแก้: เปิดเว็บ ตั้งตัวกรองด้วยมือ คัดลอกที่อยู่จากแถบที่อยู่ แล้วแยกตามเครื่องหมาย & แต่ละคู่ key เท่ากับ value คือพารามิเตอร์
ขั้นตอนที่ 2: ศึกษาโครงสร้างของหน้าผลการค้นหา
เป้าหมายของขั้นนี้: หาองค์ประกอบใน HTML ที่เราจะดึงราคา หัวข้อ ที่อยู่ และลิงก์ออกมา นี่คือขั้นตอนที่ต้องค้นคว้ามากที่สุด และเป็นที่ที่มือใหม่มักหลงทาง เราจึงจะไปช้า ๆ
- เปิดเบราว์เซอร์ไปที่หน้าผลการค้นหา CIAN พร้อมตัวกรองของคุณ ตรวจสอบว่าคุณเห็นรายการคอนโด
- วางเคอร์เซอร์บนราคาของคอนโดใด ๆ คลิกขวาและเลือก ตรวจสอบ (ใน Edge เรียกว่า Inspect) แผง developer tools จะเปิดขึ้นและไฮไลต์องค์ประกอบที่มีราคา
- ดูบรรทัดที่ไฮไลต์ ณ เวลาที่เขียนคู่มือ นี่คือแท็ก span ที่มีแอตทริบิวต์ data-mark เท่ากับ MainPrice จดแอตทริบิวต์นี้ไว้ มันจะเป็น selector ของราคา
- ไล่ขึ้นไปบนต้นไม้ขององค์ประกอบ คลิกแท็กพ่อแม่ไปเรื่อย ๆ จนเจอแท็กที่ครอบคลุมการ์ดประกาศทั้งหมด มักเป็น article ที่มีแอตทริบิวต์ data-name เท่ากับ CardComponent เมื่อคุณชี้ที่มันในแผง ทั้งการ์ดที่มีรูปและราคาจะถูกไฮไลต์บนหน้า
- ภายใน card หาหัวข้อ (span ที่มี data-mark เท่ากับ OfferTitle) ที่อยู่ (ลิงก์ a หลายอันที่มี data-name เท่ากับ GeoLabel ซึ่งรวมกันเป็นที่อยู่) และลิงก์ไปยังประกาศ (แท็ก a ที่มี href นำไปสู่ที่อยู่รูปแบบ cian.ru/sale/flat/หมายเลข) จด selector ทั้งสี่
- หาบล็อก pagination ด้านล่างหน้า เลื่อนผลการค้นหาจนสุด คลิกขวาที่เลขหน้าสองและดูว่าที่อยู่เป็นอย่างไร คุณจะเห็นพารามิเตอร์ p เท่ากับ 2 แสดงว่าเพียงเปลี่ยนพารามิเตอร์นี้ก็เปลี่ยนหน้าได้
ข้อควรระวัง: ชื่อแอตทริบิวต์ data-mark และ data-name บนเว็บเปลี่ยนเป็นระยะเมื่ออัปเดตดีไซน์ อย่าคัดลอก selector จากข้อความนี้แบบไม่ตรวจสอบ ต้องเทียบกับหน้าจริงใน developer tools ทุกครั้ง ทักษะการหา selector ด้วยตัวเองสำคัญกว่ารายการสำเร็จรูปใด ๆ
ตรวจสอบว่ามี JSON ที่ซ่อนอยู่หรือไม่
หลายเว็บเก็บข้อมูลผลการค้นหาในรูปแบบพร้อมใช้ภายในแท็ก script ซึ่งสะดวกกว่า HTML เพราะไม่ต้องประกอบที่อยู่จากชิ้นส่วน
- ใน developer tools กด Ctrl+F (บน macOS Cmd+F) แล้วพิมพ์คำว่า offers หรือ initialState
- ถ้าค้นเจอแท็ก script ที่มีข้อความยาว ดูคล้าย dictionary ที่มีปีกกา แสดงว่ามีข้อมูลใน JSON จดชื่อตัวแปรที่ต้นบล็อกนั้นไว้
- ถ้าไม่เจอ ก็ไม่เป็นไร วิธี HTML ในขั้นตอนถัดไปใช้ได้เสมอ
เคล็ดลับ: เปิดแท็บ Network ใน developer tools รีเฟรชหน้า และกรองคำขอตามประเภท Fetch/XHR บางครั้งเว็บโหลดผลการค้นหาเป็นคำขอแยกในรูปแบบ JSON ถ้าคุณเห็นคำขอเช่นนั้นที่มีฟิลด์ offers การ parse จะง่ายที่สุด เพราะได้ข้อมูลสะอาดโดยไม่มี HTML
ตรวจสอบ: คุณมี selector ของ card ราคา หัวข้อ ที่อยู่ และลิงก์ และรู้ชื่อพารามิเตอร์ pagination เมื่อคลิกแต่ละ selector ในแผง คุณเห็นการไฮไลต์องค์ประกอบที่ต้องการบนหน้า
ปัญหาที่อาจเกิดขึ้น
Developer tools แสดง HTML แต่ไม่มีราคา สาเหตุ: เว็บวาดข้อมูลบางส่วนด้วยสคริปต์หลังโหลด วิธีแก้: ในแท็บ Network ดูว่าราคามาเป็นคำขอแยกหรือไม่ หรือใช้ browser automation จากส่วนสำหรับผู้สูง
ขั้นตอนที่ 3: เขียน parser CIAN ตัวแรกสำหรับหน้าผลการค้นหา
เป้าหมายของขั้นนี้: ได้สคริปต์ที่ดาวน์โหลดหน้าผลการค้นหา ดึงรายการประกาศ และพิมพ์ออกคอนโซล หลังขั้นนี้คุณมีโครงที่ใช้งานได้ ซึ่งเราจะเพิ่มฟังก์ชันต่อ
- สร้างไฟล์ parser.py ในโฟลเดอร์โปรเจกต์
- นำเข้าไลบรารีและการตั้งค่าที่ต้นไฟล์:
import time
import random
import requests
from bs4 import BeautifulSoup
from config import BASE_URL, SEARCH_PARAMS, MAX_PAGES, PAUSE_MIN, PAUSE_MAX- กำหนด header ของคำขอ เซิร์ฟเวอร์ต้องเห็นว่าเป็นเบราว์เซอร์ทั่วไปที่มี locale รัสเซีย ไม่งั้นคุณอาจได้หน้าเวอร์ชันที่ไม่ต้องการ:
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36', 'Accept-Language': 'ru-RU,ru;q=0.9', 'Accept': 'text/html,application/xhtml+xml'}- เขียนฟังก์ชันโหลดหน้า มันรับเลขหน้า เพิ่มเข้าไปในพารามิเตอร์ และคืน HTML ต้องตรวจสอบรหัสตอบกลับเสมอ 200 คือสำเร็จ ที่เหลือคือสัญญาณให้หยุดและตรวจสอบ:
def fetch_page(page, session):
params = dict(SEARCH_PARAMS)
params['p'] = page
resp = session.get(BASE_URL, params=params, headers=HEADERS, timeout=30)
if resp.status_code != 200:
print('สถานะ', resp.status_code, 'ที่หน้า', page)
return None
return resp.text- เขียนฟังก์ชัน parse มันหาทุก card และดึงฟิลด์จากแต่ละอัน สังเกตโครงสร้าง if: ถ้าไม่มีองค์ประกอบ เราจะไม่ล้มด้วยข้อผิดพลาด แต่บันทึก None:
def parse_cards(html):
soup = BeautifulSoup(html, 'lxml')
cards = soup.select('article[data-name=CardComponent]')
result = []
for card in cards:
link_tag = card.select_one('a[href*=/sale/flat/]')
price_tag = card.select_one('span[data-mark=MainPrice]')
title_tag = card.select_one('span[data-mark=OfferTitle]')
geo_tags = card.select('a[data-name=GeoLabel]')
if not link_tag or not price_tag:
continue
url = link_tag.get('href')
offer_id = url.rstrip('/').split('/')[-1]
price_text = price_tag.get_text()
price = int(''.join(ch for ch in price_text if ch.isdigit()))
title = title_tag.get_text(strip=True) if title_tag else None
address = ', '.join(g.get_text(strip=True) for g in geo_tags)
result.append({'offer_id': offer_id, 'url': url, 'title': title, 'price': price, 'address': address})
return result- ประกอบลูปหลัก มันไล่ผ่านหน้า หน่วงเวลาแบบสุ่มระหว่างคำขอ และสะสมผลลัพธ์ในรายการรวม การหน่วงแบบสุ่มสำคัญ เพราะช่วงเวลาที่เท่ากันดูไม่เป็นธรรมชาติและสร้างภาระสูงสุด:
def collect_listing():
session = requests.Session()
all_offers = []
for page in range(1, MAX_PAGES + 1):
html = fetch_page(page, session)
if html is None:
break
offers = parse_cards(html)
print('หน้า', page, 'จำนวนประกาศ:', len(offers))
if not offers:
break
all_offers.extend(offers)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return all_offers
if __name__ == '__main__':
data = collect_listing()
for item in data[:5]:
print(item)
print('รวมทั้งหมด:', len(data))- บันทึกไฟล์และรันใน terminal ด้วยคำสั่ง
python parser.pyตรวจสอบว่า environment venv เปิดอยู่
มาดูจุดสำคัญ Session เก็บ cookies ระหว่างคำขอ ทำให้เว็บเห็นพฤติกรรมต่อเนื่องของผู้เข้าชมคนเดียว ไม่ใช่การเข้าถึงกระจัดกระจายสิบครั้ง ฟังก์ชัน select_one คืนองค์ประกอบแรกที่ตรงหรือ None เราจึงตรวจสอบผลก่อนเรียก get_text เสมอ รหัสประกาศเราดึงจากลิงก์ นั่นคือส่วนสุดท้ายของที่อยู่ เป็นตัวเลขเช่น 312456789 มันจะเป็นคีย์สำหรับประวัติราคา
เคล็ดลับ: ในขั้นดีบัก ให้บันทึก HTML ของหน้าแรกเป็นไฟล์ด้วยคำสั่ง open('page1.html', 'w', encoding='utf-8').write(html) แล้วคุณจะดีบักฟังก์ชัน parse บนสำเนาท้องถิ่นได้โดยไม่ต้องส่งคำขอเกินจำเป็นไปยังเว็บ
ตรวจสอบ: ในคอนโซลคุณเห็นบรรทัด หน้า 1 จำนวนประกาศ: 28, หน้า 2 จำนวนประกาศ: 28 และต่อไป และด้านล่างมี dictionary ห้าอันที่มีราคาและที่อยู่จริง ราคาต้องเป็นจำนวนเต็มไม่มีช่องว่างและไม่มีสัญลักษณ์รูเบิล ถ้าเป็นอย่างอื่นแทนที่จะเป็นตัวเลข ให้กลับไปที่ selector จากขั้นตอนที่สอง
ปัญหาที่อาจเกิดขึ้น
สคริปต์พิมพ์ จำนวนประกาศ: 0 ที่หน้าแรก สาเหตุ: selector ของ card เปลี่ยนไป หรือเซิร์ฟเวอร์ส่งหน้า placeholder มา เปิด page1.html ที่บันทึกไว้ในเบราว์เซอร์ดูว่าคุณได้อะไร ถ้าเป็นหน้าที่ขอให้ยืนยันว่าคุณไม่ใช่บอท ให้เพิ่มการหน่วงและไปต่อที่ขั้นตอนที่ห้าด้วย proxy ถ้าเป็นผลการค้นหาปกติ ให้ตรวจสอบ selector
ข้อผิดพลาด ValueError เมื่อแปลงราคา สาเหตุ: ในข้อความราคาไม่มีตัวเลข เช่นเขียนว่า ราคาตามสอบถาม วิธีแก้: ครอบการแปลงด้วย try และบันทึก None สำหรับกรณีนั้น
ขั้นตอนที่ 4: เก็บข้อมูลจากหน้ารายละเอียดประกาศ
เป้าหมายของขั้นนี้: สอน parser ให้เข้าไปในหน้าของแต่ละประกาศและดึงคุณสมบัติละเอียด: พื้นที่ ชั้น ประเภทอาคาร ปีที่สร้าง ฟิลด์เหล่านี้จำเป็นสำหรับคำนวณราคาต่อตารางเมตรและเปรียบเทียบคอนโดที่คล้ายกัน
- เปิดหน้าของประกาศใด ๆ จากผลการค้นหาในเบราว์เซอร์ กด Ctrl+U เพื่อดูซอร์สโค้ดของหน้า
- กด Ctrl+F แล้วพิมพ์คำว่า totalArea ณ เวลาที่เขียนคู่มือ ข้อมูลของ card อยู่ในแท็ก script ภายในออบเจกต์ configuration ของ frontend ในคีย์ชื่อประมาณ frontend-offer-card คุณจะเห็นฟิลด์ totalArea, floorNumber, floorsCount, buildYear, materialType และอื่น ๆ
- ถ้าค้นหา totalArea ไม่เจอ ให้หาคุณสมบัติใน HTML มักเป็นบล็อกที่มีคู่ ชื่อ และ ค่า เช่น พื้นที่รวม และ 38.5 ตร.ม. จด selector ของบล็อกนั้น
- เพิ่มฟังก์ชันดึง JSON จาก card ใน parser.py เราหา script ที่ต้องการ ตัดออบเจกต์ตามปีกกา และแยกด้วยโมดูล json:
import json
import re
def parse_offer_page(html):
soup = BeautifulSoup(html, 'lxml')
details = {}
for script in soup.find_all('script'):
text = script.string or ''
if 'totalArea' in text and 'offerData' in text:
m = re.search(r'totalArea[^0-9]*([0-9.,]+)', text)
if m:
details['area'] = float(m.group(1).replace(',', '.'))
m = re.search(r'floorNumber[^0-9]*([0-9]+)', text)
if m:
details['floor'] = int(m.group(1))
m = re.search(r'floorsCount[^0-9]*([0-9]+)', text)
if m:
details['floors_total'] = int(m.group(1))
m = re.search(r'buildYear[^0-9]*([0-9]{4})', text)
if m:
details['build_year'] = int(m.group(1))
break
return details- ตรงนี้เราใช้ regular expression โดยเจตนาแทนการ parse JSON แบบเต็ม เหตุผลง่ายมาก: สคริปต์ในหน้าไม่ได้มีแค่ JSON แต่มีโค้ดด้วย และการแยกออบเจกต์สะอาด ๆ บางครั้งก็ยาก regular expression ค้นหาคีย์และตัวเลขแรกหลังจากนั้น ซึ่งเชื่อถือได้พอสำหรับฟิลด์ตัวเลข
- เพิ่มฟังก์ชันที่นำรายการประกาศจากผลการค้นหาและเสริมแต่ละอันด้วยข้อมูลจาก card การหน่วงตรงนี้ยิ่งสำคัญ เพราะจำนวนคำขอเพิ่มขึ้น 28 เท่า:
def enrich_offers(offers, session):
for i, offer in enumerate(offers, 1):
try:
resp = session.get(offer['url'], headers=HEADERS, timeout=30)
if resp.status_code == 200:
offer.update(parse_offer_page(resp.text))
else:
print('card', offer['offer_id'], 'สถานะ', resp.status_code)
except requests.RequestException as e:
print('ข้อผิดพลาดเครือข่ายที่', offer['offer_id'], e)
if i % 10 == 0:
print('ประมวลผล card แล้ว:', i)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return offers- ในบล็อก if __name__ หลัง collect_listing ให้เพิ่มการเรียก enrich_offers(data, requests.Session()) และรันสคริปต์ใหม่ด้วย MAX_PAGES เท่ากับ 1 เพื่อไม่ต้องรอนาน
ใช้เวลานานแค่ไหน: 28 card ที่หน่วงเฉลี่ย 6 วินาทีคือประมาณ 3 นาที การเก็บเต็มรูปแบบ 5 หน้าผลการค้นหาพร้อม card จะใช้เวลาประมาณ 15 นาที ซึ่งเป็นเรื่องปกติ parser อสังหาฯ ไม่ควรเร็ว แตควรเสถียร
เคล็ดลับ: อย่า parse card ซ้ำทุกครั้งที่รัน คุณสมบัติของคอนโดไม่เปลี่ยน: พื้นที่และปีที่สร้างเก็บครั้งเดียวก็พอ สิ่งที่เปลี่ยนคือราคา ซึ่งมีอยู่ในผลการค้นหาแล้ว ในขั้นตอนที่หกเราจะทำให้ card ถูกเรียกเฉพาะประกาศใหม่ ซึ่งจะลดจำนวนคำขอลงหลายสิบเท่า
ตรวจสอบ: ในผลลัพธ์ dictionary มีคีย์ area, floor, floors_total และ build_year พร้อมค่าที่สมเหตุสมผล: พื้นที่ 15 ถึง 200 ชั้นไม่เกินจำนวนชั้น ปี 1900 ถึง 2026 ถ้าบางประกาศไม่มีฟิลด์ ก็เป็นเรื่องปกติ เพราะผู้ขายบางรายไม่ได้กรอกปีที่สร้าง
ปัญหาที่อาจเกิดขึ้น
Regular expression เจอพื้นที่ห้องแทนพื้นที่รวม สาเหตุ: ใน JSON มีคีย์คล้ายกันเช่น livingArea หรือ kitchenArea วิธีแก้: ปรับรูปแบบโดยเพิ่มเครื่องหมายคำพูดหรือจุดคู่ก่อนคีย์ เพื่อไม่ให้ตรงกับส่วนของคำอื่น
ขั้นตอนที่ 5: ต่อ mobile proxy และทำให้การเก็บเสถียร
เป้าหมายของขั้นนี้: กระจายคำขอผ่าน mobile proxy พร้อมหมุน IP เพิ่มการลองซ้ำ และจัดการการตอบกลับอย่างถูกต้อง หลังขั้นนี้ parser จะทำงานเป็นประจำและยาวนานได้ โดยไม่สร้างภาระมากจากที่อยู่เดียว
ทำไม parser อสังหาฯ ต้องใช้ mobile proxy
เว็บใหญ่ทุกแห่งจำกัดความถี่คำขอจาก IP เดียว นี่คือการป้องกันการโอเวอร์โหลด และมันทำงานกับการ自动化ทุกประเภท ที่อยู่บ้านหลังผ่านคำขอไปหลายร้อยครั้งจะเริ่มได้รับการตอบกลับ 429 หรือหน้าที่มีการตรวจสอบ Mobile proxy แก้ปัญหาต่างออกไป: คุณได้ IP จาก pool ของผู้ให้บริการมือถือ และที่อยู่เปลี่ยนตามคำสั่งหรือตามเวลา คำขอของคุณกระจายไปหลายที่อยู่ ภาระในแต่ละที่อยู่ยังต่ำ และ parser ทำงานราบรื่น สำหรับการติดตามราคาเป็นประจำ นี่คือหลักการ: คุณต้องการไม่ใช่ข้อมูลครั้งเดียว แต่เป็นข้อมูลรายวันต่อเนื่องหลายเดือน
การตั้งค่า
- เปิดหน้าสมาชิกของบริการ mobile proxy และหาพร็อกซีที่ซื้อมา คัดลอกสี่ค่า: โฮสต์ พอร์ต ชื่อผู้ใช้ รหัสผ่าน และคัดลอกลิงก์เปลี่ยน IP ซึ่งมักเป็นที่อยู่ที่มีคีย์ เมื่อเรียกมันพร็อกซีจะได้ที่อยู่ใหม่
- เพิ่มพารามิเตอร์พร็อกซีใน config.py อย่าเขียนรหัสผ่านในโค้ดที่คุณเผยแพร่ที่ไหน ให้เก็บในไฟล์แยกหรือ environment variable:
PROXY_HOST = 'ваш_хост'
PROXY_PORT = 'ваш_порт'
PROXY_USER = 'ваш_логин'
PROXY_PASS = 'ваш_пароль'
ROTATE_URL = 'ссылка_для_смены_ip'
ROTATE_EVERY = 25- เพิ่มฟังก์ชันสร้าง session พร้อมพร็อกซีใน parser.py ไลบรารี requests รับ dictionary ที่มีที่อยู่สำหรับ http และ https:
from config import PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS, ROTATE_URL, ROTATE_EVERY
def make_session():
session = requests.Session()
proxy_url = 'http://' + PROXY_USER + ':' + PROXY_PASS + '@' + PROXY_HOST + ':' + PROXY_PORT
session.proxies = {'http': proxy_url, 'https': proxy_url}
return session
def rotate_ip():
try:
r = requests.get(ROTATE_URL, timeout=20)
print('เปลี่ยน IP:', r.status_code)
time.sleep(5)
except requests.RequestException as e:
print('เปลี่ยน IP ไม่สำเร็จ:', e)- ตรวจสอบว่าพร็อกซีทำงาน สร้างไฟล์ชั่วคราว check_proxy.py ที่มีโค้ดขอที่อยู่ IP ผ่าน session และพิมพ์ผลลัพธ์:
from parser import make_session
s = make_session()
print(s.get('https://api.ipify.org', timeout=20).text)- รันมัน คุณควรเห็น IP ที่ไม่ใช่ที่อยู่บ้านของคุณ เรียก rotate_ip แล้วรันการตรวจสอบอีกครั้ง ที่อยู่ควรเปลี่ยน
- ตอนนี้เพิ่มฟังก์ชันคำขอพร้อมลองซ้ำ มันจัดการสามสถานการณ์: ตอบกลับสำเร็จ, ตอบกลับ 429 หรือ 403 (ต้องรอและเปลี่ยนที่อยู่), ข้อผิดพลาดเครือข่าย (ลองใหม่):
def safe_get(session, url, params=None, retries=3):
for attempt in range(1, retries + 1):
try:
resp = session.get(url, params=params, headers=HEADERS, timeout=30)
if resp.status_code == 200:
return resp
if resp.status_code in (429, 403):
print('สถานะ', resp.status_code, 'ครั้งที่', attempt, 'เปลี่ยน IP และรอ')
rotate_ip()
time.sleep(30 * attempt)
continue
print('สถานะที่ไม่คาดคิด', resp.status_code)
return None
except requests.RequestException as e:
print('ข้อผิดพลาดเครือข่าย', e, 'ครั้งที่', attempt)
time.sleep(10 * attempt)
return None- แทนที่การเรียก session.get ใน fetch_page และ enrich_offers ด้วย safe_get เพิ่มตัวนับใน enrich_offers: ทุก ROTATE_EVERY คำขอให้เรียก rotate_ip นี่คือการหมุนตามแผนที่ทำให้ที่อยู่ไม่สะสมคำขอมากเกินไป
ข้อควรระวัง: ถ้าคุณได้รับการตอบกลับ 429 หรือหน้าที่มีการตรวจสอบ อย่าพยายามเจาะด้วยการลองซ้ำบ่อย ๆ มันจะยิ่งทำให้สถานการณ์แย่ลงสำหรับที่อยู่ปัจจุบัน ปฏิกิริยาที่ถูกต้องคือ: หยุด เพิ่มการหน่วง เปลี่ยน IP แล้วทำต่อในจังหวะที่สงบ parser ที่เคารพขีดจำกัดของเว็บจะอยู่ได้นานกว่าและเก็บได้มากกว่า
เคล็ดลับ: ใช้ช่องพร็อกซีหนึ่งช่องต่อหนึ่ง thread ของการ parse การอยากรันสิบ thread ผ่านที่อยู่เดียวนั้นน่าสนใจ แต่มันคือทางตรงไปสู่การจำกัด ถ้าต้องการความเร็ว ให้ซื้อหลายช่องและกระจายภูมิภาคหรือตัวกรองต่าง ๆ ไปตามช่องเหล่านั้น
ตรวจสอบ: check_proxy.py แสดงที่อยู่ของผู้ให้บริการมือถือ หลังหมุนที่อยู่เปลี่ยน parser ผ่านสองหน้าผลการค้นหาพร้อม card โดยไม่มีสถานะ 429 สักครั้ง ในล็อกเห็นบรรทัด เปลี่ยน IP: 200 ทุก 25 card
ปัญหาที่อาจเกิดขึ้น
ข้อผิดพลาด ProxyError หรือ 407 สาเหตุ: ชื่อผู้ใช้หรือรหัสผ่านผิด หรือพอร์ตไม่ถูก วิธีแก้: ตรวจสอบข้อมูลในหน้าสมาชิก ตรวจสอบว่าคุณใช้พอร์ตสำหรับ HTTP proxy ไม่ใช่ SOCKS ถ้าเป็น SOCKS5 ให้ติดตั้งไลบรารี pysocks และใช้คำนำหน้า socks5h แทน http ใน proxy_url
หลังหมุนที่อยู่ไม่เปลี่ยน สาเหตุ: ผู้ให้บริการให้ที่อยู่เดิมหรือการหมุนยังไม่生效 วิธีแก้: เพิ่มการหน่วงหลัง rotate_ip เป็น 10 วินาที และตรวจสอบว่าอัตราการเปลี่ยน IP ในแพ็กเกจถูกจำกัดหรือไม่
ขั้นตอนที่ 6: บันทึกข้อมูลและสร้างประวัติราคา
เป้าหมายของขั้นนี้: เปลี่ยน parser จากการพิมพ์ในคอนโซลเป็นการเขียนลงฐานข้อมูล SQLite เพื่อให้ทุกครั้งที่รันเพิ่มจุดใหม่ในประวัติราคา ไม่ใช่เขียนทับของเก่า นี่คือหัวใจของโปรเจกต์ทั้งหมด
ออกแบบตาราง
เราต้องการสองตาราง ตารางแรก offers เก็บประกาศ: หนึ่งแถวต่อประกาศพร้อมคุณสมบัติและวันที่ ตารางที่สอง prices เก็บราคาตามวันที่: หลายแถวต่อประกาศ การแยกจำเป็นเพื่อไม่ให้พื้นที่และที่อยู่ซ้ำทุกครั้งที่บันทึกราคา
- สร้างไฟล์ storage.py และอธิบายการสร้างตาราง:
import sqlite3
from datetime import date
from config import DB_PATH
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.execute('CREATE TABLE IF NOT EXISTS offers (offer_id TEXT PRIMARY KEY, url TEXT, title TEXT, address TEXT, area REAL, floor INTEGER, floors_total INTEGER, build_year INTEGER, first_seen TEXT, last_seen TEXT, is_active INTEGER DEFAULT 1)')
conn.execute('CREATE TABLE IF NOT EXISTS prices (offer_id TEXT, checked_on TEXT, price INTEGER, PRIMARY KEY (offer_id, checked_on))')
conn.commit()
return conn- เพิ่มฟังก์ชันที่คืนเซ็ตของ offer_id ที่รู้จักแล้ว เพื่อให้เรียก card เฉพาะประกาศใหม่:
def known_ids(conn):
rows = conn.execute('SELECT offer_id FROM offers').fetchall()
return set(r[0] for r in rows)- เขียนฟังก์ชันบันทึก สำหรับประกาศใหม่ แทรกแถวใน offers สำหรับทุกประกาศ อัปเดต last_seen และบันทึกราคาของวันนี้ โครงสร้าง INSERT OR REPLACE ใน prices หมายถึง: ถ้าวันนี้บันทึกราคาแล้ว ให้อัปเดต ถ้าไม่ให้เพิ่ม:
def save_offers(conn, offers):
today = date.today().isoformat()
for o in offers:
conn.execute('INSERT OR IGNORE INTO offers (offer_id, url, title, address, area, floor, floors_total, build_year, first_seen, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (o['offer_id'], o['url'], o.get('title'), o.get('address'), o.get('area'), o.get('floor'), o.get('floors_total'), o.get('build_year'), today, today))
conn.execute('UPDATE offers SET last_seen = ?, is_active = 1 WHERE offer_id = ?', (today, o['offer_id']))
if o.get('price'):
conn.execute('INSERT OR REPLACE INTO prices (offer_id, checked_on, price) VALUES (?, ?, ?)', (o['offer_id'], today, o['price']))
conn.commit()- เพิ่มฟังก์ชันที่ทำเครื่องหมายประกาศที่ถูกถอด ถ้าประกาศไม่ปรากฏในผลการค้นหาเกินสามวัน ถือว่าไม่ فعالแล้ว นี่ให้ข้อมูลเกี่ยวกับความเร็วในการขาย:
def mark_inactive(conn, days=3):
conn.execute('UPDATE offers SET is_active = 0 WHERE julianday(?) - julianday(last_seen) > ?', (date.today().isoformat(), days))
conn.commit()- เขียนบล็อกหลักของ parser.py ใหม่เพื่อเก็บผลการค้นหา ระบุประกาศใหม่ เสริมเฉพาะอันใหม่ และบันทึกทั้งหมด:
from storage import get_conn, known_ids, save_offers, mark_inactive
if __name__ == '__main__':
conn = get_conn()
session = make_session()
listing = collect_listing(session)
old = known_ids(conn)
new_offers = [o for o in listing if o['offer_id'] not in old]
print('ประกาศใหม่:', len(new_offers), 'จาก', len(listing))
enrich_offers(new_offers, session)
save_offers(conn, listing)
mark_inactive(conn)
print('บันทึกแล้ว รวมในฐานข้อมูล:', conn.execute('SELECT COUNT(*) FROM offers').fetchone()[0])- อย่าลืมเปลี่ยน collect_listing ให้รับ session เป็นพารามิเตอร์ ไม่ใช่สร้างเอง รันสคริปต์ จะมีไฟล์ realty.db ปรากฏในโฟลเดอร์โปรเจกต์
ดูประวัติราคา
สร้างไฟล์ report.py ที่ส่งออกการเปลี่ยนแปลงราคาเป็น Excel คำค้นหาด้านล่างหาประกาศที่ราคาล่าสุดต่างจากราคาแรก:
import sqlite3
import pandas as pd
from config import DB_PATH
conn = sqlite3.connect(DB_PATH)
query = 'SELECT o.offer_id, o.address, o.area, o.url, MIN(p.checked_on) AS first_date, MAX(p.checked_on) AS last_date, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on ASC LIMIT 1) AS first_price, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on DESC LIMIT 1) AS last_price FROM offers o JOIN prices p ON p.offer_id = o.offer_id GROUP BY o.offer_id'
df = pd.read_sql(query, conn)
df['change_pct'] = ((df['last_price'] - df['first_price']) / df['first_price'] * 100).round(1)
df['price_per_m2'] = (df['last_price'] / df['area']).round(0)
df = df.sort_values('change_pct')
df.to_excel('report.xlsx', index=False)
print(df.head(10))หลังการรันครั้งแรก คอลัมน์ change_pct จะเป็นศูนย์ เพราะยังไม่มีประวัติ ตั้งแต่วันที่สองจะเริ่มมีการเปลี่ยนแปลง ผ่านสองสัปดาห์คุณจะเห็นภาพ: มีกี่ประกาศที่ลดราคา เฉลี่ยลดเท่าไร ย่านไหนเคลื่อนไหวเร็วที่สุด
เคล็ดลับ: เพิ่มการคัดลอกฐานข้อมูลท้าย parser.py: shutil.copy(DB_PATH, 'backup_' + date.today().isoformat() + '.db') โค้ดหนึ่งบรรทัดปกป้องข้อมูลที่สะสมมาหลายสัปดาห์ เดือนละครั้งลบสำเนาเก่า เก็บไว้ห้าอันล่าสุด
ตรวจสอบ: ไฟล์ realty.db มีอยู่ report.xlsx เปิดใน Excel ได้ มีคอลัมน์ที่อยู่ พื้นที่ ราคา และราคาต่อตารางเมตร รัน parser.py อีกครั้งหลังผ่านไปสองสามนาที บรรทัด ประกาศใหม่ ควรแสดง 0 หรือจำนวนน้อย และ card ไม่ควรถูกเรียกซ้ำ นี่คือการยืนยันว่าการกันซ้ำทำงาน
ปัญหาที่อาจเกิดขึ้น
ข้อผิดพลาด database is locked สาเหตุ: ฐานข้อมูลเปิดในโปรแกรมอื่น เช่น ตัวดู SQLite หรือมีสองอินสแตนซ์ของสคริปต์ทำงานพร้อมกัน วิธีแก้: ปิดโปรแกรมเกินจำเป็นและอย่ารันสคริปต์ซ้อนตัวเอง
ใน report.xlsx ทุกประกาศแสดงวันที่เดียวกัน สาเหตุ: สคริปต์รันเพียงวันเดียว เป็นเรื่องคาดเดาได้ แค่รอการรันครั้งต่อไป
ขั้นตอนที่ 7: ตั้งเวลารันอัตโนมัติและขยายไปยังเว็บอื่น
เป้าหมายของขั้นนี้: ทำให้ parser รันเองทุกเช้า และเตรียมโค้ดให้ต่อเว็บอสังหาฯ อื่นได้ ประวัติราคามีค่าเมื่อทำเป็นประจำ การอัตโนมัติจึงจำเป็น
ตารางเวลารัน
- บน Windows เปิด Task Scheduler ผ่านการค้นหาในเมนู Start คลิก Create Basic Task ใส่ชื่อ เช่น Parser อสังหาริมทรัพย์
- เลือกทริกเกอร์ Daily กำหนดเวลา เช่น 07:30 เช้าตรู่สะดวก เพราะภาระบนเว็บน้อยที่สุด และคุณมีข้อมูลสดก่อนเริ่มงาน
- ใน Action เลือก Start a program ในช่อง Program ใส่พาธเต็มไปยัง python.exe ภายในโฟลเดอร์ venv เช่น C:\projects\cian_parser\venv\Scripts\python.exe ในช่อง Add arguments ใส่ parser.py ในช่อง Start in ใส่โฟลเดอร์โปรเจกต์
- บันทึกงานและคลิก Run ในแผงขวาเพื่อตรวจสอบ ฐานข้อมูลในโฟลเดอร์โปรเจกต์ควรอัปเดต
- บน macOS และ Linux ใช้ cron พิมพ์ใน terminal crontab -e และเพิ่มบรรทัด:
30 7 * * * cd /путь/к/cian_parser && ./venv/bin/python parser.py >> run.log 2>&1ล็อกการรันทั้งหมดจะสะสมใน run.log
เตรียมพร้อมสำหรับเว็บอื่น
Domclick, Yandex Realty, Metr kvadratny และพอร์ทัลภูมิภาคทำงานคล้ายกัน แต่ selector และพารามิเตอร์ตัวกรองต่างกัน เพื่อไม่ต้องเขียน parser ใหม่ทุกเว็บ ให้แยกสิ่งที่แตกต่างออกเป็นโมดูลแยก
- สร้างโฟลเดอร์ sites ในโปรเจกต์ สร้างไฟล์ cian.py ข้างใน และย้ายฟังก์ชัน fetch_page และ parse_cards พร้อมพารามิเตอร์ค้นหาไปไว้ที่นั่น ปล่อยอินเทอร์เฟซให้เหมือนกัน: ฟังก์ชัน parse_cards รับ HTML และคืนรายการ dictionary ที่มีคีย์เดียวกัน offer_id, url, title, price, address
- สำหรับเว็บใหม่ สร้างไฟล์ เช่น domclick.py และทำการสำรวจจากขั้นตอนที่สองซ้ำ: เปิดผลการค้นหา หา card ราคา ลิงก์ และพารามิเตอร์ pagination เขียน fetch_page และ parse_cards เวอร์ชันของคุณเอง
- ใน offer_id ให้เติมคำนำหน้าของเว็บ เช่น cian_312456789 และ domclick_98765 ไม่งั้นรหัสจากเว็บต่างกันอาจซ้ำและปนประวัติกัน
- ใน parser.py นำเข้าโมดูลจาก sites และรันการเก็บแต่ละอันในลูป ตารางในฐานข้อมูลใช้ร่วมกัน: สคีมาเดียวสำหรับทุกแหล่ง และคอลัมน์ source บอกว่าประกาศมาจากไหน
หมายเหตุสำคัญเกี่ยวกับ Domclick และ Yandex Realty: เว็บเหล่านี้ใช้ API ภายในรูปแบบ JSON อย่างจริงจัง ซึ่งเห็นได้ในแท็บ Network ผลการค้นหาของมันมักสะดวกต่อการ parse มากกว่า HTML แต่โครงสร้างการตอบกลับเปลี่ยนบ่อยกว่า ตรวจสอบ selector และฟิลด์เดือนละครั้ง
เคล็ดลับ: สำหรับประกาศเดียวกันที่ลงในหลายเว็บ ราคาอาจต่างกัน การเปรียบเทียบคู่แบบนี้ให้ข้อมูลวิเคราะห์ที่น่าสนใจ และช่วยหาผู้ขายที่ลดราคาที่หนึ่งแต่ลืมอัปเดตที่อื่น จับคู่ประกาศได้จากที่อยู่ พื้นที่ และชั้น
ตรวจสอบ: งานใน Task Scheduler รันด้วยมือโดยไม่มีข้อผิดพลาด ใน run.log หรือประวัติของ Task Scheduler เห็นเครื่องหมายสำเร็จ โครงสร้างโฟลเดอร์มี sites ที่มีอย่างน้อยหนึ่งโมดูล parser รันจาก root ของโปรเจกต์และทำงานเหมือนเดิม
ปัญหาที่อาจเกิดขึ้น
Task Scheduler บอกว่างานเสร็จ แต่ฐานข้อมูลไม่อัปเดต สาเหตุ: สคริปต์รันจาก working directory อื่นและสร้างฐานข้อมูลว่างใหม่ที่อื่น วิธีแก้: กรอกช่อง Start in ในงาน หรือใช้พาธแบบ absolute ไปยังฐานข้อมูลใน config.py
ตรวจสอบผลลัพธ์: เช็กลิสต์ parser ที่พร้อมใช้
ไล่ตามรายการและติ๊กทุกข้อ ถ้าทำครบทุกข้อ คุณมี parser CIAN ที่สมบูรณ์พร้อมประวัติราคา
- สคริปต์ parser.py รันด้วยคำสั่งจาก environment ที่เปิดใช้งานโดยไม่มีข้อผิดพลาดการนำเข้า
- ผลการค้นหาถูกเก็บจากหลายหน้า จำนวนประกาศต่อหน้าตรงกับที่เห็นในเบราว์เซอร์
- ราคาถูกบันทึกเป็นจำนวนเต็ม ที่อยู่อ่านได้ ลิงก์เปิดได้
- Card ถูกเรียกเฉพาะประกาศใหม่ ในล็อกเห็นบรรทัด ประกาศใหม่ ที่มีจำนวนลดลงเมื่อรันซ้ำ
- คำขอผ่าน mobile proxy, check_proxy.py แสดงที่อยู่ของผู้ให้บริการ, การหมุนเปลี่ยนมันได้
- เมื่อได้สถานะ 429 parser หยุดและเปลี่ยน IP ไม่ล้ม
- ไฟล์ realty.db โตขึ้น ตาราง prices ได้แถวใหม่ทุกวัน
- report.xlsx สร้างและเปิดได้ ผ่านหลายวันจะมีการเปลี่ยนแปลงราคาที่ไม่เป็นศูนย์
- การรันอัตโนมัติตั้งไว้แล้ว ล็อกบันทึกทุกครั้ง
- สำเนาสำรองของฐานข้อมูลถูกสร้างอัตโนมัติ
วิธีทดสอบทั้งหมด
- ลบหรือเปลี่ยนชื่อ realty.db เพื่อเริ่มจากศูนย์
- ตั้ง MAX_PAGES เท่ากับ 2 และรัน parser.py จับเวลา: ควรใช้เวลาประมาณ 6-8 นาทีรวม card
- รัน report.py และตรวจสอบว่าในรายงานมีประมาณ 56 แถว
- เปิด realty.db ด้วยโปรแกรมดู SQLite ใด ๆ หรือรันคำสั่งใน Python SELECT COUNT(*) FROM prices จำนวนควรตรงกับจำนวนประกาศ
- รัน parser.py อีกครั้ง เวลารันควรลดลงเหลือหนึ่งนาที เพราะ card ไม่ถูกเรียก จำนวนแถวใน prices ไม่เปลี่ยนเพราะวันที่เดิม
- เปลี่ยนราคาของประกาศหนึ่งในฐานข้อมูลด้วยมือเป็นตัวเลขอื่น เปลี่ยนวันที่บันทึกเป็นเมื่อวาน แล้วรัน parser ใน report.xlsx ประกาศนี้ควรแสดงการเปลี่ยนแปลงราคา วิธีนี้ทำให้คุณมั่นใจว่าลอจิกประวัติทำงาน โดยไม่ต้องรอการเปลี่ยนแปลงจริง
ตัวชี้วัดความสำเร็จ
สัดส่วนประกาศที่มีพื้นที่กรอกสูงกว่า 90 เปอร์เซ็นต์ สัดส่วนคำขอที่ได้สถานะ 200 สูงกว่า 97 เปอร์เซ็นต์ ไม่มีข้อยกเว้นที่ไม่ได้จัดการสักครั้งต่อการรัน เวลารันเต็มรูปแบบคาดเดาได้และไม่เพิ่มขึ้นจากการรันครั้งก่อน ถ้าตัวชี้วัดต่ำกว่านี้ ให้กลับไปที่ส่วนข้อผิดพลาดทั่วไป
ข้อผิดพลาดทั่วไปและวิธีแก้
เรารวบรวมปัญหาที่เกือบทุกคนที่เขียน parser ประกาศอสังหาฯ ครั้งแรกต้องเจอ รูปแบบ: ปัญหา สาเหตุ วิธีแก้
Parser คืนค่า 0 ประกาศ ทั้งที่เมื่อวานทำงานได้
สาเหตุ: เว็บอัปเดตเลย์เอาต์และเปลี่ยนแอตทริบิวต์ data-mark หรือ data-name วิธีแก้: เปิดผลการค้นหาในเบราว์เซอร์ ทำขั้นตอนที่สองซ้ำ และอัปเดต selector สร้างนิสัยเก็บ selector ไว้ที่เดียวตอนต้นโมดูล เพื่อแก้ไขใช้เวลาไม่ถึงนาที เพิ่มการตรวจสอบใน parser: ถ้าหน้าแรกได้ 0 ประกาศ ให้ส่งการแจ้งเตือนไปยัง messenger
ราคาถูกบันทึกผิดเป็นพันเท่า
สาเหตุ: บางประกาศระบุราคาเป็นพันหรือมีป้าย ต่อเดือน หรือข้อความรวมราคาต่อตารางเมตร วิธีแก้: ตรวจสอบว่าคุณดึง MainPrice ไม่ใช่องค์ประกอบข้าง ๆ ที่มีราคาต่อเมตร เพิ่มการตรวจสอบความสมเหตุสมผล: ราคาคอนโดในมอสโกต่ำกว่าหนึ่งล้านรูเบิลเกือบแน่นอนว่าเป็นข้อผิดพลาดในการ parse ให้บันทึกกรณีเหล่านี้ในล็อก
ได้สถานะ 429 ตั้งแต่หน้าที่สาม
สาเหตุ: การหน่วงสั้นเกินไป หรือยังไม่ได้ต่อพร็อกซี คำขอทั้งหมดมาจาก IP บ้านเดียว วิธีแก้: เพิ่ม PAUSE_MIN และ PAUSE_MAX เป็น 6 และ 12 ต่อ mobile proxy เปิดการหมุนตามแผนทุก 20-25 คำขอ ตรวจสอบว่าคุณไม่ได้รันสคริปต์หลายอินสแตนซ์พร้อมกัน
ข้อผิดพลาด UnicodeEncodeError เมื่อพิมพ์ในคอนโซล Windows
สาเหตุ: คอนโซล Windows มาตรฐานไม่แสดงตัวอักษรรัสเซียได้ถูกต้องเสมอ วิธีแก้: รัน chcp 65001 ใน terminal ก่อนเริ่ม หรือเพิ่มบรรทัด sys.stdout.reconfigure(encoding='utf-8') ที่ต้นสคริปต์ หรือเขียนล็อกลงไฟล์แทนคอนโซล
ที่อยู่ถูกเก็บไม่ครบหรือซ้ำ
สาเหตุ: ที่อยู่บน card ประกอบจากลิงก์ GeoLabel หลายอัน บางอันซ้ำเมืองและเขต วิธีแก้: ลบรายการซ้ำโดยรักษาลำดับ หรือดึงที่อยู่จากหน้ารายละเอียดประกาศซึ่งแสดงเป็นสตริงเดียว สำหรับการวิเคราะห์ตามย่าน เพิ่มฟิลด์ district แยก โดยตัดจากที่อยู่ตามรายการย่านที่รู้จัก
Parser ทำงานเมื่อรันด้วยมือ แต่ไม่ทำงานใน Task Scheduler
สาเหตุ: Task Scheduler ใช้ Python interpreter อื่นที่ไม่มีไลบรารีติดตั้ง หรือ working directory อื่น วิธีแก้: ระบุพาธแบบ absolute ไปยัง python.exe ภายใน venv และกรอกช่อง working directory ส่งออกไปยังไฟล์ล็อกเพื่อดูข้อผิดพลาด
ฐานข้อมูลมีขนาดหลายกิกะไบต์ในหนึ่งเดือน
สาเหตุ: คุณบันทึก HTML เต็มของหน้าหรือทุกฟิลด์ JSON ลงฐานข้อมูล วิธีแก้: เก็บเฉพาะฟิลด์ที่จำเป็น ถ้าต้องการเก็บหน้าต้นฉบับเพื่อ parse ซ้ำ ให้เก็บเป็นไฟล์บีบอัดบนดิสก์ ไม่ใช่ใน SQLite ไตรมาสละครั้งรันคำสั่ง VACUUM เพื่อกระชับฐานข้อมูล
ประกาศเดียวกันซ้ำภายใต้รหัสต่างกัน
สาเหตุ: ผู้ขายถอดประกาศและลงใหม่ ได้เลขใหม่ วิธีแก้: เพิ่มคีย์จับคู่เพิ่มเติมจากที่อยู่ พื้นที่ และชั้น ประกาศที่มีคีย์เดียวกันแต่ offer_id ต่างกันสามารถเชื่อมในตารางแยกและนับประวัติราคาตามคู่ นี่คือการวิเคราะห์ขั้นสูง แต่เป็นสิ่งที่แสดงการลดราคาจริงที่ซ่อนอยู่หลังการลงใหม่
ความสามารถเพิ่มเติมสำหรับผู้สูง
Parser พื้นฐานพร้อมแล้ว ถ้าต้องการมากกว่านี่คือทิศทางที่ให้ผลตอบแทนสูงสุด
Browser automation ด้วย Playwright
บางหน้าโหลดข้อมูลด้วยสคริปต์หลังโหลด และ requests ได้แค่โครงเปล่า ในกรณีนั้นใช้ Playwright: มันควบคุมเบราว์เซอร์จริง ติดตั้งด้วยคำสั่ง pip install playwright และ playwright install chromium พร็อกซีส่งเมื่อเปิดเบราว์เซอร์ในพารามิเตอร์ proxy เป็น dictionary ที่มี server, username, password รอให้ card ปรากฏผ่าน page.wait_for_selector และส่ง page.content() ไปยังฟังก์ชัน parse_cards ที่เขียนไว้แล้ว จำไวว่าเบราว์เซอร์ใช้ทรัพยากรมากกว่าสิบเท่า จึงใช้เฉพาะกับหน้าที่มีปัญหา
เก็บข้อมูลขนานผ่านหลายช่องพร็อกซี
ถ้าต้องเก็บหลายภูมิภาค ให้ซื้อ mobile proxy แยกสำหรับแต่ละภูมิภาคและรันกระบวนการแยกต่อช่อง อย่าใช้ multithreading ภายในช่องเดียว เพราะความหมายของการกระจายภาระหายไป วิธีง่าย ๆ: พารามิเตอร์ภูมิภาคส่งให้สคริปต์เป็นอาร์กิวเมนต์บรรทัดคำสั่ง และ Task Scheduler รันหลายงานด้วยอาร์กิวเมนต์ต่างกันและเหลื่อมเวลาเล็กน้อย
การแจ้งเตือนเมื่อราคาลด
เพิ่มการเปรียบเทียบราคาวันนี้กับก่อนหน้าสำหรับแต่ละประกาศท้าย parser ถ้าลดเกินเกณฑ์ที่กำหนด เช่น 3 เปอร์เซ็นต์ ให้สร้างข้อความที่มีที่อยู่ ราคาเก่าและใหม่ ลิงก์ และส่งให้ตัวเองผ่านบอทใน messenger วิธีนี้เปลี่ยน parser จากเครื่องมือวิเคราะห์เป็นเครื่องมือปฏิบัติ: คุณจะรู้ข่าวประกาศราคาดีภายในหนึ่งชั่วโมงหลังการเปลี่ยนแปลง
การวิเคราะห์และการแสดงภาพ
ด้วย pandas คุณสามารถจัดกลุ่มข้อมูลตามย่านและคำนวณราคาต่อตารางเมตรมัธยฐาน สัดส่วนประกาศที่ลดราคา เวลาเปิดขายเฉลี่ย (ผลต่างระหว่าง first_seen และ last_seen สำหรับประกาศที่ไม่ فعال) ไลบรารี matplotlib สร้างกราฟแนวโน้มรายเดือนได้ในสามบรรทัด อัปโหลดรายงานไป Google Sheets เพื่อให้เพื่อนร่วมงานที่ไม่มีทักษะเขียนโปรแกรมได้แผงข้อมูลสด
จัดเก็บใน PostgreSQL
เมื่อประกาศเกินแสนรายการ SQLite จะเริ่มช้าลงกับคำค้นหาเชิงวิเคราะห์ การย้ายไป PostgreSQL นั้นง่าย: สคีมาตารางเหมือนเดิม เปลี่ยนแค่สตริงการเชื่อมต่อและไลบรารี (psycopg2 แทน sqlite3) ทำเมื่อจำเป็นจริงเท่านั้น: สำหรับหนึ่งเมือง SQLite เพียงพอไปหลายปี
ติดตามสุขภาพของ parser
บันทึกในตารางแยกชื่อ runs เวลาเริ่ม เวลาสิ้นสุด จำนวนประกาศที่เก็บ จำนวนข้อผิดพลาด และจำนวนการหมุน IP ถ้าจำนวนประกาศลดลงกะทันหันหรือข้อผิดพลาดเกิน 5 เปอร์เซ็นต์ ให้ส่งการแจ้งเตือน การติดตามแบบนี้ช่วยให้เห็นการเปลี่ยนเลย์เอาต์ในวันที่เกิดขึ้น ไม่ใช่ผ่านสองสัปดาห์เมื่อรายงานว่างเปล่า
FAQ: คำถามที่พบบ่อยเกี่ยวกับการสร้าง parser อสังหาริมทรัพย์
การ scrape CIAN และเว็บอื่นผิดกฎหมายหรือไม่
การเก็บข้อมูลสาธารณะของประกาศเพื่อการวิเคราะห์ส่วนตัวโดยทั่วไปถือว่ายอมรับได้ แต่เงื่อนไขของแต่ละเว็บระบุในข้อตกลงผู้ใช้ และคุณต้องอ่านมัน สิ่งที่ห้ามเด็ดขาดคือเก็บข้อมูลส่วนบุคคลของผู้ขาย เผยแพร่ฐานข้อมูลที่คัดลอกมาเป็นของตัวเอง และสร้างภาระที่รบกวนการทำงานของบริการ ถ้าวางแผนใช้ข้อมูลเชิงพาณิชย์ ให้ปรึกษาทนายความ
ทำไมต้องใช้ mobile proxy ไม่ใช่ proxy เซิร์ฟเวอร์
ที่อยู่ของผู้ให้บริการมือถือใช้ร่วมกันโดยผู้ใช้จริงหลายพันคนและเปลี่ยนอยู่ตลอด เว็บปฏิบัติต่อพวกมันอย่างผ่อนปรนกว่าที่อยู่ data center ซึ่งผู้ซื้อจริงแทบไม่เข้าจากที่นั่น บวกกับความสามารถเปลี่ยน IP ผ่านลิงก์ให้การหมุนที่ควบคุมได้โดยไม่ต้องซื้อที่อยู่หลายร้อยอัน
ควรรัน parser บ่อยแค่ไหนสำหรับประวัติราคา
วันละครั้งเหมาะสมที่สุด ราคาอสังหาฯ เปลี่ยนไม่บ่อย การเก็บบ่อยกว่าวันละครั้งไม่ค่อยมีประโยชน์ และภาระเพิ่มขึ้น ถ้าต้องจับการลดราคาอย่างรวดเร็ว ให้รันวันละสองครั้งเช้าและเย็น แต่เฉพาะกับตัวกรองแคบ ๆ
เก็บได้กี่ประกาศต่อวันผ่านพร็อกซีหนึ่งช่อง
ที่การหน่วง 4-9 วินาทีและการหมุนตามแผน ประมาณ 500-700 คำขอต่อชั่วโมงไม่มีปัญหา หรือ 8,000-12,000 ต่อวันหากทำงานตลอด 24 ชั่วโมง สำหรับการติดตามหนึ่งเมืองมักเพียงพอ 2,000-3,000 คำขอต่อวัน เพราะ card ถูกเรียกเฉพาะประกาศใหม่
ทำอย่างไรถ้า selector เปลี่ยนและฉันหาไม่เจอ
กลับไปที่ขั้นตอนที่สองและเริ่มจากราคา: คลิกขวาที่ราคา ตรวจสอบ ไล่ขึ้นไปบนต้นไม้จนถึง card หาแอตทริบิวต์ที่มีคำว่า data และชื่อที่มีความหมาย พวกมันเสถียรกว่าคลาสที่มีตัวอักษรสุ่ม ตรวจสอบแท็บ Network ด้วย: บางทีข้อมูลอาจมาเป็นคำขอ JSON แยก และการ parse จะง่ายกว่าเดิม
ทำได้ไหมโดยไม่ใช้พร็อกซีสำหรับโปรเจกต์เล็ก
สำหรับเก็บครั้งเดียวสองสามหน้าได้ แต่สำหรับการติดตามรายวันที่มีคำขอหลายร้อยครั้ง ที่อยู่บ้านจะเริ่มถูกจำกัดอย่างรวดเร็ว และข้อมูลจะไม่ครบ ประวัติราคาที่มีช่องว่างจะสูญเสียคุณค่า ดังนั้นสำหรับการทำงานเป็นประจำต้องใช้พร็อกซี
จะ parse การเช่าไม่ใช่การขายได้อย่างไร
เปลี่ยนพารามิเตอร์ deal_type เป็น rent และเพิ่มประเภทการเช่าในพารามิเตอร์ค้นหา: ระยะยาวหรือรายวัน ลิงก์ประกาศจะมี rent แทน sale ดังนั้นอัปเดต selector ลิงก์ใน parse_cards ลอจิกที่เหลือรวมถึงประวัติราคาทำงานโดยไม่ต้องแก้
ต้องเก็บรูปภาพของประกาศไหม
สำหรับการวิเคราะห์ราคาไม่ต้อง รูปใช้พื้นที่มากและไม่จำเป็นต่อการคำนวณ ถ้าสร้างแคตตาล็อกสำหรับใช้ภายใน ให้เก็บเฉพาะลิงก์ไปยังรูป ไม่ใช่ไฟล์เอง
รู้ได้อย่างไรว่าประกาศถูกขายไม่ใช่แค่ถูกถอด
เว็บไม่บอกเหตุผลการถอด สัญญาณทางอ้อม: ประกาศหายจากผลการค้นหาและไม่ปรากฏอีกในหนึ่งเดือน ประกาศที่หายและกลับมาหลังหลายวันด้วยราคาต่างกันน่าจะถูกลงใหม่ เชื่อมโยงพวกมันด้วยที่อยู่และพื้นที่ ตามที่อธิบายในส่วนข้อผิดพลาด
ถ้าต้องการข้อมูลสิบเมืองทำอย่างไร
สร้างรายการภูมิภาคใน config.py และรันการเก็บในลูปโดยใช้พร็อกซีแยกช่องต่อสองสามเมือง เหลื่อมเวลารันเพื่อไม่ให้เก็บทั้งหมดพร้อมกัน ฐานข้อมูลยังใช้ร่วมกัน เพิ่มฟิลด์ภูมิภาคในตาราง offers
บทสรุป
มาดูว่าคุณทำอะไรไปแล้วบ้าง คุณเตรียมสภาพแวดล้อมด้วย Python และไลบรารี เข้าใจว่าเว็บอสังหาฯ ทำงานอย่างไร และเรียนรู้หา selector ด้วยตัวเอง เขียน parser CIAN ที่เก็บผลการค้นหาและ card ของประกาศ ต่อ mobile proxy พร้อมการหมุนและการลองซ้ำ ทำให้การเก็บเสถียรและคาดเดาได้ ออกแบบฐานข้อมูลพร้อมประวัติราคา ตั้งรายงานและการรันอัตโนมัติตามเวลา นี่คือเครื่องมือที่ใช้งานได้จริง ไม่ใช่ตัวอย่างฝึกหัด
สิ่งที่ต้องทำต่อไป: ให้ parser ทำงานสองสัปดาห์โดยไม่แก้ไข ระหว่างนั้นประวัติจะสะสม และคุณจะเห็นจุดอ่อน: สัดส่วนฟิลด์ที่กรอกลดตรงไหน ประกาศไหนซ้ำ รายงานต้องการคอลัมน์ใหม่ตรงไหน หลังจากนั้นจึงเพิ่มฟังก์ชัน จากนั้นต่อเว็บที่สองโดยใช้โครงสร้างโมดูลาร์จากขั้นตอนที่เจ็ด: คุณจะประหลาดใจว่ามันเร็วขึ้นแค่ไหนในครั้งที่สอง
ทิศทางต่อยอด: การแจ้งเตือนเมื่อราคาลดจะเปลี่ยนเครื่องมือเป็นแหล่งดีล การวิเคราะห์ตามย่านและประเภทอาคารจะทำให้คุณเป็นผู้เชี่ยวชาญตลาดที่มีตัวเลขในมือ การเชื่อมโยงประกาศที่ลงใหม่จะแสดงส่วนลดจริงที่มองไม่เห็นบนเว็บ และทัศนคติที่ใส่ใจต่อเว็บ การหน่วง การหมุนที่อยู่ผ่าน mobile proxy และการเก็บเฉพาะข้อมูลที่จำเป็น จะทำให้เครื่องมือทำงานได้หลายเดือนโดยไม่ล่ม
การ scrape อสังหาฯ ไม่ใช่เรื่องความเร็ว แต่เป็นเรื่องความสม่ำเสมอและคุณภาพข้อมูล คุณวางรากฐานที่ถูกต้องแล้ว ขอให้โชคดีในการเก็บข้อมูล และขอให้ฐานข้อมูลของคุณเติบโตทุกเช้า