دور زدن کپچای سایت سایپا بدون پردازش تصویر

‌‌دور زدن کپچای saipa.irancar.com
‌‌دور زدن کپچای saipa.irancar.com

پیامی داشتم که دوستی می‌گفت دنبال یک اسکریپت ساده هست تا بتونه فیلدهای یک سری فرم، مثل فرم سامانه فروش سایپا رو اتوماتیک پر کنه.

خب! راه حل، سرراست و ساده به نظر می‌رسه، به جز کپچای پایین صفحه که همیشه دردسر ساز بوده!

توی این پست قصد دارم قدم به قدم، مراحلی که برای دور زدن کپچای سایت https://saipa.iranecar.com انجام دادم رو توضیح بدم.

DON'T TRY THIS AT HOME!!! :))
DON'T TRY THIS AT HOME!!! :))

تلاش اول

برای شروع کنجکاوی روی کپچا راست کلیک میکنم و inspect element رو میزنم تا ببینم اون پشت چه خبره.

You can use Inspect Element to have fun!
You can use Inspect Element to have fun!


اتفاق عجیب و شوک کننده اینکه دیتاهای کپچا به صورت inline svg داخل متن html ذخیره شدند!! با کمی دقت متوجه می‌شیم که کل تصویر کپچا به صورت یک سری داده داخل تگهای path هستند.

The Path Is Everywhere
The Path Is Everywhere

در قدم اول، همه داده های مربوط به کد کپچا رو با چند خط کد استخراج می‌کنم. انتخاب من برای این‌کار، پایتون و سلنیوم:

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'))

اوه اوه! شد این:

WoOw
WoOw

این عددهای عجیب و غریب، تصویر 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 عنصرهای داخل لیستم می‌تونم عدد مربوط بهش رو تشخیص بدم.

برای اینکه مطمئن بشم، دریافت تصویر جدید رو اونقدر می‌زنم تا به تصویری برسم که رقم تکراری داشته باشه:

7317
7317

اینجا دو تا هفت داریم و انتظار من اینه که داده های 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 رو براساس اسپیس از هم جدا کنم و سایزش رو به دست بیارم؛ سایز رو به دیکشنری بالا می‌دم و تمام!

برای تصویر پایین نتیجه میشه:

8927
8927
290
352
210
264

خب خب خب (: رقم ها رو درست تشخیص دادیم ولی ترتیب‌شون رو نه! (ترتیب اعداد بالا می‌شه: ۹۸۷۲)

تلاش سوم

عبارت اول داخل attribute d از هر رقم، شامل یک M هست به اضافه یک عدد اعشاری! (مثلا: M89.65)

M10
M10

نکته بامزه اینه که هر چی عدد کوچکتر باشه نشون دهنده این هست که اون رقم زودتر توی کپچا ظاهر شده! به عبارت دیگه اگه لیست رو بر اساس این عدد مرتب کنم، ترتیب رقم های کپچا درست می‌شه:

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)

و نتیجه کد با کمی تغییر:

https://aparat.com/v/J2v8T

هدف از این مطلب، نشون دادن یک مشکل در کپچای سایت سایپا ست که انتظار می‌ره بتونند با یک روش بهتر جایگزینش کنند.