پیامی داشتم که دوستی میگفت دنبال یک اسکریپت ساده هست تا بتونه فیلدهای یک سری فرم، مثل فرم سامانه فروش سایپا رو اتوماتیک پر کنه.
خب! راه حل، سرراست و ساده به نظر میرسه، به جز کپچای پایین صفحه که همیشه دردسر ساز بوده!
توی این پست قصد دارم قدم به قدم، مراحلی که برای دور زدن کپچای سایت https://saipa.iranecar.com انجام دادم رو توضیح بدم.
برای شروع کنجکاوی روی کپچا راست کلیک میکنم و inspect element رو میزنم تا ببینم اون پشت چه خبره.
اتفاق عجیب و شوک کننده اینکه دیتاهای کپچا به صورت inline svg داخل متن html ذخیره شدند!! با کمی دقت متوجه میشیم که کل تصویر کپچا به صورت یک سری داده داخل تگهای path هستند.
در قدم اول، همه داده های مربوط به کد کپچا رو با چند خط کد استخراج میکنم. انتخاب من برای اینکار، پایتون و سلنیوم:
from selenium import webdriver driver = webdriver.Chrome('/path/to/your/webdriver') driver.get('https://saipa.iranecar.com/registration') paths = driver.find_elements_by_tag_name('path')
مشخصه که در خط دوم، سایت سایپا رو باز میکنم و بعد تمام elementهایی که تگ path دارند رو داخل یک لیست نگه میدارم.
یک مشکل! با رفتن به لینک https://saipa.iranecar.com، مستقیماً صفحه ای که کپچا وجود داره باز نمیشه و نیازه تا یک سری کلیک و انتخاب داشته باشیم تا به صفحه مورد نظرمون برسیم.
برای جلوگیری از مشکل ساز شدنش، یک ورودی به کد بالا اضافه میکنم تا بعد از اینکه به کد کپچا رسیدم و داخل کنسول ENTER زدم برنامه ادامه پیدا کنه:
from selenium import webdriver driver = webdriver.Chrome('/path/to/your/webdriver') driver.get('https://saipa.iranecar.com/registration') # wait for input -> after captcha loaded we press enter to continue input('press "ENTER" to see the result') paths = driver.find_elements_by_tag_name('path')
یک نگاه به مقادیر attribute d از هر عنصر paths بندازیم:
from selenium import webdriver driver = webdriver.Chrome('/path/to/your/webdriver') driver.get('https://saipa.iranecar.com/registration') # wait for input -> after captcha loaded we press enter to continue input('press "ENTER" to see the result') paths = driver.find_elements_by_tag_name('path') for path in paths: print(path.get_attribute('d'))
اوه اوه! شد این:
این عددهای عجیب و غریب، تصویر svg که به عنوان کپچا میبینیم رو میسازند.
هنوز بهم ریخته و بدرد نخوره و لیستی که داریم خط خطی های اضافی روی کد کپچا رو هم شامل میشه؛ قدم بعدی اینه که اونها رو هم حذف کنم تا فقط لیستی از چهار رقم اصلی رو داشته باشم. برای اینکار کافیه element هایی که fill=none است رو پردازش نکنم!
from selenium import webdriver driver = webdriver.Chrome('/path/to/your/webdriver') driver.get('https://saipa.iranecar.com/registration') # wait for input -> after captcha loaded we press enter to continue input('press "ENTER" to see the result') paths = driver.find_elements_by_tag_name('path') for path in paths: if path.get_attribute('fill') != 'none': print(path.get_attribute('d'))
ظاهرا همه چیز روبراهه و فقط باید از لیستی که به دست آوردیم، رقم ها رو یکی یکی تشخیص بدیم؛ ایده من اینه که ظاهرا تمام ۵ ها داده های مشابه دارند، تمام ۲ ها داده های مشابه دارند و …
یعنی اگه اطلاعات attribute d از هر رقم (از ۰ تا ۹) رو یک جا ذخیر داشته باشم، با مقایسه attribute d عنصرهای داخل لیستم میتونم عدد مربوط بهش رو تشخیص بدم.
برای اینکه مطمئن بشم، دریافت تصویر جدید رو اونقدر میزنم تا به تصویری برسم که رقم تکراری داشته باشه:
اینجا دو تا هفت داریم و انتظار من اینه که داده های attribute d شون یکسان باشه؛ دیتای مربوط به اولین هفت:
<path fill="#333" d="M60.09 27.15L60.12 27.18L60.21 27.26Q60.87 27.13 62.28 26.97L62.38 27.08L62.30 27.00Q62.26 27.64 62.26 28.29L62.30 28.33L62.22 29.50L62.35 29.64Q61.45 29.57 60.61 29.65L60.58 29.62L60.66 29.69Q59.75 29.66 58.92 29.63L58.90 29.61L58.90 29.61Q56.16 35.89 52.74 40.50L52.60 40.37L52.56 40.33Q50.07 40.99 48.77 41.60L48.84 41.67L48.86 41.69Q52.99 36.00 56.04 29.72L55.91 29.59L53.47 29.78L53.31 29.62Q53.44 28.41 53.32 27.08L53.24 27.00L53.19 26.94Q55.13 27.14 57.19 27.14L57.23 27.17L59.08 23.43L59.11 23.46Q60.07 21.53 61.28 19.93L61.39 20.03L61.36 20.01Q59.73 20.09 58.13 20.09L58.12 20.07L58.09 20.04Q52.09 20.14 48.33 17.93L48.25 17.86L47.68 16.22L47.65 16.19Q47.33 15.38 46.95 14.51L46.86 14.41L46.87 14.42Q51.20 17.03 56.91 17.26L56.92 17.28L56.95 17.30Q62.07 17.51 67.21 15.53L67.33 15.66L67.18 15.51Q67.16 16.09 66.66 16.97L66.62 16.92L66.62 16.93Q63.00 21.68 60.15 27.20ZM68.17 18.25L68.21 18.29L69.26 16.33L69.18 16.25Q68.27 16.79 66.60 17.51L66.64 17.55L66.82 17.24L66.78 17.20Q66.86 17.01 66.97 16.89L66.92 16.84L66.93 16.86Q67.43 16.29 68.16 14.96L68.13 14.93L68.07 14.88Q62.75 17.17 57.00 16.94L57.09 17.03L56.90 16.83Q50.91 16.59 46.15 13.62L46.23 13.71L46.24 13.72Q47.23 15.54 48.07 18.21L48.04 18.18L48.14 18.28Q49.12 18.80 49.92 19.10L49.90 19.09L49.91 19.10Q50.12 19.50 50.58 21.37L50.56 21.35L50.63 21.41Q53.67 22.56 59.19 22.41L59.20 22.41L59.37 22.58Q58.88 22.97 56.90 26.74L57.04 26.88L56.99 26.82Q54.92 26.81 52.94 26.62L52.86 26.54L52.88 26.56Q53.02 27.46 53.02 28.33L53.08 28.40L53.13 30.16L54.69 30.05L54.79 31.56L54.67 31.43Q50.70 38.77 47.92 42.35L47.91 42.34L48.05 42.48Q49.63 41.70 51.27 41.20L51.21 41.14L51.21 41.14Q50.63 42.16 49.26 43.88L49.20 43.82L49.25 43.87Q52.37 42.72 54.80 42.49L54.82 42.51L54.79 42.48Q57.67 38.66 60.79 31.88L60.76 31.86L64.36 32.21L64.25 32.11Q64.26 31.28 64.26 30.36L64.21 30.31L64.18 28.49L64.24 28.56Q63.99 28.57 63.44 28.61L63.27 28.44L63.41 28.59Q62.87 28.63 62.60 28.63L62.55 28.57L62.55 28.58Q62.59 28.50 62.63 28.35L62.66 28.39L62.61 28.06L62.60 28.06Q65.09 22.90 68.17 18.25Z"></path>
و دیتای مربوط به دومین هفت:
<path fill="#222" d="M89.65 27.11L89.77 27.22L89.62 27.07Q90.51 27.17 91.92 27.01L91.96 27.05L91.94 27.04Q91.96 27.75 91.96 28.39L91.80 28.22L91.81 29.49L91.89 29.58Q91.14 29.67 90.31 29.74L90.19 29.63L90.18 29.61Q89.31 29.63 88.48 29.59L88.48 29.59L88.55 29.66Q85.77 35.91 82.35 40.51L82.32 40.48L82.25 40.42Q79.67 41.00 78.38 41.61L78.53 41.76L78.43 41.66Q82.61 36.02 85.66 29.74L85.67 29.75L82.98 29.69L82.96 29.67Q83.06 28.43 82.95 27.10L82.81 26.96L82.90 27.06Q84.69 27.10 86.75 27.10L86.77 27.12L88.68 23.43L88.66 23.41Q89.63 21.49 90.85 19.89L90.91 19.96L90.97 20.01Q89.43 20.19 87.83 20.19L87.68 20.04L87.76 20.11Q81.72 20.16 77.95 17.96L77.97 17.98L77.29 16.24L77.35 16.29Q76.82 15.27 76.44 14.39L76.55 14.50L76.48 14.43Q80.83 17.06 86.54 17.29L86.65 17.41L86.60 17.35Q91.81 17.65 96.95 15.67L96.78 15.50L96.82 15.55Q96.68 16.02 96.19 16.89L96.26 16.97L96.20 16.91Q92.50 21.58 89.64 27.10ZM97.85 18.33L97.85 18.32L98.85 16.32L98.83 16.30Q97.87 16.79 96.20 17.51L96.12 17.44L96.35 17.17L96.44 17.26Q96.38 16.93 96.49 16.82L96.59 16.92L96.62 16.95Q97.02 16.28 97.74 14.95L97.73 14.94L97.60 14.80Q92.41 17.23 86.67 17.00L86.69 17.02L86.64 16.97Q80.53 16.61 75.77 13.64L75.83 13.70L75.83 13.71Q76.91 15.62 77.74 18.28L77.59 18.13L77.77 18.31Q78.72 18.80 79.52 19.10L79.59 19.18L79.50 19.09Q79.70 19.48 80.16 21.35L80.12 21.30L80.12 21.30Q83.33 22.62 88.85 22.46L88.86 22.47L88.78 22.39Q88.49 22.98 86.51 26.75L86.63 26.86L86.63 26.86Q84.45 26.74 82.47 26.55L82.59 26.67L82.46 26.54Q82.68 27.53 82.68 28.40L82.69 28.41L82.64 30.07L84.26 30.02L84.23 31.39L84.41 31.57Q80.38 38.85 77.60 42.43L77.67 42.50L77.54 42.37Q79.29 41.76 80.93 41.27L80.81 41.15L80.90 41.23Q80.12 42.05 78.75 43.77L78.79 43.81L78.78 43.80Q81.92 42.68 84.36 42.45L84.32 42.41L84.42 42.51Q87.35 38.74 90.47 31.96L90.44 31.93L93.83 32.09L93.88 32.14Q93.80 31.22 93.80 30.31L93.84 30.35L93.74 28.46L93.76 28.48Q93.48 28.46 92.93 28.50L93.00 28.58L92.88 28.45Q92.49 28.66 92.23 28.66L92.07 28.50L92.23 28.66Q92.17 28.48 92.20 28.33L92.12 28.25L92.12 27.97L92.19 28.05Q94.77 22.97 97.85 18.33Z"></path>
با یک مقایسه چشمی متوجه میشیم که دو مقدار بالا با هم فرق دارند و مقادیرشون یکسان نیست؛ شکست خوردیم!!!!
بعد از کمی وَر رفتن با اعداد، نکته عجیبی خودنمایی میکنه! با کد زیر، attribute d از تمام رقم های کپچایی که واسم اومده رو میخونم، اعدادش رو بر اساس اسپیس از هم جدا میکنم، داخل یک لیست میریزم و طول هر لیست رو محاسبه میکنم ( خودم هم نفهمیدم چی گفتم! (: )
from selenium import webdriver driver = webdriver.Chrome('/path/to/your/webdriver') driver.get('https://saipa.iranecar.com/registration') # wait for input -> after captcha loaded we press enter to continue input('press "ENTER" to see the result') paths = driver.find_elements_by_tag_name('path') for path in paths: if path.get_attribute('fill') != 'none': data = path.get_attribute('d').split(' ') print(len(data))
برای کپچای بالا (7317) نتیجه میشه:
210 401 104 210
حله (: ظاهرا با وجود اینکه داده های ۱ ها، ۲ ها یا ۳ های مختلف با هم فرق دارند اما همه رقم های مشابه طول یکسان دارند. (توی خروجی بالا برای هر دو تا ۷ طولِ ۲۱۰ به دست اومد)
با چند خط کد مشابه و آزمون و خطا یک دیکشنری از ارقام ۰ تا ۹ میسازم که بر اساس len(data) بهم رقم مربوط رو بده:
digit = { 246: 0, 104: 1, 264: 2, 401: 3, 221: 4, 269: 5, 273: 6, 210: 7, 352: 8, 290: 9 }
حالا کافیه لیست paths رو پیمایش کنم، attribute d رو براساس اسپیس از هم جدا کنم و سایزش رو به دست بیارم؛ سایز رو به دیکشنری بالا میدم و تمام!
برای تصویر پایین نتیجه میشه:
290 352 210 264
خب خب خب (: رقم ها رو درست تشخیص دادیم ولی ترتیبشون رو نه! (ترتیب اعداد بالا میشه: ۹۸۷۲)
عبارت اول داخل attribute d از هر رقم، شامل یک M هست به اضافه یک عدد اعشاری! (مثلا: M89.65)
نکته بامزه اینه که هر چی عدد کوچکتر باشه نشون دهنده این هست که اون رقم زودتر توی کپچا ظاهر شده! به عبارت دیگه اگه لیست رو بر اساس این عدد مرتب کنم، ترتیب رقم های کپچا درست میشه:
from selenium import webdriver digit = { 246: 0, 104: 1, 264: 2, 401: 3, 221: 4, 269: 5, 273: 6, 210: 7, 352: 8, 290: 9 } driver = webdriver.Chrome('/path/to/your/webdriver') driver.get('https://saipa.iranecar.com/registration') # wait for input -> after captcha loaded we press enter to continue input('press "ENTER" to see the result') paths = driver.find_elements_by_tag_name('path') code = [] for path in paths: if path.get_attribute('fill') != 'none': data = path.get_attribute('d').split(' ') location = float(data[0][1:]) # get the number after M d = digit[len(data)] code.append([d, location]) real_code = sorted(code, key=lambda x: x[1]) real_code = ''.join(str(x[0]) for x in real_code) print(real_code)
و نتیجه کد با کمی تغییر:
هدف از این مطلب، نشون دادن یک مشکل در کپچای سایت سایپا ست که انتظار میره بتونند با یک روش بهتر جایگزینش کنند.